Compare commits

...
96 Commits
Author SHA1 Message Date
Pasha Sviderski c58108aa82 chore(.editorconfig): ignore formatting for release notes 2026-10-02 13:56:49 +10:00
Pasha Sviderski 5d7952952a docs(README): list cluster storage for certificates in features 2026-10-02 13:56:08 +10:00
Pasha Sviderski 87e99ae369 docs(caddy): new page Cluster storage for Caddy 2026-10-02 13:27:07 +10:00
Pasha Sviderski f78a8ca8b5 docs(caddy): regenerate CLI reference for 'caddy cert ls' command 2026-10-01 21:00:05 +10:00
Pasha Sviderski e4bd1ad443 feat(store): improve waiting for store replication to skip unavailable members 2026-10-01 20:59:15 +10:00
Pasha Sviderski 0e53f505c0 refactor(client): speed up service list call (N -> 1 broadcast RPC) 2026-10-01 19:52:18 +10:00
Pasha Sviderski 9c21ae6b08 feat(caddy): CLI command to list certificates stored in the Uncloud cluster storage for Caddy 2026-10-01 14:19:53 +10:00
Pasha Sviderski 70de7b1b8f feat(caddy): add healthcheck to default Caddy spec and Compose deployment in docs 2026-10-01 09:25:39 +10:00
Pasha Sviderski af277639dd chore(dns): require minimum server version 0.21.0 for 'uc dns set' command 2026-09-30 19:21:59 +10:00
Pasha Sviderski 25ca0a57e8 lint 2026-09-30 18:49:53 +10:00
Pasha Sviderski 4dae187aa5 chore(caddystorage): move storage module code to a separate unlabs-dev/caddy-uncloud repo 2026-09-30 16:43:46 +10:00
Pasha Sviderski 930e7ea637 feat(caddy): add ListCertificates method, refactor Caddy client layout (#31) 2026-09-30 16:43:46 +10:00
Pasha Sviderski e50c5fbe57 refactor(api): replace map[string]uint64 with api.StoreVersion for store version handling 2026-09-30 16:43:46 +10:00
Anton Ovchinnikov e4455e936a fix(cli): stop leaking terminal capability replies into the shell (#435)
* chore(deps): bump bubbletea to v2.0.10, drop the uncloud fork

v2.0.10 carries the upstream fix for terminal capability replies leaking into
the shell (charmbracelet/bubbletea#1801). It skips every query that expects a
reply when input is disabled: the startup probe for synchronized output and
Unicode core, clipboard reads, background, foreground and cursor colour
requests, cursor position reports, and terminal version and termcap queries.

That supersedes the unlabs-dev fork, whose patch covered only the startup
probe, so the replace directive goes away and uncloud tracks upstream again.

The bump on its own does not fix the leak. Bubble Tea skips the queries only
when a program declares that it has no input, and nothing in uncloud declares
that yet. Measured: this tree still leaks "^[[?2026;2$y^[[?2027;0$y" under a
PTY harness. The next commit is what makes the spinners declare it.

Refs: #286, charmbracelet/bubbletea#1801
2026-09-28 10:16:45 +10:00
Pasha Sviderski ebecf334d7 chore(dns): require minimum server version 0.21 2026-09-25 17:24:13 +10:00
Miek GiebenandPasha Sviderski 417e402c73 feat(dns): allow setting an externally managed cluster domain (uc dns set)
* feat: add uc dns set

This allows manual setting of the cluster's domain name. I use this to
make the local cluster admin aware that this has been done.
(and then manually update the external dns)

```
% uc dns set ctrl.u.science.ru.nl
Set cluster domain: ctrl.u.science.ru.nl
Deploy the Caddy reverse proxy service ('uc caddy deploy') to enable internet access to your services via the reserved or your custom domain.

% uc dns show
ctrl.u.science.ru.nl

% uc dns set ctrl.u.science.ru.nl
Error: domain already reserved

% uc dns release
Released cluster domain: ctrl.u.science.ru.nl
```

See: #37

Signed-off-by: Miek Gieben <miek@miek.nl>

* docs

Signed-off-by: Miek Gieben <miek@miek.nl>

* check validatity

Signed-off-by: Miek Gieben <miek@miek.nl>

* No need to call out to update anything, as set doesnt use uncloud dns

Signed-off-by: Miek Gieben <miek@miek.nl>

* Ignore domain when there is no endpoint

Prevents this error, when you know what you're doing:

    Error: failed to update DNS records pointing to caddy service: create cluster domain records in Uncloud DNS: rpc error: code = Unknown desc = Post "/domains/ctrl.u.science.ru.nl/records": unsupported protocol scheme ""

Signed-off-by: Miek Gieben <miek@miek.nl>

* from main

Signed-off-by: Miek Gieben <miek@miek.nl>

* mise proto && make cli-docs

Signed-off-by: Miek Gieben <miek@miek.nl>

* Rebase and and generate protos again

Signed-off-by: Miek Gieben <miek@miek.nl>

* Rebase and fix

Signed-off-by: Miek Gieben <miek@miek.nl>

* feat(dns): allow setting an externally managed cluster domain

---------

Signed-off-by: Miek Gieben <miek@miek.nl>
Co-authored-by: Pasha Sviderski <me@psviderski.name>
2026-09-25 16:36:01 +10:00
Pasha Sviderski 9ee3ca15eb feat(dns): allow setting an externally managed cluster domain 2026-09-25 16:20:54 +10:00
Pasha Sviderski 3482eeefab chore: go mod tidy 2026-09-24 22:34:47 +10:00
Pasha Sviderski ace8cbf974 fix(caddyconfig): custom global Caddy config is preserved on regeneration,warn about last load error in 'uc caddy config' (fixes #412) 2026-09-24 22:24:42 +10:00
Pasha Sviderski 45d33d87dd docs(runtime-templates): add 'Added in v0.21.0' note 2026-09-23 19:12:58 +10:00
Pasha Sviderski 5c33e4be86 feat(runtime-templates): add support for runtime templates in bind mounts (#412) 2026-09-23 15:12:10 +10:00
Pasha Sviderski 88dda435e7 chore(gitignore): ignore /research dir 2026-09-23 09:49:37 +10:00
Pasha Sviderski ae940c3003 fix(test): flaky distributed lock tests by waiting for WG mesh to become ready 2026-09-22 15:21:18 +10:00
Pasha Sviderski e525bdeb7c chore(website): update discord members 2026-09-18 21:04:53 +10:00
Pasha Sviderski bc09954cbd feat(caddy): mount uncloud API socket for storage module (#31) 2026-09-17 16:50:26 +10:00
Pasha Sviderski f68c9859d9 BREAKING CHANGE: move Uncloud API socket to /run/uncloud/api/uncloud.sock, activate it by systemd socket unit 2026-09-17 15:23:38 +10:00
Pasha Sviderski c65f352250 test(connector): test UnixConnector reconnects after socket replacement 2026-09-17 11:28:46 +10:00
Miek Gieben b28c458029 Rebase and fix
Signed-off-by: Miek Gieben <miek@miek.nl>
2026-09-15 14:39:48 +02:00
Miek Gieben 3fc1cddf01 Rebase and and generate protos again
Signed-off-by: Miek Gieben <miek@miek.nl>
2026-09-15 14:36:28 +02:00
Miek Gieben 54a7328a34 mise proto && make cli-docs
Signed-off-by: Miek Gieben <miek@miek.nl>
2026-09-15 14:36:02 +02:00
Miek Gieben 47a4a9158e from main
Signed-off-by: Miek Gieben <miek@miek.nl>
2026-09-15 14:35:43 +02:00
Miek Gieben ad2e1a06d5 Ignore domain when there is no endpoint
Prevents this error, when you know what you're doing:

    Error: failed to update DNS records pointing to caddy service: create cluster domain records in Uncloud DNS: rpc error: code = Unknown desc = Post "/domains/ctrl.u.science.ru.nl/records": unsupported protocol scheme ""

Signed-off-by: Miek Gieben <miek@miek.nl>
2026-09-15 14:35:33 +02:00
Miek Gieben 81d36edc66 No need to call out to update anything, as set doesnt use uncloud dns
Signed-off-by: Miek Gieben <miek@miek.nl>
2026-09-15 14:35:33 +02:00
Miek Gieben 5fb80e2071 check validatity
Signed-off-by: Miek Gieben <miek@miek.nl>
2026-09-15 14:35:33 +02:00
Miek Gieben d1fe23dec0 docs
Signed-off-by: Miek Gieben <miek@miek.nl>
2026-09-15 14:35:33 +02:00
Miek Gieben cec3c75a10 feat: add uc dns set
This allows manual setting of the cluster's domain name. I use this to
make the local cluster admin aware that this has been done.
(and then manually update the external dns)

```
% uc dns set ctrl.u.science.ru.nl
Set cluster domain: ctrl.u.science.ru.nl
Deploy the Caddy reverse proxy service ('uc caddy deploy') to enable internet access to your services via the reserved or your custom domain.

% uc dns show
ctrl.u.science.ru.nl

% uc dns set ctrl.u.science.ru.nl
Error: domain already reserved

% uc dns release
Released cluster domain: ctrl.u.science.ru.nl
```

See: #37

Signed-off-by: Miek Gieben <miek@miek.nl>
2026-09-15 14:35:31 +02:00
Pasha Sviderski 751ea97f47 fix(caddystorage): error formatting 2026-09-15 14:55:43 +10:00
Pasha Sviderski abb85f8734 chore(AGENTS): do not emphasize the importance of table driven tests 2026-09-15 13:27:33 +10:00
Pasha Sviderski 37f817aa58 chore(caddystorage): adjust log levels 2026-09-15 13:26:27 +10:00
Pasha Sviderski db55df64a2 chore: bump caddy Go pkg dependency to v2.11.4 2026-09-14 19:38:49 +10:00
Pasha Sviderski e9e7b3a61e chore: move legacy unused WG tunnel and client connector to experiment/wg 2026-09-14 19:22:42 +10:00
Pasha Sviderski e81e130544 chore: go mod tidy 2026-09-12 07:48:18 +10:00
Pasha Sviderski 1e471d3ec4 feat(caddystorage): lint 2026-09-11 20:25:03 +10:00
Pasha Sviderski f7be7c55c9 feat(caddystorage): minor logging formatting 2026-09-11 20:17:41 +10:00
Pasha Sviderski 6ab02f95fe feat(caddystorage): simplify active locks tracking and cleanup 2026-09-11 20:14:35 +10:00
Pasha Sviderski 9a4098694c feat(caddystorage): init Caddy storage module backed by an Uncloud cluster 2026-09-09 20:11:09 +10:00
Pasha Sviderski 992b819f5b feat(caddystorage): simplify CaddyStorage gRPC API to allow only one2one calls 2026-09-08 14:36:43 +10:00
Pasha Sviderski 842f796e38 refactor(store): unify logic and enhance logging for initial store sync (waitStoreSync) 2026-09-08 12:04:26 +10:00
Pasha Sviderski 0f6c03240b feat(store): add WaitForStoreVersion API to wait for cluster store replication up to target version 2026-09-08 10:52:53 +10:00
Pasha Sviderski c6d0371f15 feat(caddystorage): integrate CaddyStorage gRPC service in machine API 2026-09-07 10:44:08 +10:00
Pasha Sviderski 5e01db5e20 fix(compose): remove unnecessary warning validating deploy.mode that is error 2026-09-04 11:00:05 +10:00
Pasha Sviderski 8aedc2bf19 refactor(proto): move protobuf generated API to the top-level api/pb package 2026-09-04 10:50:30 +10:00
Pasha Sviderski 7a88865f64 feat(caddystorage): implement the machine-local CaddyStorage gRPC service 2026-09-04 10:50:30 +10:00
Pasha Sviderski a24fc11caf feat(caddystorage): generic namespace-scoped key-value storage using cluster table in Corrosion 2026-09-04 10:50:30 +10:00
Pasha Sviderski c7e0e36daa feat(caddystorage): define CaddyStorage gRPC service API for distributed Caddy storage 2026-09-04 10:50:30 +10:00
Pasha Sviderski 01426ca03d feat(distlock): add smoke e2e test for distributed lock (lease) in cluster 2026-09-04 10:50:30 +10:00
Pasha Sviderski 3f86b3de44 feat(distlock): add integration tests for grpc transport 2026-09-04 10:50:30 +10:00
Miek Gieben aaf67687d2 feat: validate remaining unsupported items of the support-matrix (#429)
* feat: validate remaining unsupported items of the support-matrix

Validate unsupported compose features: external configs, container_name,
and placement.

This makes the code match the support-matrix as of now (added comment to
that effect as well).

Signed-off-by: Miek Gieben <miek@miek.nl>

* support matrix: some deploy modes are supported

Add tests for the various non-supported deploy modes.
Prefix the warnings with `deploy ` to signal the section, and to
differentiate between top level 'labels' and 'deploy labels'.

Signed-off-by: Miek Gieben <miek@miek.nl>

---------

Signed-off-by: Miek Gieben <miek@miek.nl>
2026-09-04 10:40:03 +10:00
Pasha Sviderski 1c29d4d5d0 feat(distlock): integrate distributed lease management into Machine API 2026-08-28 20:59:10 +10:00
Pasha Sviderski 00d68d9465 refactor(client): make ProxySingleMachineContext and ProxyMachinesContext package functions as well 2026-08-28 20:37:50 +10:00
Pasha Sviderski 43bf2baaf7 feat(distlock): implement distributed Locker based on Redlock algorithm 2026-08-28 20:34:06 +10:00
Pasha Sviderski 613cd6e418 feat(distlock): refactor into a standalone package 2026-08-27 15:49:59 +10:00
Pasha Sviderski 7a7a313426 feat(distlock): add gRPC Lease server that adapts a machine-local Store 2026-08-27 12:27:54 +10:00
Pasha Sviderski a5ab6e3788 feat(distlock): implement in-memory lease store 2026-08-26 16:53:21 +10:00
Pasha Sviderski 5a956e72de feat(distlock): declare Lease gRPC service API for distributed locks 2026-08-26 16:06:47 +10:00
Felix Hummel ac56754281 docs(install): mise installation (#421) 2026-08-21 15:14:02 +10:00
Miek GiebenandPasha Sviderski b7e224a1ef feat(compose): support stdin_open and tty (#419)
* feat: support stdin_open and tty

This can be useful to leave a container running without specifying a
command like `sleep`, and probably in other situations as well.

Signed-off-by: Miek Gieben <miek@miek.nl>

* Complete full spec test case

Signed-off-by: Miek Gieben <miek@miek.nl>

* fix container log streaming for containers with TTY

---------

Signed-off-by: Miek Gieben <miek@miek.nl>
Co-authored-by: Pasha Sviderski <me@psviderski.name>
2026-07-30 21:50:54 +10:00
Pasha Sviderski 351698c280 fix(cli): completion with direct connections (--connect, --context, --uncloud-config) (fixes #377) 2026-07-21 17:01:21 +10:00
Pasha Sviderski fa77edf53e fix(proxy): don't shutdown 'uc proxy' when a client connection aborts 2026-07-21 14:16:46 +10:00
Pasha Sviderski b99abb7c39 website: update docs social card and favicons 2026-07-14 12:54:44 +10:00
Pasha Sviderski c2ae11a293 docs(cluster): add section about SSH user permissions when connection to the cluster 2026-07-14 12:54:44 +10:00
Pasha Sviderski f1555259de chore(docs): update command outputs in Deploy demo app doc, add section to view logs 2026-07-14 12:54:44 +10:00
Pasha Sviderski 50f8fbcfda fix(install): correctly compare installed nightly daemon version to upgrade to latest 2026-07-14 12:54:44 +10:00
Pasha Sviderski 73f29092ff docs: update use cases on the Overview page 2026-07-14 12:54:44 +10:00
Pasha Sviderski 08b24af341 website: split newsletter subscription into a separate section 2026-07-14 12:54:43 +10:00
Pasha Sviderski b9c54f1ff5 chore(website): configure ingress Caddy as trusted proxy to forward client IPs 2026-07-14 12:54:43 +10:00
Pasha Sviderski 4d76dd601c website: refine faq open source question, update og image 2026-07-14 12:54:43 +10:00
Dario Griffo 3af9936d27 docs(install): update Debian repository domain (#413)
The community repository moved from debian.griffo.io to deb.griffo.io
(Debian trademark policy; old domain redirects permanently). Also use
the keyring + signed-by layout and add the missing sudo and apt update.
2026-07-14 12:54:29 +10:00
Pasha Sviderski dc721e511e fix(daemon): extend systemd service start timeout when pulling Docker image for corrosion service 2026-07-10 20:42:27 +10:00
Pasha Sviderski 054b3b8fd4 website: change CTA link to getting started docs 2026-07-09 19:07:25 +10:00
Pasha Sviderski b3896ff642 website: minor 2026-07-09 18:56:46 +10:00
Pasha Sviderski 5b2823de1e website: update meta/og tags and images 2026-07-09 18:56:46 +10:00
Pasha Sviderski 0007f76d09 website: serve hub as /hub 2026-07-09 18:56:46 +10:00
Pasha Sviderski f8e6d1caaa website: send Hub early access form submission to posthog 2026-07-09 18:56:46 +10:00
Pasha Sviderski e684795aee website: refine styles for index and hub 2026-07-09 18:56:46 +10:00
Pasha Sviderski 7964273552 chore(experiments): go mod tidy 2026-07-09 18:56:46 +10:00
Pasha Sviderski 4ad4bce29d website: refine FAQ and form on hub page 2026-07-09 18:56:46 +10:00
Pasha Sviderski d670d18810 website: add new Hub page 2026-07-09 18:56:46 +10:00
Pasha Sviderski a1957a156e docs(install): update machine init/add output that doesn't install corrosion systemd service 2026-07-09 18:56:45 +10:00
Jiralite 6266e9ec3a chore(nightly-build): prettier warning in description (#410) 2026-07-06 12:07:59 +10:00
Anton Ovchinnikov 7c4fcde88d feat(cli): support env var UNCLOUD_DAEMON_VERSION to specify daemon version for "uc machine add/init" (#409) 2026-07-06 10:47:56 +10:00
Nick Campbell d36b28c55a fix(docs): version command instead of --version in docs (#408) 2026-07-05 07:17:34 +10:00
Miek Gieben 6b41340f1d chore(cli): use cli.Cancelled when cancellikng the command to return non-zero exit code in all cases (#406)
Use `cli.Cancelled` in all cases where we cancel an operation instead of Printf && return nil

Signed-off-by: Miek Gieben <miek@miek.nl>
2026-06-30 19:49:11 +10:00
Pasha Sviderski 4b9ea43402 docs(cli): update machine init/add help with info about installing Docker and uncloudd 2026-06-29 21:03:11 +10:00
Pasha Sviderski e5f23a9189 fix(install): allow to install uc CLI version <0.20 after the artifact renaming 2026-06-26 21:39:39 +10:00
Pasha Sviderski 15e3e32b47 chore(grpc): update min client/server versions to 0.20.0 2026-06-26 20:53:42 +10:00
228 changed files with 13690 additions and 3843 deletions

No files matched your search

+1
View File
@@ -2,6 +2,7 @@
* *
# Allow files and directories. # Allow files and directories.
!api/
!cmd/ !cmd/
!internal/ !internal/
!pkg/ !pkg/
+3
View File
@@ -18,3 +18,6 @@ indent_size = 4
[*.sh] [*.sh]
indent_style = space indent_style = space
indent_size = 4 indent_size = 4
[release-notes-*.md]
max_line_length = off
+2 -1
View File
@@ -57,7 +57,8 @@ jobs:
The commands below show how to install the nightly version of uncloud. The commands below show how to install the nightly version of uncloud.
> **Warning**: These are nightly development builds and may be unstable. > [!WARNING]
> These are nightly development builds and may be unstable.
**Install uncloud CLI:** **Install uncloud CLI:**
+1
View File
@@ -44,3 +44,4 @@ node_modules/
.devcontainer/ .devcontainer/
.zed/ .zed/
research/
+1 -2
View File
@@ -188,7 +188,7 @@ uc context use <name> # Switch context
- **Package naming**: Use clear, descriptive names - **Package naming**: Use clear, descriptive names
- **Error handling**: Wrap errors with context using `fmt.Errorf` - **Error handling**: Wrap errors with context using `fmt.Errorf`
- **Logging**: Use structured logging with levels - **Logging**: Use structured logging with levels
- **gRPC**: Services defined in `internal/machine/api/pb/` - **gRPC**: Services defined in `api/pb/`
### Testing ### Testing
@@ -196,7 +196,6 @@ uc context use <name> # Switch context
- Unit tests alongside source files (`*_test.go`) - Unit tests alongside source files (`*_test.go`)
- Integration tests in `test/e2e/` - Integration tests in `test/e2e/`
- Test fixtures in `test/fixtures/` - Test fixtures in `test/fixtures/`
- Use table driven tests whenever possible
- Use the `testify` library for assertions (e.g., `require.Equal`, `assert.Nil`) - Use the `testify` library for assertions (e.g., `require.Equal`, `assert.Nil`)
### Dependencies ### Dependencies
+1 -1
View File
@@ -107,7 +107,7 @@ make lint-and-fix
## Code generation ## Code generation
Update the generated Go code for the machine gRPC API after modifying `.proto` files: Update the generated Go code for the gRPC API after modifying files in [`api/pb`](./api/pb):
```shell ```shell
mise run proto mise run proto
+17 -18
View File
@@ -44,6 +44,8 @@ complexity of Kubernetes.
[Uncloud DNS](https://github.com/psviderski/uncloud-dns) service. [Uncloud DNS](https://github.com/psviderski/uncloud-dns) service.
* **Automatic HTTPS**: Built-in Caddy reverse proxy handles TLS certificate provisioning and renewal using Let's * **Automatic HTTPS**: Built-in Caddy reverse proxy handles TLS certificate provisioning and renewal using Let's
Encrypt. Encrypt.
* **Cluster storage for TLS certificates**: Native cluster storage for Caddy shares certificates and ACME challenge
tokens across machines and coordinates issuance.
* **Docker-like CLI**: Familiar commands for managing both infrastructure and applications. * **Docker-like CLI**: Familiar commands for managing both infrastructure and applications.
* **Remote management**: Control your entire infrastructure through SSH access to any single machine in the cluster. * **Remote management**: Control your entire infrastructure through SSH access to any single machine in the cluster.
@@ -154,7 +156,6 @@ Here is a diagram of an Uncloud multi-provider cluster of 3 machines:
```bash ```bash
$ uc machine init --name oracle-vm ubuntu@152.67.101.197 $ uc machine init --name oracle-vm ubuntu@152.67.101.197
Downloading Uncloud install script: https://raw.githubusercontent.com/psviderski/uncloud/refs/heads/main/scripts/install.sh
⏳ Running Uncloud install script... ⏳ Running Uncloud install script...
✓ Docker is already installed. ✓ Docker is already installed.
⏳ Installing Docker... ⏳ Installing Docker...
@@ -167,11 +168,10 @@ Downloading Uncloud install script: https://raw.githubusercontent.com/psviderski
✓ uncloudd binary installed: /usr/local/bin/uncloudd ✓ uncloudd binary installed: /usr/local/bin/uncloudd
⏳ Downloading uninstall script: https://raw.githubusercontent.com/psviderski/uncloud/refs/heads/main/scripts/uninstall.sh ⏳ Downloading uninstall script: https://raw.githubusercontent.com/psviderski/uncloud/refs/heads/main/scripts/uninstall.sh
✓ uncloud-uninstall script installed: /usr/local/bin/uncloud-uninstall ✓ uncloud-uninstall script installed: /usr/local/bin/uncloud-uninstall
✓ Systemd unit file created: /etc/systemd/system/uncloud.socket
✓ Systemd unit file created: /etc/systemd/system/uncloud.service ✓ Systemd unit file created: /etc/systemd/system/uncloud.service
Created symlink /etc/systemd/system/sockets.target.wants/uncloud.socket → /etc/systemd/system/uncloud.socket.
Created symlink /etc/systemd/system/multi-user.target.wants/uncloud.service → /etc/systemd/system/uncloud.service. Created symlink /etc/systemd/system/multi-user.target.wants/uncloud.service → /etc/systemd/system/uncloud.service.
⏳ Downloading uncloud-corrosion binary: https://github.com/psviderski/corrosion/releases/latest/download/corrosion-aarch64-unknown-linux-gnu.tar.gz
✓ uncloud-corrosion binary installed: /usr/local/bin/uncloud-corrosion
✓ Systemd unit file created: /etc/systemd/system/uncloud-corrosion.service
⏳ Starting Uncloud machine daemon (uncloud.service)... ⏳ Starting Uncloud machine daemon (uncloud.service)...
✓ Uncloud machine daemon started. ✓ Uncloud machine daemon started.
✓ Uncloud installed on the machine successfully! 🎉 ✓ Uncloud installed on the machine successfully! 🎉
@@ -190,21 +190,20 @@ DNS records updated to use only the internet-reachable machines running caddy se
*.xuw3xd.cluster.uncloud.run A → 152.67.101.197 *.xuw3xd.cluster.uncloud.run A → 152.67.101.197
``` ```
1. The CLI SSHs into the machine and installs Docker, the `uncloudd` machine daemon and 1. The CLI SSHs into the machine and installs Docker and the `uncloudd` machine daemon, managed by systemd.
[corrosion](https://github.com/superfly/corrosion) service, managed by systemd.
2. Generates a unique WireGuard key pair, allocates a dedicated subnet `10.210.0.0/24` for the machine and its 2. Generates a unique WireGuard key pair, allocates a dedicated subnet `10.210.0.0/24` for the machine and its
containers, and configures `uncloudd` accordingly. All subsequent communication happens with `uncloudd` containers, and configures `uncloudd` accordingly. All subsequent communication happens with `uncloudd`
through its gRPC API over SSH. through its gRPC API over SSH.
3. Configures and starts `corrosion`, a CRDT-based distributed SQLite database to share cluster state between machines. 3. Configures and starts [corrosion](https://github.com/superfly/corrosion), a CRDT-based distributed SQLite database to
share cluster state between machines, as a Docker container managed by `uncloudd`.
4. Creates a Docker bridge network connected to the WireGuard interface. 4. Creates a Docker bridge network connected to the WireGuard interface.
5. This machine becomes an entry point for the newly created cluster which is stored in the cluster config under 5. This machine becomes an entry point for the newly created cluster. Its address is stored as a connection info in the
`~/.config/uncloud` on your local machine. cluster config at `~/.config/uncloud/config.yaml` on your local machine.
**When you add another machine:** **When you add another machine:**
```bash ```bash
$ uc machine add --name hetzner-server root@5.223.45.199 $ uc machine add --name hetzner-server root@5.223.45.199
Downloading Uncloud install script: https://raw.githubusercontent.com/psviderski/uncloud/refs/heads/main/scripts/install.sh
⏳ Running Uncloud install script... ⏳ Running Uncloud install script...
✓ Docker is already installed. ✓ Docker is already installed.
✓ Linux user and group 'uncloud' created. ✓ Linux user and group 'uncloud' created.
@@ -213,11 +212,10 @@ Downloading Uncloud install script: https://raw.githubusercontent.com/psviderski
✓ uncloudd binary installed: /usr/local/bin/uncloudd ✓ uncloudd binary installed: /usr/local/bin/uncloudd
⏳ Downloading uninstall script: https://raw.githubusercontent.com/psviderski/uncloud/refs/heads/main/scripts/uninstall.sh ⏳ Downloading uninstall script: https://raw.githubusercontent.com/psviderski/uncloud/refs/heads/main/scripts/uninstall.sh
✓ uncloud-uninstall script installed: /usr/local/bin/uncloud-uninstall ✓ uncloud-uninstall script installed: /usr/local/bin/uncloud-uninstall
✓ Systemd unit file created: /etc/systemd/system/uncloud.socket
✓ Systemd unit file created: /etc/systemd/system/uncloud.service ✓ Systemd unit file created: /etc/systemd/system/uncloud.service
Created symlink /etc/systemd/system/sockets.target.wants/uncloud.socket → /etc/systemd/system/uncloud.socket.
Created symlink /etc/systemd/system/multi-user.target.wants/uncloud.service → /etc/systemd/system/uncloud.service. Created symlink /etc/systemd/system/multi-user.target.wants/uncloud.service → /etc/systemd/system/uncloud.service.
⏳ Downloading uncloud-corrosion binary: https://github.com/psviderski/corrosion/releases/latest/download/corrosion-x86_64-unknown-linux-gnu.tar.gz
✓ uncloud-corrosion binary installed: /usr/local/bin/uncloud-corrosion
✓ Systemd unit file created: /etc/systemd/system/uncloud-corrosion.service
⏳ Starting Uncloud machine daemon (uncloud.service)... ⏳ Starting Uncloud machine daemon (uncloud.service)...
✓ Uncloud machine daemon started. ✓ Uncloud machine daemon started.
✓ Uncloud installed on the machine successfully! 🎉 ✓ Uncloud installed on the machine successfully! 🎉
@@ -246,9 +244,10 @@ hetzner-server Up 10.210.1.1/24 5.223.45.199 5.223.45.199:51820, [2
3. Registers the second machine in the cluster state and exchanges WireGuard keys with the first machine. 3. Registers the second machine in the cluster state and exchanges WireGuard keys with the first machine.
4. Both machines establish a WireGuard tunnel between each other, allowing Docker containers connected to the bridge 4. Both machines establish a WireGuard tunnel between each other, allowing Docker containers connected to the bridge
network to communicate directly across machines. network to communicate directly across machines.
5. Configures and starts `corrosion` on the second machine to sync the cluster state. 5. Configures and starts the `uncloud-corrosion` container on the second machine to sync the cluster state.
6. The second machine is added as an alternative entry point in the cluster config. 6. The second machine is added as an alternative entry point in the cluster config.
7. If one of the machines goes offline, the other machine can still serve cluster operations. 7. If one of the machines goes offline, the other machine can still be used by `uc` to connect and run cluster
operations.
If one more machine is added, the process repeats with a new subnet. The new machine needs to establish a WireGuard If one more machine is added, the process repeats with a new subnet. The new machine needs to establish a WireGuard
connection with only one of the existing machines. Other machines will learn about it through the shared cluster state connection with only one of the existing machines. Other machines will learn about it through the shared cluster state
@@ -326,9 +325,9 @@ I'm grateful to the following projects that inspired Uncloud's design and implem
* [Tailscale](https://tailscale.com/) — for pioneering the vision of decentralised flat mesh networking with an amazing * [Tailscale](https://tailscale.com/) — for pioneering the vision of decentralised flat mesh networking with an amazing
user experience that feels like magic. user experience that feels like magic.
* [Talos Linux](https://github.com/siderolabs/talos) * [Talos Linux](https://github.com/siderolabs/talos)
and [KubeSpan](https://www.talos.dev/v1.10/talos-guides/network/kubespan/) — for the machine API design using and [KubeSpan](https://www.talos.dev/v1.10/talos-guides/network/kubespan/) — for inspiring Uncloud's API routing
[grpc-proxy](https://github.com/siderolabs/grpc-proxy) and for its elegant approach to secure WireGuard-based overlay design with [grpc-proxy](https://github.com/siderolabs/grpc-proxy) and for their elegant approach to secure
networking with zero configuration. WireGuard-based overlay networking with zero configuration.
* [Docker Swarm Classic](https://github.com/docker-archive/classicswarm) and * [Docker Swarm Classic](https://github.com/docker-archive/classicswarm) and
[Rancher 1.x](http://rancher-com-website-main-elb-elb-1798790864.us-west-2.elb.amazonaws.com/docs/rancher/v1.6/en/) [Rancher 1.x](http://rancher-com-website-main-elb-elb-1798790864.us-west-2.elb.amazonaws.com/docs/rancher/v1.6/en/)
— for showing the power of simplicity and pragmatism in container orchestration and that not every problem needs the — for showing the power of simplicity and pragmatism in container orchestration and that not every problem needs the
@@ -2,7 +2,7 @@
// versions: // versions:
// protoc-gen-go v1.34.2 // protoc-gen-go v1.34.2
// protoc v5.27.3 // protoc v5.27.3
// source: internal/machine/api/pb/caddy.proto // source: api/pb/caddy.proto
package pb package pb
@@ -27,16 +27,18 @@ type GetCaddyConfigResponse struct {
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields unknownFields protoimpl.UnknownFields
// The generated Caddyfile content. // The saved Caddyfile content.
Caddyfile string `protobuf:"bytes,1,opt,name=caddyfile,proto3" json:"caddyfile,omitempty"` Caddyfile string `protobuf:"bytes,1,opt,name=caddyfile,proto3" json:"caddyfile,omitempty"`
// Timestamp when the config was last modified. // Timestamp when the config was last modified.
ModifiedAt *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=modified_at,json=modifiedAt,proto3" json:"modified_at,omitempty"` ModifiedAt *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=modified_at,json=modifiedAt,proto3" json:"modified_at,omitempty"`
// Error from the latest unsuccessful reconciliation attempt.
LastReconciliationError string `protobuf:"bytes,3,opt,name=last_reconciliation_error,json=lastReconciliationError,proto3" json:"last_reconciliation_error,omitempty"`
} }
func (x *GetCaddyConfigResponse) Reset() { func (x *GetCaddyConfigResponse) Reset() {
*x = GetCaddyConfigResponse{} *x = GetCaddyConfigResponse{}
if protoimpl.UnsafeEnabled { if protoimpl.UnsafeEnabled {
mi := &file_internal_machine_api_pb_caddy_proto_msgTypes[0] mi := &file_api_pb_caddy_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -49,7 +51,7 @@ func (x *GetCaddyConfigResponse) String() string {
func (*GetCaddyConfigResponse) ProtoMessage() {} func (*GetCaddyConfigResponse) ProtoMessage() {}
func (x *GetCaddyConfigResponse) ProtoReflect() protoreflect.Message { func (x *GetCaddyConfigResponse) ProtoReflect() protoreflect.Message {
mi := &file_internal_machine_api_pb_caddy_proto_msgTypes[0] mi := &file_api_pb_caddy_proto_msgTypes[0]
if protoimpl.UnsafeEnabled && x != nil { if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -62,7 +64,7 @@ func (x *GetCaddyConfigResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use GetCaddyConfigResponse.ProtoReflect.Descriptor instead. // Deprecated: Use GetCaddyConfigResponse.ProtoReflect.Descriptor instead.
func (*GetCaddyConfigResponse) Descriptor() ([]byte, []int) { func (*GetCaddyConfigResponse) Descriptor() ([]byte, []int) {
return file_internal_machine_api_pb_caddy_proto_rawDescGZIP(), []int{0} return file_api_pb_caddy_proto_rawDescGZIP(), []int{0}
} }
func (x *GetCaddyConfigResponse) GetCaddyfile() string { func (x *GetCaddyConfigResponse) GetCaddyfile() string {
@@ -79,53 +81,62 @@ func (x *GetCaddyConfigResponse) GetModifiedAt() *timestamppb.Timestamp {
return nil return nil
} }
var File_internal_machine_api_pb_caddy_proto protoreflect.FileDescriptor func (x *GetCaddyConfigResponse) GetLastReconciliationError() string {
if x != nil {
return x.LastReconciliationError
}
return ""
}
var file_internal_machine_api_pb_caddy_proto_rawDesc = []byte{ var File_api_pb_caddy_proto protoreflect.FileDescriptor
0x0a, 0x23, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69,
0x6e, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x62, 0x2f, 0x63, 0x61, 0x64, 0x64, 0x79, 0x2e, var file_api_pb_caddy_proto_rawDesc = []byte{
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x03, 0x61, 0x70, 0x69, 0x1a, 0x1b, 0x67, 0x6f, 0x6f, 0x67, 0x0a, 0x12, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x62, 0x2f, 0x63, 0x61, 0x64, 0x64, 0x79, 0x2e, 0x70,
0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x65, 0x6d, 0x70, 0x74, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x03, 0x61, 0x70, 0x69, 0x1a, 0x1b, 0x67, 0x6f, 0x6f, 0x67, 0x6c,
0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x65, 0x6d, 0x70, 0x74, 0x79,
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70,
0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x73, 0x0a, 0x16, 0x47, 0x65, 0x74, 0x43, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d,
0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xaf, 0x01, 0x0a, 0x16, 0x47, 0x65, 0x74, 0x43,
0x61, 0x64, 0x64, 0x79, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x61, 0x64, 0x64, 0x79, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
0x73, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x63, 0x61, 0x64, 0x64, 0x79, 0x66, 0x69, 0x6c, 0x65, 0x18, 0x73, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x63, 0x61, 0x64, 0x64, 0x79, 0x66, 0x69, 0x6c, 0x65, 0x18,
0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x63, 0x61, 0x64, 0x64, 0x79, 0x66, 0x69, 0x6c, 0x65, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x63, 0x61, 0x64, 0x64, 0x79, 0x66, 0x69, 0x6c, 0x65,
0x12, 0x3b, 0x0a, 0x0b, 0x6d, 0x6f, 0x64, 0x69, 0x66, 0x69, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x12, 0x3b, 0x0a, 0x0b, 0x6d, 0x6f, 0x64, 0x69, 0x66, 0x69, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18,
0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70,
0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d,
0x70, 0x52, 0x0a, 0x6d, 0x6f, 0x64, 0x69, 0x66, 0x69, 0x65, 0x64, 0x41, 0x74, 0x32, 0x49, 0x0a, 0x70, 0x52, 0x0a, 0x6d, 0x6f, 0x64, 0x69, 0x66, 0x69, 0x65, 0x64, 0x41, 0x74, 0x12, 0x3a, 0x0a,
0x05, 0x43, 0x61, 0x64, 0x64, 0x79, 0x12, 0x40, 0x0a, 0x09, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x19, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x72, 0x65, 0x63, 0x6f, 0x6e, 0x63, 0x69, 0x6c, 0x69, 0x61,
0x66, 0x69, 0x67, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09,
0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1b, 0x2e, 0x61, 0x70, 0x52, 0x17, 0x6c, 0x61, 0x73, 0x74, 0x52, 0x65, 0x63, 0x6f, 0x6e, 0x63, 0x69, 0x6c, 0x69, 0x61,
0x69, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x61, 0x64, 0x64, 0x79, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x32, 0x49, 0x0a, 0x05, 0x43, 0x61, 0x64,
0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x37, 0x5a, 0x35, 0x67, 0x69, 0x74, 0x68, 0x64, 0x79, 0x12, 0x40, 0x0a, 0x09, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12,
0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x70, 0x73, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x6b, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75,
0x69, 0x2f, 0x75, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1b, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x47, 0x65,
0x61, 0x6c, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x74, 0x43, 0x61, 0x64, 0x64, 0x79, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x73, 0x70,
0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x26, 0x5a, 0x24, 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, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72,
0x6f, 0x74, 0x6f, 0x33,
} }
var ( var (
file_internal_machine_api_pb_caddy_proto_rawDescOnce sync.Once file_api_pb_caddy_proto_rawDescOnce sync.Once
file_internal_machine_api_pb_caddy_proto_rawDescData = file_internal_machine_api_pb_caddy_proto_rawDesc file_api_pb_caddy_proto_rawDescData = file_api_pb_caddy_proto_rawDesc
) )
func file_internal_machine_api_pb_caddy_proto_rawDescGZIP() []byte { func file_api_pb_caddy_proto_rawDescGZIP() []byte {
file_internal_machine_api_pb_caddy_proto_rawDescOnce.Do(func() { file_api_pb_caddy_proto_rawDescOnce.Do(func() {
file_internal_machine_api_pb_caddy_proto_rawDescData = protoimpl.X.CompressGZIP(file_internal_machine_api_pb_caddy_proto_rawDescData) file_api_pb_caddy_proto_rawDescData = protoimpl.X.CompressGZIP(file_api_pb_caddy_proto_rawDescData)
}) })
return file_internal_machine_api_pb_caddy_proto_rawDescData return file_api_pb_caddy_proto_rawDescData
} }
var file_internal_machine_api_pb_caddy_proto_msgTypes = make([]protoimpl.MessageInfo, 1) var file_api_pb_caddy_proto_msgTypes = make([]protoimpl.MessageInfo, 1)
var file_internal_machine_api_pb_caddy_proto_goTypes = []any{ var file_api_pb_caddy_proto_goTypes = []any{
(*GetCaddyConfigResponse)(nil), // 0: api.GetCaddyConfigResponse (*GetCaddyConfigResponse)(nil), // 0: api.GetCaddyConfigResponse
(*timestamppb.Timestamp)(nil), // 1: google.protobuf.Timestamp (*timestamppb.Timestamp)(nil), // 1: google.protobuf.Timestamp
(*emptypb.Empty)(nil), // 2: google.protobuf.Empty (*emptypb.Empty)(nil), // 2: google.protobuf.Empty
} }
var file_internal_machine_api_pb_caddy_proto_depIdxs = []int32{ var file_api_pb_caddy_proto_depIdxs = []int32{
1, // 0: api.GetCaddyConfigResponse.modified_at:type_name -> google.protobuf.Timestamp 1, // 0: api.GetCaddyConfigResponse.modified_at:type_name -> google.protobuf.Timestamp
2, // 1: api.Caddy.GetConfig:input_type -> google.protobuf.Empty 2, // 1: api.Caddy.GetConfig:input_type -> google.protobuf.Empty
0, // 2: api.Caddy.GetConfig:output_type -> api.GetCaddyConfigResponse 0, // 2: api.Caddy.GetConfig:output_type -> api.GetCaddyConfigResponse
@@ -136,13 +147,13 @@ var file_internal_machine_api_pb_caddy_proto_depIdxs = []int32{
0, // [0:1] is the sub-list for field type_name 0, // [0:1] is the sub-list for field type_name
} }
func init() { file_internal_machine_api_pb_caddy_proto_init() } func init() { file_api_pb_caddy_proto_init() }
func file_internal_machine_api_pb_caddy_proto_init() { func file_api_pb_caddy_proto_init() {
if File_internal_machine_api_pb_caddy_proto != nil { if File_api_pb_caddy_proto != nil {
return return
} }
if !protoimpl.UnsafeEnabled { if !protoimpl.UnsafeEnabled {
file_internal_machine_api_pb_caddy_proto_msgTypes[0].Exporter = func(v any, i int) any { file_api_pb_caddy_proto_msgTypes[0].Exporter = func(v any, i int) any {
switch v := v.(*GetCaddyConfigResponse); i { switch v := v.(*GetCaddyConfigResponse); i {
case 0: case 0:
return &v.state return &v.state
@@ -159,18 +170,18 @@ func file_internal_machine_api_pb_caddy_proto_init() {
out := protoimpl.TypeBuilder{ out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{ File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(), GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_internal_machine_api_pb_caddy_proto_rawDesc, RawDescriptor: file_api_pb_caddy_proto_rawDesc,
NumEnums: 0, NumEnums: 0,
NumMessages: 1, NumMessages: 1,
NumExtensions: 0, NumExtensions: 0,
NumServices: 1, NumServices: 1,
}, },
GoTypes: file_internal_machine_api_pb_caddy_proto_goTypes, GoTypes: file_api_pb_caddy_proto_goTypes,
DependencyIndexes: file_internal_machine_api_pb_caddy_proto_depIdxs, DependencyIndexes: file_api_pb_caddy_proto_depIdxs,
MessageInfos: file_internal_machine_api_pb_caddy_proto_msgTypes, MessageInfos: file_api_pb_caddy_proto_msgTypes,
}.Build() }.Build()
File_internal_machine_api_pb_caddy_proto = out.File File_api_pb_caddy_proto = out.File
file_internal_machine_api_pb_caddy_proto_rawDesc = nil file_api_pb_caddy_proto_rawDesc = nil
file_internal_machine_api_pb_caddy_proto_goTypes = nil file_api_pb_caddy_proto_goTypes = nil
file_internal_machine_api_pb_caddy_proto_depIdxs = nil file_api_pb_caddy_proto_depIdxs = nil
} }
@@ -2,7 +2,7 @@ syntax = "proto3";
package api; package api;
option go_package = "github.com/psviderski/uncloud/internal/machine/api/pb"; option go_package = "github.com/psviderski/uncloud/api/pb";
import "google/protobuf/empty.proto"; import "google/protobuf/empty.proto";
import "google/protobuf/timestamp.proto"; import "google/protobuf/timestamp.proto";
@@ -13,8 +13,10 @@ service Caddy {
} }
message GetCaddyConfigResponse { message GetCaddyConfigResponse {
// The generated Caddyfile content. // The saved Caddyfile content.
string caddyfile = 1; string caddyfile = 1;
// Timestamp when the config was last modified. // Timestamp when the config was last modified.
google.protobuf.Timestamp modified_at = 2; google.protobuf.Timestamp modified_at = 2;
} // Error from the latest unsuccessful reconciliation attempt.
string last_reconciliation_error = 3;
}
@@ -2,7 +2,7 @@
// versions: // versions:
// - protoc-gen-go-grpc v1.5.1 // - protoc-gen-go-grpc v1.5.1
// - protoc v5.27.3 // - protoc v5.27.3
// source: internal/machine/api/pb/caddy.proto // source: api/pb/caddy.proto
package pb package pb
@@ -120,5 +120,5 @@ var Caddy_ServiceDesc = grpc.ServiceDesc{
}, },
}, },
Streams: []grpc.StreamDesc{}, Streams: []grpc.StreamDesc{},
Metadata: "internal/machine/api/pb/caddy.proto", Metadata: "api/pb/caddy.proto",
} }
+848
View File
@@ -0,0 +1,848 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.34.2
// protoc v5.27.3
// source: api/pb/caddy_storage.proto
package pb
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
emptypb "google.golang.org/protobuf/types/known/emptypb"
timestamppb "google.golang.org/protobuf/types/known/timestamppb"
reflect "reflect"
sync "sync"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type StoreCaddyStorageRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"`
Value []byte `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"`
}
func (x *StoreCaddyStorageRequest) Reset() {
*x = StoreCaddyStorageRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_api_pb_caddy_storage_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *StoreCaddyStorageRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*StoreCaddyStorageRequest) ProtoMessage() {}
func (x *StoreCaddyStorageRequest) ProtoReflect() protoreflect.Message {
mi := &file_api_pb_caddy_storage_proto_msgTypes[0]
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 StoreCaddyStorageRequest.ProtoReflect.Descriptor instead.
func (*StoreCaddyStorageRequest) Descriptor() ([]byte, []int) {
return file_api_pb_caddy_storage_proto_rawDescGZIP(), []int{0}
}
func (x *StoreCaddyStorageRequest) GetKey() string {
if x != nil {
return x.Key
}
return ""
}
func (x *StoreCaddyStorageRequest) GetValue() []byte {
if x != nil {
return x.Value
}
return nil
}
type LoadCaddyStorageRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"`
}
func (x *LoadCaddyStorageRequest) Reset() {
*x = LoadCaddyStorageRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_api_pb_caddy_storage_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *LoadCaddyStorageRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*LoadCaddyStorageRequest) ProtoMessage() {}
func (x *LoadCaddyStorageRequest) ProtoReflect() protoreflect.Message {
mi := &file_api_pb_caddy_storage_proto_msgTypes[1]
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 LoadCaddyStorageRequest.ProtoReflect.Descriptor instead.
func (*LoadCaddyStorageRequest) Descriptor() ([]byte, []int) {
return file_api_pb_caddy_storage_proto_rawDescGZIP(), []int{1}
}
func (x *LoadCaddyStorageRequest) GetKey() string {
if x != nil {
return x.Key
}
return ""
}
type LoadCaddyStorageResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Value []byte `protobuf:"bytes,1,opt,name=value,proto3" json:"value,omitempty"`
UpdatedAt *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=updated_at,json=updatedAt,proto3" json:"updated_at,omitempty"`
}
func (x *LoadCaddyStorageResponse) Reset() {
*x = LoadCaddyStorageResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_api_pb_caddy_storage_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *LoadCaddyStorageResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*LoadCaddyStorageResponse) ProtoMessage() {}
func (x *LoadCaddyStorageResponse) ProtoReflect() protoreflect.Message {
mi := &file_api_pb_caddy_storage_proto_msgTypes[2]
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 LoadCaddyStorageResponse.ProtoReflect.Descriptor instead.
func (*LoadCaddyStorageResponse) Descriptor() ([]byte, []int) {
return file_api_pb_caddy_storage_proto_rawDescGZIP(), []int{2}
}
func (x *LoadCaddyStorageResponse) GetValue() []byte {
if x != nil {
return x.Value
}
return nil
}
func (x *LoadCaddyStorageResponse) GetUpdatedAt() *timestamppb.Timestamp {
if x != nil {
return x.UpdatedAt
}
return nil
}
type DeleteCaddyStorageRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"`
}
func (x *DeleteCaddyStorageRequest) Reset() {
*x = DeleteCaddyStorageRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_api_pb_caddy_storage_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *DeleteCaddyStorageRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*DeleteCaddyStorageRequest) ProtoMessage() {}
func (x *DeleteCaddyStorageRequest) ProtoReflect() protoreflect.Message {
mi := &file_api_pb_caddy_storage_proto_msgTypes[3]
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 DeleteCaddyStorageRequest.ProtoReflect.Descriptor instead.
func (*DeleteCaddyStorageRequest) Descriptor() ([]byte, []int) {
return file_api_pb_caddy_storage_proto_rawDescGZIP(), []int{3}
}
func (x *DeleteCaddyStorageRequest) GetKey() string {
if x != nil {
return x.Key
}
return ""
}
type ListCaddyStorageRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Prefix string `protobuf:"bytes,1,opt,name=prefix,proto3" json:"prefix,omitempty"`
Recursive bool `protobuf:"varint,2,opt,name=recursive,proto3" json:"recursive,omitempty"`
}
func (x *ListCaddyStorageRequest) Reset() {
*x = ListCaddyStorageRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_api_pb_caddy_storage_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *ListCaddyStorageRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ListCaddyStorageRequest) ProtoMessage() {}
func (x *ListCaddyStorageRequest) ProtoReflect() protoreflect.Message {
mi := &file_api_pb_caddy_storage_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 ListCaddyStorageRequest.ProtoReflect.Descriptor instead.
func (*ListCaddyStorageRequest) Descriptor() ([]byte, []int) {
return file_api_pb_caddy_storage_proto_rawDescGZIP(), []int{4}
}
func (x *ListCaddyStorageRequest) GetPrefix() string {
if x != nil {
return x.Prefix
}
return ""
}
func (x *ListCaddyStorageRequest) GetRecursive() bool {
if x != nil {
return x.Recursive
}
return false
}
type ListCaddyStorageResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Keys []string `protobuf:"bytes,1,rep,name=keys,proto3" json:"keys,omitempty"`
}
func (x *ListCaddyStorageResponse) Reset() {
*x = ListCaddyStorageResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_api_pb_caddy_storage_proto_msgTypes[5]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *ListCaddyStorageResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ListCaddyStorageResponse) ProtoMessage() {}
func (x *ListCaddyStorageResponse) ProtoReflect() protoreflect.Message {
mi := &file_api_pb_caddy_storage_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 ListCaddyStorageResponse.ProtoReflect.Descriptor instead.
func (*ListCaddyStorageResponse) Descriptor() ([]byte, []int) {
return file_api_pb_caddy_storage_proto_rawDescGZIP(), []int{5}
}
func (x *ListCaddyStorageResponse) GetKeys() []string {
if x != nil {
return x.Keys
}
return nil
}
type StatCaddyStorageRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"`
}
func (x *StatCaddyStorageRequest) Reset() {
*x = StatCaddyStorageRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_api_pb_caddy_storage_proto_msgTypes[6]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *StatCaddyStorageRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*StatCaddyStorageRequest) ProtoMessage() {}
func (x *StatCaddyStorageRequest) ProtoReflect() protoreflect.Message {
mi := &file_api_pb_caddy_storage_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 StatCaddyStorageRequest.ProtoReflect.Descriptor instead.
func (*StatCaddyStorageRequest) Descriptor() ([]byte, []int) {
return file_api_pb_caddy_storage_proto_rawDescGZIP(), []int{6}
}
func (x *StatCaddyStorageRequest) GetKey() string {
if x != nil {
return x.Key
}
return ""
}
type StatCaddyStorageResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"`
UpdatedAt *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=updated_at,json=updatedAt,proto3" json:"updated_at,omitempty"`
Size int64 `protobuf:"varint,3,opt,name=size,proto3" json:"size,omitempty"`
IsTerminal bool `protobuf:"varint,4,opt,name=is_terminal,json=isTerminal,proto3" json:"is_terminal,omitempty"`
}
func (x *StatCaddyStorageResponse) Reset() {
*x = StatCaddyStorageResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_api_pb_caddy_storage_proto_msgTypes[7]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *StatCaddyStorageResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*StatCaddyStorageResponse) ProtoMessage() {}
func (x *StatCaddyStorageResponse) ProtoReflect() protoreflect.Message {
mi := &file_api_pb_caddy_storage_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 StatCaddyStorageResponse.ProtoReflect.Descriptor instead.
func (*StatCaddyStorageResponse) Descriptor() ([]byte, []int) {
return file_api_pb_caddy_storage_proto_rawDescGZIP(), []int{7}
}
func (x *StatCaddyStorageResponse) GetKey() string {
if x != nil {
return x.Key
}
return ""
}
func (x *StatCaddyStorageResponse) GetUpdatedAt() *timestamppb.Timestamp {
if x != nil {
return x.UpdatedAt
}
return nil
}
func (x *StatCaddyStorageResponse) GetSize() int64 {
if x != nil {
return x.Size
}
return 0
}
func (x *StatCaddyStorageResponse) GetIsTerminal() bool {
if x != nil {
return x.IsTerminal
}
return false
}
type ListCertificatesResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Certificates []*IssuedCertificate `protobuf:"bytes,1,rep,name=certificates,proto3" json:"certificates,omitempty"`
}
func (x *ListCertificatesResponse) Reset() {
*x = ListCertificatesResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_api_pb_caddy_storage_proto_msgTypes[8]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *ListCertificatesResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ListCertificatesResponse) ProtoMessage() {}
func (x *ListCertificatesResponse) ProtoReflect() protoreflect.Message {
mi := &file_api_pb_caddy_storage_proto_msgTypes[8]
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 ListCertificatesResponse.ProtoReflect.Descriptor instead.
func (*ListCertificatesResponse) Descriptor() ([]byte, []int) {
return file_api_pb_caddy_storage_proto_rawDescGZIP(), []int{8}
}
func (x *ListCertificatesResponse) GetCertificates() []*IssuedCertificate {
if x != nil {
return x.Certificates
}
return nil
}
type IssuedCertificate struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
// Subject Alternative Name (SAN) of the certificate. Caddy doesn't issue certificates with multiple SANs.
San string `protobuf:"bytes,1,opt,name=san,proto3" json:"san,omitempty"`
// The PEM-encoding of DER-encoded ASN.1 data for the cert or chain, leaf first.
Chain []byte `protobuf:"bytes,2,opt,name=chain,proto3" json:"chain,omitempty"`
// Any extra information associated with the certificate, usually provided by the issuer implementation.
// JSON-encoded, may be absent or null.
IssuerData []byte `protobuf:"bytes,3,opt,name=issuer_data,json=issuerData,proto3" json:"issuer_data,omitempty"`
}
func (x *IssuedCertificate) Reset() {
*x = IssuedCertificate{}
if protoimpl.UnsafeEnabled {
mi := &file_api_pb_caddy_storage_proto_msgTypes[9]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *IssuedCertificate) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*IssuedCertificate) ProtoMessage() {}
func (x *IssuedCertificate) ProtoReflect() protoreflect.Message {
mi := &file_api_pb_caddy_storage_proto_msgTypes[9]
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 IssuedCertificate.ProtoReflect.Descriptor instead.
func (*IssuedCertificate) Descriptor() ([]byte, []int) {
return file_api_pb_caddy_storage_proto_rawDescGZIP(), []int{9}
}
func (x *IssuedCertificate) GetSan() string {
if x != nil {
return x.San
}
return ""
}
func (x *IssuedCertificate) GetChain() []byte {
if x != nil {
return x.Chain
}
return nil
}
func (x *IssuedCertificate) GetIssuerData() []byte {
if x != nil {
return x.IssuerData
}
return nil
}
var File_api_pb_caddy_storage_proto protoreflect.FileDescriptor
var file_api_pb_caddy_storage_proto_rawDesc = []byte{
0x0a, 0x1a, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x62, 0x2f, 0x63, 0x61, 0x64, 0x64, 0x79, 0x5f, 0x73,
0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x03, 0x61, 0x70,
0x69, 0x1a, 0x1b, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62,
0x75, 0x66, 0x2f, 0x65, 0x6d, 0x70, 0x74, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1f,
0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f,
0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22,
0x42, 0x0a, 0x18, 0x53, 0x74, 0x6f, 0x72, 0x65, 0x43, 0x61, 0x64, 0x64, 0x79, 0x53, 0x74, 0x6f,
0x72, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x6b,
0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a,
0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x76, 0x61,
0x6c, 0x75, 0x65, 0x22, 0x2b, 0x0a, 0x17, 0x4c, 0x6f, 0x61, 0x64, 0x43, 0x61, 0x64, 0x64, 0x79,
0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x10,
0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79,
0x22, 0x6b, 0x0a, 0x18, 0x4c, 0x6f, 0x61, 0x64, 0x43, 0x61, 0x64, 0x64, 0x79, 0x53, 0x74, 0x6f,
0x72, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x14, 0x0a, 0x05,
0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x76, 0x61, 0x6c,
0x75, 0x65, 0x12, 0x39, 0x0a, 0x0a, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74,
0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e,
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61,
0x6d, 0x70, 0x52, 0x09, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x22, 0x2d, 0x0a,
0x19, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x43, 0x61, 0x64, 0x64, 0x79, 0x53, 0x74, 0x6f, 0x72,
0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65,
0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x22, 0x4f, 0x0a, 0x17,
0x4c, 0x69, 0x73, 0x74, 0x43, 0x61, 0x64, 0x64, 0x79, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65,
0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x72, 0x65, 0x66, 0x69,
0x78, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x12,
0x1c, 0x0a, 0x09, 0x72, 0x65, 0x63, 0x75, 0x72, 0x73, 0x69, 0x76, 0x65, 0x18, 0x02, 0x20, 0x01,
0x28, 0x08, 0x52, 0x09, 0x72, 0x65, 0x63, 0x75, 0x72, 0x73, 0x69, 0x76, 0x65, 0x22, 0x2e, 0x0a,
0x18, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x61, 0x64, 0x64, 0x79, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67,
0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6b, 0x65, 0x79,
0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x04, 0x6b, 0x65, 0x79, 0x73, 0x22, 0x2b, 0x0a,
0x17, 0x53, 0x74, 0x61, 0x74, 0x43, 0x61, 0x64, 0x64, 0x79, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67,
0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18,
0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x22, 0x9c, 0x01, 0x0a, 0x18, 0x53,
0x74, 0x61, 0x74, 0x43, 0x61, 0x64, 0x64, 0x79, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x52,
0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01,
0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x39, 0x0a, 0x0a, 0x75, 0x70, 0x64,
0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e,
0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e,
0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x75, 0x70, 0x64, 0x61, 0x74,
0x65, 0x64, 0x41, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x03, 0x20, 0x01,
0x28, 0x03, 0x52, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x69, 0x73, 0x5f, 0x74,
0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x69,
0x73, 0x54, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x6c, 0x22, 0x56, 0x0a, 0x18, 0x4c, 0x69, 0x73,
0x74, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x73, 0x52, 0x65, 0x73,
0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3a, 0x0a, 0x0c, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69,
0x63, 0x61, 0x74, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x61, 0x70,
0x69, 0x2e, 0x49, 0x73, 0x73, 0x75, 0x65, 0x64, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63,
0x61, 0x74, 0x65, 0x52, 0x0c, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65,
0x73, 0x22, 0x5c, 0x0a, 0x11, 0x49, 0x73, 0x73, 0x75, 0x65, 0x64, 0x43, 0x65, 0x72, 0x74, 0x69,
0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x73, 0x61, 0x6e, 0x18, 0x01, 0x20,
0x01, 0x28, 0x09, 0x52, 0x03, 0x73, 0x61, 0x6e, 0x12, 0x14, 0x0a, 0x05, 0x63, 0x68, 0x61, 0x69,
0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x12, 0x1f,
0x0a, 0x0b, 0x69, 0x73, 0x73, 0x75, 0x65, 0x72, 0x5f, 0x64, 0x61, 0x74, 0x61, 0x18, 0x03, 0x20,
0x01, 0x28, 0x0c, 0x52, 0x0a, 0x69, 0x73, 0x73, 0x75, 0x65, 0x72, 0x44, 0x61, 0x74, 0x61, 0x32,
0xaa, 0x03, 0x0a, 0x0c, 0x43, 0x61, 0x64, 0x64, 0x79, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65,
0x12, 0x3e, 0x0a, 0x05, 0x53, 0x74, 0x6f, 0x72, 0x65, 0x12, 0x1d, 0x2e, 0x61, 0x70, 0x69, 0x2e,
0x53, 0x74, 0x6f, 0x72, 0x65, 0x43, 0x61, 0x64, 0x64, 0x79, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67,
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, 0x43, 0x0a, 0x04, 0x4c, 0x6f, 0x61, 0x64, 0x12, 0x1c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c,
0x6f, 0x61, 0x64, 0x43, 0x61, 0x64, 0x64, 0x79, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x52,
0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x6f, 0x61,
0x64, 0x43, 0x61, 0x64, 0x64, 0x79, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73,
0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x40, 0x0a, 0x06, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x12,
0x1e, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x43, 0x61, 0x64, 0x64,
0x79, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 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, 0x43, 0x0a, 0x04, 0x4c, 0x69, 0x73, 0x74, 0x12,
0x1c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x61, 0x64, 0x64, 0x79, 0x53,
0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e,
0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x61, 0x64, 0x64, 0x79, 0x53, 0x74, 0x6f,
0x72, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x43, 0x0a, 0x04,
0x53, 0x74, 0x61, 0x74, 0x12, 0x1c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x43,
0x61, 0x64, 0x64, 0x79, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65,
0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x43, 0x61, 0x64,
0x64, 0x79, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
0x65, 0x12, 0x49, 0x0a, 0x10, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69,
0x63, 0x61, 0x74, 0x65, 0x73, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70,
0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1d, 0x2e,
0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63,
0x61, 0x74, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x26, 0x5a, 0x24,
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, 0x61, 0x70,
0x69, 0x2f, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
file_api_pb_caddy_storage_proto_rawDescOnce sync.Once
file_api_pb_caddy_storage_proto_rawDescData = file_api_pb_caddy_storage_proto_rawDesc
)
func file_api_pb_caddy_storage_proto_rawDescGZIP() []byte {
file_api_pb_caddy_storage_proto_rawDescOnce.Do(func() {
file_api_pb_caddy_storage_proto_rawDescData = protoimpl.X.CompressGZIP(file_api_pb_caddy_storage_proto_rawDescData)
})
return file_api_pb_caddy_storage_proto_rawDescData
}
var file_api_pb_caddy_storage_proto_msgTypes = make([]protoimpl.MessageInfo, 10)
var file_api_pb_caddy_storage_proto_goTypes = []any{
(*StoreCaddyStorageRequest)(nil), // 0: api.StoreCaddyStorageRequest
(*LoadCaddyStorageRequest)(nil), // 1: api.LoadCaddyStorageRequest
(*LoadCaddyStorageResponse)(nil), // 2: api.LoadCaddyStorageResponse
(*DeleteCaddyStorageRequest)(nil), // 3: api.DeleteCaddyStorageRequest
(*ListCaddyStorageRequest)(nil), // 4: api.ListCaddyStorageRequest
(*ListCaddyStorageResponse)(nil), // 5: api.ListCaddyStorageResponse
(*StatCaddyStorageRequest)(nil), // 6: api.StatCaddyStorageRequest
(*StatCaddyStorageResponse)(nil), // 7: api.StatCaddyStorageResponse
(*ListCertificatesResponse)(nil), // 8: api.ListCertificatesResponse
(*IssuedCertificate)(nil), // 9: api.IssuedCertificate
(*timestamppb.Timestamp)(nil), // 10: google.protobuf.Timestamp
(*emptypb.Empty)(nil), // 11: google.protobuf.Empty
}
var file_api_pb_caddy_storage_proto_depIdxs = []int32{
10, // 0: api.LoadCaddyStorageResponse.updated_at:type_name -> google.protobuf.Timestamp
10, // 1: api.StatCaddyStorageResponse.updated_at:type_name -> google.protobuf.Timestamp
9, // 2: api.ListCertificatesResponse.certificates:type_name -> api.IssuedCertificate
0, // 3: api.CaddyStorage.Store:input_type -> api.StoreCaddyStorageRequest
1, // 4: api.CaddyStorage.Load:input_type -> api.LoadCaddyStorageRequest
3, // 5: api.CaddyStorage.Delete:input_type -> api.DeleteCaddyStorageRequest
4, // 6: api.CaddyStorage.List:input_type -> api.ListCaddyStorageRequest
6, // 7: api.CaddyStorage.Stat:input_type -> api.StatCaddyStorageRequest
11, // 8: api.CaddyStorage.ListCertificates:input_type -> google.protobuf.Empty
11, // 9: api.CaddyStorage.Store:output_type -> google.protobuf.Empty
2, // 10: api.CaddyStorage.Load:output_type -> api.LoadCaddyStorageResponse
11, // 11: api.CaddyStorage.Delete:output_type -> google.protobuf.Empty
5, // 12: api.CaddyStorage.List:output_type -> api.ListCaddyStorageResponse
7, // 13: api.CaddyStorage.Stat:output_type -> api.StatCaddyStorageResponse
8, // 14: api.CaddyStorage.ListCertificates:output_type -> api.ListCertificatesResponse
9, // [9:15] is the sub-list for method output_type
3, // [3:9] is the sub-list for method input_type
3, // [3:3] is the sub-list for extension type_name
3, // [3:3] is the sub-list for extension extendee
0, // [0:3] is the sub-list for field type_name
}
func init() { file_api_pb_caddy_storage_proto_init() }
func file_api_pb_caddy_storage_proto_init() {
if File_api_pb_caddy_storage_proto != nil {
return
}
if !protoimpl.UnsafeEnabled {
file_api_pb_caddy_storage_proto_msgTypes[0].Exporter = func(v any, i int) any {
switch v := v.(*StoreCaddyStorageRequest); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_api_pb_caddy_storage_proto_msgTypes[1].Exporter = func(v any, i int) any {
switch v := v.(*LoadCaddyStorageRequest); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_api_pb_caddy_storage_proto_msgTypes[2].Exporter = func(v any, i int) any {
switch v := v.(*LoadCaddyStorageResponse); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_api_pb_caddy_storage_proto_msgTypes[3].Exporter = func(v any, i int) any {
switch v := v.(*DeleteCaddyStorageRequest); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_api_pb_caddy_storage_proto_msgTypes[4].Exporter = func(v any, i int) any {
switch v := v.(*ListCaddyStorageRequest); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_api_pb_caddy_storage_proto_msgTypes[5].Exporter = func(v any, i int) any {
switch v := v.(*ListCaddyStorageResponse); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_api_pb_caddy_storage_proto_msgTypes[6].Exporter = func(v any, i int) any {
switch v := v.(*StatCaddyStorageRequest); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_api_pb_caddy_storage_proto_msgTypes[7].Exporter = func(v any, i int) any {
switch v := v.(*StatCaddyStorageResponse); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_api_pb_caddy_storage_proto_msgTypes[8].Exporter = func(v any, i int) any {
switch v := v.(*ListCertificatesResponse); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_api_pb_caddy_storage_proto_msgTypes[9].Exporter = func(v any, i int) any {
switch v := v.(*IssuedCertificate); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_api_pb_caddy_storage_proto_rawDesc,
NumEnums: 0,
NumMessages: 10,
NumExtensions: 0,
NumServices: 1,
},
GoTypes: file_api_pb_caddy_storage_proto_goTypes,
DependencyIndexes: file_api_pb_caddy_storage_proto_depIdxs,
MessageInfos: file_api_pb_caddy_storage_proto_msgTypes,
}.Build()
File_api_pb_caddy_storage_proto = out.File
file_api_pb_caddy_storage_proto_rawDesc = nil
file_api_pb_caddy_storage_proto_goTypes = nil
file_api_pb_caddy_storage_proto_depIdxs = nil
}
+77
View File
@@ -0,0 +1,77 @@
syntax = "proto3";
package api;
option go_package = "github.com/psviderski/uncloud/api/pb";
import "google/protobuf/empty.proto";
import "google/protobuf/timestamp.proto";
// CaddyStorage exposes the CertMagic storage operations backed by the distributed cluster store.
// Each request operates on the receiving machine's store replica. Callers that need replication to catch up before
// reading should capture store versions with Machine.InspectMachine and call Machine.WaitForStoreVersion on the
// receiving machine, subject to that RPC's data-availability limitations (see WaitForStoreVersion docs).
// See Storage interface in https://github.com/caddyserver/certmagic/blob/master/storage.go.
service CaddyStorage {
rpc Store(StoreCaddyStorageRequest) returns (google.protobuf.Empty);
rpc Load(LoadCaddyStorageRequest) returns (LoadCaddyStorageResponse);
rpc Delete(DeleteCaddyStorageRequest) returns (google.protobuf.Empty);
rpc List(ListCaddyStorageRequest) returns (ListCaddyStorageResponse);
rpc Stat(StatCaddyStorageRequest) returns (StatCaddyStorageResponse);
// ListCertificates lists certificates issued by Caddy and stored in the cluster store.
// Unreadable entries and invalid resource metadata are skipped. Chains and issuer data are returned as stored.
rpc ListCertificates(google.protobuf.Empty) returns (ListCertificatesResponse);
}
message StoreCaddyStorageRequest {
string key = 1;
bytes value = 2;
}
message LoadCaddyStorageRequest {
string key = 1;
}
message LoadCaddyStorageResponse {
bytes value = 1;
google.protobuf.Timestamp updated_at = 2;
}
message DeleteCaddyStorageRequest {
string key = 1;
}
message ListCaddyStorageRequest {
string prefix = 1;
bool recursive = 2;
}
message ListCaddyStorageResponse {
repeated string keys = 1;
}
message StatCaddyStorageRequest {
string key = 1;
}
message StatCaddyStorageResponse {
string key = 1;
google.protobuf.Timestamp updated_at = 2;
int64 size = 3;
bool is_terminal = 4;
}
message ListCertificatesResponse {
repeated IssuedCertificate certificates = 1;
}
message IssuedCertificate {
// Subject Alternative Name (SAN) of the certificate. Caddy doesn't issue certificates with multiple SANs.
string san = 1;
// The PEM-encoding of DER-encoded ASN.1 data for the cert or chain, leaf first.
bytes chain = 2;
// Any extra information associated with the certificate, usually provided by the issuer implementation.
// JSON-encoded, may be absent or null.
bytes issuer_data = 3;
}
+328
View File
@@ -0,0 +1,328 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.5.1
// - protoc v5.27.3
// source: api/pb/caddy_storage.proto
package pb
import (
context "context"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
emptypb "google.golang.org/protobuf/types/known/emptypb"
)
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
// Requires gRPC-Go v1.64.0 or later.
const _ = grpc.SupportPackageIsVersion9
const (
CaddyStorage_Store_FullMethodName = "/api.CaddyStorage/Store"
CaddyStorage_Load_FullMethodName = "/api.CaddyStorage/Load"
CaddyStorage_Delete_FullMethodName = "/api.CaddyStorage/Delete"
CaddyStorage_List_FullMethodName = "/api.CaddyStorage/List"
CaddyStorage_Stat_FullMethodName = "/api.CaddyStorage/Stat"
CaddyStorage_ListCertificates_FullMethodName = "/api.CaddyStorage/ListCertificates"
)
// CaddyStorageClient is the client API for CaddyStorage service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
//
// CaddyStorage exposes the CertMagic storage operations backed by the distributed cluster store.
// Each request operates on the receiving machine's store replica. Callers that need replication to catch up before
// reading should capture store versions with Machine.InspectMachine and call Machine.WaitForStoreVersion on the
// receiving machine, subject to that RPC's data-availability limitations (see WaitForStoreVersion docs).
// See Storage interface in https://github.com/caddyserver/certmagic/blob/master/storage.go.
type CaddyStorageClient interface {
Store(ctx context.Context, in *StoreCaddyStorageRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
Load(ctx context.Context, in *LoadCaddyStorageRequest, opts ...grpc.CallOption) (*LoadCaddyStorageResponse, error)
Delete(ctx context.Context, in *DeleteCaddyStorageRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
List(ctx context.Context, in *ListCaddyStorageRequest, opts ...grpc.CallOption) (*ListCaddyStorageResponse, error)
Stat(ctx context.Context, in *StatCaddyStorageRequest, opts ...grpc.CallOption) (*StatCaddyStorageResponse, error)
// ListCertificates lists certificates issued by Caddy and stored in the cluster store.
// Unreadable entries and invalid resource metadata are skipped. Chains and issuer data are returned as stored.
ListCertificates(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*ListCertificatesResponse, error)
}
type caddyStorageClient struct {
cc grpc.ClientConnInterface
}
func NewCaddyStorageClient(cc grpc.ClientConnInterface) CaddyStorageClient {
return &caddyStorageClient{cc}
}
func (c *caddyStorageClient) Store(ctx context.Context, in *StoreCaddyStorageRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(emptypb.Empty)
err := c.cc.Invoke(ctx, CaddyStorage_Store_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *caddyStorageClient) Load(ctx context.Context, in *LoadCaddyStorageRequest, opts ...grpc.CallOption) (*LoadCaddyStorageResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(LoadCaddyStorageResponse)
err := c.cc.Invoke(ctx, CaddyStorage_Load_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *caddyStorageClient) Delete(ctx context.Context, in *DeleteCaddyStorageRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(emptypb.Empty)
err := c.cc.Invoke(ctx, CaddyStorage_Delete_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *caddyStorageClient) List(ctx context.Context, in *ListCaddyStorageRequest, opts ...grpc.CallOption) (*ListCaddyStorageResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ListCaddyStorageResponse)
err := c.cc.Invoke(ctx, CaddyStorage_List_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *caddyStorageClient) Stat(ctx context.Context, in *StatCaddyStorageRequest, opts ...grpc.CallOption) (*StatCaddyStorageResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(StatCaddyStorageResponse)
err := c.cc.Invoke(ctx, CaddyStorage_Stat_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *caddyStorageClient) ListCertificates(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*ListCertificatesResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ListCertificatesResponse)
err := c.cc.Invoke(ctx, CaddyStorage_ListCertificates_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
// CaddyStorageServer is the server API for CaddyStorage service.
// All implementations must embed UnimplementedCaddyStorageServer
// for forward compatibility.
//
// CaddyStorage exposes the CertMagic storage operations backed by the distributed cluster store.
// Each request operates on the receiving machine's store replica. Callers that need replication to catch up before
// reading should capture store versions with Machine.InspectMachine and call Machine.WaitForStoreVersion on the
// receiving machine, subject to that RPC's data-availability limitations (see WaitForStoreVersion docs).
// See Storage interface in https://github.com/caddyserver/certmagic/blob/master/storage.go.
type CaddyStorageServer interface {
Store(context.Context, *StoreCaddyStorageRequest) (*emptypb.Empty, error)
Load(context.Context, *LoadCaddyStorageRequest) (*LoadCaddyStorageResponse, error)
Delete(context.Context, *DeleteCaddyStorageRequest) (*emptypb.Empty, error)
List(context.Context, *ListCaddyStorageRequest) (*ListCaddyStorageResponse, error)
Stat(context.Context, *StatCaddyStorageRequest) (*StatCaddyStorageResponse, error)
// ListCertificates lists certificates issued by Caddy and stored in the cluster store.
// Unreadable entries and invalid resource metadata are skipped. Chains and issuer data are returned as stored.
ListCertificates(context.Context, *emptypb.Empty) (*ListCertificatesResponse, error)
mustEmbedUnimplementedCaddyStorageServer()
}
// UnimplementedCaddyStorageServer must be embedded to have
// forward compatible implementations.
//
// NOTE: this should be embedded by value instead of pointer to avoid a nil
// pointer dereference when methods are called.
type UnimplementedCaddyStorageServer struct{}
func (UnimplementedCaddyStorageServer) Store(context.Context, *StoreCaddyStorageRequest) (*emptypb.Empty, error) {
return nil, status.Errorf(codes.Unimplemented, "method Store not implemented")
}
func (UnimplementedCaddyStorageServer) Load(context.Context, *LoadCaddyStorageRequest) (*LoadCaddyStorageResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method Load not implemented")
}
func (UnimplementedCaddyStorageServer) Delete(context.Context, *DeleteCaddyStorageRequest) (*emptypb.Empty, error) {
return nil, status.Errorf(codes.Unimplemented, "method Delete not implemented")
}
func (UnimplementedCaddyStorageServer) List(context.Context, *ListCaddyStorageRequest) (*ListCaddyStorageResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method List not implemented")
}
func (UnimplementedCaddyStorageServer) Stat(context.Context, *StatCaddyStorageRequest) (*StatCaddyStorageResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method Stat not implemented")
}
func (UnimplementedCaddyStorageServer) ListCertificates(context.Context, *emptypb.Empty) (*ListCertificatesResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ListCertificates not implemented")
}
func (UnimplementedCaddyStorageServer) mustEmbedUnimplementedCaddyStorageServer() {}
func (UnimplementedCaddyStorageServer) testEmbeddedByValue() {}
// UnsafeCaddyStorageServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to CaddyStorageServer will
// result in compilation errors.
type UnsafeCaddyStorageServer interface {
mustEmbedUnimplementedCaddyStorageServer()
}
func RegisterCaddyStorageServer(s grpc.ServiceRegistrar, srv CaddyStorageServer) {
// If the following call pancis, it indicates UnimplementedCaddyStorageServer was
// embedded by pointer and is nil. This will cause panics if an
// unimplemented method is ever invoked, so we test this at initialization
// time to prevent it from happening at runtime later due to I/O.
if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
t.testEmbeddedByValue()
}
s.RegisterService(&CaddyStorage_ServiceDesc, srv)
}
func _CaddyStorage_Store_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(StoreCaddyStorageRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(CaddyStorageServer).Store(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: CaddyStorage_Store_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(CaddyStorageServer).Store(ctx, req.(*StoreCaddyStorageRequest))
}
return interceptor(ctx, in, info, handler)
}
func _CaddyStorage_Load_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(LoadCaddyStorageRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(CaddyStorageServer).Load(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: CaddyStorage_Load_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(CaddyStorageServer).Load(ctx, req.(*LoadCaddyStorageRequest))
}
return interceptor(ctx, in, info, handler)
}
func _CaddyStorage_Delete_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(DeleteCaddyStorageRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(CaddyStorageServer).Delete(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: CaddyStorage_Delete_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(CaddyStorageServer).Delete(ctx, req.(*DeleteCaddyStorageRequest))
}
return interceptor(ctx, in, info, handler)
}
func _CaddyStorage_List_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ListCaddyStorageRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(CaddyStorageServer).List(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: CaddyStorage_List_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(CaddyStorageServer).List(ctx, req.(*ListCaddyStorageRequest))
}
return interceptor(ctx, in, info, handler)
}
func _CaddyStorage_Stat_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(StatCaddyStorageRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(CaddyStorageServer).Stat(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: CaddyStorage_Stat_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(CaddyStorageServer).Stat(ctx, req.(*StatCaddyStorageRequest))
}
return interceptor(ctx, in, info, handler)
}
func _CaddyStorage_ListCertificates_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(emptypb.Empty)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(CaddyStorageServer).ListCertificates(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: CaddyStorage_ListCertificates_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(CaddyStorageServer).ListCertificates(ctx, req.(*emptypb.Empty))
}
return interceptor(ctx, in, info, handler)
}
// CaddyStorage_ServiceDesc is the grpc.ServiceDesc for CaddyStorage service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
var CaddyStorage_ServiceDesc = grpc.ServiceDesc{
ServiceName: "api.CaddyStorage",
HandlerType: (*CaddyStorageServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "Store",
Handler: _CaddyStorage_Store_Handler,
},
{
MethodName: "Load",
Handler: _CaddyStorage_Load_Handler,
},
{
MethodName: "Delete",
Handler: _CaddyStorage_Delete_Handler,
},
{
MethodName: "List",
Handler: _CaddyStorage_List_Handler,
},
{
MethodName: "Stat",
Handler: _CaddyStorage_Stat_Handler,
},
{
MethodName: "ListCertificates",
Handler: _CaddyStorage_ListCertificates_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "api/pb/caddy_storage.proto",
}
File renamed without changes.
@@ -2,7 +2,7 @@
// versions: // versions:
// protoc-gen-go v1.34.2 // protoc-gen-go v1.34.2
// protoc v5.27.3 // protoc v5.27.3
// source: internal/machine/api/pb/cluster.proto // source: api/pb/cluster.proto
package pb package pb
@@ -62,11 +62,11 @@ func (x MachineMember_MembershipState) String() string {
} }
func (MachineMember_MembershipState) Descriptor() protoreflect.EnumDescriptor { func (MachineMember_MembershipState) Descriptor() protoreflect.EnumDescriptor {
return file_internal_machine_api_pb_cluster_proto_enumTypes[0].Descriptor() return file_api_pb_cluster_proto_enumTypes[0].Descriptor()
} }
func (MachineMember_MembershipState) Type() protoreflect.EnumType { func (MachineMember_MembershipState) Type() protoreflect.EnumType {
return &file_internal_machine_api_pb_cluster_proto_enumTypes[0] return &file_api_pb_cluster_proto_enumTypes[0]
} }
func (x MachineMember_MembershipState) Number() protoreflect.EnumNumber { func (x MachineMember_MembershipState) Number() protoreflect.EnumNumber {
@@ -75,7 +75,7 @@ func (x MachineMember_MembershipState) Number() protoreflect.EnumNumber {
// Deprecated: Use MachineMember_MembershipState.Descriptor instead. // Deprecated: Use MachineMember_MembershipState.Descriptor instead.
func (MachineMember_MembershipState) EnumDescriptor() ([]byte, []int) { func (MachineMember_MembershipState) EnumDescriptor() ([]byte, []int) {
return file_internal_machine_api_pb_cluster_proto_rawDescGZIP(), []int{2, 0} return file_api_pb_cluster_proto_rawDescGZIP(), []int{2, 0}
} }
type DNSRecord_RecordType int32 type DNSRecord_RecordType int32
@@ -111,11 +111,11 @@ func (x DNSRecord_RecordType) String() string {
} }
func (DNSRecord_RecordType) Descriptor() protoreflect.EnumDescriptor { func (DNSRecord_RecordType) Descriptor() protoreflect.EnumDescriptor {
return file_internal_machine_api_pb_cluster_proto_enumTypes[1].Descriptor() return file_api_pb_cluster_proto_enumTypes[1].Descriptor()
} }
func (DNSRecord_RecordType) Type() protoreflect.EnumType { func (DNSRecord_RecordType) Type() protoreflect.EnumType {
return &file_internal_machine_api_pb_cluster_proto_enumTypes[1] return &file_api_pb_cluster_proto_enumTypes[1]
} }
func (x DNSRecord_RecordType) Number() protoreflect.EnumNumber { func (x DNSRecord_RecordType) Number() protoreflect.EnumNumber {
@@ -124,7 +124,7 @@ func (x DNSRecord_RecordType) Number() protoreflect.EnumNumber {
// Deprecated: Use DNSRecord_RecordType.Descriptor instead. // Deprecated: Use DNSRecord_RecordType.Descriptor instead.
func (DNSRecord_RecordType) EnumDescriptor() ([]byte, []int) { func (DNSRecord_RecordType) EnumDescriptor() ([]byte, []int) {
return file_internal_machine_api_pb_cluster_proto_rawDescGZIP(), []int{9, 0} return file_api_pb_cluster_proto_rawDescGZIP(), []int{10, 0}
} }
type AddMachineRequest struct { type AddMachineRequest struct {
@@ -140,7 +140,7 @@ type AddMachineRequest struct {
func (x *AddMachineRequest) Reset() { func (x *AddMachineRequest) Reset() {
*x = AddMachineRequest{} *x = AddMachineRequest{}
if protoimpl.UnsafeEnabled { if protoimpl.UnsafeEnabled {
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[0] mi := &file_api_pb_cluster_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -153,7 +153,7 @@ func (x *AddMachineRequest) String() string {
func (*AddMachineRequest) ProtoMessage() {} func (*AddMachineRequest) ProtoMessage() {}
func (x *AddMachineRequest) ProtoReflect() protoreflect.Message { func (x *AddMachineRequest) ProtoReflect() protoreflect.Message {
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[0] mi := &file_api_pb_cluster_proto_msgTypes[0]
if protoimpl.UnsafeEnabled && x != nil { if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -166,7 +166,7 @@ func (x *AddMachineRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use AddMachineRequest.ProtoReflect.Descriptor instead. // Deprecated: Use AddMachineRequest.ProtoReflect.Descriptor instead.
func (*AddMachineRequest) Descriptor() ([]byte, []int) { func (*AddMachineRequest) Descriptor() ([]byte, []int) {
return file_internal_machine_api_pb_cluster_proto_rawDescGZIP(), []int{0} return file_api_pb_cluster_proto_rawDescGZIP(), []int{0}
} }
func (x *AddMachineRequest) GetName() string { func (x *AddMachineRequest) GetName() string {
@@ -201,7 +201,7 @@ type AddMachineResponse struct {
func (x *AddMachineResponse) Reset() { func (x *AddMachineResponse) Reset() {
*x = AddMachineResponse{} *x = AddMachineResponse{}
if protoimpl.UnsafeEnabled { if protoimpl.UnsafeEnabled {
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[1] mi := &file_api_pb_cluster_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -214,7 +214,7 @@ func (x *AddMachineResponse) String() string {
func (*AddMachineResponse) ProtoMessage() {} func (*AddMachineResponse) ProtoMessage() {}
func (x *AddMachineResponse) ProtoReflect() protoreflect.Message { func (x *AddMachineResponse) ProtoReflect() protoreflect.Message {
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[1] mi := &file_api_pb_cluster_proto_msgTypes[1]
if protoimpl.UnsafeEnabled && x != nil { if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -227,7 +227,7 @@ func (x *AddMachineResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use AddMachineResponse.ProtoReflect.Descriptor instead. // Deprecated: Use AddMachineResponse.ProtoReflect.Descriptor instead.
func (*AddMachineResponse) Descriptor() ([]byte, []int) { func (*AddMachineResponse) Descriptor() ([]byte, []int) {
return file_internal_machine_api_pb_cluster_proto_rawDescGZIP(), []int{1} return file_api_pb_cluster_proto_rawDescGZIP(), []int{1}
} }
func (x *AddMachineResponse) GetMachine() *MachineInfo { func (x *AddMachineResponse) GetMachine() *MachineInfo {
@@ -249,7 +249,7 @@ type MachineMember struct {
func (x *MachineMember) Reset() { func (x *MachineMember) Reset() {
*x = MachineMember{} *x = MachineMember{}
if protoimpl.UnsafeEnabled { if protoimpl.UnsafeEnabled {
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[2] mi := &file_api_pb_cluster_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -262,7 +262,7 @@ func (x *MachineMember) String() string {
func (*MachineMember) ProtoMessage() {} func (*MachineMember) ProtoMessage() {}
func (x *MachineMember) ProtoReflect() protoreflect.Message { func (x *MachineMember) ProtoReflect() protoreflect.Message {
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[2] mi := &file_api_pb_cluster_proto_msgTypes[2]
if protoimpl.UnsafeEnabled && x != nil { if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -275,7 +275,7 @@ func (x *MachineMember) ProtoReflect() protoreflect.Message {
// Deprecated: Use MachineMember.ProtoReflect.Descriptor instead. // Deprecated: Use MachineMember.ProtoReflect.Descriptor instead.
func (*MachineMember) Descriptor() ([]byte, []int) { func (*MachineMember) Descriptor() ([]byte, []int) {
return file_internal_machine_api_pb_cluster_proto_rawDescGZIP(), []int{2} return file_api_pb_cluster_proto_rawDescGZIP(), []int{2}
} }
func (x *MachineMember) GetMachine() *MachineInfo { func (x *MachineMember) GetMachine() *MachineInfo {
@@ -303,7 +303,7 @@ type ListMachinesResponse struct {
func (x *ListMachinesResponse) Reset() { func (x *ListMachinesResponse) Reset() {
*x = ListMachinesResponse{} *x = ListMachinesResponse{}
if protoimpl.UnsafeEnabled { if protoimpl.UnsafeEnabled {
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[3] mi := &file_api_pb_cluster_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -316,7 +316,7 @@ func (x *ListMachinesResponse) String() string {
func (*ListMachinesResponse) ProtoMessage() {} func (*ListMachinesResponse) ProtoMessage() {}
func (x *ListMachinesResponse) ProtoReflect() protoreflect.Message { func (x *ListMachinesResponse) ProtoReflect() protoreflect.Message {
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[3] mi := &file_api_pb_cluster_proto_msgTypes[3]
if protoimpl.UnsafeEnabled && x != nil { if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -329,7 +329,7 @@ func (x *ListMachinesResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use ListMachinesResponse.ProtoReflect.Descriptor instead. // Deprecated: Use ListMachinesResponse.ProtoReflect.Descriptor instead.
func (*ListMachinesResponse) Descriptor() ([]byte, []int) { func (*ListMachinesResponse) Descriptor() ([]byte, []int) {
return file_internal_machine_api_pb_cluster_proto_rawDescGZIP(), []int{3} return file_api_pb_cluster_proto_rawDescGZIP(), []int{3}
} }
func (x *ListMachinesResponse) GetMachines() []*MachineMember { func (x *ListMachinesResponse) GetMachines() []*MachineMember {
@@ -350,7 +350,7 @@ type RemoveMachineRequest struct {
func (x *RemoveMachineRequest) Reset() { func (x *RemoveMachineRequest) Reset() {
*x = RemoveMachineRequest{} *x = RemoveMachineRequest{}
if protoimpl.UnsafeEnabled { if protoimpl.UnsafeEnabled {
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[4] mi := &file_api_pb_cluster_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -363,7 +363,7 @@ func (x *RemoveMachineRequest) String() string {
func (*RemoveMachineRequest) ProtoMessage() {} func (*RemoveMachineRequest) ProtoMessage() {}
func (x *RemoveMachineRequest) ProtoReflect() protoreflect.Message { func (x *RemoveMachineRequest) ProtoReflect() protoreflect.Message {
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[4] mi := &file_api_pb_cluster_proto_msgTypes[4]
if protoimpl.UnsafeEnabled && x != nil { if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -376,7 +376,7 @@ func (x *RemoveMachineRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use RemoveMachineRequest.ProtoReflect.Descriptor instead. // Deprecated: Use RemoveMachineRequest.ProtoReflect.Descriptor instead.
func (*RemoveMachineRequest) Descriptor() ([]byte, []int) { func (*RemoveMachineRequest) Descriptor() ([]byte, []int) {
return file_internal_machine_api_pb_cluster_proto_rawDescGZIP(), []int{4} return file_api_pb_cluster_proto_rawDescGZIP(), []int{4}
} }
func (x *RemoveMachineRequest) GetId() string { func (x *RemoveMachineRequest) GetId() string {
@@ -392,12 +392,15 @@ type Domain struct {
unknownFields protoimpl.UnknownFields unknownFields protoimpl.UnknownFields
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
// Whether the domain is reserved in Uncloud DNS. Older daemons (<0.21) omit this field
// and only support reserved domains, so clients should treat absence as true.
Reserved *bool `protobuf:"varint,2,opt,name=reserved,proto3,oneof" json:"reserved,omitempty"`
} }
func (x *Domain) Reset() { func (x *Domain) Reset() {
*x = Domain{} *x = Domain{}
if protoimpl.UnsafeEnabled { if protoimpl.UnsafeEnabled {
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[5] mi := &file_api_pb_cluster_proto_msgTypes[5]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -410,7 +413,7 @@ func (x *Domain) String() string {
func (*Domain) ProtoMessage() {} func (*Domain) ProtoMessage() {}
func (x *Domain) ProtoReflect() protoreflect.Message { func (x *Domain) ProtoReflect() protoreflect.Message {
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[5] mi := &file_api_pb_cluster_proto_msgTypes[5]
if protoimpl.UnsafeEnabled && x != nil { if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -423,7 +426,7 @@ func (x *Domain) ProtoReflect() protoreflect.Message {
// Deprecated: Use Domain.ProtoReflect.Descriptor instead. // Deprecated: Use Domain.ProtoReflect.Descriptor instead.
func (*Domain) Descriptor() ([]byte, []int) { func (*Domain) Descriptor() ([]byte, []int) {
return file_internal_machine_api_pb_cluster_proto_rawDescGZIP(), []int{5} return file_api_pb_cluster_proto_rawDescGZIP(), []int{5}
} }
func (x *Domain) GetName() string { func (x *Domain) GetName() string {
@@ -433,6 +436,13 @@ func (x *Domain) GetName() string {
return "" return ""
} }
func (x *Domain) GetReserved() bool {
if x != nil && x.Reserved != nil {
return *x.Reserved
}
return false
}
type ReserveDomainRequest struct { type ReserveDomainRequest struct {
state protoimpl.MessageState state protoimpl.MessageState
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
@@ -444,7 +454,7 @@ type ReserveDomainRequest struct {
func (x *ReserveDomainRequest) Reset() { func (x *ReserveDomainRequest) Reset() {
*x = ReserveDomainRequest{} *x = ReserveDomainRequest{}
if protoimpl.UnsafeEnabled { if protoimpl.UnsafeEnabled {
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[6] mi := &file_api_pb_cluster_proto_msgTypes[6]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -457,7 +467,7 @@ func (x *ReserveDomainRequest) String() string {
func (*ReserveDomainRequest) ProtoMessage() {} func (*ReserveDomainRequest) ProtoMessage() {}
func (x *ReserveDomainRequest) ProtoReflect() protoreflect.Message { func (x *ReserveDomainRequest) ProtoReflect() protoreflect.Message {
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[6] mi := &file_api_pb_cluster_proto_msgTypes[6]
if protoimpl.UnsafeEnabled && x != nil { if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -470,7 +480,7 @@ func (x *ReserveDomainRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use ReserveDomainRequest.ProtoReflect.Descriptor instead. // Deprecated: Use ReserveDomainRequest.ProtoReflect.Descriptor instead.
func (*ReserveDomainRequest) Descriptor() ([]byte, []int) { func (*ReserveDomainRequest) Descriptor() ([]byte, []int) {
return file_internal_machine_api_pb_cluster_proto_rawDescGZIP(), []int{6} return file_api_pb_cluster_proto_rawDescGZIP(), []int{6}
} }
func (x *ReserveDomainRequest) GetEndpoint() string { func (x *ReserveDomainRequest) GetEndpoint() string {
@@ -480,6 +490,54 @@ func (x *ReserveDomainRequest) GetEndpoint() string {
return "" return ""
} }
type SetDomainRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
// An externally managed domain name. Empty clears a manually set domain.
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
}
func (x *SetDomainRequest) Reset() {
*x = SetDomainRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_api_pb_cluster_proto_msgTypes[7]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *SetDomainRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*SetDomainRequest) ProtoMessage() {}
func (x *SetDomainRequest) ProtoReflect() protoreflect.Message {
mi := &file_api_pb_cluster_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 SetDomainRequest.ProtoReflect.Descriptor instead.
func (*SetDomainRequest) Descriptor() ([]byte, []int) {
return file_api_pb_cluster_proto_rawDescGZIP(), []int{7}
}
func (x *SetDomainRequest) GetName() string {
if x != nil {
return x.Name
}
return ""
}
type CreateDomainRecordsRequest struct { type CreateDomainRecordsRequest struct {
state protoimpl.MessageState state protoimpl.MessageState
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
@@ -491,7 +549,7 @@ type CreateDomainRecordsRequest struct {
func (x *CreateDomainRecordsRequest) Reset() { func (x *CreateDomainRecordsRequest) Reset() {
*x = CreateDomainRecordsRequest{} *x = CreateDomainRecordsRequest{}
if protoimpl.UnsafeEnabled { if protoimpl.UnsafeEnabled {
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[7] mi := &file_api_pb_cluster_proto_msgTypes[8]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -504,7 +562,7 @@ func (x *CreateDomainRecordsRequest) String() string {
func (*CreateDomainRecordsRequest) ProtoMessage() {} func (*CreateDomainRecordsRequest) ProtoMessage() {}
func (x *CreateDomainRecordsRequest) ProtoReflect() protoreflect.Message { func (x *CreateDomainRecordsRequest) ProtoReflect() protoreflect.Message {
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[7] mi := &file_api_pb_cluster_proto_msgTypes[8]
if protoimpl.UnsafeEnabled && x != nil { if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -517,7 +575,7 @@ func (x *CreateDomainRecordsRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use CreateDomainRecordsRequest.ProtoReflect.Descriptor instead. // Deprecated: Use CreateDomainRecordsRequest.ProtoReflect.Descriptor instead.
func (*CreateDomainRecordsRequest) Descriptor() ([]byte, []int) { func (*CreateDomainRecordsRequest) Descriptor() ([]byte, []int) {
return file_internal_machine_api_pb_cluster_proto_rawDescGZIP(), []int{7} return file_api_pb_cluster_proto_rawDescGZIP(), []int{8}
} }
func (x *CreateDomainRecordsRequest) GetRecords() []*DNSRecord { func (x *CreateDomainRecordsRequest) GetRecords() []*DNSRecord {
@@ -538,7 +596,7 @@ type CreateDomainRecordsResponse struct {
func (x *CreateDomainRecordsResponse) Reset() { func (x *CreateDomainRecordsResponse) Reset() {
*x = CreateDomainRecordsResponse{} *x = CreateDomainRecordsResponse{}
if protoimpl.UnsafeEnabled { if protoimpl.UnsafeEnabled {
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[8] mi := &file_api_pb_cluster_proto_msgTypes[9]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -551,7 +609,7 @@ func (x *CreateDomainRecordsResponse) String() string {
func (*CreateDomainRecordsResponse) ProtoMessage() {} func (*CreateDomainRecordsResponse) ProtoMessage() {}
func (x *CreateDomainRecordsResponse) ProtoReflect() protoreflect.Message { func (x *CreateDomainRecordsResponse) ProtoReflect() protoreflect.Message {
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[8] mi := &file_api_pb_cluster_proto_msgTypes[9]
if protoimpl.UnsafeEnabled && x != nil { if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -564,7 +622,7 @@ func (x *CreateDomainRecordsResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use CreateDomainRecordsResponse.ProtoReflect.Descriptor instead. // Deprecated: Use CreateDomainRecordsResponse.ProtoReflect.Descriptor instead.
func (*CreateDomainRecordsResponse) Descriptor() ([]byte, []int) { func (*CreateDomainRecordsResponse) Descriptor() ([]byte, []int) {
return file_internal_machine_api_pb_cluster_proto_rawDescGZIP(), []int{8} return file_api_pb_cluster_proto_rawDescGZIP(), []int{9}
} }
func (x *CreateDomainRecordsResponse) GetRecords() []*DNSRecord { func (x *CreateDomainRecordsResponse) GetRecords() []*DNSRecord {
@@ -587,7 +645,7 @@ type DNSRecord struct {
func (x *DNSRecord) Reset() { func (x *DNSRecord) Reset() {
*x = DNSRecord{} *x = DNSRecord{}
if protoimpl.UnsafeEnabled { if protoimpl.UnsafeEnabled {
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[9] mi := &file_api_pb_cluster_proto_msgTypes[10]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -600,7 +658,7 @@ func (x *DNSRecord) String() string {
func (*DNSRecord) ProtoMessage() {} func (*DNSRecord) ProtoMessage() {}
func (x *DNSRecord) ProtoReflect() protoreflect.Message { func (x *DNSRecord) ProtoReflect() protoreflect.Message {
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[9] mi := &file_api_pb_cluster_proto_msgTypes[10]
if protoimpl.UnsafeEnabled && x != nil { if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -613,7 +671,7 @@ func (x *DNSRecord) ProtoReflect() protoreflect.Message {
// Deprecated: Use DNSRecord.ProtoReflect.Descriptor instead. // Deprecated: Use DNSRecord.ProtoReflect.Descriptor instead.
func (*DNSRecord) Descriptor() ([]byte, []int) { func (*DNSRecord) Descriptor() ([]byte, []int) {
return file_internal_machine_api_pb_cluster_proto_rawDescGZIP(), []int{9} return file_api_pb_cluster_proto_rawDescGZIP(), []int{10}
} }
func (x *DNSRecord) GetName() string { func (x *DNSRecord) GetName() string {
@@ -637,123 +695,128 @@ func (x *DNSRecord) GetValues() []string {
return nil return nil
} }
var File_internal_machine_api_pb_cluster_proto protoreflect.FileDescriptor var File_api_pb_cluster_proto protoreflect.FileDescriptor
var file_internal_machine_api_pb_cluster_proto_rawDesc = []byte{ var file_api_pb_cluster_proto_rawDesc = []byte{
0x0a, 0x25, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x0a, 0x14, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x62, 0x2f, 0x63, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72,
0x6e, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x62, 0x2f, 0x63, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x03, 0x61, 0x70, 0x69, 0x1a, 0x1b, 0x67, 0x6f, 0x6f,
0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x03, 0x61, 0x70, 0x69, 0x1a, 0x1b, 0x67, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x65, 0x6d, 0x70,
0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x65, 0x6d, 0x74, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x13, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x62,
0x70, 0x74, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x24, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x2f, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x14, 0x61,
0x6e, 0x61, 0x6c, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x69, 0x2f, 0x70, 0x62, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x2e, 0x70, 0x72,
0x70, 0x62, 0x2f, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x6f, 0x74, 0x6f, 0x22, 0x7b, 0x0a, 0x11, 0x41, 0x64, 0x64, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e,
0x25, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65,
0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x62, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x2c, 0x0a, 0x07,
0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x7b, 0x0a, 0x11, 0x41, 0x64, 0x64, 0x4d, 0x61, 0x63, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e,
0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x70, 0x69, 0x2e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x43, 0x6f, 0x6e, 0x66, 0x69,
0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x67, 0x52, 0x07, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x12, 0x24, 0x0a, 0x09, 0x70, 0x75,
0x2c, 0x0a, 0x07, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x69, 0x70, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x07, 0x2e,
0x32, 0x12, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x43, 0x6f, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x50, 0x52, 0x08, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x49, 0x70,
0x6e, 0x66, 0x69, 0x67, 0x52, 0x07, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x12, 0x24, 0x0a, 0x22, 0x40, 0x0a, 0x12, 0x41, 0x64, 0x64, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65,
0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x69, 0x70, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2a, 0x0a, 0x07, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e,
0x32, 0x07, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x50, 0x52, 0x08, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d, 0x61,
0x63, 0x49, 0x70, 0x22, 0x40, 0x0a, 0x12, 0x41, 0x64, 0x64, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x07, 0x6d, 0x61, 0x63, 0x68, 0x69,
0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2a, 0x0a, 0x07, 0x6d, 0x61, 0x63, 0x6e, 0x65, 0x22, 0xb4, 0x01, 0x0a, 0x0d, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x4d, 0x65,
0x68, 0x69, 0x6e, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x61, 0x70, 0x69, 0x6d, 0x62, 0x65, 0x72, 0x12, 0x2a, 0x0a, 0x07, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x18,
0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x07, 0x6d, 0x61, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d, 0x61, 0x63, 0x68,
0x63, 0x68, 0x69, 0x6e, 0x65, 0x22, 0xb4, 0x01, 0x0a, 0x0d, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x07, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65,
0x65, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x12, 0x2a, 0x0a, 0x07, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x12, 0x38, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32,
0x6e, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d, 0x22, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x4d, 0x65, 0x6d,
0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x07, 0x6d, 0x61, 0x63, 0x68, 0x62, 0x65, 0x72, 0x2e, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, 0x68, 0x69, 0x70, 0x53, 0x74,
0x69, 0x6e, 0x65, 0x12, 0x38, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x61, 0x74, 0x65, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x22, 0x3d, 0x0a, 0x0f, 0x4d, 0x65,
0x28, 0x0e, 0x32, 0x22, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, 0x68, 0x69, 0x70, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x0b, 0x0a,
0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x2e, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, 0x68, 0x69, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x06, 0x0a, 0x02, 0x55, 0x50,
0x70, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x22, 0x3d, 0x0a, 0x10, 0x01, 0x12, 0x0b, 0x0a, 0x07, 0x53, 0x55, 0x53, 0x50, 0x45, 0x43, 0x54, 0x10, 0x02, 0x12,
0x0f, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, 0x68, 0x69, 0x70, 0x53, 0x74, 0x61, 0x74, 0x65, 0x08, 0x0a, 0x04, 0x44, 0x4f, 0x57, 0x4e, 0x10, 0x03, 0x22, 0x46, 0x0a, 0x14, 0x4c, 0x69, 0x73,
0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x06, 0x0a, 0x74, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
0x02, 0x55, 0x50, 0x10, 0x01, 0x12, 0x0b, 0x0a, 0x07, 0x53, 0x55, 0x53, 0x50, 0x45, 0x43, 0x54, 0x65, 0x12, 0x2e, 0x0a, 0x08, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x18, 0x01, 0x20,
0x10, 0x02, 0x12, 0x08, 0x0a, 0x04, 0x44, 0x4f, 0x57, 0x4e, 0x10, 0x03, 0x22, 0x46, 0x0a, 0x14, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e,
0x4c, 0x69, 0x73, 0x74, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x65, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x52, 0x08, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65,
0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2e, 0x0a, 0x08, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x73, 0x22, 0x26, 0x0a, 0x14, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x4d, 0x61, 0x63, 0x68, 0x69,
0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d, 0x61, 0x63, 0x6e, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18,
0x68, 0x69, 0x6e, 0x65, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x52, 0x08, 0x6d, 0x61, 0x63, 0x68, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x22, 0x4a, 0x0a, 0x06, 0x44, 0x6f, 0x6d,
0x69, 0x6e, 0x65, 0x73, 0x22, 0x26, 0x0a, 0x14, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x4d, 0x61, 0x61, 0x69, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28,
0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x1f, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x65, 0x72,
0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x22, 0x1c, 0x0a, 0x06, 0x76, 0x65, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x48, 0x00, 0x52, 0x08, 0x72, 0x65, 0x73,
0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x65, 0x72, 0x76, 0x65, 0x64, 0x88, 0x01, 0x01, 0x42, 0x0b, 0x0a, 0x09, 0x5f, 0x72, 0x65, 0x73,
0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x32, 0x0a, 0x14, 0x52, 0x65, 0x65, 0x72, 0x76, 0x65, 0x64, 0x22, 0x32, 0x0a, 0x14, 0x52, 0x65, 0x73, 0x65, 0x72, 0x76, 0x65,
0x73, 0x65, 0x72, 0x76, 0x65, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a,
0x73, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x01, 0x08, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52,
0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x22, 0x46, 0x08, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x22, 0x26, 0x0a, 0x10, 0x53, 0x65, 0x74,
0x0a, 0x1a, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x52, 0x65, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a,
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, 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, 0x65, 0x22, 0x46, 0x0a, 0x1a, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x44, 0x6f, 0x6d, 0x61, 0x69,
0x19, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x44, 0x4e, 0x53, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x2e, 0x6e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12,
0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x28, 0x0a, 0x07, 0x72, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b,
0x12, 0x16, 0x0a, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x32, 0x0e, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x44, 0x4e, 0x53, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64,
0x52, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x22, 0x2e, 0x0a, 0x0a, 0x52, 0x65, 0x63, 0x6f, 0x52, 0x07, 0x72, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x22, 0x47, 0x0a, 0x1b, 0x43, 0x72, 0x65,
0x72, 0x64, 0x54, 0x79, 0x70, 0x65, 0x12, 0x0f, 0x0a, 0x0b, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x61, 0x74, 0x65, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73,
0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x05, 0x0a, 0x01, 0x41, 0x10, 0x01, 0x12, 0x08, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x28, 0x0a, 0x07, 0x72, 0x65, 0x63, 0x6f,
0x0a, 0x04, 0x41, 0x41, 0x41, 0x41, 0x10, 0x02, 0x32, 0xca, 0x03, 0x0a, 0x07, 0x43, 0x6c, 0x75, 0x72, 0x64, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x61, 0x70, 0x69, 0x2e,
0x73, 0x74, 0x65, 0x72, 0x12, 0x3d, 0x0a, 0x0a, 0x41, 0x64, 0x64, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x44, 0x4e, 0x53, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x52, 0x07, 0x72, 0x65, 0x63, 0x6f, 0x72,
0x6e, 0x65, 0x12, 0x16, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x41, 0x64, 0x64, 0x4d, 0x61, 0x63, 0x68, 0x64, 0x73, 0x22, 0x96, 0x01, 0x0a, 0x09, 0x44, 0x4e, 0x53, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64,
0x69, 0x6e, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x17, 0x2e, 0x61, 0x70, 0x69, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04,
0x2e, 0x41, 0x64, 0x64, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x2d, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01,
0x6e, 0x73, 0x65, 0x12, 0x41, 0x0a, 0x0c, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x28, 0x0e, 0x32, 0x19, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x44, 0x4e, 0x53, 0x52, 0x65, 0x63, 0x6f,
0x6e, 0x65, 0x73, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x72, 0x64, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74,
0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x19, 0x2e, 0x61, 0x70, 0x79, 0x70, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x03, 0x20,
0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x52, 0x65, 0x03, 0x28, 0x09, 0x52, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x22, 0x2e, 0x0a, 0x0a, 0x52,
0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x42, 0x0a, 0x0d, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x54, 0x79, 0x70, 0x65, 0x12, 0x0f, 0x0a, 0x0b, 0x55, 0x4e, 0x53,
0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x12, 0x19, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x52, 0x65, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x05, 0x0a, 0x01, 0x41, 0x10,
0x6d, 0x6f, 0x76, 0x65, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x01, 0x12, 0x08, 0x0a, 0x04, 0x41, 0x41, 0x41, 0x41, 0x10, 0x02, 0x32, 0x86, 0x04, 0x0a, 0x07,
0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x12, 0x3d, 0x0a, 0x0a, 0x41, 0x64, 0x64, 0x4d, 0x61,
0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x37, 0x0a, 0x0d, 0x52, 0x65, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x12, 0x16, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x41, 0x64, 0x64, 0x4d,
0x73, 0x65, 0x72, 0x76, 0x65, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x19, 0x2e, 0x61, 0x70, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x17, 0x2e,
0x69, 0x2e, 0x52, 0x65, 0x73, 0x65, 0x72, 0x76, 0x65, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x52, 0x61, 0x70, 0x69, 0x2e, 0x41, 0x64, 0x64, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65,
0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0b, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x44, 0x6f, 0x6d, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x41, 0x0a, 0x0c, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x61,
0x61, 0x69, 0x6e, 0x12, 0x30, 0x0a, 0x09, 0x47, 0x65, 0x74, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e,
0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x19,
0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x0b, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x44, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65,
0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x34, 0x0a, 0x0d, 0x52, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x42, 0x0a, 0x0d, 0x52, 0x65, 0x6d,
0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x6f, 0x76, 0x65, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x12, 0x19, 0x2e, 0x61, 0x70, 0x69,
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x0b, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65,
0x2e, 0x61, 0x70, 0x69, 0x2e, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x58, 0x0a, 0x13, 0x43, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70,
0x72, 0x65, 0x61, 0x74, 0x65, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x37, 0x0a,
0x64, 0x73, 0x12, 0x1f, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x44, 0x0d, 0x52, 0x65, 0x73, 0x65, 0x72, 0x76, 0x65, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x19,
0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x52, 0x65, 0x71, 0x75, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x52, 0x65, 0x73, 0x65, 0x72, 0x76, 0x65, 0x44, 0x6f, 0x6d, 0x61,
0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0b, 0x2e, 0x61, 0x70, 0x69, 0x2e,
0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x52, 0x65, 0x73, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x3a, 0x0a, 0x09, 0x53, 0x65, 0x74, 0x44, 0x6f, 0x6d,
0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x37, 0x5a, 0x35, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x61, 0x69, 0x6e, 0x12, 0x15, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x53, 0x65, 0x74, 0x44, 0x6f, 0x6d,
0x63, 0x6f, 0x6d, 0x2f, 0x70, 0x73, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x6b, 0x69, 0x2f, 0x75, 0x61, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f,
0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70,
0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x62, 0x62, 0x06, 0x74, 0x79, 0x12, 0x30, 0x0a, 0x09, 0x47, 0x65, 0x74, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12,
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, 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, 0x26, 0x5a, 0x24, 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, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72,
0x6f, 0x74, 0x6f, 0x33,
} }
var ( var (
file_internal_machine_api_pb_cluster_proto_rawDescOnce sync.Once file_api_pb_cluster_proto_rawDescOnce sync.Once
file_internal_machine_api_pb_cluster_proto_rawDescData = file_internal_machine_api_pb_cluster_proto_rawDesc file_api_pb_cluster_proto_rawDescData = file_api_pb_cluster_proto_rawDesc
) )
func file_internal_machine_api_pb_cluster_proto_rawDescGZIP() []byte { func file_api_pb_cluster_proto_rawDescGZIP() []byte {
file_internal_machine_api_pb_cluster_proto_rawDescOnce.Do(func() { file_api_pb_cluster_proto_rawDescOnce.Do(func() {
file_internal_machine_api_pb_cluster_proto_rawDescData = protoimpl.X.CompressGZIP(file_internal_machine_api_pb_cluster_proto_rawDescData) file_api_pb_cluster_proto_rawDescData = protoimpl.X.CompressGZIP(file_api_pb_cluster_proto_rawDescData)
}) })
return file_internal_machine_api_pb_cluster_proto_rawDescData return file_api_pb_cluster_proto_rawDescData
} }
var file_internal_machine_api_pb_cluster_proto_enumTypes = make([]protoimpl.EnumInfo, 2) var file_api_pb_cluster_proto_enumTypes = make([]protoimpl.EnumInfo, 2)
var file_internal_machine_api_pb_cluster_proto_msgTypes = make([]protoimpl.MessageInfo, 10) var file_api_pb_cluster_proto_msgTypes = make([]protoimpl.MessageInfo, 11)
var file_internal_machine_api_pb_cluster_proto_goTypes = []any{ var file_api_pb_cluster_proto_goTypes = []any{
(MachineMember_MembershipState)(0), // 0: api.MachineMember.MembershipState (MachineMember_MembershipState)(0), // 0: api.MachineMember.MembershipState
(DNSRecord_RecordType)(0), // 1: api.DNSRecord.RecordType (DNSRecord_RecordType)(0), // 1: api.DNSRecord.RecordType
(*AddMachineRequest)(nil), // 2: api.AddMachineRequest (*AddMachineRequest)(nil), // 2: api.AddMachineRequest
@@ -763,54 +826,57 @@ var file_internal_machine_api_pb_cluster_proto_goTypes = []any{
(*RemoveMachineRequest)(nil), // 6: api.RemoveMachineRequest (*RemoveMachineRequest)(nil), // 6: api.RemoveMachineRequest
(*Domain)(nil), // 7: api.Domain (*Domain)(nil), // 7: api.Domain
(*ReserveDomainRequest)(nil), // 8: api.ReserveDomainRequest (*ReserveDomainRequest)(nil), // 8: api.ReserveDomainRequest
(*CreateDomainRecordsRequest)(nil), // 9: api.CreateDomainRecordsRequest (*SetDomainRequest)(nil), // 9: api.SetDomainRequest
(*CreateDomainRecordsResponse)(nil), // 10: api.CreateDomainRecordsResponse (*CreateDomainRecordsRequest)(nil), // 10: api.CreateDomainRecordsRequest
(*DNSRecord)(nil), // 11: api.DNSRecord (*CreateDomainRecordsResponse)(nil), // 11: api.CreateDomainRecordsResponse
(*NetworkConfig)(nil), // 12: api.NetworkConfig (*DNSRecord)(nil), // 12: api.DNSRecord
(*IP)(nil), // 13: api.IP (*NetworkConfig)(nil), // 13: api.NetworkConfig
(*MachineInfo)(nil), // 14: api.MachineInfo (*IP)(nil), // 14: api.IP
(*emptypb.Empty)(nil), // 15: google.protobuf.Empty (*MachineInfo)(nil), // 15: api.MachineInfo
(*emptypb.Empty)(nil), // 16: google.protobuf.Empty
} }
var file_internal_machine_api_pb_cluster_proto_depIdxs = []int32{ var file_api_pb_cluster_proto_depIdxs = []int32{
12, // 0: api.AddMachineRequest.network:type_name -> api.NetworkConfig 13, // 0: api.AddMachineRequest.network:type_name -> api.NetworkConfig
13, // 1: api.AddMachineRequest.public_ip:type_name -> api.IP 14, // 1: api.AddMachineRequest.public_ip:type_name -> api.IP
14, // 2: api.AddMachineResponse.machine:type_name -> api.MachineInfo 15, // 2: api.AddMachineResponse.machine:type_name -> api.MachineInfo
14, // 3: api.MachineMember.machine:type_name -> api.MachineInfo 15, // 3: api.MachineMember.machine:type_name -> api.MachineInfo
0, // 4: api.MachineMember.state:type_name -> api.MachineMember.MembershipState 0, // 4: api.MachineMember.state:type_name -> api.MachineMember.MembershipState
4, // 5: api.ListMachinesResponse.machines:type_name -> api.MachineMember 4, // 5: api.ListMachinesResponse.machines:type_name -> api.MachineMember
11, // 6: api.CreateDomainRecordsRequest.records:type_name -> api.DNSRecord 12, // 6: api.CreateDomainRecordsRequest.records:type_name -> api.DNSRecord
11, // 7: api.CreateDomainRecordsResponse.records:type_name -> api.DNSRecord 12, // 7: api.CreateDomainRecordsResponse.records:type_name -> api.DNSRecord
1, // 8: api.DNSRecord.type:type_name -> api.DNSRecord.RecordType 1, // 8: api.DNSRecord.type:type_name -> api.DNSRecord.RecordType
2, // 9: api.Cluster.AddMachine:input_type -> api.AddMachineRequest 2, // 9: api.Cluster.AddMachine:input_type -> api.AddMachineRequest
15, // 10: api.Cluster.ListMachines:input_type -> google.protobuf.Empty 16, // 10: api.Cluster.ListMachines:input_type -> google.protobuf.Empty
6, // 11: api.Cluster.RemoveMachine:input_type -> api.RemoveMachineRequest 6, // 11: api.Cluster.RemoveMachine:input_type -> api.RemoveMachineRequest
8, // 12: api.Cluster.ReserveDomain:input_type -> api.ReserveDomainRequest 8, // 12: api.Cluster.ReserveDomain:input_type -> api.ReserveDomainRequest
15, // 13: api.Cluster.GetDomain:input_type -> google.protobuf.Empty 9, // 13: api.Cluster.SetDomain:input_type -> api.SetDomainRequest
15, // 14: api.Cluster.ReleaseDomain:input_type -> google.protobuf.Empty 16, // 14: api.Cluster.GetDomain:input_type -> google.protobuf.Empty
9, // 15: api.Cluster.CreateDomainRecords:input_type -> api.CreateDomainRecordsRequest 16, // 15: api.Cluster.ReleaseDomain:input_type -> google.protobuf.Empty
3, // 16: api.Cluster.AddMachine:output_type -> api.AddMachineResponse 10, // 16: api.Cluster.CreateDomainRecords:input_type -> api.CreateDomainRecordsRequest
5, // 17: api.Cluster.ListMachines:output_type -> api.ListMachinesResponse 3, // 17: api.Cluster.AddMachine:output_type -> api.AddMachineResponse
15, // 18: api.Cluster.RemoveMachine:output_type -> google.protobuf.Empty 5, // 18: api.Cluster.ListMachines:output_type -> api.ListMachinesResponse
7, // 19: api.Cluster.ReserveDomain:output_type -> api.Domain 16, // 19: api.Cluster.RemoveMachine:output_type -> google.protobuf.Empty
7, // 20: api.Cluster.GetDomain:output_type -> api.Domain 7, // 20: api.Cluster.ReserveDomain:output_type -> api.Domain
7, // 21: api.Cluster.ReleaseDomain:output_type -> api.Domain 16, // 21: api.Cluster.SetDomain:output_type -> google.protobuf.Empty
10, // 22: api.Cluster.CreateDomainRecords:output_type -> api.CreateDomainRecordsResponse 7, // 22: api.Cluster.GetDomain:output_type -> api.Domain
16, // [16:23] is the sub-list for method output_type 7, // 23: api.Cluster.ReleaseDomain:output_type -> api.Domain
9, // [9:16] is the sub-list for method input_type 11, // 24: api.Cluster.CreateDomainRecords:output_type -> api.CreateDomainRecordsResponse
17, // [17:25] is the sub-list for method output_type
9, // [9:17] 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 type_name
9, // [9:9] is the sub-list for extension extendee 9, // [9:9] is the sub-list for extension extendee
0, // [0:9] is the sub-list for field type_name 0, // [0:9] is the sub-list for field type_name
} }
func init() { file_internal_machine_api_pb_cluster_proto_init() } func init() { file_api_pb_cluster_proto_init() }
func file_internal_machine_api_pb_cluster_proto_init() { func file_api_pb_cluster_proto_init() {
if File_internal_machine_api_pb_cluster_proto != nil { if File_api_pb_cluster_proto != nil {
return return
} }
file_internal_machine_api_pb_common_proto_init() file_api_pb_common_proto_init()
file_internal_machine_api_pb_machine_proto_init() file_api_pb_machine_proto_init()
if !protoimpl.UnsafeEnabled { if !protoimpl.UnsafeEnabled {
file_internal_machine_api_pb_cluster_proto_msgTypes[0].Exporter = func(v any, i int) any { file_api_pb_cluster_proto_msgTypes[0].Exporter = func(v any, i int) any {
switch v := v.(*AddMachineRequest); i { switch v := v.(*AddMachineRequest); i {
case 0: case 0:
return &v.state return &v.state
@@ -822,7 +888,7 @@ func file_internal_machine_api_pb_cluster_proto_init() {
return nil return nil
} }
} }
file_internal_machine_api_pb_cluster_proto_msgTypes[1].Exporter = func(v any, i int) any { file_api_pb_cluster_proto_msgTypes[1].Exporter = func(v any, i int) any {
switch v := v.(*AddMachineResponse); i { switch v := v.(*AddMachineResponse); i {
case 0: case 0:
return &v.state return &v.state
@@ -834,7 +900,7 @@ func file_internal_machine_api_pb_cluster_proto_init() {
return nil return nil
} }
} }
file_internal_machine_api_pb_cluster_proto_msgTypes[2].Exporter = func(v any, i int) any { file_api_pb_cluster_proto_msgTypes[2].Exporter = func(v any, i int) any {
switch v := v.(*MachineMember); i { switch v := v.(*MachineMember); i {
case 0: case 0:
return &v.state return &v.state
@@ -846,7 +912,7 @@ func file_internal_machine_api_pb_cluster_proto_init() {
return nil return nil
} }
} }
file_internal_machine_api_pb_cluster_proto_msgTypes[3].Exporter = func(v any, i int) any { file_api_pb_cluster_proto_msgTypes[3].Exporter = func(v any, i int) any {
switch v := v.(*ListMachinesResponse); i { switch v := v.(*ListMachinesResponse); i {
case 0: case 0:
return &v.state return &v.state
@@ -858,7 +924,7 @@ func file_internal_machine_api_pb_cluster_proto_init() {
return nil return nil
} }
} }
file_internal_machine_api_pb_cluster_proto_msgTypes[4].Exporter = func(v any, i int) any { file_api_pb_cluster_proto_msgTypes[4].Exporter = func(v any, i int) any {
switch v := v.(*RemoveMachineRequest); i { switch v := v.(*RemoveMachineRequest); i {
case 0: case 0:
return &v.state return &v.state
@@ -870,7 +936,7 @@ func file_internal_machine_api_pb_cluster_proto_init() {
return nil return nil
} }
} }
file_internal_machine_api_pb_cluster_proto_msgTypes[5].Exporter = func(v any, i int) any { file_api_pb_cluster_proto_msgTypes[5].Exporter = func(v any, i int) any {
switch v := v.(*Domain); i { switch v := v.(*Domain); i {
case 0: case 0:
return &v.state return &v.state
@@ -882,7 +948,7 @@ func file_internal_machine_api_pb_cluster_proto_init() {
return nil return nil
} }
} }
file_internal_machine_api_pb_cluster_proto_msgTypes[6].Exporter = func(v any, i int) any { file_api_pb_cluster_proto_msgTypes[6].Exporter = func(v any, i int) any {
switch v := v.(*ReserveDomainRequest); i { switch v := v.(*ReserveDomainRequest); i {
case 0: case 0:
return &v.state return &v.state
@@ -894,7 +960,19 @@ func file_internal_machine_api_pb_cluster_proto_init() {
return nil return nil
} }
} }
file_internal_machine_api_pb_cluster_proto_msgTypes[7].Exporter = func(v any, i int) any { file_api_pb_cluster_proto_msgTypes[7].Exporter = func(v any, i int) any {
switch v := v.(*SetDomainRequest); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_api_pb_cluster_proto_msgTypes[8].Exporter = func(v any, i int) any {
switch v := v.(*CreateDomainRecordsRequest); i { switch v := v.(*CreateDomainRecordsRequest); i {
case 0: case 0:
return &v.state return &v.state
@@ -906,7 +984,7 @@ func file_internal_machine_api_pb_cluster_proto_init() {
return nil return nil
} }
} }
file_internal_machine_api_pb_cluster_proto_msgTypes[8].Exporter = func(v any, i int) any { file_api_pb_cluster_proto_msgTypes[9].Exporter = func(v any, i int) any {
switch v := v.(*CreateDomainRecordsResponse); i { switch v := v.(*CreateDomainRecordsResponse); i {
case 0: case 0:
return &v.state return &v.state
@@ -918,7 +996,7 @@ func file_internal_machine_api_pb_cluster_proto_init() {
return nil return nil
} }
} }
file_internal_machine_api_pb_cluster_proto_msgTypes[9].Exporter = func(v any, i int) any { file_api_pb_cluster_proto_msgTypes[10].Exporter = func(v any, i int) any {
switch v := v.(*DNSRecord); i { switch v := v.(*DNSRecord); i {
case 0: case 0:
return &v.state return &v.state
@@ -931,23 +1009,24 @@ func file_internal_machine_api_pb_cluster_proto_init() {
} }
} }
} }
file_api_pb_cluster_proto_msgTypes[5].OneofWrappers = []any{}
type x struct{} type x struct{}
out := protoimpl.TypeBuilder{ out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{ File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(), GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_internal_machine_api_pb_cluster_proto_rawDesc, RawDescriptor: file_api_pb_cluster_proto_rawDesc,
NumEnums: 2, NumEnums: 2,
NumMessages: 10, NumMessages: 11,
NumExtensions: 0, NumExtensions: 0,
NumServices: 1, NumServices: 1,
}, },
GoTypes: file_internal_machine_api_pb_cluster_proto_goTypes, GoTypes: file_api_pb_cluster_proto_goTypes,
DependencyIndexes: file_internal_machine_api_pb_cluster_proto_depIdxs, DependencyIndexes: file_api_pb_cluster_proto_depIdxs,
EnumInfos: file_internal_machine_api_pb_cluster_proto_enumTypes, EnumInfos: file_api_pb_cluster_proto_enumTypes,
MessageInfos: file_internal_machine_api_pb_cluster_proto_msgTypes, MessageInfos: file_api_pb_cluster_proto_msgTypes,
}.Build() }.Build()
File_internal_machine_api_pb_cluster_proto = out.File File_api_pb_cluster_proto = out.File
file_internal_machine_api_pb_cluster_proto_rawDesc = nil file_api_pb_cluster_proto_rawDesc = nil
file_internal_machine_api_pb_cluster_proto_goTypes = nil file_api_pb_cluster_proto_goTypes = nil
file_internal_machine_api_pb_cluster_proto_depIdxs = nil file_api_pb_cluster_proto_depIdxs = nil
} }
@@ -2,11 +2,11 @@ syntax = "proto3";
package api; package api;
option go_package = "github.com/psviderski/uncloud/internal/machine/api/pb"; option go_package = "github.com/psviderski/uncloud/api/pb";
import "google/protobuf/empty.proto"; import "google/protobuf/empty.proto";
import "internal/machine/api/pb/common.proto"; import "api/pb/common.proto";
import "internal/machine/api/pb/machine.proto"; import "api/pb/machine.proto";
service Cluster { service Cluster {
rpc AddMachine(AddMachineRequest) returns (AddMachineResponse); rpc AddMachine(AddMachineRequest) returns (AddMachineResponse);
@@ -14,6 +14,7 @@ service Cluster {
rpc RemoveMachine(RemoveMachineRequest) returns (google.protobuf.Empty); rpc RemoveMachine(RemoveMachineRequest) returns (google.protobuf.Empty);
rpc ReserveDomain(ReserveDomainRequest) returns (Domain); rpc ReserveDomain(ReserveDomainRequest) returns (Domain);
rpc SetDomain(SetDomainRequest) returns (google.protobuf.Empty);
rpc GetDomain(google.protobuf.Empty) returns (Domain); rpc GetDomain(google.protobuf.Empty) returns (Domain);
rpc ReleaseDomain(google.protobuf.Empty) returns (Domain); rpc ReleaseDomain(google.protobuf.Empty) returns (Domain);
rpc CreateDomainRecords(CreateDomainRecordsRequest) returns (CreateDomainRecordsResponse); rpc CreateDomainRecords(CreateDomainRecordsRequest) returns (CreateDomainRecordsResponse);
@@ -56,12 +57,20 @@ message RemoveMachineRequest {
message Domain { message Domain {
string name = 1; string name = 1;
// Whether the domain is reserved in Uncloud DNS. Older daemons (<0.21) omit this field
// and only support reserved domains, so clients should treat absence as true.
optional bool reserved = 2;
} }
message ReserveDomainRequest { message ReserveDomainRequest {
string endpoint = 1; string endpoint = 1;
} }
message SetDomainRequest {
// An externally managed domain name. Empty clears a manually set domain.
string name = 1;
}
message CreateDomainRecordsRequest { message CreateDomainRecordsRequest {
repeated DNSRecord records = 1; repeated DNSRecord records = 1;
} }
@@ -2,7 +2,7 @@
// versions: // versions:
// - protoc-gen-go-grpc v1.5.1 // - protoc-gen-go-grpc v1.5.1
// - protoc v5.27.3 // - protoc v5.27.3
// source: internal/machine/api/pb/cluster.proto // source: api/pb/cluster.proto
package pb package pb
@@ -24,6 +24,7 @@ const (
Cluster_ListMachines_FullMethodName = "/api.Cluster/ListMachines" Cluster_ListMachines_FullMethodName = "/api.Cluster/ListMachines"
Cluster_RemoveMachine_FullMethodName = "/api.Cluster/RemoveMachine" Cluster_RemoveMachine_FullMethodName = "/api.Cluster/RemoveMachine"
Cluster_ReserveDomain_FullMethodName = "/api.Cluster/ReserveDomain" Cluster_ReserveDomain_FullMethodName = "/api.Cluster/ReserveDomain"
Cluster_SetDomain_FullMethodName = "/api.Cluster/SetDomain"
Cluster_GetDomain_FullMethodName = "/api.Cluster/GetDomain" Cluster_GetDomain_FullMethodName = "/api.Cluster/GetDomain"
Cluster_ReleaseDomain_FullMethodName = "/api.Cluster/ReleaseDomain" Cluster_ReleaseDomain_FullMethodName = "/api.Cluster/ReleaseDomain"
Cluster_CreateDomainRecords_FullMethodName = "/api.Cluster/CreateDomainRecords" Cluster_CreateDomainRecords_FullMethodName = "/api.Cluster/CreateDomainRecords"
@@ -37,6 +38,7 @@ type ClusterClient interface {
ListMachines(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*ListMachinesResponse, error) ListMachines(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*ListMachinesResponse, error)
RemoveMachine(ctx context.Context, in *RemoveMachineRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) RemoveMachine(ctx context.Context, in *RemoveMachineRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
ReserveDomain(ctx context.Context, in *ReserveDomainRequest, opts ...grpc.CallOption) (*Domain, error) ReserveDomain(ctx context.Context, in *ReserveDomainRequest, opts ...grpc.CallOption) (*Domain, error)
SetDomain(ctx context.Context, in *SetDomainRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
GetDomain(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*Domain, error) GetDomain(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*Domain, error)
ReleaseDomain(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*Domain, error) ReleaseDomain(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*Domain, error)
CreateDomainRecords(ctx context.Context, in *CreateDomainRecordsRequest, opts ...grpc.CallOption) (*CreateDomainRecordsResponse, error) CreateDomainRecords(ctx context.Context, in *CreateDomainRecordsRequest, opts ...grpc.CallOption) (*CreateDomainRecordsResponse, error)
@@ -90,6 +92,16 @@ func (c *clusterClient) ReserveDomain(ctx context.Context, in *ReserveDomainRequ
return out, nil return out, nil
} }
func (c *clusterClient) SetDomain(ctx context.Context, in *SetDomainRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(emptypb.Empty)
err := c.cc.Invoke(ctx, Cluster_SetDomain_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *clusterClient) GetDomain(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*Domain, error) { func (c *clusterClient) GetDomain(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*Domain, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(Domain) out := new(Domain)
@@ -128,6 +140,7 @@ type ClusterServer interface {
ListMachines(context.Context, *emptypb.Empty) (*ListMachinesResponse, error) ListMachines(context.Context, *emptypb.Empty) (*ListMachinesResponse, error)
RemoveMachine(context.Context, *RemoveMachineRequest) (*emptypb.Empty, error) RemoveMachine(context.Context, *RemoveMachineRequest) (*emptypb.Empty, error)
ReserveDomain(context.Context, *ReserveDomainRequest) (*Domain, error) ReserveDomain(context.Context, *ReserveDomainRequest) (*Domain, error)
SetDomain(context.Context, *SetDomainRequest) (*emptypb.Empty, error)
GetDomain(context.Context, *emptypb.Empty) (*Domain, error) GetDomain(context.Context, *emptypb.Empty) (*Domain, error)
ReleaseDomain(context.Context, *emptypb.Empty) (*Domain, error) ReleaseDomain(context.Context, *emptypb.Empty) (*Domain, error)
CreateDomainRecords(context.Context, *CreateDomainRecordsRequest) (*CreateDomainRecordsResponse, error) CreateDomainRecords(context.Context, *CreateDomainRecordsRequest) (*CreateDomainRecordsResponse, error)
@@ -153,6 +166,9 @@ func (UnimplementedClusterServer) RemoveMachine(context.Context, *RemoveMachineR
func (UnimplementedClusterServer) ReserveDomain(context.Context, *ReserveDomainRequest) (*Domain, error) { func (UnimplementedClusterServer) ReserveDomain(context.Context, *ReserveDomainRequest) (*Domain, error) {
return nil, status.Errorf(codes.Unimplemented, "method ReserveDomain not implemented") return nil, status.Errorf(codes.Unimplemented, "method ReserveDomain not implemented")
} }
func (UnimplementedClusterServer) SetDomain(context.Context, *SetDomainRequest) (*emptypb.Empty, error) {
return nil, status.Errorf(codes.Unimplemented, "method SetDomain not implemented")
}
func (UnimplementedClusterServer) GetDomain(context.Context, *emptypb.Empty) (*Domain, error) { func (UnimplementedClusterServer) GetDomain(context.Context, *emptypb.Empty) (*Domain, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetDomain not implemented") return nil, status.Errorf(codes.Unimplemented, "method GetDomain not implemented")
} }
@@ -255,6 +271,24 @@ func _Cluster_ReserveDomain_Handler(srv interface{}, ctx context.Context, dec fu
return interceptor(ctx, in, info, handler) return interceptor(ctx, in, info, handler)
} }
func _Cluster_SetDomain_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(SetDomainRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(ClusterServer).SetDomain(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: Cluster_SetDomain_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ClusterServer).SetDomain(ctx, req.(*SetDomainRequest))
}
return interceptor(ctx, in, info, handler)
}
func _Cluster_GetDomain_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { func _Cluster_GetDomain_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(emptypb.Empty) in := new(emptypb.Empty)
if err := dec(in); err != nil { if err := dec(in); err != nil {
@@ -332,6 +366,10 @@ var Cluster_ServiceDesc = grpc.ServiceDesc{
MethodName: "ReserveDomain", MethodName: "ReserveDomain",
Handler: _Cluster_ReserveDomain_Handler, Handler: _Cluster_ReserveDomain_Handler,
}, },
{
MethodName: "SetDomain",
Handler: _Cluster_SetDomain_Handler,
},
{ {
MethodName: "GetDomain", MethodName: "GetDomain",
Handler: _Cluster_GetDomain_Handler, Handler: _Cluster_GetDomain_Handler,
@@ -346,5 +384,5 @@ var Cluster_ServiceDesc = grpc.ServiceDesc{
}, },
}, },
Streams: []grpc.StreamDesc{}, Streams: []grpc.StreamDesc{},
Metadata: "internal/machine/api/pb/cluster.proto", Metadata: "api/pb/cluster.proto",
} }
File renamed without changes.
@@ -2,7 +2,7 @@
// versions: // versions:
// protoc-gen-go v1.34.2 // protoc-gen-go v1.34.2
// protoc v5.27.3 // protoc v5.27.3
// source: internal/machine/api/pb/common.proto // source: api/pb/common.proto
package pb package pb
@@ -58,11 +58,11 @@ func (x LogEntry_StreamType) String() string {
} }
func (LogEntry_StreamType) Descriptor() protoreflect.EnumDescriptor { func (LogEntry_StreamType) Descriptor() protoreflect.EnumDescriptor {
return file_internal_machine_api_pb_common_proto_enumTypes[0].Descriptor() return file_api_pb_common_proto_enumTypes[0].Descriptor()
} }
func (LogEntry_StreamType) Type() protoreflect.EnumType { func (LogEntry_StreamType) Type() protoreflect.EnumType {
return &file_internal_machine_api_pb_common_proto_enumTypes[0] return &file_api_pb_common_proto_enumTypes[0]
} }
func (x LogEntry_StreamType) Number() protoreflect.EnumNumber { func (x LogEntry_StreamType) Number() protoreflect.EnumNumber {
@@ -71,7 +71,7 @@ func (x LogEntry_StreamType) Number() protoreflect.EnumNumber {
// Deprecated: Use LogEntry_StreamType.Descriptor instead. // Deprecated: Use LogEntry_StreamType.Descriptor instead.
func (LogEntry_StreamType) EnumDescriptor() ([]byte, []int) { func (LogEntry_StreamType) EnumDescriptor() ([]byte, []int) {
return file_internal_machine_api_pb_common_proto_rawDescGZIP(), []int{7, 0} return file_api_pb_common_proto_rawDescGZIP(), []int{7, 0}
} }
// Common metadata message nested in all reply message types, injected by the gRPC proxy to provide information // Common metadata message nested in all reply message types, injected by the gRPC proxy to provide information
@@ -96,7 +96,7 @@ type Metadata struct {
func (x *Metadata) Reset() { func (x *Metadata) Reset() {
*x = Metadata{} *x = Metadata{}
if protoimpl.UnsafeEnabled { if protoimpl.UnsafeEnabled {
mi := &file_internal_machine_api_pb_common_proto_msgTypes[0] mi := &file_api_pb_common_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -109,7 +109,7 @@ func (x *Metadata) String() string {
func (*Metadata) ProtoMessage() {} func (*Metadata) ProtoMessage() {}
func (x *Metadata) ProtoReflect() protoreflect.Message { func (x *Metadata) ProtoReflect() protoreflect.Message {
mi := &file_internal_machine_api_pb_common_proto_msgTypes[0] mi := &file_api_pb_common_proto_msgTypes[0]
if protoimpl.UnsafeEnabled && x != nil { if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -122,7 +122,7 @@ func (x *Metadata) ProtoReflect() protoreflect.Message {
// Deprecated: Use Metadata.ProtoReflect.Descriptor instead. // Deprecated: Use Metadata.ProtoReflect.Descriptor instead.
func (*Metadata) Descriptor() ([]byte, []int) { func (*Metadata) Descriptor() ([]byte, []int) {
return file_internal_machine_api_pb_common_proto_rawDescGZIP(), []int{0} return file_api_pb_common_proto_rawDescGZIP(), []int{0}
} }
func (x *Metadata) GetMachineId() string { func (x *Metadata) GetMachineId() string {
@@ -172,7 +172,7 @@ type Empty struct {
func (x *Empty) Reset() { func (x *Empty) Reset() {
*x = Empty{} *x = Empty{}
if protoimpl.UnsafeEnabled { if protoimpl.UnsafeEnabled {
mi := &file_internal_machine_api_pb_common_proto_msgTypes[1] mi := &file_api_pb_common_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -185,7 +185,7 @@ func (x *Empty) String() string {
func (*Empty) ProtoMessage() {} func (*Empty) ProtoMessage() {}
func (x *Empty) ProtoReflect() protoreflect.Message { func (x *Empty) ProtoReflect() protoreflect.Message {
mi := &file_internal_machine_api_pb_common_proto_msgTypes[1] mi := &file_api_pb_common_proto_msgTypes[1]
if protoimpl.UnsafeEnabled && x != nil { if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -198,7 +198,7 @@ func (x *Empty) ProtoReflect() protoreflect.Message {
// Deprecated: Use Empty.ProtoReflect.Descriptor instead. // Deprecated: Use Empty.ProtoReflect.Descriptor instead.
func (*Empty) Descriptor() ([]byte, []int) { func (*Empty) Descriptor() ([]byte, []int) {
return file_internal_machine_api_pb_common_proto_rawDescGZIP(), []int{1} return file_api_pb_common_proto_rawDescGZIP(), []int{1}
} }
func (x *Empty) GetMetadata() *Metadata { func (x *Empty) GetMetadata() *Metadata {
@@ -221,7 +221,7 @@ type EmptyResponse struct {
func (x *EmptyResponse) Reset() { func (x *EmptyResponse) Reset() {
*x = EmptyResponse{} *x = EmptyResponse{}
if protoimpl.UnsafeEnabled { if protoimpl.UnsafeEnabled {
mi := &file_internal_machine_api_pb_common_proto_msgTypes[2] mi := &file_api_pb_common_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -234,7 +234,7 @@ func (x *EmptyResponse) String() string {
func (*EmptyResponse) ProtoMessage() {} func (*EmptyResponse) ProtoMessage() {}
func (x *EmptyResponse) ProtoReflect() protoreflect.Message { func (x *EmptyResponse) ProtoReflect() protoreflect.Message {
mi := &file_internal_machine_api_pb_common_proto_msgTypes[2] mi := &file_api_pb_common_proto_msgTypes[2]
if protoimpl.UnsafeEnabled && x != nil { if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -247,7 +247,7 @@ func (x *EmptyResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use EmptyResponse.ProtoReflect.Descriptor instead. // Deprecated: Use EmptyResponse.ProtoReflect.Descriptor instead.
func (*EmptyResponse) Descriptor() ([]byte, []int) { func (*EmptyResponse) Descriptor() ([]byte, []int) {
return file_internal_machine_api_pb_common_proto_rawDescGZIP(), []int{2} return file_api_pb_common_proto_rawDescGZIP(), []int{2}
} }
func (x *EmptyResponse) GetMessages() []*Empty { func (x *EmptyResponse) GetMessages() []*Empty {
@@ -268,7 +268,7 @@ type IP struct {
func (x *IP) Reset() { func (x *IP) Reset() {
*x = IP{} *x = IP{}
if protoimpl.UnsafeEnabled { if protoimpl.UnsafeEnabled {
mi := &file_internal_machine_api_pb_common_proto_msgTypes[3] mi := &file_api_pb_common_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -281,7 +281,7 @@ func (x *IP) String() string {
func (*IP) ProtoMessage() {} func (*IP) ProtoMessage() {}
func (x *IP) ProtoReflect() protoreflect.Message { func (x *IP) ProtoReflect() protoreflect.Message {
mi := &file_internal_machine_api_pb_common_proto_msgTypes[3] mi := &file_api_pb_common_proto_msgTypes[3]
if protoimpl.UnsafeEnabled && x != nil { if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -294,7 +294,7 @@ func (x *IP) ProtoReflect() protoreflect.Message {
// Deprecated: Use IP.ProtoReflect.Descriptor instead. // Deprecated: Use IP.ProtoReflect.Descriptor instead.
func (*IP) Descriptor() ([]byte, []int) { func (*IP) Descriptor() ([]byte, []int) {
return file_internal_machine_api_pb_common_proto_rawDescGZIP(), []int{3} return file_api_pb_common_proto_rawDescGZIP(), []int{3}
} }
func (x *IP) GetIp() []byte { func (x *IP) GetIp() []byte {
@@ -316,7 +316,7 @@ type IPPort struct {
func (x *IPPort) Reset() { func (x *IPPort) Reset() {
*x = IPPort{} *x = IPPort{}
if protoimpl.UnsafeEnabled { if protoimpl.UnsafeEnabled {
mi := &file_internal_machine_api_pb_common_proto_msgTypes[4] mi := &file_api_pb_common_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -329,7 +329,7 @@ func (x *IPPort) String() string {
func (*IPPort) ProtoMessage() {} func (*IPPort) ProtoMessage() {}
func (x *IPPort) ProtoReflect() protoreflect.Message { func (x *IPPort) ProtoReflect() protoreflect.Message {
mi := &file_internal_machine_api_pb_common_proto_msgTypes[4] mi := &file_api_pb_common_proto_msgTypes[4]
if protoimpl.UnsafeEnabled && x != nil { if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -342,7 +342,7 @@ func (x *IPPort) ProtoReflect() protoreflect.Message {
// Deprecated: Use IPPort.ProtoReflect.Descriptor instead. // Deprecated: Use IPPort.ProtoReflect.Descriptor instead.
func (*IPPort) Descriptor() ([]byte, []int) { func (*IPPort) Descriptor() ([]byte, []int) {
return file_internal_machine_api_pb_common_proto_rawDescGZIP(), []int{4} return file_api_pb_common_proto_rawDescGZIP(), []int{4}
} }
func (x *IPPort) GetIp() *IP { func (x *IPPort) GetIp() *IP {
@@ -371,7 +371,7 @@ type IPPrefix struct {
func (x *IPPrefix) Reset() { func (x *IPPrefix) Reset() {
*x = IPPrefix{} *x = IPPrefix{}
if protoimpl.UnsafeEnabled { if protoimpl.UnsafeEnabled {
mi := &file_internal_machine_api_pb_common_proto_msgTypes[5] mi := &file_api_pb_common_proto_msgTypes[5]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -384,7 +384,7 @@ func (x *IPPrefix) String() string {
func (*IPPrefix) ProtoMessage() {} func (*IPPrefix) ProtoMessage() {}
func (x *IPPrefix) ProtoReflect() protoreflect.Message { func (x *IPPrefix) ProtoReflect() protoreflect.Message {
mi := &file_internal_machine_api_pb_common_proto_msgTypes[5] mi := &file_api_pb_common_proto_msgTypes[5]
if protoimpl.UnsafeEnabled && x != nil { if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -397,7 +397,7 @@ func (x *IPPrefix) ProtoReflect() protoreflect.Message {
// Deprecated: Use IPPrefix.ProtoReflect.Descriptor instead. // Deprecated: Use IPPrefix.ProtoReflect.Descriptor instead.
func (*IPPrefix) Descriptor() ([]byte, []int) { func (*IPPrefix) Descriptor() ([]byte, []int) {
return file_internal_machine_api_pb_common_proto_rawDescGZIP(), []int{5} return file_api_pb_common_proto_rawDescGZIP(), []int{5}
} }
func (x *IPPrefix) GetIp() *IP { func (x *IPPrefix) GetIp() *IP {
@@ -430,7 +430,7 @@ type LogsRequest struct {
func (x *LogsRequest) Reset() { func (x *LogsRequest) Reset() {
*x = LogsRequest{} *x = LogsRequest{}
if protoimpl.UnsafeEnabled { if protoimpl.UnsafeEnabled {
mi := &file_internal_machine_api_pb_common_proto_msgTypes[6] mi := &file_api_pb_common_proto_msgTypes[6]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -443,7 +443,7 @@ func (x *LogsRequest) String() string {
func (*LogsRequest) ProtoMessage() {} func (*LogsRequest) ProtoMessage() {}
func (x *LogsRequest) ProtoReflect() protoreflect.Message { func (x *LogsRequest) ProtoReflect() protoreflect.Message {
mi := &file_internal_machine_api_pb_common_proto_msgTypes[6] mi := &file_api_pb_common_proto_msgTypes[6]
if protoimpl.UnsafeEnabled && x != nil { if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -456,7 +456,7 @@ func (x *LogsRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use LogsRequest.ProtoReflect.Descriptor instead. // Deprecated: Use LogsRequest.ProtoReflect.Descriptor instead.
func (*LogsRequest) Descriptor() ([]byte, []int) { func (*LogsRequest) Descriptor() ([]byte, []int) {
return file_internal_machine_api_pb_common_proto_rawDescGZIP(), []int{6} return file_api_pb_common_proto_rawDescGZIP(), []int{6}
} }
func (x *LogsRequest) GetId() string { func (x *LogsRequest) GetId() string {
@@ -508,7 +508,7 @@ type LogEntry struct {
func (x *LogEntry) Reset() { func (x *LogEntry) Reset() {
*x = LogEntry{} *x = LogEntry{}
if protoimpl.UnsafeEnabled { if protoimpl.UnsafeEnabled {
mi := &file_internal_machine_api_pb_common_proto_msgTypes[7] mi := &file_api_pb_common_proto_msgTypes[7]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -521,7 +521,7 @@ func (x *LogEntry) String() string {
func (*LogEntry) ProtoMessage() {} func (*LogEntry) ProtoMessage() {}
func (x *LogEntry) ProtoReflect() protoreflect.Message { func (x *LogEntry) ProtoReflect() protoreflect.Message {
mi := &file_internal_machine_api_pb_common_proto_msgTypes[7] mi := &file_api_pb_common_proto_msgTypes[7]
if protoimpl.UnsafeEnabled && x != nil { if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -534,7 +534,7 @@ func (x *LogEntry) ProtoReflect() protoreflect.Message {
// Deprecated: Use LogEntry.ProtoReflect.Descriptor instead. // Deprecated: Use LogEntry.ProtoReflect.Descriptor instead.
func (*LogEntry) Descriptor() ([]byte, []int) { func (*LogEntry) Descriptor() ([]byte, []int) {
return file_internal_machine_api_pb_common_proto_rawDescGZIP(), []int{7} return file_api_pb_common_proto_rawDescGZIP(), []int{7}
} }
func (x *LogEntry) GetStream() LogEntry_StreamType { func (x *LogEntry) GetStream() LogEntry_StreamType {
@@ -558,84 +558,82 @@ func (x *LogEntry) GetMessage() []byte {
return nil return nil
} }
var File_internal_machine_api_pb_common_proto protoreflect.FileDescriptor var File_api_pb_common_proto protoreflect.FileDescriptor
var file_internal_machine_api_pb_common_proto_rawDesc = []byte{ var file_api_pb_common_proto_rawDesc = []byte{
0x0a, 0x24, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x0a, 0x13, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x62, 0x2f, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e,
0x6e, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x62, 0x2f, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x03, 0x61, 0x70, 0x69, 0x1a, 0x17, 0x67, 0x6f, 0x6f, 0x67,
0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x03, 0x61, 0x70, 0x69, 0x1a, 0x17, 0x67, 0x6f, 0x6f, 0x6c, 0x65, 0x2f, 0x72, 0x70, 0x63, 0x2f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x2e, 0x70, 0x72,
0x67, 0x6c, 0x65, 0x2f, 0x72, 0x70, 0x63, 0x2f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x2e, 0x70, 0x6f, 0x74, 0x6f, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74,
0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70,
0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xb1, 0x01, 0x0a, 0x08, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74,
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xb1, 0x01, 0x0a, 0x08, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x61, 0x12, 0x1d, 0x0a, 0x0a, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x5f, 0x69, 0x64, 0x18,
0x74, 0x61, 0x12, 0x1d, 0x0a, 0x0a, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x5f, 0x69, 0x64, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x64,
0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x12, 0x21, 0x0a, 0x0c, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65,
0x64, 0x12, 0x21, 0x0a, 0x0c, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x4e,
0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x61, 0x6d, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x5f, 0x61,
0x4e, 0x61, 0x6d, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x5f, 0x64, 0x64, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x6d, 0x61, 0x63, 0x68, 0x69,
0x61, 0x64, 0x64, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x6d, 0x61, 0x63, 0x68, 0x6e, 0x65, 0x41, 0x64, 0x64, 0x72, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18,
0x69, 0x6e, 0x65, 0x41, 0x64, 0x64, 0x72, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x2a, 0x0a, 0x06,
0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x2a, 0x0a, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x67,
0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73,
0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x32, 0x0a, 0x05, 0x45, 0x6d, 0x70, 0x74,
0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x32, 0x0a, 0x05, 0x45, 0x6d, 0x70, 0x79, 0x12, 0x29, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20,
0x74, 0x79, 0x12, 0x29, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61,
0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x22, 0x37, 0x0a, 0x0d,
0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x22, 0x37, 0x0a, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x26, 0x0a,
0x0d, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x26, 0x08, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32,
0x0a, 0x08, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x0a, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x52, 0x08, 0x6d, 0x65, 0x73,
0x32, 0x0a, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x52, 0x08, 0x6d, 0x65, 0x73, 0x61, 0x67, 0x65, 0x73, 0x22, 0x14, 0x0a, 0x02, 0x49, 0x50, 0x12, 0x0e, 0x0a, 0x02, 0x69,
0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x22, 0x14, 0x0a, 0x02, 0x49, 0x50, 0x12, 0x0e, 0x0a, 0x02, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x02, 0x69, 0x70, 0x22, 0x35, 0x0a, 0x06, 0x49,
0x69, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x02, 0x69, 0x70, 0x22, 0x35, 0x0a, 0x06, 0x50, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x17, 0x0a, 0x02, 0x69, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28,
0x49, 0x50, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x17, 0x0a, 0x02, 0x69, 0x70, 0x18, 0x01, 0x20, 0x01, 0x0b, 0x32, 0x07, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x50, 0x52, 0x02, 0x69, 0x70, 0x12, 0x12,
0x28, 0x0b, 0x32, 0x07, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x50, 0x52, 0x02, 0x69, 0x70, 0x12, 0x0a, 0x04, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x04, 0x70, 0x6f,
0x12, 0x0a, 0x04, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x04, 0x70, 0x72, 0x74, 0x22, 0x37, 0x0a, 0x08, 0x49, 0x50, 0x50, 0x72, 0x65, 0x66, 0x69, 0x78, 0x12, 0x17,
0x6f, 0x72, 0x74, 0x22, 0x37, 0x0a, 0x08, 0x49, 0x50, 0x50, 0x72, 0x65, 0x66, 0x69, 0x78, 0x12, 0x0a, 0x02, 0x69, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x07, 0x2e, 0x61, 0x70, 0x69,
0x17, 0x0a, 0x02, 0x69, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x07, 0x2e, 0x61, 0x70, 0x2e, 0x49, 0x50, 0x52, 0x02, 0x69, 0x70, 0x12, 0x12, 0x0a, 0x04, 0x62, 0x69, 0x74, 0x73, 0x18,
0x69, 0x2e, 0x49, 0x50, 0x52, 0x02, 0x69, 0x70, 0x12, 0x12, 0x0a, 0x04, 0x62, 0x69, 0x74, 0x73, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x04, 0x62, 0x69, 0x74, 0x73, 0x22, 0x75, 0x0a, 0x0b, 0x4c,
0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x04, 0x62, 0x69, 0x74, 0x73, 0x22, 0x75, 0x0a, 0x0b, 0x6f, 0x67, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64,
0x4c, 0x6f, 0x67, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x66, 0x6f,
0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x66, 0x6c, 0x6c, 0x6f, 0x77, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x66, 0x6f, 0x6c, 0x6c,
0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x66, 0x6f, 0x6c, 0x6f, 0x77, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x61, 0x69, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05,
0x6c, 0x6f, 0x77, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x61, 0x69, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x52, 0x04, 0x74, 0x61, 0x69, 0x6c, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x69, 0x6e, 0x63, 0x65, 0x18,
0x05, 0x52, 0x04, 0x74, 0x61, 0x69, 0x6c, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x69, 0x6e, 0x63, 0x65, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x73, 0x69, 0x6e, 0x63, 0x65, 0x12, 0x14, 0x0a, 0x05,
0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x73, 0x69, 0x6e, 0x63, 0x65, 0x12, 0x14, 0x0a, 0x75, 0x6e, 0x74, 0x69, 0x6c, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x75, 0x6e, 0x74,
0x05, 0x75, 0x6e, 0x74, 0x69, 0x6c, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x75, 0x6e, 0x69, 0x6c, 0x22, 0xd2, 0x01, 0x0a, 0x08, 0x4c, 0x6f, 0x67, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12,
0x74, 0x69, 0x6c, 0x22, 0xd2, 0x01, 0x0a, 0x08, 0x4c, 0x6f, 0x67, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x30, 0x0a, 0x06, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32,
0x12, 0x30, 0x0a, 0x06, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x18, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x6f, 0x67, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x53,
0x32, 0x18, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x6f, 0x67, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x54, 0x79, 0x70, 0x65, 0x52, 0x06, 0x73, 0x74, 0x72, 0x65, 0x61,
0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x54, 0x79, 0x70, 0x65, 0x52, 0x06, 0x73, 0x74, 0x72, 0x65, 0x6d, 0x12, 0x38, 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x02,
0x61, 0x6d, 0x12, 0x38, 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72,
0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70,
0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x12, 0x18, 0x0a, 0x07, 0x6d,
0x70, 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x12, 0x18, 0x0a, 0x07, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x6d, 0x65,
0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x6d, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x40, 0x0a, 0x0a, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x54,
0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x40, 0x0a, 0x0a, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x79, 0x70, 0x65, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00,
0x54, 0x79, 0x70, 0x65, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x12, 0x0a, 0x0a, 0x06, 0x53, 0x54, 0x44, 0x4f, 0x55, 0x54, 0x10, 0x01, 0x12, 0x0a, 0x0a, 0x06,
0x00, 0x12, 0x0a, 0x0a, 0x06, 0x53, 0x54, 0x44, 0x4f, 0x55, 0x54, 0x10, 0x01, 0x12, 0x0a, 0x0a, 0x53, 0x54, 0x44, 0x45, 0x52, 0x52, 0x10, 0x02, 0x12, 0x0d, 0x0a, 0x09, 0x48, 0x45, 0x41, 0x52,
0x06, 0x53, 0x54, 0x44, 0x45, 0x52, 0x52, 0x10, 0x02, 0x12, 0x0d, 0x0a, 0x09, 0x48, 0x45, 0x41, 0x54, 0x42, 0x45, 0x41, 0x54, 0x10, 0x03, 0x42, 0x26, 0x5a, 0x24, 0x67, 0x69, 0x74, 0x68, 0x75,
0x52, 0x54, 0x42, 0x45, 0x41, 0x54, 0x10, 0x03, 0x42, 0x37, 0x5a, 0x35, 0x67, 0x69, 0x74, 0x68, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x70, 0x73, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x6b, 0x69,
0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x70, 0x73, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x6b, 0x2f, 0x75, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x62, 0x62,
0x69, 0x2f, 0x75, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
0x61, 0x6c, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70,
0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
} }
var ( var (
file_internal_machine_api_pb_common_proto_rawDescOnce sync.Once file_api_pb_common_proto_rawDescOnce sync.Once
file_internal_machine_api_pb_common_proto_rawDescData = file_internal_machine_api_pb_common_proto_rawDesc file_api_pb_common_proto_rawDescData = file_api_pb_common_proto_rawDesc
) )
func file_internal_machine_api_pb_common_proto_rawDescGZIP() []byte { func file_api_pb_common_proto_rawDescGZIP() []byte {
file_internal_machine_api_pb_common_proto_rawDescOnce.Do(func() { file_api_pb_common_proto_rawDescOnce.Do(func() {
file_internal_machine_api_pb_common_proto_rawDescData = protoimpl.X.CompressGZIP(file_internal_machine_api_pb_common_proto_rawDescData) file_api_pb_common_proto_rawDescData = protoimpl.X.CompressGZIP(file_api_pb_common_proto_rawDescData)
}) })
return file_internal_machine_api_pb_common_proto_rawDescData return file_api_pb_common_proto_rawDescData
} }
var file_internal_machine_api_pb_common_proto_enumTypes = make([]protoimpl.EnumInfo, 1) var file_api_pb_common_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
var file_internal_machine_api_pb_common_proto_msgTypes = make([]protoimpl.MessageInfo, 8) var file_api_pb_common_proto_msgTypes = make([]protoimpl.MessageInfo, 8)
var file_internal_machine_api_pb_common_proto_goTypes = []any{ var file_api_pb_common_proto_goTypes = []any{
(LogEntry_StreamType)(0), // 0: api.LogEntry.StreamType (LogEntry_StreamType)(0), // 0: api.LogEntry.StreamType
(*Metadata)(nil), // 1: api.Metadata (*Metadata)(nil), // 1: api.Metadata
(*Empty)(nil), // 2: api.Empty (*Empty)(nil), // 2: api.Empty
@@ -648,7 +646,7 @@ var file_internal_machine_api_pb_common_proto_goTypes = []any{
(*status.Status)(nil), // 9: google.rpc.Status (*status.Status)(nil), // 9: google.rpc.Status
(*timestamppb.Timestamp)(nil), // 10: google.protobuf.Timestamp (*timestamppb.Timestamp)(nil), // 10: google.protobuf.Timestamp
} }
var file_internal_machine_api_pb_common_proto_depIdxs = []int32{ var file_api_pb_common_proto_depIdxs = []int32{
9, // 0: api.Metadata.status:type_name -> google.rpc.Status 9, // 0: api.Metadata.status:type_name -> google.rpc.Status
1, // 1: api.Empty.metadata:type_name -> api.Metadata 1, // 1: api.Empty.metadata:type_name -> api.Metadata
2, // 2: api.EmptyResponse.messages:type_name -> api.Empty 2, // 2: api.EmptyResponse.messages:type_name -> api.Empty
@@ -663,13 +661,13 @@ var file_internal_machine_api_pb_common_proto_depIdxs = []int32{
0, // [0:7] is the sub-list for field type_name 0, // [0:7] is the sub-list for field type_name
} }
func init() { file_internal_machine_api_pb_common_proto_init() } func init() { file_api_pb_common_proto_init() }
func file_internal_machine_api_pb_common_proto_init() { func file_api_pb_common_proto_init() {
if File_internal_machine_api_pb_common_proto != nil { if File_api_pb_common_proto != nil {
return return
} }
if !protoimpl.UnsafeEnabled { if !protoimpl.UnsafeEnabled {
file_internal_machine_api_pb_common_proto_msgTypes[0].Exporter = func(v any, i int) any { file_api_pb_common_proto_msgTypes[0].Exporter = func(v any, i int) any {
switch v := v.(*Metadata); i { switch v := v.(*Metadata); i {
case 0: case 0:
return &v.state return &v.state
@@ -681,7 +679,7 @@ func file_internal_machine_api_pb_common_proto_init() {
return nil return nil
} }
} }
file_internal_machine_api_pb_common_proto_msgTypes[1].Exporter = func(v any, i int) any { file_api_pb_common_proto_msgTypes[1].Exporter = func(v any, i int) any {
switch v := v.(*Empty); i { switch v := v.(*Empty); i {
case 0: case 0:
return &v.state return &v.state
@@ -693,7 +691,7 @@ func file_internal_machine_api_pb_common_proto_init() {
return nil return nil
} }
} }
file_internal_machine_api_pb_common_proto_msgTypes[2].Exporter = func(v any, i int) any { file_api_pb_common_proto_msgTypes[2].Exporter = func(v any, i int) any {
switch v := v.(*EmptyResponse); i { switch v := v.(*EmptyResponse); i {
case 0: case 0:
return &v.state return &v.state
@@ -705,7 +703,7 @@ func file_internal_machine_api_pb_common_proto_init() {
return nil return nil
} }
} }
file_internal_machine_api_pb_common_proto_msgTypes[3].Exporter = func(v any, i int) any { file_api_pb_common_proto_msgTypes[3].Exporter = func(v any, i int) any {
switch v := v.(*IP); i { switch v := v.(*IP); i {
case 0: case 0:
return &v.state return &v.state
@@ -717,7 +715,7 @@ func file_internal_machine_api_pb_common_proto_init() {
return nil return nil
} }
} }
file_internal_machine_api_pb_common_proto_msgTypes[4].Exporter = func(v any, i int) any { file_api_pb_common_proto_msgTypes[4].Exporter = func(v any, i int) any {
switch v := v.(*IPPort); i { switch v := v.(*IPPort); i {
case 0: case 0:
return &v.state return &v.state
@@ -729,7 +727,7 @@ func file_internal_machine_api_pb_common_proto_init() {
return nil return nil
} }
} }
file_internal_machine_api_pb_common_proto_msgTypes[5].Exporter = func(v any, i int) any { file_api_pb_common_proto_msgTypes[5].Exporter = func(v any, i int) any {
switch v := v.(*IPPrefix); i { switch v := v.(*IPPrefix); i {
case 0: case 0:
return &v.state return &v.state
@@ -741,7 +739,7 @@ func file_internal_machine_api_pb_common_proto_init() {
return nil return nil
} }
} }
file_internal_machine_api_pb_common_proto_msgTypes[6].Exporter = func(v any, i int) any { file_api_pb_common_proto_msgTypes[6].Exporter = func(v any, i int) any {
switch v := v.(*LogsRequest); i { switch v := v.(*LogsRequest); i {
case 0: case 0:
return &v.state return &v.state
@@ -753,7 +751,7 @@ func file_internal_machine_api_pb_common_proto_init() {
return nil return nil
} }
} }
file_internal_machine_api_pb_common_proto_msgTypes[7].Exporter = func(v any, i int) any { file_api_pb_common_proto_msgTypes[7].Exporter = func(v any, i int) any {
switch v := v.(*LogEntry); i { switch v := v.(*LogEntry); i {
case 0: case 0:
return &v.state return &v.state
@@ -770,19 +768,19 @@ func file_internal_machine_api_pb_common_proto_init() {
out := protoimpl.TypeBuilder{ out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{ File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(), GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_internal_machine_api_pb_common_proto_rawDesc, RawDescriptor: file_api_pb_common_proto_rawDesc,
NumEnums: 1, NumEnums: 1,
NumMessages: 8, NumMessages: 8,
NumExtensions: 0, NumExtensions: 0,
NumServices: 0, NumServices: 0,
}, },
GoTypes: file_internal_machine_api_pb_common_proto_goTypes, GoTypes: file_api_pb_common_proto_goTypes,
DependencyIndexes: file_internal_machine_api_pb_common_proto_depIdxs, DependencyIndexes: file_api_pb_common_proto_depIdxs,
EnumInfos: file_internal_machine_api_pb_common_proto_enumTypes, EnumInfos: file_api_pb_common_proto_enumTypes,
MessageInfos: file_internal_machine_api_pb_common_proto_msgTypes, MessageInfos: file_api_pb_common_proto_msgTypes,
}.Build() }.Build()
File_internal_machine_api_pb_common_proto = out.File File_api_pb_common_proto = out.File
file_internal_machine_api_pb_common_proto_rawDesc = nil file_api_pb_common_proto_rawDesc = nil
file_internal_machine_api_pb_common_proto_goTypes = nil file_api_pb_common_proto_goTypes = nil
file_internal_machine_api_pb_common_proto_depIdxs = nil file_api_pb_common_proto_depIdxs = nil
} }
@@ -2,9 +2,9 @@ syntax = "proto3";
package api; package api;
option go_package = "github.com/psviderski/uncloud/internal/machine/api/pb"; option go_package = "github.com/psviderski/uncloud/api/pb";
// Vendored at internal/machine/api/vendor/google/rpc/status.proto. // Vendored at api/pb/vendor/google/rpc/status.proto.
import "google/rpc/status.proto"; import "google/rpc/status.proto";
import "google/protobuf/timestamp.proto"; import "google/protobuf/timestamp.proto";
File diff suppressed because it is too large. Load diff
@@ -2,10 +2,10 @@ syntax = "proto3";
package api; package api;
option go_package = "github.com/psviderski/uncloud/internal/machine/api/pb"; option go_package = "github.com/psviderski/uncloud/api/pb";
import "google/protobuf/empty.proto"; import "google/protobuf/empty.proto";
import "internal/machine/api/pb/common.proto"; import "api/pb/common.proto";
service Docker { service Docker {
rpc CreateContainer(CreateContainerRequest) returns (CreateContainerResponse); rpc CreateContainer(CreateContainerRequest) returns (CreateContainerResponse);
@@ -2,7 +2,7 @@
// versions: // versions:
// - protoc-gen-go-grpc v1.5.1 // - protoc-gen-go-grpc v1.5.1
// - protoc v5.27.3 // - protoc v5.27.3
// source: internal/machine/api/pb/docker.proto // source: api/pb/docker.proto
package pb package pb
@@ -807,5 +807,5 @@ var Docker_ServiceDesc = grpc.ServiceDesc{
ServerStreams: true, ServerStreams: true,
}, },
}, },
Metadata: "internal/machine/api/pb/docker.proto", Metadata: "api/pb/docker.proto",
} }
File diff suppressed because it is too large. Load diff
@@ -2,12 +2,12 @@ syntax = "proto3";
package api; package api;
option go_package = "github.com/psviderski/uncloud/internal/machine/api/pb"; option go_package = "github.com/psviderski/uncloud/api/pb";
import "google/protobuf/duration.proto"; import "google/protobuf/duration.proto";
import "google/protobuf/empty.proto"; import "google/protobuf/empty.proto";
import "google/protobuf/timestamp.proto"; import "google/protobuf/timestamp.proto";
import "internal/machine/api/pb/common.proto"; import "api/pb/common.proto";
service Machine { service Machine {
// CheckPrerequisites verifies if the machine meets all necessary system requirements to participate in the cluster. // CheckPrerequisites verifies if the machine meets all necessary system requirements to participate in the cluster.
@@ -19,6 +19,22 @@ service Machine {
rpc Inspect(google.protobuf.Empty) returns (MachineInfo); rpc Inspect(google.protobuf.Empty) returns (MachineInfo);
// InspectMachine retrieves detailed information about the machine. Supports broadcasting to multiple machines. // InspectMachine retrieves detailed information about the machine. Supports broadcasting to multiple machines.
rpc InspectMachine(google.protobuf.Empty) returns (InspectMachineResponse); rpc InspectMachine(google.protobuf.Empty) returns (InspectMachineResponse);
// WaitForStoreVersion waits until the cluster store on this machine has reached each requested actor version, with
// no known missing or pending transactions through those versions from active members.
// Corrosion may satisfy a version by applying its surviving changes or by marking it complete because its changes
// have been superseded.
//
// Waiting normally makes the captured data available on this machine. However, another write may replace some of that
// data before it arrives. Corrosion can then complete the older version without transferring the replaced data.
// If the replacement is outside the requested versions, this RPC can succeed while the affected data is still missing
// or outdated. This can happen during concurrent updates even when all machines are well connected.
//
// Success does not guarantee an exact snapshot or delivery of every historical value. Callers that require a specific
// record or condition should verify it after waiting.
//
// This RPC observes native replication without initiating synchronisation.
// Use the RPC deadline to bound the wait.
rpc WaitForStoreVersion(WaitForStoreVersionRequest) returns (google.protobuf.Empty);
// UpdateMachine updates the configuration of the machine. // UpdateMachine updates the configuration of the machine.
rpc UpdateMachine(UpdateMachineRequest) returns (UpdateMachineResponse); rpc UpdateMachine(UpdateMachineRequest) returns (UpdateMachineResponse);
// InspectWireGuardNetwork retrieves the current WireGuard network configuration and peer status. // InspectWireGuardNetwork retrieves the current WireGuard network configuration and peer status.
@@ -105,8 +121,8 @@ message JoinClusterRequest {
// MTU of the WireGuard interface on this machine. The daemon auto-detects the optimal MTU if 0 or not set. // MTU of the WireGuard interface on this machine. The daemon auto-detects the optimal MTU if 0 or not set.
int32 wireguard_mtu = 7; int32 wireguard_mtu = 7;
// Cluster store version this machine must reach before participating. // Cluster store version this machine must reach before participating.
// Per-actor vector (Corrosion actor UUID → max applied db_version). // Per-actor vector (Corrosion actor UUID → minimum required db_version).
map<string, int64> min_store_version = 6; map<string, uint64> min_store_version = 6;
} }
message InspectMachineResponse { message InspectMachineResponse {
@@ -119,9 +135,24 @@ message MachineDetails {
MachineInfo machine = 2; MachineInfo machine = 2;
// Round-trip times to other machines in the cluster, keyed by peer machine ID. // Round-trip times to other machines in the cluster, keyed by peer machine ID.
map<string, RTTStats> rtts = 4; map<string, RTTStats> rtts = 4;
// Current cluster store version observed on this machine. // Store replication progress observed on this machine.
// Per-actor vector (Corrosion actor UUID → max applied db_version) read from Corrosion crsql_db_versions. // Maps each Corrosion actor UUID to its highest processed database version. Corrosion also counts versions whose
map<string, int64> store_version = 5; // changes were superseded and skipped. Versions at or below a reported version may still be missing or pending
// locally.
//
// Pass this vector to WaitForStoreVersion on another machine to wait for replication through these versions.
// See that RPC's data-availability limitations. To combine observations from several machines, take the maximum
// for each actor.
// Capturing this vector does not wait for replication or prevent further writes.
map<string, uint64> store_version = 5;
}
message WaitForStoreVersionRequest {
// Minimum database version to reach for each Corrosion actor UUID.
// Obtain a vector from InspectMachine. To combine several responses, take the maximum version for each actor.
// Actors omitted from this vector and versions above its targets are not awaited. An empty vector requires
// no replication. Unknown actors with positive targets remain pending until replication reaches them or the RPC ends.
map<string, uint64> min_version = 1;
} }
message TokenResponse { message TokenResponse {
@@ -2,7 +2,7 @@
// versions: // versions:
// - protoc-gen-go-grpc v1.5.1 // - protoc-gen-go-grpc v1.5.1
// - protoc v5.27.3 // - protoc v5.27.3
// source: internal/machine/api/pb/machine.proto // source: api/pb/machine.proto
package pb package pb
@@ -26,6 +26,7 @@ const (
Machine_Token_FullMethodName = "/api.Machine/Token" Machine_Token_FullMethodName = "/api.Machine/Token"
Machine_Inspect_FullMethodName = "/api.Machine/Inspect" Machine_Inspect_FullMethodName = "/api.Machine/Inspect"
Machine_InspectMachine_FullMethodName = "/api.Machine/InspectMachine" Machine_InspectMachine_FullMethodName = "/api.Machine/InspectMachine"
Machine_WaitForStoreVersion_FullMethodName = "/api.Machine/WaitForStoreVersion"
Machine_UpdateMachine_FullMethodName = "/api.Machine/UpdateMachine" Machine_UpdateMachine_FullMethodName = "/api.Machine/UpdateMachine"
Machine_InspectWireGuardNetwork_FullMethodName = "/api.Machine/InspectWireGuardNetwork" Machine_InspectWireGuardNetwork_FullMethodName = "/api.Machine/InspectWireGuardNetwork"
Machine_Reset_FullMethodName = "/api.Machine/Reset" Machine_Reset_FullMethodName = "/api.Machine/Reset"
@@ -46,6 +47,22 @@ type MachineClient interface {
Inspect(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*MachineInfo, error) Inspect(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*MachineInfo, error)
// InspectMachine retrieves detailed information about the machine. Supports broadcasting to multiple machines. // InspectMachine retrieves detailed information about the machine. Supports broadcasting to multiple machines.
InspectMachine(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*InspectMachineResponse, error) InspectMachine(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*InspectMachineResponse, error)
// WaitForStoreVersion waits until the cluster store on this machine has reached each requested actor version, with
// no known missing or pending transactions through those versions from active members.
// Corrosion may satisfy a version by applying its surviving changes or by marking it complete because its changes
// have been superseded.
//
// Waiting normally makes the captured data available on this machine. However, another write may replace some of that
// data before it arrives. Corrosion can then complete the older version without transferring the replaced data.
// If the replacement is outside the requested versions, this RPC can succeed while the affected data is still missing
// or outdated. This can happen during concurrent updates even when all machines are well connected.
//
// Success does not guarantee an exact snapshot or delivery of every historical value. Callers that require a specific
// record or condition should verify it after waiting.
//
// This RPC observes native replication without initiating synchronisation.
// Use the RPC deadline to bound the wait.
WaitForStoreVersion(ctx context.Context, in *WaitForStoreVersionRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
// UpdateMachine updates the configuration of the machine. // UpdateMachine updates the configuration of the machine.
UpdateMachine(ctx context.Context, in *UpdateMachineRequest, opts ...grpc.CallOption) (*UpdateMachineResponse, error) UpdateMachine(ctx context.Context, in *UpdateMachineRequest, opts ...grpc.CallOption) (*UpdateMachineResponse, error)
// InspectWireGuardNetwork retrieves the current WireGuard network configuration and peer status. // InspectWireGuardNetwork retrieves the current WireGuard network configuration and peer status.
@@ -124,6 +141,16 @@ func (c *machineClient) InspectMachine(ctx context.Context, in *emptypb.Empty, o
return out, nil return out, nil
} }
func (c *machineClient) WaitForStoreVersion(ctx context.Context, in *WaitForStoreVersionRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(emptypb.Empty)
err := c.cc.Invoke(ctx, Machine_WaitForStoreVersion_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *machineClient) UpdateMachine(ctx context.Context, in *UpdateMachineRequest, opts ...grpc.CallOption) (*UpdateMachineResponse, error) { func (c *machineClient) UpdateMachine(ctx context.Context, in *UpdateMachineRequest, opts ...grpc.CallOption) (*UpdateMachineResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(UpdateMachineResponse) out := new(UpdateMachineResponse)
@@ -196,6 +223,22 @@ type MachineServer interface {
Inspect(context.Context, *emptypb.Empty) (*MachineInfo, error) Inspect(context.Context, *emptypb.Empty) (*MachineInfo, error)
// InspectMachine retrieves detailed information about the machine. Supports broadcasting to multiple machines. // InspectMachine retrieves detailed information about the machine. Supports broadcasting to multiple machines.
InspectMachine(context.Context, *emptypb.Empty) (*InspectMachineResponse, error) InspectMachine(context.Context, *emptypb.Empty) (*InspectMachineResponse, error)
// WaitForStoreVersion waits until the cluster store on this machine has reached each requested actor version, with
// no known missing or pending transactions through those versions from active members.
// Corrosion may satisfy a version by applying its surviving changes or by marking it complete because its changes
// have been superseded.
//
// Waiting normally makes the captured data available on this machine. However, another write may replace some of that
// data before it arrives. Corrosion can then complete the older version without transferring the replaced data.
// If the replacement is outside the requested versions, this RPC can succeed while the affected data is still missing
// or outdated. This can happen during concurrent updates even when all machines are well connected.
//
// Success does not guarantee an exact snapshot or delivery of every historical value. Callers that require a specific
// record or condition should verify it after waiting.
//
// This RPC observes native replication without initiating synchronisation.
// Use the RPC deadline to bound the wait.
WaitForStoreVersion(context.Context, *WaitForStoreVersionRequest) (*emptypb.Empty, error)
// UpdateMachine updates the configuration of the machine. // UpdateMachine updates the configuration of the machine.
UpdateMachine(context.Context, *UpdateMachineRequest) (*UpdateMachineResponse, error) UpdateMachine(context.Context, *UpdateMachineRequest) (*UpdateMachineResponse, error)
// InspectWireGuardNetwork retrieves the current WireGuard network configuration and peer status. // InspectWireGuardNetwork retrieves the current WireGuard network configuration and peer status.
@@ -232,6 +275,9 @@ func (UnimplementedMachineServer) Inspect(context.Context, *emptypb.Empty) (*Mac
func (UnimplementedMachineServer) InspectMachine(context.Context, *emptypb.Empty) (*InspectMachineResponse, error) { func (UnimplementedMachineServer) InspectMachine(context.Context, *emptypb.Empty) (*InspectMachineResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method InspectMachine not implemented") return nil, status.Errorf(codes.Unimplemented, "method InspectMachine not implemented")
} }
func (UnimplementedMachineServer) WaitForStoreVersion(context.Context, *WaitForStoreVersionRequest) (*emptypb.Empty, error) {
return nil, status.Errorf(codes.Unimplemented, "method WaitForStoreVersion not implemented")
}
func (UnimplementedMachineServer) UpdateMachine(context.Context, *UpdateMachineRequest) (*UpdateMachineResponse, error) { func (UnimplementedMachineServer) UpdateMachine(context.Context, *UpdateMachineRequest) (*UpdateMachineResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method UpdateMachine not implemented") return nil, status.Errorf(codes.Unimplemented, "method UpdateMachine not implemented")
} }
@@ -376,6 +422,24 @@ func _Machine_InspectMachine_Handler(srv interface{}, ctx context.Context, dec f
return interceptor(ctx, in, info, handler) return interceptor(ctx, in, info, handler)
} }
func _Machine_WaitForStoreVersion_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(WaitForStoreVersionRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(MachineServer).WaitForStoreVersion(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: Machine_WaitForStoreVersion_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(MachineServer).WaitForStoreVersion(ctx, req.(*WaitForStoreVersionRequest))
}
return interceptor(ctx, in, info, handler)
}
func _Machine_UpdateMachine_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { func _Machine_UpdateMachine_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(UpdateMachineRequest) in := new(UpdateMachineRequest)
if err := dec(in); err != nil { if err := dec(in); err != nil {
@@ -490,6 +554,10 @@ var Machine_ServiceDesc = grpc.ServiceDesc{
MethodName: "InspectMachine", MethodName: "InspectMachine",
Handler: _Machine_InspectMachine_Handler, Handler: _Machine_InspectMachine_Handler,
}, },
{
MethodName: "WaitForStoreVersion",
Handler: _Machine_WaitForStoreVersion_Handler,
},
{ {
MethodName: "UpdateMachine", MethodName: "UpdateMachine",
Handler: _Machine_UpdateMachine_Handler, Handler: _Machine_UpdateMachine_Handler,
@@ -514,5 +582,5 @@ var Machine_ServiceDesc = grpc.ServiceDesc{
ServerStreams: true, ServerStreams: true,
}, },
}, },
Metadata: "internal/machine/api/pb/machine.proto", Metadata: "api/pb/machine.proto",
} }
File renamed without changes.
+167
View File
@@ -0,0 +1,167 @@
package cert
import (
"context"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"os"
"slices"
"strings"
"time"
"charm.land/lipgloss/v2"
"github.com/caddyserver/certmagic"
"github.com/docker/go-units"
"github.com/psviderski/uncloud/internal/cli"
"github.com/psviderski/uncloud/internal/cli/completion"
"github.com/psviderski/uncloud/internal/cli/tui"
"github.com/psviderski/uncloud/pkg/api"
"github.com/psviderski/uncloud/pkg/client"
"github.com/spf13/cobra"
)
type listOptions struct {
machine string
output string
}
func NewListCommand() *cobra.Command {
opts := listOptions{}
cmd := &cobra.Command{
Use: "ls",
Aliases: []string{"list"},
Short: "List certificates in cluster storage for Caddy.",
Long: `List certificates stored in the Uncloud cluster storage for Caddy.
Caddy must use the Uncloud storage module (https://github.com/unlabs-dev/caddy-uncloud)
configured with 'storage uncloud' in the global options for its certificates to appear here.
This inventory does not check whether Caddy currently serves or trusts a certificate.`,
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, _ []string) error {
uncli := cmd.Context().Value("cli").(*cli.CLI)
return list(cmd.Context(), uncli, opts)
},
}
cmd.Flags().StringVarP(&opts.machine, "machine", "m", "",
"Name or ID of the machine to read certificate storage from. (default is connected machine)")
cmd.Flags().StringVarP(&opts.output, "output", "o", "",
"Output format: 'json' or empty for a human-readable table.")
completion.MachinesFlag(cmd)
return cmd
}
func list(ctx context.Context, uncli *cli.CLI, opts listOptions) error {
if opts.output != "" && opts.output != "json" {
return fmt.Errorf("unsupported output format '%s' (supported: json)", opts.output)
}
clusterClient, err := uncli.ConnectCluster(ctx)
if err != nil {
return fmt.Errorf("connect to cluster: %w", err)
}
defer clusterClient.Close()
certs, listErr := clusterClient.Caddy.ListCertificates(ctx,
client.CaddyListCertificatesOptions{Machine: opts.machine})
if len(certs) > 0 || listErr == nil {
if err = printCertificates(os.Stdout, certs, opts.output); err != nil {
return fmt.Errorf("print Caddy certificates: %w", err)
}
}
if listErr != nil {
if len(certs) > 0 {
return fmt.Errorf("certificate list is partial: %w", listErr)
}
return fmt.Errorf("list Caddy certificates: %w", listErr)
}
return nil
}
func printCertificates(out io.Writer, certs []api.IssuedCertificate, output string) error {
type certificate struct {
api.IssuedCertificate
Fingerprint string
}
items := make([]certificate, 0, len(certs))
for _, cert := range certs {
fingerprint := cert.Fingerprint()
items = append(items, certificate{
IssuedCertificate: cert,
Fingerprint: hex.EncodeToString(fingerprint[:]),
})
}
slices.SortFunc(items, func(a, b certificate) int {
if cmp := strings.Compare(a.SAN, b.SAN); cmp != 0 {
return cmp
}
if cmp := a.Chain[0].NotAfter.Compare(b.Chain[0].NotAfter); cmp != 0 {
return cmp
}
return strings.Compare(a.Fingerprint, b.Fingerprint)
})
if output == "json" {
encoder := json.NewEncoder(out)
encoder.SetIndent("", " ")
if err := encoder.Encode(items); err != nil {
return fmt.Errorf("marshal certificates: %w", err)
}
return nil
}
if len(items) == 0 {
_, err := fmt.Fprintln(out, "No Caddy certificates found in cluster storage.")
return err
}
t := tui.NewTable()
t.Headers("ID", "NAME", "ISSUER", "EXPIRES")
for _, item := range items {
t.Row(
item.Fingerprint[:12],
item.SAN,
formatIssuer(item.IssuerData),
formatExpiry(item.Chain[0].NotAfter),
)
}
_, err := lipgloss.Fprintln(out, t)
return err
}
func formatIssuer(data api.CertificateIssuerData) string {
if data.ACME == nil {
return "unknown"
}
switch data.ACME.CA {
case certmagic.LetsEncryptProductionCA:
return "Let's Encrypt"
case certmagic.LetsEncryptStagingCA:
return "Let's Encrypt (staging)"
case certmagic.ZeroSSLProductionCA:
return "ZeroSSL"
case certmagic.GoogleTrustProductionCA:
return "Google Trust Services"
case certmagic.GoogleTrustStagingCA:
return "Google Trust Services (staging)"
default:
return data.ACME.CA
}
}
func formatExpiry(expires time.Time) string {
delta := expires.Sub(time.Now())
if delta <= 0 {
return fmt.Sprintf("%s (expired %s ago)", expires.UTC().Format(time.DateOnly),
strings.ToLower(units.HumanDuration(-delta)))
}
return fmt.Sprintf("%s (%s)", expires.UTC().Format(time.DateOnly),
strings.ToLower(units.HumanDuration(delta)))
}
+128
View File
@@ -0,0 +1,128 @@
package cert
import (
"bytes"
"crypto/sha256"
"crypto/x509"
"encoding/hex"
"encoding/json"
"strings"
"testing"
"time"
"github.com/caddyserver/certmagic"
"github.com/psviderski/uncloud/pkg/api"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestPrintCertificates(t *testing.T) {
now := time.Now().UTC().Truncate(time.Hour)
certs := []api.IssuedCertificate{
testIssuedCertificate("z.example.com", "z", now.Add(48*time.Hour), certmagic.LetsEncryptProductionCA),
testIssuedCertificate("a.example.com", "old", now.Add(-48*time.Hour), certmagic.LetsEncryptStagingCA),
testIssuedCertificate("a.example.com", "new", now.Add(72*time.Hour), "https://ca.example/directory"),
testIssuedCertificate("unknown.example.com", "unknown", now.Add(96*time.Hour), ""),
}
var table bytes.Buffer
require.NoError(t, printCertificates(&table, certs, ""))
output := table.String()
assert.Contains(t, output, "ID")
assert.Contains(t, output, "NAME")
assert.Contains(t, output, "ISSUER")
assert.Contains(t, output, "EXPIRES")
assert.Contains(t, output, now.Add(-48*time.Hour).Format(time.DateOnly)+" (expired ")
assert.Contains(t, output, now.Add(48*time.Hour).Format(time.DateOnly)+" (")
assert.Contains(t, output, "Let's Encrypt (staging)")
assert.Contains(t, output, "https://ca.example/directory")
assert.Less(t, strings.Index(output, now.Add(-48*time.Hour).Format(time.DateOnly)),
strings.Index(output, now.Add(72*time.Hour).Format(time.DateOnly)))
assert.Less(t, strings.Index(output, "a.example.com"), strings.Index(output, "z.example.com"))
assert.Contains(t, output, testFingerprint("old")[:12])
var encoded bytes.Buffer
require.NoError(t, printCertificates(&encoded, certs, "json"))
var items []struct {
api.IssuedCertificate
Fingerprint string
}
require.NoError(t, json.Unmarshal(encoded.Bytes(), &items))
require.Len(t, items, 4)
assert.Equal(t, testFingerprint("old"), items[0].Fingerprint)
assert.Equal(t, "a.example.com", items[0].SAN)
require.Len(t, items[0].Chain, 1)
assert.Equal(t, []byte("old"), items[0].Chain[0].Raw)
assert.Equal(t, now.Add(-48*time.Hour), items[0].Chain[0].NotAfter)
require.NotNil(t, items[0].IssuerData.ACME)
assert.Equal(t, certmagic.LetsEncryptStagingCA, items[0].IssuerData.ACME.CA)
assert.Equal(t, testFingerprint("new"), items[1].Fingerprint)
require.NotNil(t, items[1].IssuerData.ACME)
assert.Equal(t, "https://ca.example/directory", items[1].IssuerData.ACME.CA)
assert.Nil(t, items[2].IssuerData.ACME)
assert.Equal(t, "z.example.com", items[3].SAN)
}
func TestPrintCertificates_Empty(t *testing.T) {
for _, tt := range []struct {
output string
want string
}{
{output: "", want: "No Caddy certificates found in cluster storage.\n"},
{output: "json", want: "[]\n"},
} {
var out bytes.Buffer
require.NoError(t, printCertificates(&out, nil, tt.output))
assert.Equal(t, tt.want, out.String())
}
}
func TestFormatIssuer(t *testing.T) {
for _, tt := range []struct {
name string
ca string
want string
}{
{name: "production", ca: certmagic.LetsEncryptProductionCA, want: "Let's Encrypt"},
{name: "staging", ca: certmagic.LetsEncryptStagingCA, want: "Let's Encrypt (staging)"},
{name: "ZeroSSL", ca: certmagic.ZeroSSLProductionCA, want: "ZeroSSL"},
{name: "Google Trust Services", ca: certmagic.GoogleTrustProductionCA, want: "Google Trust Services"},
{name: "Google Trust Services staging", ca: certmagic.GoogleTrustStagingCA, want: "Google Trust Services (staging)"},
{name: "other", ca: "https://ca.example/directory", want: "https://ca.example/directory"},
} {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.want, formatIssuer(api.CertificateIssuerData{
ACME: &api.ACMEIssuerData{CA: tt.ca},
}))
})
}
assert.Equal(t, "unknown", formatIssuer(api.CertificateIssuerData{}))
}
func TestFormatExpiry_UTC(t *testing.T) {
now := time.Now().UTC()
brisbane := time.FixedZone("AEST", 10*60*60)
expires := time.Date(now.Year(), now.Month(), now.Day()+3, 3, 0, 0, 0, brisbane)
assert.True(t, strings.HasPrefix(formatExpiry(expires), expires.UTC().Format(time.DateOnly)+" ("))
assert.NotEqual(t, expires.Format(time.DateOnly), expires.UTC().Format(time.DateOnly))
expired := now.Add(-2 * time.Hour)
assert.True(t, strings.HasPrefix(formatExpiry(expired), expired.UTC().Format(time.DateOnly)+" (expired "))
assert.Contains(t, formatExpiry(expired), " ago)")
}
func testIssuedCertificate(name, raw string, expires time.Time, ca string) api.IssuedCertificate {
cert := api.IssuedCertificate{
SAN: name,
Chain: []*x509.Certificate{{Raw: []byte(raw), NotAfter: expires}},
}
if ca != "" {
cert.IssuerData.ACME = &api.ACMEIssuerData{CA: ca}
}
return cert
}
func testFingerprint(raw string) string {
sum := sha256.Sum256([]byte(raw))
return hex.EncodeToString(sum[:])
}
+12
View File
@@ -0,0 +1,12 @@
package cert
import "github.com/spf13/cobra"
func NewRootCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "cert",
Short: "Inspect certificates in cluster storage for Caddy.",
}
cmd.AddCommand(NewListCommand())
return cmd
}
+8 -6
View File
@@ -8,6 +8,8 @@ import (
"github.com/alecthomas/chroma/v2/quick" "github.com/alecthomas/chroma/v2/quick"
"github.com/psviderski/uncloud/internal/cli" "github.com/psviderski/uncloud/internal/cli"
"github.com/psviderski/uncloud/internal/cli/completion" "github.com/psviderski/uncloud/internal/cli/completion"
"github.com/psviderski/uncloud/internal/cli/tui"
"github.com/psviderski/uncloud/pkg/client"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@@ -46,12 +48,7 @@ func runConfig(ctx context.Context, uncli *cli.CLI, opts configOptions) error {
} }
defer clusterClient.Close() defer clusterClient.Close()
if opts.machine != "" { config, err := clusterClient.Caddy.Config(ctx, client.CaddyConfigOptions{Machine: opts.machine})
// If a specific machine is requested, use it to get the Caddy configuration.
ctx = clusterClient.ProxySingleMachineContext(ctx, opts.machine)
}
config, err := clusterClient.Caddy.GetConfig(ctx, nil)
if err != nil { if err != nil {
return fmt.Errorf("get Caddy config: %w", err) return fmt.Errorf("get Caddy config: %w", err)
} }
@@ -66,5 +63,10 @@ func runConfig(ctx context.Context, uncli *cli.CLI, opts configOptions) error {
} }
} }
if config.LastReconciliationError != "" {
tui.PrintWarning(fmt.Sprintf("last Caddy config load failed: %s\nShowing the last saved Caddyfile.",
config.LastReconciliationError))
}
return nil return nil
} }
+9 -3
View File
@@ -12,10 +12,10 @@ import (
"charm.land/lipgloss/v2" "charm.land/lipgloss/v2"
"github.com/docker/cli/cli/streams" "github.com/docker/cli/cli/streams"
"github.com/docker/compose/v2/pkg/progress" "github.com/docker/compose/v2/pkg/progress"
"github.com/psviderski/uncloud/api/pb"
"github.com/psviderski/uncloud/internal/cli" "github.com/psviderski/uncloud/internal/cli"
"github.com/psviderski/uncloud/internal/cli/completion" "github.com/psviderski/uncloud/internal/cli/completion"
"github.com/psviderski/uncloud/internal/cli/tui" "github.com/psviderski/uncloud/internal/cli/tui"
"github.com/psviderski/uncloud/internal/machine/api/pb"
"github.com/psviderski/uncloud/pkg/api" "github.com/psviderski/uncloud/pkg/api"
"github.com/psviderski/uncloud/pkg/client" "github.com/psviderski/uncloud/pkg/client"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@@ -110,7 +110,9 @@ func runDeploy(ctx context.Context, uncli *cli.CLI, opts deployOptions) error {
placement := api.Placement{ placement := api.Placement{
Machines: cli.ExpandCommaSeparatedValues(opts.machines), Machines: cli.ExpandCommaSeparatedValues(opts.machines),
} }
d, err := clusterClient.NewCaddyDeployment(opts.image, caddyfile, placement) d, err := clusterClient.Caddy.NewDeployment(ctx, client.CaddyDeploymentOptions{
Image: opts.image, Config: caddyfile, Placement: placement,
})
if err != nil { if err != nil {
return fmt.Errorf("create caddy deployment: %w", err) return fmt.Errorf("create caddy deployment: %w", err)
} }
@@ -202,6 +204,10 @@ func UpdateDomainRecords(ctx context.Context, clusterClient *client.Client, prog
} }
return fmt.Errorf("get cluster domain: %w", err) return fmt.Errorf("get cluster domain: %w", err)
} }
if !domain.Reserved {
fmt.Printf("Skipping DNS records update as cluster domain '%s' is managed externally.\n", domain.Name)
return nil
}
fmt.Println("Updating cluster domain records in Uncloud DNS to point to machines running caddy service...") fmt.Println("Updating cluster domain records in Uncloud DNS to point to machines running caddy service...")
// TODO: split the method into two: one to get the records and one to update them to ask for update confirmation. // TODO: split the method into two: one to get the records and one to update them to ask for update confirmation.
@@ -216,7 +222,7 @@ func UpdateDomainRecords(ctx context.Context, clusterClient *client.Client, prog
if errors.Is(err, client.ErrNoReachableMachines) { if errors.Is(err, client.ErrNoReachableMachines) {
fmt.Println() fmt.Println()
fmt.Printf("DNS records for domain '%s' could not be updated as there are no internet-reachable "+ fmt.Printf("DNS records for domain '%s' could not be updated as there are no internet-reachable "+
"machines running caddy containers.\n", domain) "machines running caddy containers.\n", domain.Name)
fmt.Println() fmt.Println()
fmt.Println("Possible solutions:") fmt.Println("Possible solutions:")
fmt.Println("- Ensure your machines have public IP addresses") fmt.Println("- Ensure your machines have public IP addresses")
+2
View File
@@ -1,6 +1,7 @@
package caddy package caddy
import ( import (
"github.com/psviderski/uncloud/cmd/uc/caddy/cert"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@@ -10,6 +11,7 @@ func NewRootCommand() *cobra.Command {
Short: "Manage Caddy reverse proxy service.", Short: "Manage Caddy reverse proxy service.",
} }
cmd.AddCommand( cmd.AddCommand(
cert.NewRootCommand(),
NewConfigCommand(), NewConfigCommand(),
NewDeployCommand(), NewDeployCommand(),
NewLogsCommand(), NewLogsCommand(),
+1
View File
@@ -37,6 +37,7 @@ func release(ctx context.Context, uncli *cli.CLI) error {
if status.Convert(err).Code() == codes.NotFound { if status.Convert(err).Code() == codes.NotFound {
return errors.New("no domain reserved") return errors.New("no domain reserved")
} }
return fmt.Errorf("release cluster domain: %w", err)
} }
fmt.Printf("Released cluster domain: %s\n", domain.Name) fmt.Printf("Released cluster domain: %s\n", domain.Name)
+3 -2
View File
@@ -5,9 +5,9 @@ import (
"errors" "errors"
"fmt" "fmt"
"github.com/psviderski/uncloud/api/pb"
"github.com/psviderski/uncloud/cmd/uc/caddy" "github.com/psviderski/uncloud/cmd/uc/caddy"
"github.com/psviderski/uncloud/internal/cli" "github.com/psviderski/uncloud/internal/cli"
"github.com/psviderski/uncloud/internal/machine/api/pb"
"github.com/psviderski/uncloud/pkg/api" "github.com/psviderski/uncloud/pkg/api"
"github.com/psviderski/uncloud/pkg/client" "github.com/psviderski/uncloud/pkg/client"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@@ -49,7 +49,8 @@ func reserve(ctx context.Context, uncli *cli.CLI, opts reserveOptions) error {
domain, err := clusterClient.ReserveDomain(ctx, &pb.ReserveDomainRequest{Endpoint: opts.endpoint}) domain, err := clusterClient.ReserveDomain(ctx, &pb.ReserveDomainRequest{Endpoint: opts.endpoint})
if err != nil { if err != nil {
if status.Convert(err).Code() == codes.AlreadyExists { if status.Convert(err).Code() == codes.AlreadyExists {
return errors.New("domain already reserved") return errors.New("cluster domain already configured, unset it with 'uc dns set \"\"' " +
"or release a reservation with 'uc dns release' first")
} }
return err return err
} }
+6 -3
View File
@@ -7,16 +7,19 @@ import (
func NewRootCommand() *cobra.Command { func NewRootCommand() *cobra.Command {
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "dns", Use: "dns",
Short: "Manage cluster domain in Uncloud DNS.", Short: "Manage the cluster domain.",
Long: "Manage cluster domain in Uncloud DNS.\n" + Long: "Manage the cluster domain.\n" +
"DNS commands allow you to reserve or release a unique 'xxxxxx.uncld.dev' domain for your " + "DNS commands allow you to reserve or release a unique 'xxxxxx.uncld.dev' domain for your " +
"cluster. When reserved, Caddy service deployments will automatically update DNS records to route " + "cluster. When reserved, Caddy service deployments will automatically update DNS records to route " +
"traffic to the services in the cluster.", "traffic to the services in the cluster.\n\n" +
"EXPERIMENTAL: Use 'uc dns set' to configure an externally managed domain instead. " +
"Uncloud does not manage DNS records for manually set domains.",
} }
cmd.AddCommand( cmd.AddCommand(
NewReleaseCommand(), NewReleaseCommand(),
NewReserveCommand(), NewReserveCommand(),
NewShowCommand(), NewShowCommand(),
NewSetCommand(),
) )
return cmd return cmd
} }
+72
View File
@@ -0,0 +1,72 @@
package dns
import (
"context"
"errors"
"fmt"
"github.com/miekg/dns"
"github.com/psviderski/uncloud/api/pb"
"github.com/psviderski/uncloud/internal/cli"
"github.com/psviderski/uncloud/internal/cli/tui"
"github.com/psviderski/uncloud/internal/grpcversion"
"github.com/spf13/cobra"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
)
func NewSetCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "set DOMAIN_NAME",
Args: cobra.ExactArgs(1),
Short: "Set or unset an externally managed cluster domain (EXPERIMENTAL).",
Long: "EXPERIMENTAL: Set the cluster domain used to generate ingress hostnames for services.\n" +
"Configure wildcard DNS records for this domain with your DNS provider. " +
"Uncloud will not create, verify, update, or delete external DNS records.\n" +
"Pass an empty string to unset a manually set domain. " +
"Use 'uc dns release' to release a domain reserved in Uncloud DNS. " +
"Setting or unsetting the domain does not change existing service hostnames.",
Example: " uc dns set apps.example.com\n uc dns set \"\"",
RunE: func(cmd *cobra.Command, args []string) error {
uncli := cmd.Context().Value("cli").(*cli.CLI)
return set(cmd.Context(), uncli, args[0])
},
}
return cmd
}
func set(ctx context.Context, uncli *cli.CLI, name string) error {
if name != "" {
if labels, ok := dns.IsDomainName(name); !ok || labels < 2 {
return fmt.Errorf("invalid cluster domain %q: must be a valid domain name with at least two labels", name)
}
}
tui.PrintWarning("Setting an externally managed cluster domain is experimental.")
clusterClient, err := uncli.ConnectCluster(ctx)
if err != nil {
return fmt.Errorf("connect to cluster: %w", err)
}
defer clusterClient.Close()
ctx = metadata.AppendToOutgoingContext(ctx, grpcversion.MetadataKeyMinServerVersion, "0.21.0")
_, err = clusterClient.SetDomain(ctx, &pb.SetDomainRequest{Name: name})
if err != nil {
if status.Convert(err).Code() == codes.AlreadyExists {
return errors.New("cluster domain already configured, unset it with 'uc dns set \"\"' or " +
"release a reservation with 'uc dns release' first")
}
return fmt.Errorf("set cluster domain: %w", err)
}
if name == "" {
fmt.Println("Unset cluster domain. External DNS records have not been changed.")
return nil
}
fmt.Printf("Set cluster domain: %s\n", name)
fmt.Println("DNS is managed externally. " +
"Configure wildcard DNS records pointing to machines running the Caddy service.")
return nil
}
+2 -2
View File
@@ -33,11 +33,11 @@ func show(ctx context.Context, uncli *cli.CLI) error {
domain, err := clusterClient.GetDomain(ctx) domain, err := clusterClient.GetDomain(ctx)
if err != nil { if err != nil {
if errors.Is(err, api.ErrNotFound) { if errors.Is(err, api.ErrNotFound) {
return errors.New("no domain reserved") return errors.New("no cluster domain configured")
} }
return err return err
} }
fmt.Println(domain) fmt.Println(domain.Name)
return nil return nil
} }
+12 -6
View File
@@ -40,13 +40,19 @@ func NewAddCommand() *cobra.Command {
Short: "Add a remote machine to a cluster.", Short: "Add a remote machine to a cluster.",
Long: `Add a new machine to an existing Uncloud cluster. Long: `Add a new machine to an existing Uncloud cluster.
By default, it installs Docker and the Uncloud daemon on the machine over SSH unless --no-install
is specified, which assumes they are already installed and running.
Connection methods: Connection methods:
[ssh://]user@host - Use system 'ssh' command with full SSH config support (default, no prefix required) [ssh://]user@host - Use system 'ssh' command with full SSH config support (default, no prefix required)
ssh+go://user@host - Use Go's built-in SSH library`, ssh+go://user@host - Use Go's built-in SSH library`,
Args: cobra.ExactArgs(1), Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error { PreRunE: func(cmd *cobra.Command, args []string) error {
cli.BindEnvToFlag(cmd, "yes", "UNCLOUD_AUTO_CONFIRM") cli.BindEnvToFlag(cmd, "yes", "UNCLOUD_AUTO_CONFIRM")
cli.BindEnvToFlag(cmd, "version", "UNCLOUD_DAEMON_VERSION")
return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
uncli := cmd.Context().Value("cli").(*cli.CLI) uncli := cmd.Context().Value("cli").(*cli.CLI)
// Determine connection mode and strip scheme. // Determine connection mode and strip scheme.
@@ -79,7 +85,7 @@ Connection methods:
) )
cmd.Flags().BoolVar( cmd.Flags().BoolVar(
&opts.noInstall, "no-install", false, &opts.noInstall, "no-install", false,
"Skip installation of Docker, Uncloud daemon, and dependencies on the machine. "+ "Skip installation of Docker and the Uncloud daemon on the machine. "+
"Assumes they're already installed and running.", "Assumes they're already installed and running.",
) )
cmd.Flags().StringVar( cmd.Flags().StringVar(
@@ -94,7 +100,7 @@ Connection methods:
) )
cmd.Flags().StringVar( cmd.Flags().StringVar(
&opts.version, "version", "latest", &opts.version, "version", "latest",
"Version of the Uncloud daemon to install on the machine.", "Version of the Uncloud daemon to install on the machine. [$UNCLOUD_DAEMON_VERSION]",
) )
cmd.Flags().StringSliceVar( cmd.Flags().StringSliceVar(
&opts.wgEndpoints, "wg-endpoint", nil, &opts.wgEndpoints, "wg-endpoint", nil,
@@ -186,7 +192,7 @@ func add(ctx context.Context, uncli *cli.CLI, remoteMachine *cli.RemoteMachine,
// Deploy a Caddy service container to the added machine. If caddy service is already deployed on other machines, // Deploy a Caddy service container to the added machine. If caddy service is already deployed on other machines,
// use the deployed image version. // use the deployed image version.
// NOTE: We use the cluster client to inspect and scale the Caddy service because the newly added machine may have // NOTE: We use the cluster client to inspect and scale the Caddy service because the newly added machine may have
// issues accessing the Machine API of existing machines in the cluster. // issues accessing the API of existing machines in the cluster.
// See the issue for more details: https://github.com/psviderski/uncloud/issues/65. // See the issue for more details: https://github.com/psviderski/uncloud/issues/65.
caddyImage := "" caddyImage := ""
caddySvc, err := clusterClient.InspectService(ctx, client.CaddyServiceName) caddySvc, err := clusterClient.InspectService(ctx, client.CaddyServiceName)
@@ -213,7 +219,7 @@ func add(ctx context.Context, uncli *cli.CLI, remoteMachine *cli.RemoteMachine,
fmt.Println() fmt.Println()
fmt.Println("Preparing Caddy deployment...") fmt.Println("Preparing Caddy deployment...")
d, err := clusterClient.NewCaddyDeployment(caddyImage, "", api.Placement{}) d, err := clusterClient.Caddy.NewDeployment(ctx, client.CaddyDeploymentOptions{Image: caddyImage})
if err != nil { if err != nil {
return fmt.Errorf("create caddy deployment: %w", err) return fmt.Errorf("create caddy deployment: %w", err)
} }
+72
View File
@@ -0,0 +1,72 @@
package machine
import (
"testing"
"github.com/spf13/cobra"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// runVersionFlagEnvTest builds the given command for each case, stubs out RunE so only flag
// parsing and PreRunE run (no SSH calls), and asserts the resolved --version flag value.
// Note: cannot use t.Parallel() because subtests use t.Setenv().
func runVersionFlagEnvTest(t *testing.T, newCommand func() *cobra.Command) {
t.Helper()
tests := []struct {
name string
args []string
env string
want string
}{
{
name: "env var sets version when flag not passed",
args: []string{"root@localhost"},
env: "v1.2.3",
want: "v1.2.3",
},
{
name: "explicit flag wins over env var",
args: []string{"root@localhost", "--version", "v9.9.9"},
env: "v1.2.3",
want: "v9.9.9",
},
{
name: "defaults to latest when neither flag nor env var set",
args: []string{"root@localhost"},
want: "latest",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.env != "" {
t.Setenv("UNCLOUD_DAEMON_VERSION", tt.env)
}
cmd := newCommand()
cmd.RunE = func(*cobra.Command, []string) error {
return nil
}
cmd.SetArgs(tt.args)
require.NoError(t, cmd.Execute())
version, err := cmd.Flags().GetString("version")
require.NoError(t, err)
assert.Equal(t, tt.want, version)
})
}
}
// TestInitCommandVersionFlagEnvVar verifies that the UNCLOUD_DAEMON_VERSION environment variable
// is bound to the --version flag of 'machine init', and that an explicit flag wins.
func TestInitCommandVersionFlagEnvVar(t *testing.T) {
runVersionFlagEnvTest(t, NewInitCommand)
}
// TestAddCommandVersionFlagEnvVar verifies that the UNCLOUD_DAEMON_VERSION environment variable
// is bound to the --version flag of 'machine add', and that an explicit flag wins.
func TestAddCommandVersionFlagEnvVar(t *testing.T) {
runVersionFlagEnvTest(t, NewAddCommand)
}
+18 -12
View File
@@ -8,15 +8,15 @@ import (
"time" "time"
"github.com/docker/compose/v2/pkg/progress" "github.com/docker/compose/v2/pkg/progress"
"github.com/psviderski/uncloud/api/pb"
"github.com/psviderski/uncloud/cmd/uc/caddy" "github.com/psviderski/uncloud/cmd/uc/caddy"
"github.com/psviderski/uncloud/cmd/uc/dns" "github.com/psviderski/uncloud/cmd/uc/dns"
"github.com/psviderski/uncloud/internal/cli" "github.com/psviderski/uncloud/internal/cli"
"github.com/psviderski/uncloud/internal/cli/config" "github.com/psviderski/uncloud/internal/cli/config"
"github.com/psviderski/uncloud/internal/cli/tui" "github.com/psviderski/uncloud/internal/cli/tui"
"github.com/psviderski/uncloud/internal/machine/api/pb"
"github.com/psviderski/uncloud/internal/machine/cluster" "github.com/psviderski/uncloud/internal/machine/cluster"
"github.com/psviderski/uncloud/internal/machine/network" "github.com/psviderski/uncloud/internal/machine/network"
"github.com/psviderski/uncloud/pkg/api" "github.com/psviderski/uncloud/pkg/client"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@@ -45,6 +45,9 @@ func NewInitCommand() *cobra.Command {
Long: `Initialise a new cluster by setting up a remote machine as the first member. Long: `Initialise a new cluster by setting up a remote machine as the first member.
This command creates a new context in your Uncloud config to manage the cluster. This command creates a new context in your Uncloud config to manage the cluster.
By default, it installs Docker and the Uncloud daemon on the machine over SSH unless --no-install
is specified, which assumes they are already installed and running.
Connection methods: Connection methods:
[ssh://]user@host - Use system 'ssh' command with full SSH config support (default, no prefix required) [ssh://]user@host - Use system 'ssh' command with full SSH config support (default, no prefix required)
ssh+go://user@host - Use Go's built-in SSH library`, ssh+go://user@host - Use Go's built-in SSH library`,
@@ -62,9 +65,12 @@ Connection methods:
uc machine init root@<your-server-ip> --no-caddy --no-dns`, uc machine init root@<your-server-ip> --no-caddy --no-dns`,
// TODO: support initialising a cluster on the local machine. // TODO: support initialising a cluster on the local machine.
Args: cobra.MaximumNArgs(1), Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error { PreRunE: func(cmd *cobra.Command, args []string) error {
cli.BindEnvToFlag(cmd, "yes", "UNCLOUD_AUTO_CONFIRM") cli.BindEnvToFlag(cmd, "yes", "UNCLOUD_AUTO_CONFIRM")
cli.BindEnvToFlag(cmd, "version", "UNCLOUD_DAEMON_VERSION")
return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
uncli := cmd.Context().Value("cli").(*cli.CLI) uncli := cmd.Context().Value("cli").(*cli.CLI)
var remoteMachine *cli.RemoteMachine var remoteMachine *cli.RemoteMachine
@@ -117,7 +123,7 @@ Connection methods:
) )
cmd.Flags().BoolVar( cmd.Flags().BoolVar(
&opts.noInstall, "no-install", false, &opts.noInstall, "no-install", false,
"Skip installation of Docker, Uncloud daemon, and dependencies on the machine. "+ "Skip installation of Docker and the Uncloud daemon on the machine. "+
"Assumes they're already installed and running.", "Assumes they're already installed and running.",
) )
cmd.Flags().StringVar( cmd.Flags().StringVar(
@@ -132,7 +138,7 @@ Connection methods:
) )
cmd.Flags().StringVar( cmd.Flags().StringVar(
&opts.version, "version", "latest", &opts.version, "version", "latest",
"Version of the Uncloud daemon to install on the machine.", "Version of the Uncloud daemon to install on the machine. [$UNCLOUD_DAEMON_VERSION]",
) )
cmd.Flags().StringSliceVar( cmd.Flags().StringSliceVar(
&opts.wgEndpoints, "wg-endpoint", nil, &opts.wgEndpoints, "wg-endpoint", nil,
@@ -215,17 +221,17 @@ func initCluster(ctx context.Context, uncli *cli.CLI, remoteMachine *cli.RemoteM
initOpts.WireguardEndpoints = endpoints initOpts.WireguardEndpoints = endpoints
} }
client, err := uncli.InitCluster(ctx, initOpts) clusterClient, err := uncli.InitCluster(ctx, initOpts)
if err != nil { if err != nil {
return err return err
} }
defer client.Close() defer clusterClient.Close()
// Since the cluster API needs a few moments to become ready after cluster initialisation, // Since the cluster API needs a few moments to become ready after cluster initialisation,
// we keep the user informed during this wait. We wait here even if no Caddy or DNS is requested // we keep the user informed during this wait. We wait here even if no Caddy or DNS is requested
// as the cluster needs to be ready so that commands such as 'uc machine ls' work immediately after init. // as the cluster needs to be ready so that commands such as 'uc machine ls' work immediately after init.
err = tui.RunSpinner(ctx, "Waiting for the cluster to be ready...", func(ctx context.Context) error { err = tui.RunSpinner(ctx, "Waiting for the cluster to be ready...", func(ctx context.Context) error {
return client.WaitClusterReady(ctx, 1*time.Minute) return clusterClient.WaitClusterReady(ctx, 1*time.Minute)
}) })
if err != nil { if err != nil {
return fmt.Errorf("wait for cluster to be ready: %w", err) return fmt.Errorf("wait for cluster to be ready: %w", err)
@@ -239,7 +245,7 @@ func initCluster(ctx context.Context, uncli *cli.CLI, remoteMachine *cli.RemoteM
fmt.Println() fmt.Println()
if !opts.noDNS { if !opts.noDNS {
domain, err := client.ReserveDomain(ctx, &pb.ReserveDomainRequest{Endpoint: opts.dnsEndpoint}) domain, err := clusterClient.ReserveDomain(ctx, &pb.ReserveDomainRequest{Endpoint: opts.dnsEndpoint})
if err != nil { if err != nil {
return fmt.Errorf("reserve cluster domain in Uncloud DNS: %w", err) return fmt.Errorf("reserve cluster domain in Uncloud DNS: %w", err)
} }
@@ -247,7 +253,7 @@ func initCluster(ctx context.Context, uncli *cli.CLI, remoteMachine *cli.RemoteM
} }
if !opts.noCaddy { if !opts.noCaddy {
d, err := client.NewCaddyDeployment("", "", api.Placement{}) d, err := clusterClient.Caddy.NewDeployment(ctx, client.CaddyDeploymentOptions{})
if err != nil { if err != nil {
return fmt.Errorf("create caddy deployment: %w", err) return fmt.Errorf("create caddy deployment: %w", err)
} }
@@ -263,7 +269,7 @@ func initCluster(ctx context.Context, uncli *cli.CLI, remoteMachine *cli.RemoteM
} }
fmt.Println() fmt.Println()
return caddy.UpdateDomainRecords(ctx, client, uncli.ProgressOut()) return caddy.UpdateDomainRecords(ctx, clusterClient, uncli.ProgressOut())
} }
return nil return nil
+2 -3
View File
@@ -13,10 +13,10 @@ import (
"charm.land/lipgloss/v2/tree" "charm.land/lipgloss/v2/tree"
"github.com/docker/compose/v2/pkg/progress" "github.com/docker/compose/v2/pkg/progress"
"github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/container"
"github.com/psviderski/uncloud/api/pb"
"github.com/psviderski/uncloud/internal/cli" "github.com/psviderski/uncloud/internal/cli"
"github.com/psviderski/uncloud/internal/cli/completion" "github.com/psviderski/uncloud/internal/cli/completion"
"github.com/psviderski/uncloud/internal/cli/tui" "github.com/psviderski/uncloud/internal/cli/tui"
"github.com/psviderski/uncloud/internal/machine/api/pb"
"github.com/psviderski/uncloud/pkg/api" "github.com/psviderski/uncloud/pkg/api"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@@ -136,8 +136,7 @@ func remove(ctx context.Context, uncli *cli.CLI, nameOrID string, opts removeOpt
return fmt.Errorf("confirm removal: %w", err) return fmt.Errorf("confirm removal: %w", err)
} }
if !confirmed { if !confirmed {
fmt.Println("Cancelled. Machine was not removed.") return cli.Cancelled("Cancelled. Machine was not removed.")
return nil
} }
} }
+1 -1
View File
@@ -6,9 +6,9 @@ import (
"net/netip" "net/netip"
"strings" "strings"
"github.com/psviderski/uncloud/api/pb"
"github.com/psviderski/uncloud/internal/cli" "github.com/psviderski/uncloud/internal/cli"
"github.com/psviderski/uncloud/internal/cli/completion" "github.com/psviderski/uncloud/internal/cli/completion"
"github.com/psviderski/uncloud/internal/machine/api/pb"
"github.com/psviderski/uncloud/internal/machine/network" "github.com/psviderski/uncloud/internal/machine/network"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
+35 -2
View File
@@ -24,6 +24,7 @@ import (
"github.com/psviderski/uncloud/internal/machine" "github.com/psviderski/uncloud/internal/machine"
"github.com/psviderski/uncloud/internal/version" "github.com/psviderski/uncloud/internal/version"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/pflag"
) )
type globalOptions struct { type globalOptions struct {
@@ -42,6 +43,13 @@ func main() {
SilenceUsage: true, SilenceUsage: true,
SilenceErrors: true, SilenceErrors: true,
PersistentPreRunE: func(cmd *cobra.Command, args []string) error { PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
// Shell completion runs through the hidden __complete command which has flag parsing disabled,
// so the global flags from the completed command line are never parsed. Apply them manually to make
// completion work with --connect, --context, and --uncloud-config.
if cmd.Name() == cobra.ShellCompRequestCmd {
applyGlobalFlagsFromCompletionArgs(cmd.Root().PersistentFlags(), os.Args[1:])
}
cli.BindEnvToFlag(cmd, "connect", "UNCLOUD_CONNECT") cli.BindEnvToFlag(cmd, "connect", "UNCLOUD_CONNECT")
cli.BindEnvToFlag(cmd, "context", "UNCLOUD_CONTEXT") cli.BindEnvToFlag(cmd, "context", "UNCLOUD_CONTEXT")
cli.BindEnvToFlag(cmd, "uncloud-config", "UNCLOUD_CONFIG") cli.BindEnvToFlag(cmd, "uncloud-config", "UNCLOUD_CONFIG")
@@ -80,10 +88,11 @@ func main() {
configPath := fs.ExpandHomeDir(opts.configPath) configPath := fs.ExpandHomeDir(opts.configPath)
// Make uc connect via the local Unix socket when running on a cluster machine.
if opts.connect == "" { if opts.connect == "" {
if !fs.Exists(configPath) && fs.Exists(machine.DefaultUncloudSockPath) { if !fs.Exists(configPath) && fs.Exists(machine.DefaultClusterAPISockPath) {
conn = &config.MachineConnection{ conn = &config.MachineConnection{
Unix: machine.DefaultUncloudSockPath, Unix: machine.DefaultClusterAPISockPath,
} }
} }
} }
@@ -159,3 +168,27 @@ func main() {
cobra.CheckErr(err) cobra.CheckErr(err)
} }
} }
// applyGlobalFlagsFromCompletionArgs parses the global flags from the raw arguments of a __complete command and applies
// the ones found to flags. The trailing word being completed, unknown flags, and positional arguments are ignored.
func applyGlobalFlagsFromCompletionArgs(flags *pflag.FlagSet, args []string) {
// The shell always passes the word being completed as the last argument, even if it's empty.
// Exclude it from parsing as its value may not be complete yet.
if len(args) == 0 {
return
}
args = args[:len(args)-1]
fset := pflag.NewFlagSet("global", pflag.ContinueOnError)
fset.ParseErrorsAllowlist.UnknownFlags = true
fset.String("connect", "", "")
fset.StringP("context", "c", "", "")
fset.String("uncloud-config", "", "")
// Parsing an incomplete command line may fail, apply the flags parsed so far anyway.
_ = fset.Parse(args)
fset.Visit(func(f *pflag.Flag) {
// Setting the flag marks it as changed so it takes precedence over environment variables.
_ = flags.Set(f.Name, f.Value.String())
})
}
+116
View File
@@ -0,0 +1,116 @@
package main
import (
"slices"
"testing"
"github.com/spf13/pflag"
"github.com/stretchr/testify/assert"
)
func TestApplyGlobalFlagsFromCompletionArgs(t *testing.T) {
defaultConfigPath := "~/.config/uncloud/config.yaml"
tests := []struct {
name string
args []string
wantConnect string
wantContext string
wantConfigPath string
// Flag names expected to be marked as changed on the target flag set.
wantChanged []string
}{
{
name: "no flags",
args: []string{"__complete", "inspect", ""},
},
{
name: "connect with space",
args: []string{"__complete", "--connect", "ssh://user@host", "inspect", ""},
wantConnect: "ssh://user@host",
wantChanged: []string{"connect"},
},
{
name: "connect with equals",
args: []string{"__complete", "--connect=tcp://127.0.0.1:51000", "inspect", ""},
wantConnect: "tcp://127.0.0.1:51000",
wantChanged: []string{"connect"},
},
{
name: "context shorthand",
args: []string{"__complete", "-c", "prod", "inspect", ""},
wantContext: "prod",
wantChanged: []string{"context"},
},
{
name: "all flags",
args: []string{"__complete", "--connect", "user@host", "-c", "prod", "--uncloud-config", "/tmp/uncloud.yaml", "inspect", ""},
wantConnect: "user@host",
wantContext: "prod",
wantConfigPath: "/tmp/uncloud.yaml",
wantChanged: []string{"connect", "context", "uncloud-config"},
},
{
name: "unknown flags are ignored",
args: []string{"__complete", "--quiet", "-n", "5", "--connect", "user@host", "logs", ""},
wantConnect: "user@host",
wantChanged: []string{"connect"},
},
{
name: "flags after double dash are ignored",
args: []string{"__complete", "exec", "svc", "--", "sh", "--connect", "user@host"},
},
{
name: "flags before double dash are applied",
args: []string{"__complete", "--connect", "user@host", "exec", "svc", "--", "sh", "-c", "env"},
wantConnect: "user@host",
wantChanged: []string{"connect"},
},
{
name: "partial flag name being completed is excluded",
args: []string{"__complete", "--connect", "user@host", "inspect", "--context"},
wantConnect: "user@host",
wantChanged: []string{"connect"},
},
{
name: "partial flag value being completed is excluded",
args: []string{"__complete", "--uncloud-config", "/tmp/"},
},
{
name: "partial connect value being completed is excluded",
args: []string{"__complete", "--connect", "tcp://127.0.0.1:5"},
},
{
name: "completed flag value with partial command word",
args: []string{"__complete", "--uncloud-config", "/tmp/uncloud.yaml", "insp"},
wantConfigPath: "/tmp/uncloud.yaml",
wantChanged: []string{"uncloud-config"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Mirror the global persistent flags defined on the root command.
var opts globalOptions
flags := pflag.NewFlagSet("test", pflag.ContinueOnError)
flags.StringVar(&opts.connect, "connect", "", "")
flags.StringVarP(&opts.context, "context", "c", "", "")
flags.StringVar(&opts.configPath, "uncloud-config", defaultConfigPath, "")
applyGlobalFlagsFromCompletionArgs(flags, tt.args)
assert.Equal(t, tt.wantConnect, opts.connect)
assert.Equal(t, tt.wantContext, opts.context)
wantConfigPath := tt.wantConfigPath
if wantConfigPath == "" {
wantConfigPath = defaultConfigPath
}
assert.Equal(t, wantConfigPath, opts.configPath)
for _, name := range []string{"connect", "context", "uncloud-config"} {
assert.Equal(t, slices.Contains(tt.wantChanged, name), flags.Changed(name),
"changed status of flag '%s'", name)
}
})
}
}
+15 -16
View File
@@ -132,35 +132,34 @@ func runProxy(ctx context.Context, uncli *cli.CLI, opts proxyOptions) error {
// endpoint and shuffles the data, *it* will actually experience errors. // endpoint and shuffles the data, *it* will actually experience errors.
remoteAddr := net.JoinHostPort(ip.String(), strconv.Itoa(opts.remotePort)) remoteAddr := net.JoinHostPort(ip.String(), strconv.Itoa(opts.remotePort))
ctx, cancel := context.WithCancel(ctx)
defer cancel()
p := &proxy.Proxy{ p := &proxy.Proxy{
Listener: listener, Listener: listener,
RemoteAddr: remoteAddr, RemoteAddr: remoteAddr,
DialContext: dialer.DialContext, DialContext: dialer.DialContext,
OnError: func(err error) { OnError: func(err error) {
fmt.Printf("Failed to proxy to '%s': %v\n", remoteAddr, err) if proxy.IsConnectionClosedError(err) {
cancel() return
}
// A more actionable error instead of the cryptic [ssh -W] command error.
if strings.Contains(err.Error(), "Session open refused by peer") {
fmt.Printf("Could not connect to '%s': connection refused. "+
"Check that the service is running and listening on port %d inside the container.\n",
remoteAddr, opts.remotePort)
return
}
fmt.Printf("Failed to proxy a connection to '%s': %v\n", remoteAddr, err)
}, },
} }
// Run the proxy in the background and signal when it has fully shut down.
done := make(chan struct{})
go func() {
p.Run(ctx)
close(done)
}()
// Prefix the local address with the scheme for common HTTP ports so it becomes control-clickable in most // Prefix the local address with the scheme for common HTTP ports so it becomes control-clickable in most
// terminals. We assume plain HTTP since TLS is typically terminated by Caddy in front of the service. // terminals. We assume plain HTTP since TLS is typically terminated by Caddy in front of the service.
fmt.Printf("%s%s → %s (%s%s%s)\n", schemeForPort(opts.remotePort), p.Listener.Addr().String(), fmt.Printf("%s%s → %s (%s%s%s)\n", schemeForPort(opts.remotePort), p.Listener.Addr().String(),
remoteAddr, opts.service, tui.Faint.Render("/"), containerID) remoteAddr, opts.service, tui.Faint.Render("/"), containerID)
<-ctx.Done() if err = p.Run(ctx); err != nil {
// Wait for the proxy to drain in-flight connections and shut down gracefully. return fmt.Errorf("run proxy to '%s': %w", remoteAddr, err)
<-done }
return nil return nil
} }
+1 -1
View File
@@ -187,7 +187,7 @@ func printContainers(containers []containerInfo) error {
} }
func collectContainers(ctx context.Context, cli *client.Client) ([]containerInfo, error) { func collectContainers(ctx context.Context, cli *client.Client) ([]containerInfo, error) {
listCtx := cli.ProxyMachinesContext(ctx, nil) listCtx := client.ProxyMachinesContext(ctx, nil)
// List all service containers across all machines in the cluster. // List all service containers across all machines in the cluster.
machineContainers, err := cli.Docker.ListServiceContainers( machineContainers, err := cli.Docker.ListServiceContainers(
+1 -1
View File
@@ -5,7 +5,7 @@ import (
"encoding/json" "encoding/json"
"testing" "testing"
"github.com/psviderski/uncloud/internal/machine/api/pb" "github.com/psviderski/uncloud/api/pb"
"github.com/psviderski/uncloud/internal/machine/docker" "github.com/psviderski/uncloud/internal/machine/docker"
"github.com/psviderski/uncloud/pkg/client" "github.com/psviderski/uncloud/pkg/client"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
+1
View File
@@ -122,6 +122,7 @@ func NewRunCommand(groupID string) *cobra.Command {
"Mount a data volume or host path into service containers. Service containers will be scheduled on the machine(s) where\n"+ "Mount a data volume or host path into service containers. Service containers will be scheduled on the machine(s) where\n"+
"the volume is located. Can be specified multiple times.\n"+ "the volume is located. Can be specified multiple times.\n"+
"Format: volume_name:/container/path[:ro|volume-nocopy] or /host/path:/container/path[:ro]\n"+ "Format: volume_name:/container/path[:ro|volume-nocopy] or /host/path:/container/path[:ro]\n"+
"Host and container paths support runtime templates such as {{.Container.Name}}.\n"+
"Examples:\n"+ "Examples:\n"+
" -v postgres-data:/var/lib/postgresql/data Mount volume 'postgres-data' to /var/lib/postgresql/data in container\n"+ " -v postgres-data:/var/lib/postgresql/data Mount volume 'postgres-data' to /var/lib/postgresql/data in container\n"+
" -v /data/uploads:/app/uploads Bind mount /data/uploads host directory to /app/uploads in container\n"+ " -v /data/uploads:/app/uploads Bind mount /data/uploads host directory to /app/uploads in container\n"+
+4 -2
View File
@@ -30,9 +30,11 @@ func NewScaleCommand(groupID string) *cobra.Command {
Short: "Scale a replicated service by changing the number of replicas.", Short: "Scale a replicated service by changing the number of replicas.",
Long: "Scale a replicated service by changing the number of replicas.", Long: "Scale a replicated service by changing the number of replicas.",
Args: cobra.ExactArgs(2), Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error { PreRunE: func(cmd *cobra.Command, args []string) error {
cli.BindEnvToFlag(cmd, "yes", "UNCLOUD_AUTO_CONFIRM") cli.BindEnvToFlag(cmd, "yes", "UNCLOUD_AUTO_CONFIRM")
return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
uncli := cmd.Context().Value("cli").(*cli.CLI) uncli := cmd.Context().Value("cli").(*cli.CLI)
opts.service = args[0] opts.service = args[0]
+1 -1
View File
@@ -8,9 +8,9 @@ import (
"charm.land/huh/v2" "charm.land/huh/v2"
"github.com/docker/docker/api/types/volume" "github.com/docker/docker/api/types/volume"
"github.com/psviderski/uncloud/api/pb"
"github.com/psviderski/uncloud/internal/cli" "github.com/psviderski/uncloud/internal/cli"
"github.com/psviderski/uncloud/internal/cli/completion" "github.com/psviderski/uncloud/internal/cli/completion"
"github.com/psviderski/uncloud/internal/machine/api/pb"
"github.com/psviderski/uncloud/pkg/api" "github.com/psviderski/uncloud/pkg/api"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
+1 -2
View File
@@ -92,8 +92,7 @@ func remove(ctx context.Context, uncli *cli.CLI, names []string, opts removeOpti
return fmt.Errorf("confirm removal: %w", err) return fmt.Errorf("confirm removal: %w", err)
} }
if !confirmed { if !confirmed {
fmt.Println("Cancelled. No volumes were removed.") return cli.Cancelled("Cancelled. No volumes were removed.")
return nil
} }
} }
+1 -1
View File
@@ -8,10 +8,10 @@ import (
"charm.land/lipgloss/v2" "charm.land/lipgloss/v2"
"github.com/docker/go-units" "github.com/docker/go-units"
"github.com/psviderski/uncloud/api/pb"
"github.com/psviderski/uncloud/internal/cli" "github.com/psviderski/uncloud/internal/cli"
"github.com/psviderski/uncloud/internal/cli/completion" "github.com/psviderski/uncloud/internal/cli/completion"
"github.com/psviderski/uncloud/internal/cli/tui" "github.com/psviderski/uncloud/internal/cli/tui"
"github.com/psviderski/uncloud/internal/machine/api/pb"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes" "golang.zx2c4.com/wireguard/wgctrl/wgtypes"
"google.golang.org/grpc/codes" "google.golang.org/grpc/codes"
+1 -1
View File
@@ -23,7 +23,7 @@ func newDialStdioCommand() *cobra.Command {
}, },
} }
cmd.Flags().StringVar(&socketPath, "socket", machine.DefaultUncloudSockPath, cmd.Flags().StringVar(&socketPath, "socket", machine.DefaultClusterAPISockPath,
"Path to the Uncloud API socket") "Path to the Uncloud API socket")
return cmd return cmd
+27 -22
View File
@@ -1,6 +1,6 @@
module github.com/psviderski/uncloud/experiment module github.com/psviderski/uncloud/experiment
go 1.26 go 1.26.0
require ( require (
github.com/dgraph-io/badger/v3 v3.2103.5 github.com/dgraph-io/badger/v3 v3.2103.5
@@ -18,17 +18,22 @@ require (
github.com/psviderski/uncloud v0.0.0 github.com/psviderski/uncloud v0.0.0
github.com/siderolabs/discovery-api v0.1.4 github.com/siderolabs/discovery-api v0.1.4
github.com/siderolabs/discovery-client v0.1.9 github.com/siderolabs/discovery-client v0.1.9
go.uber.org/zap v1.27.0 go.uber.org/zap v1.28.0
golang.org/x/net v0.59.0
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173
google.golang.org/grpc v1.81.0
) )
require ( require (
github.com/Masterminds/goutils v1.1.1 // indirect github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/semver v1.5.0 // indirect
github.com/Masterminds/semver/v3 v3.4.0 // indirect github.com/Masterminds/semver/v3 v3.4.0 // indirect
github.com/Masterminds/sprig/v3 v3.2.3 // indirect github.com/Masterminds/sprig/v3 v3.2.3 // indirect
github.com/armon/circbuf v0.0.0-20190214190532-5111143e8da2 // indirect github.com/armon/circbuf v0.0.0-20190214190532-5111143e8da2 // indirect
github.com/armon/go-metrics v0.4.1 // indirect github.com/armon/go-metrics v0.4.1 // indirect
github.com/armon/go-radix v1.0.0 // indirect github.com/armon/go-radix v1.0.0 // indirect
github.com/bgentry/speakeasy v0.2.0 // indirect github.com/bgentry/speakeasy v0.2.0 // indirect
github.com/caarlos0/go-version v0.2.2 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cespare/xxhash v1.1.0 // indirect github.com/cespare/xxhash v1.1.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect
@@ -74,18 +79,18 @@ require (
github.com/ishidawataru/sctp v0.0.0-20230406120618-7ff4192f6ff2 // indirect github.com/ishidawataru/sctp v0.0.0-20230406120618-7ff4192f6ff2 // indirect
github.com/jbenet/goprocess v0.1.4 // indirect github.com/jbenet/goprocess v0.1.4 // indirect
github.com/josharian/native v1.1.0 // indirect github.com/josharian/native v1.1.0 // indirect
github.com/klauspost/compress v1.18.0 // indirect github.com/klauspost/compress v1.19.1 // indirect
github.com/klauspost/cpuid/v2 v2.2.9 // indirect github.com/klauspost/cpuid/v2 v2.4.0 // indirect
github.com/libp2p/go-buffer-pool v0.1.0 // indirect github.com/libp2p/go-buffer-pool v0.1.0 // indirect
github.com/libp2p/go-libp2p v0.35.4 // indirect github.com/libp2p/go-libp2p v0.35.4 // indirect
github.com/libp2p/go-libp2p-pubsub v0.11.0 // indirect github.com/libp2p/go-libp2p-pubsub v0.11.0 // indirect
github.com/libp2p/go-msgio v0.3.0 // indirect github.com/libp2p/go-msgio v0.3.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mdlayher/genetlink v1.3.2 // indirect github.com/mdlayher/genetlink v1.3.2 // indirect
github.com/mdlayher/netlink v1.7.2 // indirect github.com/mdlayher/netlink v1.7.2 // indirect
github.com/mdlayher/socket v0.5.1 // indirect github.com/mdlayher/socket v0.5.1 // indirect
github.com/miekg/dns v1.1.65 // indirect github.com/miekg/dns v1.1.73 // indirect
github.com/minio/sha256-simd v1.0.1 // indirect github.com/minio/sha256-simd v1.0.1 // indirect
github.com/mitchellh/cli v1.1.5 // indirect github.com/mitchellh/cli v1.1.5 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect
@@ -108,32 +113,32 @@ require (
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 // indirect github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 // indirect
github.com/shopspring/decimal v1.4.0 // indirect github.com/shopspring/decimal v1.4.0 // indirect
github.com/siderolabs/gen v0.4.8 // indirect github.com/siderolabs/gen v0.4.8 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect github.com/sirupsen/logrus v1.9.4 // indirect
github.com/spaolacci/murmur3 v1.1.0 // indirect github.com/spaolacci/murmur3 v1.1.0 // indirect
github.com/spf13/cast v1.7.0 // indirect github.com/spf13/cast v1.7.0 // indirect
github.com/vishvananda/netlink v1.3.1 // indirect github.com/vishvananda/netlink v1.3.1 // indirect
github.com/vishvananda/netns v0.0.5 // indirect github.com/vishvananda/netns v0.0.5 // indirect
go.opencensus.io v0.24.0 // indirect go.opencensus.io v0.24.0 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/otel v1.36.0 // indirect go.opentelemetry.io/otel v1.43.0 // indirect
go.opentelemetry.io/otel/metric v1.36.0 // indirect go.opentelemetry.io/otel/metric v1.43.0 // indirect
go.opentelemetry.io/otel/trace v1.36.0 // indirect go.opentelemetry.io/otel/trace v1.43.0 // indirect
go.uber.org/multierr v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
golang.org/x/crypto v0.41.0 // indirect golang.org/x/crypto v0.57.0 // indirect
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect
golang.org/x/mod v0.27.0 // indirect golang.org/x/mod v0.41.0 // indirect
golang.org/x/net v0.43.0 // indirect golang.org/x/sync v0.23.0 // indirect
golang.org/x/sync v0.19.0 // indirect golang.org/x/sys v0.48.0 // indirect
golang.org/x/sys v0.42.0 // indirect golang.org/x/text v0.42.0 // indirect
golang.org/x/text v0.28.0 // indirect golang.org/x/time v0.16.0 // indirect
golang.org/x/tools v0.36.0 // indirect golang.org/x/tools v0.50.0 // indirect
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 // indirect golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6 // indirect golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260427160629-7cedc36a6bc4 // indirect
google.golang.org/grpc v1.74.2 // indirect google.golang.org/protobuf v1.36.12 // indirect
google.golang.org/protobuf v1.36.9 // indirect
gotest.tools/v3 v3.5.2 // indirect gotest.tools/v3 v3.5.2 // indirect
gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259 // indirect
lukechampine.com/blake3 v1.3.0 // indirect lukechampine.com/blake3 v1.3.0 // indirect
) )
+35 -4
View File
@@ -5,6 +5,8 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03
github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww=
github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
@@ -38,6 +40,8 @@ github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6r
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/bgentry/speakeasy v0.2.0 h1:tgObeVOf8WAvtuAX6DhJ4xks4CFNwPDZiqzGqIHE51E= github.com/bgentry/speakeasy v0.2.0 h1:tgObeVOf8WAvtuAX6DhJ4xks4CFNwPDZiqzGqIHE51E=
github.com/bgentry/speakeasy v0.2.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bgentry/speakeasy v0.2.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/caarlos0/go-version v0.2.2 h1:5r+nlrg4H2wOVwWjqRqRRIRbZ7ytRmjC9xoMIP0a5kQ=
github.com/caarlos0/go-version v0.2.2/go.mod h1:X+rI5VAtJDpcjCjeEIXpxGa5+rTcgur1FK66wS0/944=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
@@ -298,8 +302,10 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o
github.com/klauspost/compress v1.12.3/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= github.com/klauspost/compress v1.12.3/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/klauspost/compress v1.19.1/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY= github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY=
github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8= github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8=
github.com/klauspost/cpuid/v2 v2.4.0/go.mod h1:19jmZ9mjzoF//ddRSUsv0zfBTJWh3QJh9FNxZTMrGxU=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/koron/go-ssdp v0.0.4 h1:1IDwrghSKYM7yLf7XCzbByg2sJ/JcNOZRXS2jczTwz0= github.com/koron/go-ssdp v0.0.4 h1:1IDwrghSKYM7yLf7XCzbByg2sJ/JcNOZRXS2jczTwz0=
github.com/koron/go-ssdp v0.0.4/go.mod h1:oDXq+E5IL5q0U8uSBcoAXzTzInwy5lEgC91HoKtbmZk= github.com/koron/go-ssdp v0.0.4/go.mod h1:oDXq+E5IL5q0U8uSBcoAXzTzInwy5lEgC91HoKtbmZk=
@@ -345,6 +351,7 @@ github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVc
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
@@ -364,6 +371,7 @@ github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKju
github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI= github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI=
github.com/miekg/dns v1.1.65 h1:0+tIPHzUW0GCge7IiK3guGP57VAw7hoPDfApjkMD1Fc= github.com/miekg/dns v1.1.65 h1:0+tIPHzUW0GCge7IiK3guGP57VAw7hoPDfApjkMD1Fc=
github.com/miekg/dns v1.1.65/go.mod h1:Dzw9769uoKVaLuODMDZz9M6ynFU6Em65csPuoi8G0ck= github.com/miekg/dns v1.1.65/go.mod h1:Dzw9769uoKVaLuODMDZz9M6ynFU6Em65csPuoi8G0ck=
github.com/miekg/dns v1.1.73/go.mod h1:RW2Obtfd5NZHvOFe3zYG0W8koWOQtAzyHaLo8vASBuQ=
github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721 h1:RlZweED6sbSArvlE924+mUcZuXKLBHA35U7LN621Bws= github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721 h1:RlZweED6sbSArvlE924+mUcZuXKLBHA35U7LN621Bws=
github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721/go.mod h1:Ickgr2WtCLZ2MDGd4Gr0geeCH5HybhRJbonOgQpvSxc= github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721/go.mod h1:Ickgr2WtCLZ2MDGd4Gr0geeCH5HybhRJbonOgQpvSxc=
github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b h1:z78hV3sbSMAUoyUMM0I83AUIT6Hu17AWfgjzIbtrYFc= github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b h1:z78hV3sbSMAUoyUMM0I83AUIT6Hu17AWfgjzIbtrYFc=
@@ -525,6 +533,7 @@ github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPx
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
github.com/smartystreets/assertions v1.2.0 h1:42S6lae5dvLc7BrLu/0ugRtcFVjoJNMC/N3yZFZkDFs= github.com/smartystreets/assertions v1.2.0 h1:42S6lae5dvLc7BrLu/0ugRtcFVjoJNMC/N3yZFZkDFs=
github.com/smartystreets/assertions v1.2.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo= github.com/smartystreets/assertions v1.2.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo=
github.com/smartystreets/goconvey v1.7.2 h1:9RBaZCeXEQ3UselpuwUQHltGVXvdwm6cv1hgR6gDIPg= github.com/smartystreets/goconvey v1.7.2 h1:9RBaZCeXEQ3UselpuwUQHltGVXvdwm6cv1hgR6gDIPg=
@@ -577,16 +586,20 @@ go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg= go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg=
go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E= go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E=
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE= go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE=
go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs= go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs=
go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs= go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs=
go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY= go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY=
go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFwHX4dThiPis= go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFwHX4dThiPis=
go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4= go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4=
go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w= go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w=
go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA= go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA=
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/dig v1.17.1 h1:Tga8Lz8PcYNsWsyHMZ1Vm0OQOUaJNDyvPImgbAu9YSc= go.uber.org/dig v1.17.1 h1:Tga8Lz8PcYNsWsyHMZ1Vm0OQOUaJNDyvPImgbAu9YSc=
go.uber.org/dig v1.17.1/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE= go.uber.org/dig v1.17.1/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE=
@@ -603,6 +616,7 @@ go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN8
go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI= go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
go.uber.org/zap v1.28.0/go.mod h1:rDLpOi171uODNm/mxFcuYWxDsqWSAVkFdX4XojSKg/Q=
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M= go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M=
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y= go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
@@ -618,6 +632,7 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y
golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM= golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM=
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8= golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8=
@@ -631,6 +646,7 @@ golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ= golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
golang.org/x/mod v0.41.0/go.mod h1:Ek9pY8RKWXwsWvd3rQiHYtMqkjSUV+s1Rj7j4H5Ur6o=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -650,6 +666,7 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
golang.org/x/net v0.59.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -660,8 +677,9 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -694,8 +712,9 @@ golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
@@ -707,6 +726,10 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E=
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/time v0.16.0/go.mod h1:rVKOqvZeKvrDKTQiAHJ7wmwP0RzleSphoEA9RcdLA0s=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
@@ -721,10 +744,13 @@ golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
golang.org/x/tools v0.50.0/go.mod h1:7ulVMw3831Mwi5EZD6RomGyffr4VFjuNYXf2BbCEAV0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 h1:/jFs0duh4rdb8uIfPMv78iAJGcPKDeqAFnaLBropIC4= golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 h1:/jFs0duh4rdb8uIfPMv78iAJGcPKDeqAFnaLBropIC4=
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173/go.mod h1:tkCQ4FQXmpAgYVh++1cq16/dH4QJtmvpRv19DWGAHSA= golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173/go.mod h1:tkCQ4FQXmpAgYVh++1cq16/dH4QJtmvpRv19DWGAHSA=
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6 h1:CawjfCvYQH2OU3/TnxLx97WDSUDRABfT18pCOYwc2GE= golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6 h1:CawjfCvYQH2OU3/TnxLx97WDSUDRABfT18pCOYwc2GE=
@@ -737,6 +763,7 @@ google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a h1:v2PbRU4K3llS09c7zodFpNePeamkAwG3mPrAery9VeE= google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a h1:v2PbRU4K3llS09c7zodFpNePeamkAwG3mPrAery9VeE=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260427160629-7cedc36a6bc4/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
@@ -745,6 +772,7 @@ google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.74.2 h1:WoosgB65DlWVC9FqI82dGsZhWFNBSLjQ84bjROOpMu4= google.golang.org/grpc v1.74.2 h1:WoosgB65DlWVC9FqI82dGsZhWFNBSLjQ84bjROOpMu4=
google.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM= google.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM=
google.golang.org/grpc v1.81.0/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
@@ -756,6 +784,7 @@ google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpAD
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw= google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@@ -776,6 +805,8 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259 h1:TbRPT0HtzFP3Cno1zZo7yPzEEnfu8EjLfl6IU9VfqkQ=
gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259/go.mod h1:AVgIgHMwK63XvmAzWG9vLQ41YnVHN0du0tEC46fI7yY=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
lukechampine.com/blake3 v1.3.0 h1:sJ3XhFINmHSrYCgl958hscfIa3bw8x4DqMP3u1YvoYE= lukechampine.com/blake3 v1.3.0 h1:sJ3XhFINmHSrYCgl958hscfIa3bw8x4DqMP3u1YvoYE=
@@ -1,4 +1,4 @@
package connector package wg
import ( import (
"context" "context"
@@ -7,39 +7,48 @@ import (
"net/netip" "net/netip"
"strconv" "strconv"
"github.com/psviderski/uncloud/internal/cli/config"
"github.com/psviderski/uncloud/internal/grpcversion" "github.com/psviderski/uncloud/internal/grpcversion"
"github.com/psviderski/uncloud/internal/machine/constants" "github.com/psviderski/uncloud/internal/machine/constants"
"github.com/psviderski/uncloud/internal/machine/network" "github.com/psviderski/uncloud/internal/machine/network"
"github.com/psviderski/uncloud/internal/machine/network/tunnel" "github.com/psviderski/uncloud/internal/secret"
"github.com/psviderski/uncloud/pkg/client"
"golang.org/x/net/proxy" "golang.org/x/net/proxy"
"google.golang.org/grpc" "google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure" "google.golang.org/grpc/credentials/insecure"
) )
// Machine describes a remote machine that the experimental connector can reach.
type Machine struct {
Host string
PublicKey secret.Secret
}
type User interface {
ManagementIP() netip.Addr
PrivateKey() secret.Secret
}
// WireGuardConnector establishes a connection to the cluster API through a WireGuard tunnel // WireGuardConnector establishes a connection to the cluster API through a WireGuard tunnel
// to one of the cluster machines. // to one of the cluster machines.
type WireGuardConnector struct { type WireGuardConnector struct {
user *client.User user User
machines []config.MachineConnection machines []Machine
tun *tunnel.Tunnel tun *Tunnel
} }
func NewWireGuardConnector(user *client.User, machines []config.MachineConnection) *WireGuardConnector { func NewWireGuardConnector(user User, machines []Machine) *WireGuardConnector {
return &WireGuardConnector{ return &WireGuardConnector{
user: user, user: user,
machines: machines, machines: machines,
} }
} }
// TODO: handle context cancelation. // TODO: handle context cancellation.
func (c *WireGuardConnector) Connect(ctx context.Context) (*grpc.ClientConn, error) { func (c *WireGuardConnector) Connect(ctx context.Context) (*grpc.ClientConn, error) {
if len(c.machines) == 0 { if len(c.machines) == 0 {
return nil, fmt.Errorf("no machines to connect to") return nil, fmt.Errorf("no machines to connect to")
} }
// TODO: iterate over machines and try to connect to each one until successful. // TODO: iterate over machines and try to connect to each one until successful.
// For now, try to connect to only the first machine. // For now, try to connect to only the first machine.
machine := c.machines[0] machine := c.machines[0]
endpointIPs, err := net.LookupIP(machine.Host) endpointIPs, err := net.LookupIP(machine.Host)
if err != nil { if err != nil {
@@ -49,25 +58,24 @@ func (c *WireGuardConnector) Connect(ctx context.Context) (*grpc.ClientConn, err
if err != nil { if err != nil {
return nil, fmt.Errorf("parse IP address %q: %w", endpointIPs[0].String(), err) return nil, fmt.Errorf("parse IP address %q: %w", endpointIPs[0].String(), err)
} }
endpoint := netip.AddrPortFrom(endpointAddr, tunnel.DefaultEndpointPort) endpoint := netip.AddrPortFrom(endpointAddr, DefaultEndpointPort)
machineManagementIP := network.ManagementIP(machine.PublicKey) machineManagementIP := network.ManagementIP(machine.PublicKey)
machineAPIAddr := net.JoinHostPort(machineManagementIP.String(), strconv.Itoa(constants.MachineAPIPort)) machineAPIAddr := net.JoinHostPort(machineManagementIP.String(), strconv.Itoa(constants.UncloudAPIPort))
tunCfg := &tunnel.Config{ tunCfg := &Config{
LocalAddress: c.user.ManagementIP(), LocalAddress: c.user.ManagementIP(),
LocalPrivateKey: c.user.PrivateKey(), LocalPrivateKey: c.user.PrivateKey(),
RemotePublicKey: machine.PublicKey, RemotePublicKey: machine.PublicKey,
RemoteNetwork: netip.PrefixFrom(machineManagementIP, 128), RemoteNetwork: netip.PrefixFrom(machineManagementIP, 128),
Endpoint: endpoint, Endpoint: endpoint,
} }
if c.tun, err = tunnel.Connect(tunCfg); err != nil { if c.tun, err = Connect(tunCfg); err != nil {
return nil, fmt.Errorf("establish WireGuard tunnel to %q: %w", endpoint, err) return nil, fmt.Errorf("establish WireGuard tunnel to %q: %w", endpoint, err)
} }
conn, err := grpc.NewClient( conn, err := grpc.NewClient(
machineAPIAddr, machineAPIAddr,
grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithTransportCredentials(insecure.NewCredentials()),
grpc.WithDefaultServiceConfig(defaultServiceConfig),
grpc.WithContextDialer(func(ctx context.Context, addr string) (net.Conn, error) { grpc.WithContextDialer(func(ctx context.Context, addr string) (net.Conn, error) {
return c.tun.DialContext(ctx, "tcp", addr) return c.tun.DialContext(ctx, "tcp", addr)
}), }),
@@ -1,4 +1,4 @@
package tunnel package wg
import ( import (
"context" "context"
@@ -15,7 +15,7 @@ import (
const ( const (
DefaultEndpointPort = 51820 DefaultEndpointPort = 51820
// DefaultKeepaliveInterval is sensible interval that works with a wide variety of firewalls. // DefaultKeepaliveInterval is a sensible interval that works with a wide variety of firewalls.
DefaultKeepaliveInterval = 25 * time.Second DefaultKeepaliveInterval = 25 * time.Second
) )
+96 -155
View File
@@ -1,18 +1,19 @@
module github.com/psviderski/uncloud module github.com/psviderski/uncloud
go 1.26 go 1.26.0
require ( require (
charm.land/bubbles/v2 v2.0.0 charm.land/bubbles/v2 v2.0.0
charm.land/bubbletea/v2 v2.0.2 charm.land/bubbletea/v2 v2.0.10
charm.land/huh/v2 v2.0.1 charm.land/huh/v2 v2.0.1
charm.land/lipgloss/v2 v2.0.1 charm.land/lipgloss/v2 v2.0.1
github.com/BurntSushi/toml v1.5.0 github.com/BurntSushi/toml v1.6.0
github.com/Masterminds/semver v1.5.0 github.com/Masterminds/semver v1.5.0
github.com/Masterminds/squirrel v1.5.4 github.com/Masterminds/squirrel v1.5.4
github.com/alecthomas/chroma/v2 v2.20.0 github.com/alecthomas/chroma/v2 v2.24.1
github.com/caarlos0/go-version v0.2.2 github.com/caarlos0/go-version v0.2.2
github.com/caddyserver/caddy/v2 v2.8.4 github.com/caddyserver/caddy/v2 v2.11.4
github.com/caddyserver/certmagic v0.25.4
github.com/cenkalti/backoff/v4 v4.3.0 github.com/cenkalti/backoff/v4 v4.3.0
github.com/charmbracelet/colorprofile v0.4.3 github.com/charmbracelet/colorprofile v0.4.3
github.com/charmbracelet/x/ansi v0.11.7 github.com/charmbracelet/x/ansi v0.11.7
@@ -33,78 +34,69 @@ require (
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/jmoiron/sqlx v1.4.0 github.com/jmoiron/sqlx v1.4.0
github.com/mattn/go-shellwords v1.0.12 github.com/mattn/go-shellwords v1.0.12
github.com/miekg/dns v1.1.65 github.com/mholt/acmez/v3 v3.1.6
github.com/miekg/dns v1.1.73
github.com/mitchellh/mapstructure v1.5.0 github.com/mitchellh/mapstructure v1.5.0
github.com/moby/term v0.5.2 github.com/moby/term v0.5.2
github.com/opencontainers/go-digest v1.0.0 github.com/opencontainers/go-digest v1.0.0
github.com/opencontainers/image-spec v1.1.1 github.com/opencontainers/image-spec v1.1.1
github.com/prometheus/client_golang v1.22.0 github.com/prometheus/client_golang v1.24.1
github.com/psviderski/unregistry v0.4.1 github.com/psviderski/unregistry v0.4.1
github.com/siderolabs/grpc-proxy v0.5.1 github.com/siderolabs/grpc-proxy v0.5.1
github.com/spf13/cobra v1.10.1 github.com/spf13/cobra v1.10.2
github.com/spf13/pflag v1.0.10 github.com/spf13/pflag v1.0.10
github.com/stretchr/testify v1.11.1 github.com/stretchr/testify v1.12.1
github.com/vishvananda/netlink v1.3.1 github.com/vishvananda/netlink v1.3.1
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba go4.org/netipx v0.0.0-20231129151722-fdeea329fbba
golang.org/x/crypto v0.41.0 golang.org/x/crypto v0.57.0
golang.org/x/net v0.43.0 golang.org/x/net v0.59.0
golang.org/x/sync v0.20.0 golang.org/x/sync v0.23.0
golang.org/x/sys v0.45.0 golang.org/x/sys v0.48.0
golang.org/x/term v0.34.0 golang.org/x/term v0.46.0
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6 golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6
google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a google.golang.org/genproto/googleapis/rpc v0.0.0-20260427160629-7cedc36a6bc4
google.golang.org/grpc v1.74.2 google.golang.org/grpc v1.81.0
google.golang.org/protobuf v1.36.9 google.golang.org/protobuf v1.36.12
modernc.org/sqlite v1.36.3 modernc.org/sqlite v1.49.1
tags.cncf.io/container-device-interface v1.0.1 tags.cncf.io/container-device-interface v1.0.1
) )
require ( require (
cloud.google.com/go/kms v1.19.0 // indirect
cloud.google.com/go/longrunning v0.6.0 // indirect
filippo.io/edwards25519 v1.1.0 // indirect
github.com/AlecAivazis/survey/v2 v2.3.7 // indirect github.com/AlecAivazis/survey/v2 v2.3.7 // indirect
github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 // indirect
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
github.com/DefangLabs/secret-detector v0.0.0-20250403165618-22662109213e // indirect github.com/DefangLabs/secret-detector v0.0.0-20250403165618-22662109213e // indirect
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/semver/v3 v3.4.0 // indirect github.com/Masterminds/semver/v3 v3.4.0 // indirect
github.com/Masterminds/sprig/v3 v3.2.3 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/Microsoft/hcsshim v0.13.0 // indirect github.com/Microsoft/hcsshim v0.13.0 // indirect
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect
github.com/antlr4-go/antlr/v4 v4.13.0 // indirect
github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b // indirect
github.com/atotto/clipboard v0.1.4 // indirect github.com/atotto/clipboard v0.1.4 // indirect
github.com/aws/aws-sdk-go-v2 v1.30.3 // indirect github.com/aws/aws-sdk-go-v2 v1.41.7 // indirect
github.com/aws/aws-sdk-go-v2/config v1.27.27 // indirect github.com/aws/aws-sdk-go-v2/config v1.32.17 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.17.27 // indirect github.com/aws/aws-sdk-go-v2/credentials v1.19.16 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11 // indirect github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.23 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.15 // indirect github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.23 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.15 // indirect github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.23 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 // indirect github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.24 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3 // indirect github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.9 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17 // indirect github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.23 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.22.4 // indirect github.com/aws/aws-sdk-go-v2/service/signin v1.0.11 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4 // indirect github.com/aws/aws-sdk-go-v2/service/sso v1.30.17 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.30.3 // indirect github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.21 // indirect
github.com/aws/smithy-go v1.20.3 // indirect github.com/aws/aws-sdk-go-v2/service/sts v1.42.1 // indirect
github.com/aws/smithy-go v1.25.1 // indirect
github.com/beorn7/perks v1.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect
github.com/buger/goterm v1.0.4 // indirect github.com/buger/goterm v1.0.4 // indirect
github.com/caddyserver/certmagic v0.21.4 // indirect github.com/caddyserver/zerossl v0.1.5 // indirect
github.com/caddyserver/zerossl v0.1.3 // indirect
github.com/catppuccin/go v0.3.0 // indirect github.com/catppuccin/go v0.3.0 // indirect
github.com/cespare/xxhash v1.1.0 // indirect github.com/cenkalti/backoff/v5 v5.0.3 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/charmbracelet/ultraviolet v0.0.0-20260525132238-948f4557a654 // indirect github.com/charmbracelet/ultraviolet v0.0.0-20260703014108-f5a850f9c2b7 // indirect
github.com/charmbracelet/x/exp/ordered v0.1.0 // indirect github.com/charmbracelet/x/exp/ordered v0.1.0 // indirect
github.com/charmbracelet/x/exp/strings v0.0.0-20240919170804-a4978c8e603a // indirect github.com/charmbracelet/x/exp/strings v0.0.0-20240919170804-a4978c8e603a // indirect
github.com/charmbracelet/x/term v0.2.2 // indirect github.com/charmbracelet/x/term v0.2.2 // indirect
github.com/charmbracelet/x/termios v0.1.1 // indirect github.com/charmbracelet/x/termios v0.1.1 // indirect
github.com/charmbracelet/x/windows v0.2.2 // indirect github.com/charmbracelet/x/windows v0.2.2 // indirect
github.com/chzyer/readline v1.5.1 // indirect
github.com/clipperhouse/displaywidth v0.11.0 // indirect github.com/clipperhouse/displaywidth v0.11.0 // indirect
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
github.com/containerd/cgroups/v3 v3.0.5 // indirect github.com/containerd/cgroups/v3 v3.0.5 // indirect
@@ -121,13 +113,9 @@ require (
github.com/containerd/typeurl/v2 v2.2.3 // indirect github.com/containerd/typeurl/v2 v2.2.3 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/dgraph-io/badger v1.6.2 // indirect
github.com/dgraph-io/badger/v2 v2.2007.4 // indirect
github.com/dgraph-io/ristretto v0.1.1 // indirect
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/distribution/distribution/v3 v3.0.0 // indirect github.com/distribution/distribution/v3 v3.0.0 // indirect
github.com/dlclark/regexp2 v1.11.5 // indirect github.com/dlclark/regexp2 v1.12.0 // indirect
github.com/docker/buildx v0.29.1 // indirect github.com/docker/buildx v0.29.1 // indirect
github.com/docker/cli-docs-tool v0.10.0 // indirect github.com/docker/cli-docs-tool v0.10.0 // indirect
github.com/docker/distribution v2.8.3+incompatible // indirect github.com/docker/distribution v2.8.3+incompatible // indirect
@@ -141,84 +129,59 @@ require (
github.com/felixge/httpsnoop v1.0.4 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fsnotify/fsevents v0.2.0 // indirect github.com/fsnotify/fsevents v0.2.0 // indirect
github.com/fvbommel/sortorder v1.1.0 // indirect github.com/fvbommel/sortorder v1.1.0 // indirect
github.com/fxamacker/cbor/v2 v2.7.0 // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect
github.com/go-jose/go-jose/v3 v3.0.3 // indirect
github.com/go-kit/kit v0.13.0 // indirect
github.com/go-kit/log v0.2.1 // indirect
github.com/go-logfmt/logfmt v0.6.0 // indirect
github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/jsonpointer v0.21.0 // indirect github.com/go-openapi/jsonpointer v0.21.0 // indirect
github.com/go-openapi/jsonreference v0.20.2 // indirect github.com/go-openapi/jsonreference v0.20.2 // indirect
github.com/go-openapi/swag v0.23.0 // indirect github.com/go-openapi/swag v0.23.0 // indirect
github.com/go-sql-driver/mysql v1.8.1 // indirect
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/godbus/dbus/v5 v5.1.0 // indirect github.com/godbus/dbus/v5 v5.1.0 // indirect
github.com/gofrs/flock v0.12.1 // indirect github.com/gofrs/flock v0.12.1 // indirect
github.com/gogo/protobuf v1.3.2 // indirect github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang-jwt/jwt/v5 v5.2.2 // indirect github.com/golang-jwt/jwt/v5 v5.3.1 // indirect
github.com/golang/glog v1.2.5 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.4 // indirect github.com/golang/protobuf v1.5.4 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/google/btree v1.1.2 // indirect
github.com/google/cel-go v0.20.1 // indirect
github.com/google/gnostic-models v0.6.8 // indirect github.com/google/gnostic-models v0.6.8 // indirect
github.com/google/gofuzz v1.2.0 // indirect github.com/google/gofuzz v1.2.0 // indirect
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/gorilla/handlers v1.5.2 // indirect github.com/gorilla/handlers v1.5.2 // indirect
github.com/gorilla/mux v1.8.1 // indirect github.com/gorilla/mux v1.8.1 // indirect
github.com/gorilla/websocket v1.5.3 // indirect github.com/gorilla/websocket v1.5.3 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/go-version v1.7.0 // indirect github.com/hashicorp/go-version v1.7.0 // indirect
github.com/hashicorp/golang-lru/arc/v2 v2.0.7 // indirect github.com/hashicorp/golang-lru/arc/v2 v2.0.7 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/huandu/xstrings v1.5.0 // indirect github.com/hashicorp/hcl v1.0.1-vault-7 // indirect
github.com/imdario/mergo v0.3.16 // indirect
github.com/in-toto/in-toto-golang v0.9.0 // indirect github.com/in-toto/in-toto-golang v0.9.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/inhies/go-bytesize v0.0.0-20220417184213-4913239db9cf // indirect github.com/inhies/go-bytesize v0.0.0-20220417184213-4913239db9cf // indirect
github.com/ishidawataru/sctp v0.0.0-20230406120618-7ff4192f6ff2 // indirect github.com/ishidawataru/sctp v0.0.0-20230406120618-7ff4192f6ff2 // indirect
github.com/jackc/chunkreader/v2 v2.0.1 // indirect
github.com/jackc/pgconn v1.14.3 // indirect
github.com/jackc/pgio v1.0.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgproto3/v2 v2.3.3 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/jackc/pgtype v1.14.0 // indirect
github.com/jackc/pgx/v4 v4.18.3 // indirect
github.com/jonboulle/clockwork v0.5.0 // indirect github.com/jonboulle/clockwork v0.5.0 // indirect
github.com/josharian/intern v1.0.0 // indirect github.com/josharian/intern v1.0.0 // indirect
github.com/josharian/native v1.1.0 // indirect github.com/josharian/native v1.1.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect github.com/json-iterator/go v1.1.12 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/klauspost/compress v1.18.0 // indirect github.com/klauspost/compress v1.19.1 // indirect
github.com/klauspost/cpuid/v2 v2.2.9 // indirect github.com/klauspost/cpuid/v2 v2.4.0 // indirect
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect
github.com/libdns/libdns v0.2.2 // indirect github.com/libdns/libdns v1.1.1 // indirect
github.com/lucasb-eyer/go-colorful v1.4.0 // indirect github.com/lucasb-eyer/go-colorful v1.4.0 // indirect
github.com/mailru/easyjson v0.7.7 // indirect github.com/mailru/easyjson v0.7.7 // indirect
github.com/manifoldco/promptui v0.9.0 // indirect github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.23 // indirect github.com/mattn/go-runewidth v0.0.23 // indirect
github.com/mdlayher/genetlink v1.3.2 // indirect github.com/mdlayher/genetlink v1.3.2 // indirect
github.com/mdlayher/netlink v1.7.2 // indirect github.com/mdlayher/netlink v1.7.2 // indirect
github.com/mdlayher/socket v0.5.1 // indirect github.com/mdlayher/socket v0.5.1 // indirect
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
github.com/mholt/acmez/v2 v2.0.3 // indirect github.com/miekg/pkcs11 v1.1.2 // indirect
github.com/miekg/pkcs11 v1.1.1 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/go-ps v1.0.0 // indirect
github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/moby/buildkit v0.25.0 // indirect github.com/moby/buildkit v0.25.0 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/go-archive v0.1.0 // indirect github.com/moby/go-archive v0.1.0 // indirect
@@ -239,45 +202,36 @@ require (
github.com/muesli/cancelreader v0.2.2 // indirect github.com/muesli/cancelreader v0.2.2 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect github.com/ncruces/go-strftime v1.0.0 // indirect
github.com/onsi/ginkgo/v2 v2.22.0 // indirect github.com/onsi/ginkgo/v2 v2.22.0 // indirect
github.com/opencontainers/runtime-spec v1.2.1 // indirect github.com/opencontainers/runtime-spec v1.2.1 // indirect
github.com/opencontainers/selinux v1.12.0 // indirect github.com/opencontainers/selinux v1.12.0 // indirect
github.com/opentracing/opentracing-go v1.2.0 // indirect
github.com/pelletier/go-toml v1.9.5 // indirect github.com/pelletier/go-toml v1.9.5 // indirect
github.com/pires/go-proxyproto v0.7.0 // indirect
github.com/pkg/errors v0.9.1 // indirect github.com/pkg/errors v0.9.1 // indirect
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_model v0.6.3 // indirect
github.com/prometheus/client_model v0.6.1 // indirect github.com/prometheus/common v0.71.0 // indirect
github.com/prometheus/common v0.62.0 // indirect github.com/prometheus/otlptranslator v1.0.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect github.com/prometheus/procfs v0.22.0 // indirect
github.com/quic-go/qpack v0.5.1 // indirect github.com/quic-go/qpack v0.6.0 // indirect
github.com/quic-go/quic-go v0.48.2 // indirect github.com/quic-go/quic-go v0.62.0 // indirect
github.com/redis/go-redis/extra/rediscmd/v9 v9.0.5 // indirect github.com/redis/go-redis/extra/rediscmd/v9 v9.0.5 // indirect
github.com/redis/go-redis/extra/redisotel/v9 v9.0.5 // indirect github.com/redis/go-redis/extra/redisotel/v9 v9.0.5 // indirect
github.com/redis/go-redis/v9 v9.7.3 // indirect github.com/redis/go-redis/v9 v9.7.3 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rivo/uniseg v0.4.7 // indirect github.com/rivo/uniseg v0.4.7 // indirect
github.com/rs/xid v1.5.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/santhosh-tekuri/jsonschema/v6 v6.0.1 // indirect github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect
github.com/secure-systems-lab/go-securesystemslib v0.6.0 // indirect github.com/secure-systems-lab/go-securesystemslib v0.6.0 // indirect
github.com/serialx/hashring v0.0.0-20200727003509-22c0c7ab6b1b // indirect github.com/serialx/hashring v0.0.0-20200727003509-22c0c7ab6b1b // indirect
github.com/shibumi/go-pathspec v1.3.0 // indirect github.com/shibumi/go-pathspec v1.3.0 // indirect
github.com/shopspring/decimal v1.4.0 // indirect github.com/sirupsen/logrus v1.9.4 // indirect
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 // indirect github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 // indirect
github.com/slackhq/nebula v1.6.1 // indirect github.com/spf13/afero v1.14.0 // indirect
github.com/smallstep/certificates v0.26.1 // indirect github.com/spf13/viper v1.12.0 // indirect
github.com/smallstep/nosql v0.6.1 // indirect github.com/stretchr/objx v0.5.3 // indirect
github.com/smallstep/pkcs7 v0.1.1 // indirect github.com/subosito/gotenv v1.4.1 // indirect
github.com/smallstep/scep v0.0.0-20231024192529-aee96d7ad34d // indirect
github.com/smallstep/truststore v0.13.0 // indirect
github.com/spf13/cast v1.7.0 // indirect
github.com/stoewer/go-strcase v1.2.0 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/tailscale/tscert v0.0.0-20240517230440-bbccfbf48933 // indirect
github.com/theupdateframework/notary v0.7.0 // indirect github.com/theupdateframework/notary v0.7.0 // indirect
github.com/tilt-dev/fsnotify v1.4.8-0.20220602155310-fff9c274a375 // indirect github.com/tilt-dev/fsnotify v1.4.8-0.20220602155310-fff9c274a375 // indirect
github.com/tonistiigi/dchapes-mode v0.0.0-20250318174251-73d941a28323 // indirect github.com/tonistiigi/dchapes-mode v0.0.0-20250318174251-73d941a28323 // indirect
@@ -285,7 +239,6 @@ require (
github.com/tonistiigi/go-csvvalue v0.0.0-20240814133006-030d3b2625d0 // indirect github.com/tonistiigi/go-csvvalue v0.0.0-20240814133006-030d3b2625d0 // indirect
github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea // indirect github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea // indirect
github.com/tonistiigi/vt100 v0.0.0-20240514184818-90bafcd6abab // indirect github.com/tonistiigi/vt100 v0.0.0-20240514184818-90bafcd6abab // indirect
github.com/urfave/cli v1.22.17 // indirect
github.com/vbatts/tar-split v0.12.1 // indirect github.com/vbatts/tar-split v0.12.1 // indirect
github.com/vishvananda/netns v0.0.5 // indirect github.com/vishvananda/netns v0.0.5 // indirect
github.com/x448/float16 v0.8.4 // indirect github.com/x448/float16 v0.8.4 // indirect
@@ -293,70 +246,58 @@ require (
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
github.com/zclconf/go-cty v1.17.0 // indirect github.com/zclconf/go-cty v1.17.0 // indirect
github.com/zeebo/blake3 v0.2.4 // indirect github.com/zeebo/blake3 v0.2.4 // indirect
go.etcd.io/bbolt v1.4.3 // indirect
go.opencensus.io v0.24.0 // indirect go.opencensus.io v0.24.0 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/bridges/prometheus v0.57.0 // indirect go.opentelemetry.io/contrib/bridges/prometheus v0.68.0 // indirect
go.opentelemetry.io/contrib/exporters/autoexport v0.57.0 // indirect go.opentelemetry.io/contrib/exporters/autoexport v0.68.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.60.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.60.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 // indirect
go.opentelemetry.io/otel v1.36.0 // indirect go.opentelemetry.io/otel v1.43.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.8.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.19.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.8.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.19.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.35.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.43.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.35.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.43.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 // indirect
go.opentelemetry.io/otel/exporters/prometheus v0.54.0 // indirect go.opentelemetry.io/otel/exporters/prometheus v0.65.0 // indirect
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.8.0 // indirect go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.19.0 // indirect
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.32.0 // indirect go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.43.0 // indirect
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.32.0 // indirect go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.43.0 // indirect
go.opentelemetry.io/otel/log v0.8.0 // indirect go.opentelemetry.io/otel/log v0.19.0 // indirect
go.opentelemetry.io/otel/metric v1.36.0 // indirect go.opentelemetry.io/otel/metric v1.43.0 // indirect
go.opentelemetry.io/otel/sdk v1.36.0 // indirect go.opentelemetry.io/otel/sdk v1.43.0 // indirect
go.opentelemetry.io/otel/sdk/log v0.8.0 // indirect go.opentelemetry.io/otel/sdk/log v0.19.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.36.0 // indirect go.opentelemetry.io/otel/sdk/metric v1.43.0 // indirect
go.opentelemetry.io/otel/trace v1.36.0 // indirect go.opentelemetry.io/otel/trace v1.43.0 // indirect
go.opentelemetry.io/proto/otlp v1.5.0 // indirect go.opentelemetry.io/proto/otlp v1.10.0 // indirect
go.step.sm/cli-utils v0.9.0 // indirect
go.step.sm/crypto v0.45.0 // indirect
go.step.sm/linkedca v0.20.1 // indirect
go.uber.org/automaxprocs v1.5.3 // indirect
go.uber.org/mock v0.6.0 // indirect go.uber.org/mock v0.6.0 // indirect
go.uber.org/multierr v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.0 // indirect go.uber.org/zap v1.28.0 // indirect
go.uber.org/zap/exp v0.2.0 // indirect go.uber.org/zap/exp v0.3.0 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect go.yaml.in/yaml/v3 v3.0.5 // indirect
golang.org/x/crypto/x509roots/fallback v0.0.0-20240507223354-67b13616a595 // indirect golang.org/x/oauth2 v0.36.0 // indirect
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect golang.org/x/text v0.42.0 // indirect
golang.org/x/mod v0.27.0 // indirect golang.org/x/time v0.16.0 // indirect
golang.org/x/oauth2 v0.30.0 // indirect golang.org/x/tools v0.50.0 // indirect
golang.org/x/text v0.28.0 // indirect golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 // indirect
golang.org/x/time v0.11.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260406210006-6f92a3bedf2d // indirect
golang.org/x/tools v0.36.0 // indirect
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a // indirect
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259 // indirect
howett.net/plist v1.0.0 // indirect
k8s.io/api v0.32.3 // indirect k8s.io/api v0.32.3 // indirect
k8s.io/apimachinery v0.32.3 // indirect k8s.io/apimachinery v0.32.3 // indirect
k8s.io/client-go v0.32.3 // indirect k8s.io/client-go v0.32.3 // indirect
k8s.io/klog/v2 v2.130.1 // indirect k8s.io/klog/v2 v2.130.1 // indirect
k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f // indirect k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f // indirect
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect
modernc.org/libc v1.61.13 // indirect modernc.org/libc v1.72.0 // indirect
modernc.org/mathutil v1.7.1 // indirect modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.8.2 // indirect modernc.org/memory v1.11.0 // indirect
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.4.2 // indirect sigs.k8s.io/structured-merge-diff/v4 v4.4.2 // indirect
sigs.k8s.io/yaml v1.4.0 // indirect sigs.k8s.io/yaml v1.4.0 // indirect
) )
replace charm.land/bubbletea/v2 => github.com/unlabs-dev/bubbletea/v2 v2.0.8-uncloud.1
+230 -567
View File
File diff suppressed because it is too large. Load diff
+3 -3
View File
@@ -8,9 +8,9 @@ import (
"slices" "slices"
"github.com/docker/cli/cli/streams" "github.com/docker/cli/cli/streams"
"github.com/psviderski/uncloud/api/pb"
"github.com/psviderski/uncloud/internal/cli/config" "github.com/psviderski/uncloud/internal/cli/config"
"github.com/psviderski/uncloud/internal/machine" "github.com/psviderski/uncloud/internal/machine"
"github.com/psviderski/uncloud/internal/machine/api/pb"
"github.com/psviderski/uncloud/internal/machine/cluster" "github.com/psviderski/uncloud/internal/machine/cluster"
"github.com/psviderski/uncloud/internal/machine/network" "github.com/psviderski/uncloud/internal/machine/network"
"github.com/psviderski/uncloud/internal/sshexec" "github.com/psviderski/uncloud/internal/sshexec"
@@ -458,7 +458,7 @@ func (cli *CLI) AddMachine(ctx context.Context, opts AddMachineOptions) (_ *clie
} }
// Snapshot the cluster store version so the new machine can catch up before participating. // Snapshot the cluster store version so the new machine can catch up before participating.
var storeVersion map[string]int64 var storeVersion api.StoreVersion
inspectResp, err = c.MachineClient.InspectMachine(ctx, &emptypb.Empty{}) inspectResp, err = c.MachineClient.InspectMachine(ctx, &emptypb.Empty{})
if err != nil { if err != nil {
// TODO(lhf): remove Unimplemented check when v0.17.0 is released. // TODO(lhf): remove Unimplemented check when v0.17.0 is released.
@@ -603,7 +603,7 @@ func provisionOrConnectRemoteMachine(
if remoteMachine.User != rootUser { if remoteMachine.User != rootUser {
// provisionMachine has just added the user to the uncloud group. Any SSH ControlMaster left over from // provisionMachine has just added the user to the uncloud group. Any SSH ControlMaster left over from
// a previous uc invocation (e.g. a failed uc command against the uninitialised machine) still holds // a previous uc invocation (e.g. a failed uc command against the uninitialised machine) still holds
// the old user groups and would deny access to /run/uncloud/uncloud.sock. Close the current session // the old user groups and would deny access to /run/uncloud/api/uncloud.sock. Close the current session
// if it exists so the next session picks up the new groups. // if it exists so the next session picks up the new groups.
conn.CloseControlMaster(ctx) conn.CloseControlMaster(ctx)
} }
+5 -1
View File
@@ -11,6 +11,11 @@ import (
) )
func Contexts(ctx context.Context, uncli *cli.CLI, args []string, toComplete string) ([]cobra.Completion, cobra.ShellCompDirective) { func Contexts(ctx context.Context, uncli *cli.CLI, args []string, toComplete string) ([]cobra.Completion, cobra.ShellCompDirective) {
// There are no contexts to complete when the CLI uses a direct machine connection (--connect) without a config.
if uncli.Config == nil {
return nil, cobra.ShellCompDirectiveNoFileComp
}
contexts := slices.Sorted(maps.Keys(uncli.Config.Contexts)) contexts := slices.Sorted(maps.Keys(uncli.Config.Contexts))
names := []cobra.Completion{} names := []cobra.Completion{}
@@ -21,7 +26,6 @@ func Contexts(ctx context.Context, uncli *cli.CLI, args []string, toComplete str
if strings.HasPrefix(context, toComplete) { if strings.HasPrefix(context, toComplete) {
names = append(names, context) names = append(names, context)
} }
names = append(names, context)
} }
return names, cobra.ShellCompDirectiveNoFileComp return names, cobra.ShellCompDirectiveNoFileComp
+2 -1
View File
@@ -10,7 +10,8 @@ import (
) )
func Machines(ctx context.Context, uncli *cli.CLI, args []string, toComplete string) ([]cobra.Completion, cobra.ShellCompDirective) { func Machines(ctx context.Context, uncli *cli.CLI, args []string, toComplete string) ([]cobra.Completion, cobra.ShellCompDirective) {
client, err := uncli.ConnectCluster(ctx) // Disable the connection progress output to not interfere with the shell completion output.
client, err := uncli.ConnectClusterWithOptions(ctx, cli.ConnectOptions{})
if err != nil { if err != nil {
return nil, cobra.ShellCompDirectiveError return nil, cobra.ShellCompDirectiveError
} }
+2 -1
View File
@@ -11,7 +11,8 @@ import (
) )
func Services(ctx context.Context, uncli *cli.CLI, args []string, toComplete string) ([]cobra.Completion, cobra.ShellCompDirective) { func Services(ctx context.Context, uncli *cli.CLI, args []string, toComplete string) ([]cobra.Completion, cobra.ShellCompDirective) {
client, err := uncli.ConnectCluster(ctx) // Disable the connection progress output to not interfere with the shell completion output.
client, err := uncli.ConnectClusterWithOptions(ctx, cli.ConnectOptions{})
if err != nil { if err != nil {
return nil, cobra.ShellCompDirectiveError return nil, cobra.ShellCompDirectiveError
} }
+2 -1
View File
@@ -11,7 +11,8 @@ import (
) )
func Volumes(ctx context.Context, uncli *cli.CLI, args []string, toComplete string) ([]cobra.Completion, cobra.ShellCompDirective) { func Volumes(ctx context.Context, uncli *cli.CLI, args []string, toComplete string) ([]cobra.Completion, cobra.ShellCompDirective) {
client, err := uncli.ConnectCluster(ctx) // Disable the connection progress output to not interfere with the shell completion output.
client, err := uncli.ConnectClusterWithOptions(ctx, cli.ConnectOptions{})
if err != nil { if err != nil {
return nil, cobra.ShellCompDirectiveError return nil, cobra.ShellCompDirectiveError
} }
+2 -6
View File
@@ -7,8 +7,6 @@ import (
"net/netip" "net/netip"
"strconv" "strconv"
"strings" "strings"
"github.com/psviderski/uncloud/internal/secret"
) )
type MachineConnection struct { type MachineConnection struct {
@@ -23,10 +21,8 @@ type MachineConnection struct {
// The pointer is used to omit the field when not set. Otherwise, yaml marshalling includes an empty object. // The pointer is used to omit the field when not set. Otherwise, yaml marshalling includes an empty object.
TCP *netip.AddrPort `yaml:"tcp,omitempty"` TCP *netip.AddrPort `yaml:"tcp,omitempty"`
// Unix is the path to the machine's API unix socket. // Unix is the path to the machine's API unix socket.
Unix string `yaml:"unix,omitempty"` Unix string `yaml:"unix,omitempty"`
Host string `yaml:"host,omitempty"` MachineID string `yaml:"machine_id,omitempty"`
PublicKey secret.Secret `yaml:"public_key,omitempty"`
MachineID string `yaml:"machine_id,omitempty"`
} }
func (c *MachineConnection) String() string { func (c *MachineConnection) String() string {
+2 -2
View File
@@ -70,9 +70,9 @@ func TestMachineConnection_String(t *testing.T) {
{ {
name: "unix connection", name: "unix connection",
conn: MachineConnection{ conn: MachineConnection{
Unix: "/run/uncloud/uncloud.sock", Unix: "/run/uncloud/api/uncloud.sock",
}, },
want: "unix:///run/uncloud/uncloud.sock", want: "unix:///run/uncloud/api/uncloud.sock",
}, },
{ {
name: "no connection", name: "no connection",
+11 -8
View File
@@ -45,7 +45,17 @@ func connectClusterWithProgress(ctx context.Context, conn config.MachineConnecti
} }
// Run the connection TUI model. Render to stderr so stdout stays clean for command output. // Run the connection TUI model. Render to stderr so stdout stays clean for command output.
p := tea.NewProgram(newConnectModel(ctx, conn), tea.WithOutput(os.Stderr)) p := tea.NewProgram(newConnectModel(ctx, conn),
tea.WithOutput(os.Stderr),
tea.WithInput(nil),
// Ctrl+C handling
tea.WithFilter(func(_ tea.Model, msg tea.Msg) tea.Msg {
if _, ok := msg.(tea.InterruptMsg); ok {
return connectResultMsg{err: errors.New("connection cancelled")}
}
return msg
}),
)
model, err := p.Run() model, err := p.Run()
if err != nil { if err != nil {
return nil, fmt.Errorf("run connection TUI: %w", err) return nil, fmt.Errorf("run connection TUI: %w", err)
@@ -190,13 +200,6 @@ func (m connectModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmd tea.Cmd var cmd tea.Cmd
m.spinner, cmd = m.spinner.Update(msg) m.spinner, cmd = m.spinner.Update(msg)
return m, cmd return m, cmd
case tea.KeyPressMsg:
if msg.String() == "ctrl+c" {
m.result.err = fmt.Errorf("connection cancelled")
m.done = true
return m, tea.Quit
}
} }
return m, nil return m, nil
+1 -1
View File
@@ -7,7 +7,7 @@ import (
"os" "os"
"strings" "strings"
"github.com/psviderski/uncloud/internal/machine/api/pb" "github.com/psviderski/uncloud/api/pb"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
+1 -1
View File
@@ -11,8 +11,8 @@ import (
"charm.land/huh/v2" "charm.land/huh/v2"
"github.com/cenkalti/backoff/v4" "github.com/cenkalti/backoff/v4"
"github.com/psviderski/uncloud/api/pb"
"github.com/psviderski/uncloud/internal/cli/tui" "github.com/psviderski/uncloud/internal/cli/tui"
"github.com/psviderski/uncloud/internal/machine/api/pb"
"github.com/psviderski/uncloud/internal/sshexec" "github.com/psviderski/uncloud/internal/sshexec"
"github.com/psviderski/uncloud/scripts" "github.com/psviderski/uncloud/scripts"
"google.golang.org/protobuf/types/known/emptypb" "google.golang.org/protobuf/types/known/emptypb"
+2
View File
@@ -31,6 +31,8 @@ func RunSpinner(ctx context.Context, title string, action func(ctx context.Conte
} }
})). })).
WithOutput(os.Stderr). WithOutput(os.Stderr).
// A spinner reads no keys, saying so makes Bubble Tea skip unnecessary terminal capability queries.
WithInput(nil).
Context(ctx). Context(ctx).
ActionWithErr(action). ActionWithErr(action).
Run() Run()
+8 -1
View File
@@ -13,6 +13,8 @@ import (
"time" "time"
) )
const adminCommandTimeout = 5 * time.Second
// AdminClient is a client for the Corrosion admin API. // AdminClient is a client for the Corrosion admin API.
type AdminClient struct { type AdminClient struct {
sockPath string sockPath string
@@ -32,10 +34,15 @@ type Response struct {
// The channel will be closed after sending the last or error response. The caller must read from the channel until // The channel will be closed after sending the last or error response. The caller must read from the channel until
// it is closed. // it is closed.
func (c *AdminClient) SendCommand(cmd []byte) (<-chan Response, error) { func (c *AdminClient) SendCommand(cmd []byte) (<-chan Response, error) {
conn, err := net.Dial("unix", c.sockPath) // TODO: Accept contexts in admin methods so callers can cancel requests and set their deadlines.
conn, err := net.DialTimeout("unix", c.sockPath, adminCommandTimeout)
if err != nil { if err != nil {
return nil, fmt.Errorf("connect to admin socket: %w", err) return nil, fmt.Errorf("connect to admin socket: %w", err)
} }
if err = conn.SetDeadline(time.Now().Add(adminCommandTimeout)); err != nil {
conn.Close()
return nil, fmt.Errorf("set admin connection deadline: %w", err)
}
if _, err = conn.Write(encodeFrame(cmd)); err != nil { if _, err = conn.Write(encodeFrame(cmd)); err != nil {
conn.Close() conn.Close()
+62 -2
View File
@@ -2,24 +2,50 @@ package daemon
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"log/slog" "log/slog"
"net"
"github.com/coreos/go-systemd/activation"
systemd "github.com/coreos/go-systemd/daemon" systemd "github.com/coreos/go-systemd/daemon"
"github.com/psviderski/uncloud/internal/machine" "github.com/psviderski/uncloud/internal/machine"
) )
const systemdSocketUnit = "uncloud.socket"
type Daemon struct { type Daemon struct {
machine *machine.Machine machine *machine.Machine
} }
func New(dataDir string) (*Daemon, error) { func New(dataDir string) (*Daemon, error) {
listeners, err := activation.ListenersWithNames()
if err != nil {
return nil, fmt.Errorf("get systemd-activated sockets: %w", err)
}
listener, err := selectActivatedListener(listeners)
if err != nil {
return nil, err
}
if listener != nil {
slog.Info("Using systemd-activated API socket.", "addr", listener.Addr().String())
}
config := &machine.Config{ config := &machine.Config{
DataDir: dataDir, DataDir: dataDir,
ClusterAPISockPath: machine.DefaultClusterAPISockPath,
ClusterAPIListener: listener,
} }
mach, err := machine.NewMachine(config) mach, err := machine.NewMachine(config)
if err != nil { if err != nil {
return nil, fmt.Errorf("init machine: %w", err) initErr := fmt.Errorf("init machine: %w", err)
if listener != nil {
if closeErr := listener.Close(); closeErr != nil {
initErr = errors.Join(initErr, fmt.Errorf("close systemd-activated socket: %w", closeErr))
}
}
return nil, initErr
} }
return &Daemon{ return &Daemon{
@@ -27,6 +53,40 @@ func New(dataDir string) (*Daemon, error) {
}, nil }, nil
} }
// selectActivatedListener returns the systemd activated listener. If systemd supplied multiple listeners,
// it returns the one associated with systemdSocketUnit.
func selectActivatedListener(listeners map[string][]net.Listener) (net.Listener, error) {
var activated []net.Listener
for _, named := range listeners {
activated = append(activated, named...)
}
if len(activated) == 0 {
return nil, nil
}
if len(activated) == 1 {
return activated[0], nil
}
named, ok := listeners[systemdSocketUnit]
if !ok || len(named) != 1 {
return nil, fmt.Errorf("expected one systemd-activated socket named '%s', received %d",
systemdSocketUnit, len(named))
}
// Close all other unused listeners.
for name, unused := range listeners {
if name == systemdSocketUnit {
continue
}
for _, l := range unused {
_ = l.Close()
}
}
return named[0], nil
}
func (d *Daemon) Run(ctx context.Context) error { func (d *Daemon) Run(ctx context.Context) error {
slog.Info("Starting machine.") slog.Info("Starting machine.")
+71
View File
@@ -0,0 +1,71 @@
package daemon
import (
"net"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestSelectActivatedListener(t *testing.T) {
t.Run("no activated listeners", func(t *testing.T) {
listener, err := selectActivatedListener(nil)
require.NoError(t, err)
assert.Nil(t, listener)
})
t.Run("only listener regardless of name", func(t *testing.T) {
activated := testListener(t)
listener, err := selectActivatedListener(map[string][]net.Listener{"another.socket": {activated}})
require.NoError(t, err)
assert.Same(t, activated, listener)
})
t.Run("named listener when multiple", func(t *testing.T) {
activated := testListener(t)
extra := testListener(t)
listener, err := selectActivatedListener(map[string][]net.Listener{
systemdSocketUnit: {activated},
"another.socket": {extra},
})
require.NoError(t, err)
assert.Same(t, activated, listener)
assert.Error(t, extra.Close())
})
t.Run("multiple listeners without expected name", func(t *testing.T) {
first := testListener(t)
second := testListener(t)
listener, err := selectActivatedListener(map[string][]net.Listener{
"first.socket": {first},
"second.socket": {second},
})
require.ErrorContains(t, err, "expected one systemd-activated socket")
assert.Nil(t, listener)
})
t.Run("multiple listeners under expected name", func(t *testing.T) {
first := testListener(t)
second := testListener(t)
listener, err := selectActivatedListener(map[string][]net.Listener{
systemdSocketUnit: {first, second},
})
require.ErrorContains(t, err, "expected one systemd-activated socket")
assert.Nil(t, listener)
})
}
func testListener(t *testing.T) net.Listener {
t.Helper()
listener, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err)
t.Cleanup(func() { _ = listener.Close() })
return listener
}
+2 -3
View File
@@ -31,9 +31,8 @@ const (
// //
// The two minimums are independent: a client might require a newer daemon for new // The two minimums are independent: a client might require a newer daemon for new
// features, while that same daemon could still handle requests from older clients. // features, while that same daemon could still handle requests from older clients.
// TODO: update to 0.20.0 before releasing 0.20.0. MinClientVersion = "0.20.0"
MinClientVersion = "0.20.0-nightly" MinServerVersion = "0.20.0"
MinServerVersion = "0.20.0-nightly"
ReleaseURL = "https://github.com/psviderski/uncloud/releases/latest" ReleaseURL = "https://github.com/psviderski/uncloud/releases/latest"
) )
+1 -1
View File
@@ -3,7 +3,7 @@ package proxy
import ( import (
"fmt" "fmt"
"github.com/psviderski/uncloud/internal/machine/api/pb" "github.com/psviderski/uncloud/api/pb"
"github.com/siderolabs/grpc-proxy/proxy" "github.com/siderolabs/grpc-proxy/proxy"
"google.golang.org/grpc/status" "google.golang.org/grpc/status"
"google.golang.org/protobuf/encoding/protowire" "google.golang.org/protobuf/encoding/protowire"
+1 -1
View File
@@ -6,7 +6,7 @@ import (
"slices" "slices"
"strings" "strings"
"github.com/psviderski/uncloud/internal/machine/api/pb" "github.com/psviderski/uncloud/api/pb"
) )
// MachineTarget represents a resolved machine target. // MachineTarget represents a resolved machine target.
+1 -1
View File
@@ -6,7 +6,7 @@ import (
"net/netip" "net/netip"
"testing" "testing"
"github.com/psviderski/uncloud/internal/machine/api/pb" "github.com/psviderski/uncloud/api/pb"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
+62 -60
View File
@@ -4,6 +4,7 @@ import (
"bytes" "bytes"
"cmp" "cmp"
"context" "context"
"errors"
"fmt" "fmt"
"log/slog" "log/slog"
"maps" "maps"
@@ -57,14 +58,12 @@ https://{{$hostname}} {
log log
}{{end}} }{{end}}
` `
caddyfileUnavailabeFooter = `# NOTE: User-defined configs for services were skipped because Caddy is not running on this machine bootstrapMarker = "# Uncloud bootstrap for Caddy container "
# (not accessible via the shared admin socket /run/uncloud/caddy/admin.sock) or the latest legacyBootstrapMarker = "# NOTE: User-defined configs for services were skipped because Caddy is not running"
# generated config is invalid. Please check the service 'caddy' is running (uc inspect caddy)
# and its logs for more details (uc logs caddy).
`
) )
// CaddyfileGenerator generates a Caddyfile configuration for the Caddy reverse proxy. // CaddyfileGenerator generates a Caddyfile configuration for the Caddy reverse proxy.
// It combines generated routes from published ports with user-defined Caddyfile snippets from service specs (x-caddy).
type CaddyfileGenerator struct { type CaddyfileGenerator struct {
// machineID is the unique identifier of the machine where the controller is running. // machineID is the unique identifier of the machine where the controller is running.
machineID string machineID string
@@ -74,11 +73,18 @@ type CaddyfileGenerator struct {
log *slog.Logger log *slog.Logger
} }
// CaddyfileValidator is an interface for validating Caddyfile configurations. // CaddyfileValidator checks a candidate Caddyfile without loading it. Validate returns InvalidCaddyfileError only
// when the candidate is known to be invalid. Other errors mean the check could not be completed. A successful check
// does not guarantee that the configuration will load.
type CaddyfileValidator interface { type CaddyfileValidator interface {
Validate(ctx context.Context, caddyfile string) error Validate(ctx context.Context, caddyfile string) error
} }
// InvalidCaddyfileError means validation completed and the candidate Caddyfile was rejected.
type InvalidCaddyfileError struct{ Message string }
func (e *InvalidCaddyfileError) Error() string { return e.Message }
func NewCaddyfileGenerator( func NewCaddyfileGenerator(
machineID, machineName string, validator CaddyfileValidator, log *slog.Logger, machineID, machineName string, validator CaddyfileValidator, log *slog.Logger,
) *CaddyfileGenerator { ) *CaddyfileGenerator {
@@ -93,35 +99,41 @@ func NewCaddyfileGenerator(
} }
} }
// Generate creates a Caddyfile configuration based on the provided service containers. // Generate creates a Caddyfile for the local Caddy container from the supplied healthy application containers.
// The Caddyfile is generated from the service ports of the healthy containers. // Application service ports provide the generated sites. The Caddy container's custom Caddy config (x-caddy), if
// If a 'caddy' service container is running on this machine and defines a custom Caddy config (x-caddy) in its service // defined, provides the global block even when that container is unhealthy. When application custom Caddy configs
// spec, it will be validated and prepended to the generated Caddyfile. Custom Caddy configs (x-caddy) defined in other // are included, the newest container for each service provides its config.
// service specs are validated and appended to the generated Caddyfile. Invalid configs are logged and skipped to ensure
// the generated Caddyfile remains valid.
// //
// The final Caddyfile structure includes: // The resulting Caddyfile has this structure:
// //
// [caddy x-caddy (global config)] // [caddy custom Caddy config (global)]
// [generated Caddyfile from all service ports] // [generated sites from application service ports]
// [service-a x-caddy] // [service-a custom Caddy config]
// ... // ...
// [service-z x-caddy] // [service-z custom Caddy config]
// //
// If includeCustom is false, custom Caddy configs (x-caddy) are not included in the generated Caddyfile. // Bootstrap mode keeps the global custom Caddy config and generated sites, but omits application custom Caddy configs.
// It skips validation because Caddy may not be running yet. In this case, Caddy validates the config when it starts.
// Global template errors still stop generation. In full mode, invalid globals stop generation, while invalid
// application custom Caddy configs are logged and skipped.
func (g *CaddyfileGenerator) Generate( func (g *CaddyfileGenerator) Generate(
ctx context.Context, records []store.ContainerRecord, includeCustom bool, ctx context.Context,
caddyCtr api.ServiceContainer,
records []store.ContainerRecord,
bootstrap bool,
) (string, error) { ) (string, error) {
records = slices.Clone(records)
// Sort records by local machine first, then by service name and creation time. Placing containers on the local // Sort records by local machine first, then by service name and creation time. Placing containers on the local
// machine first lets user-defined Caddy configs pair this ordering with the "first" lb_policy to always send // machine first lets user-defined Caddy configs pair this ordering with the "first" lb_policy to always send
// traffic to the same-host replica (skipping the cross-machine hop) and only fall back to remote upstreams when // traffic to the same-host replica (skipping the cross-machine hop) and only fall back to remote upstreams when
// the local one is unhealthy. // the local one is unhealthy.
// The service name and creation time tiebreakers keep the generated Caddyfile stable across regenerations. // The service name, creation time, and container ID tiebreakers keep the file stable across regenerations.
slices.SortStableFunc(records, func(a, b store.ContainerRecord) int { slices.SortStableFunc(records, func(a, b store.ContainerRecord) int {
return cmp.Or( return cmp.Or(
g.localMachineRank(a.MachineID)-g.localMachineRank(b.MachineID), g.localMachineRank(a.MachineID)-g.localMachineRank(b.MachineID),
strings.Compare(a.Container.ServiceName(), b.Container.ServiceName()), strings.Compare(a.Container.ServiceName(), b.Container.ServiceName()),
a.Container.CreatedTime().Compare(b.Container.CreatedTime()), a.Container.CreatedTime().Compare(b.Container.CreatedTime()),
strings.Compare(a.Container.ID, b.Container.ID),
) )
}) })
@@ -136,26 +148,11 @@ func (g *CaddyfileGenerator) Generate(
} }
caddyfileHeader := fmt.Sprintf(caddyfileHeaderFmt, g.machineName, time.Now().UTC().Format(time.RFC3339)) caddyfileHeader := fmt.Sprintf(caddyfileHeaderFmt, g.machineName, time.Now().UTC().Format(time.RFC3339))
if !includeCustom {
return fmt.Sprintf("%s\n%s\n%s", caddyfileHeader, caddyfile, caddyfileUnavailabeFooter), nil
}
upstreams := serviceUpstreams(containers) upstreams := serviceUpstreams(containers)
// Track validation errors for reporting. // Track validation errors for reporting.
var configErrors []string var configErrors []string
// Find the 'caddy' service container on this machine. Use the most recent one if multiple exist. if caddyCtr.ServiceSpec.CaddyConfig() != "" {
var caddyCtr *api.ServiceContainer
for _, cr := range records {
if cr.MachineID == g.machineID && cr.Container.ServiceName() == CaddyServiceName &&
(caddyCtr == nil || cr.Container.CreatedTime().Compare(caddyCtr.CreatedTime()) > 0) {
caddyCtr = &cr.Container
}
}
// If the caddy container is running on this machine and has a custom Caddy config (global),
// prepend it to the generated Caddyfile and validate it.
if caddyCtr != nil && caddyCtr.ServiceSpec.CaddyConfig() != "" {
// Render the custom global Caddy config as a Go template with the upstreams. // Render the custom global Caddy config as a Go template with the upstreams.
tmplCtx := templateContext{ tmplCtx := templateContext{
Name: caddyCtr.ServiceName(), Name: caddyCtr.ServiceName(),
@@ -163,23 +160,23 @@ func (g *CaddyfileGenerator) Generate(
} }
renderedConfig, err := renderCaddyfile(tmplCtx, caddyCtr.ServiceSpec.CaddyConfig()) renderedConfig, err := renderCaddyfile(tmplCtx, caddyCtr.ServiceSpec.CaddyConfig())
if err != nil { if err != nil {
g.log.Error("Failed to render template directives in user-defined global Caddy config, skipping it.", return "", fmt.Errorf(
"service", caddyCtr.ServiceName(), "container", caddyCtr.ID, "err", err) "render template directives in user-defined global Caddy config from Caddy container %s: %w",
configErrors = append(configErrors, caddyCtr.ShortID(), err)
fmt.Sprintf("service '%s': failed to render template: %v", caddyCtr.ServiceName(), err)) }
} else { caddyfileCandidate := fmt.Sprintf("# User-defined global config from service '%s'.\n%s\n\n%s",
caddyfileCandidate := fmt.Sprintf("# User-defined global config from service '%s'.\n%s\n\n%s", caddyCtr.ServiceName(), renderedConfig, caddyfile)
caddyCtr.ServiceName(), renderedConfig, caddyfile) if !bootstrap && g.validator != nil {
if err = g.validator.Validate(ctx, caddyfileCandidate); err != nil { if err = g.validator.Validate(ctx, caddyfileCandidate); err != nil {
g.log.Error("User-defined global Caddy config is invalid, skipping it.", return "", fmt.Errorf("validate user-defined global Caddy config from Caddy container %s: %w",
"service", caddyCtr.ServiceName(), "container", caddyCtr.ID, "err", err) caddyCtr.ShortID(), err)
configErrors = append(configErrors,
fmt.Sprintf("service '%s': validation failed: %v", caddyCtr.ServiceName(), err))
} else {
caddyfile = caddyfileCandidate
} }
} }
caddyfile = caddyfileCandidate
}
if bootstrap {
return caddyfileHeader + bootstrapMarker + caddyCtr.ID + "\n\n" + caddyfile, nil
} }
// There could be multiple service containers for the same service with different custom Caddy configs, for example, // There could be multiple service containers for the same service with different custom Caddy configs, for example,
@@ -200,7 +197,7 @@ func (g *CaddyfileGenerator) Generate(
// Append a custom Caddy config for each service to the Caddyfile and validate it. If the config for a service // Append a custom Caddy config for each service to the Caddyfile and validate it. If the config for a service
// is invalid, skip it but continue processing other services to ensure the Caddyfile remains valid. // is invalid, skip it but continue processing other services to ensure the Caddyfile remains valid.
for _, serviceName := range sortedServiceNames { for _, serviceName := range sortedServiceNames {
// Skip the caddy container as we already processed it. // Skip the caddy container as we already processed it as the global config.
if serviceName == CaddyServiceName { if serviceName == CaddyServiceName {
continue continue
} }
@@ -226,16 +223,22 @@ func (g *CaddyfileGenerator) Generate(
caddyfileCandidate := fmt.Sprintf("%s\n# User-defined config for service '%s'.\n%s\n", caddyfileCandidate := fmt.Sprintf("%s\n# User-defined config for service '%s'.\n%s\n",
caddyfile, serviceName, renderedConfig) caddyfile, serviceName, renderedConfig)
if err = g.validator.Validate(ctx, caddyfileCandidate); err != nil { if g.validator != nil {
g.log.Error("User-defined Caddy config for service is invalid, skipping it.", if err = g.validator.Validate(ctx, caddyfileCandidate); err != nil {
"service", serviceName, "err", err) if _, ok := errors.AsType[*InvalidCaddyfileError](err); !ok {
configErrors = append(configErrors, fmt.Sprintf("service '%s': validation failed: %v", serviceName, err)) return "", fmt.Errorf("validate Caddy config for service '%s': %w", serviceName, err)
} else { }
caddyfile = caddyfileCandidate g.log.Error("User-defined Caddy config for service is invalid, skipping it.",
"service", serviceName, "err", err)
configErrors = append(configErrors,
fmt.Sprintf("service '%s': validation failed: %v", serviceName, err))
continue
}
} }
caddyfile = caddyfileCandidate
} }
// Append error summary as comment if there were any invalid configs. // Keep skipped-snippet errors in the saved file so an operator can see why routes are missing after a restart.
if len(configErrors) > 0 { if len(configErrors) > 0 {
var errorsComment strings.Builder var errorsComment strings.Builder
errorsComment.WriteString("# Skipped invalid user-defined configs:\n") errorsComment.WriteString("# Skipped invalid user-defined configs:\n")
@@ -249,8 +252,7 @@ func (g *CaddyfileGenerator) Generate(
return caddyfileHeader + "\n" + caddyfile, nil return caddyfileHeader + "\n" + caddyfile, nil
} }
// localMachineRank returns 0 if the given machineID matches the local machine and 1 otherwise. // localMachineRank is a sorting function for containers that puts running on the local machine first.
// Useful for sorting containers running locally first.
func (g *CaddyfileGenerator) localMachineRank(machineID string) int { func (g *CaddyfileGenerator) localMachineRank(machineID string) int {
if g.machineID == machineID { if g.machineID == machineID {
return 0 return 0
+187 -116
View File
@@ -208,10 +208,11 @@ http://app.example.com {
ctx := context.Background() ctx := context.Background()
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
// Validator is not expected to be called in these tests. validator := NewMockCaddyfileValidator(t)
generator := NewCaddyfileGenerator("test-machine-id", "test-machine", nil, nil) generator := NewCaddyfileGenerator("test-machine-id", "test-machine", validator, nil)
config, err := generator.Generate(ctx, tt.containers, true) caddyCtr := newContainerRecordWithCaddyConfig("caddy", "10.210.0.1", "", "", time.Now()).Container
config, err := generator.Generate(ctx, caddyCtr, tt.containers, false)
if tt.wantErr { if tt.wantErr {
assert.Error(t, err) assert.Error(t, err)
@@ -332,7 +333,7 @@ bad.template.com {
`, `,
}, },
{ {
name: "caddy service with invalid global config is skipped", name: "caddy service with invalid global config aborts",
containers: []store.ContainerRecord{ containers: []store.ContainerRecord{
newContainerRecordWithCaddyConfig( newContainerRecordWithCaddyConfig(
"caddy", "caddy",
@@ -345,10 +346,7 @@ localhost {
time.Now(), time.Now(),
), ),
}, },
want: testCaddyfileHeader + ` wantErr: true,
# Skipped invalid user-defined configs:
# - service 'caddy': validation failed: invalid config detected
`,
}, },
{ {
name: "caddy service on different machine is ignored", name: "caddy service on different machine is ignored",
@@ -786,7 +784,7 @@ web-v2.example.com {
`, `,
}, },
{ {
name: "multiple errors: invalid global, template error, and validation error", name: "invalid global aborts before application configs",
containers: []store.ContainerRecord{ containers: []store.ContainerRecord{
newContainerRecordWithCaddyConfig( newContainerRecordWithCaddyConfig(
"caddy", "caddy",
@@ -827,17 +825,7 @@ invalid.example.com {
time.Now(), time.Now(),
), ),
}, },
want: testCaddyfileHeader + ` wantErr: true,
# User-defined config for service 'valid'.
valid.example.com {
respond "Valid config"
}
# Skipped invalid user-defined configs:
# - service 'caddy': validation failed: invalid config detected
# - service 'broken-template': failed to render template: parse config as Go template: template: Caddyfile:2: unterminated quoted string
# - service 'invalid': validation failed: invalid config detected
`,
}, },
} }
@@ -846,7 +834,7 @@ valid.example.com {
validator.EXPECT().Validate(mock.Anything, mock.Anything).RunAndReturn( validator.EXPECT().Validate(mock.Anything, mock.Anything).RunAndReturn(
func(ctx context.Context, caddyfile string) error { func(ctx context.Context, caddyfile string) error {
if strings.Contains(caddyfile, "# test:invalid") { if strings.Contains(caddyfile, "# test:invalid") {
return errors.New("invalid config detected") return &InvalidCaddyfileError{Message: "invalid config detected"}
} }
return nil return nil
}) })
@@ -855,7 +843,8 @@ valid.example.com {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
generator := NewCaddyfileGenerator("test-machine-id", "test-machine", validator, nil) generator := NewCaddyfileGenerator("test-machine-id", "test-machine", validator, nil)
config, err := generator.Generate(ctx, tt.containers, true) caddyCtr := caddyContainerFromTestContainers(tt.containers)
config, err := generator.Generate(ctx, caddyCtr, tt.containers, false)
if tt.wantErr { if tt.wantErr {
assert.Error(t, err) assert.Error(t, err)
@@ -868,6 +857,167 @@ valid.example.com {
} }
} }
func TestCaddyfileGeneratorBootstrap(t *testing.T) {
// Bootstrap keeps the local Caddy container's globals and generated routes, but omits application custom configs.
tests := []struct {
name string
containers []store.ContainerRecord
want string
}{
{
name: "global config retained and application custom configs skipped",
containers: []store.ContainerRecord{
newContainerRecordWithCaddyConfig(
"caddy",
"10.210.0.1",
`# Global config
{
global directive
}`,
"test-machine-id",
time.Now(),
),
newContainerRecordWithCaddyConfig(
"web",
"10.210.0.2",
`web.example.com {
reverse_proxy web:3000
}`,
"test-machine-id",
time.Now(),
),
newContainerRecordWithPorts(
"api",
"10.210.0.3",
[]string{"api.example.com:8080/http"},
"test-machine-id",
),
},
want: strings.Replace(testCaddyfileHeader, "\n\n# Health check endpoint", `
# Uncloud bootstrap for Caddy container caddy-10.210.0.1
# User-defined global config from service 'caddy'.
# Global config
{
global directive
}
# Health check endpoint`, 1) + `
# Sites generated from service ports.
http://api.example.com {
reverse_proxy 10.210.0.3:8080 {
import common_proxy
}
log
}
`,
},
{
name: "no containers with x-caddy configs",
containers: []store.ContainerRecord{
newContainerRecordWithPorts(
"api",
"10.210.0.3",
[]string{"api.example.com:8080/http"},
"test-machine-id",
),
},
want: strings.Replace(testCaddyfileHeader, "\n\n# Health check endpoint", `
# Uncloud bootstrap for Caddy container caddy-10.210.0.1
# Health check endpoint`, 1) + `
# Sites generated from service ports.
http://api.example.com {
reverse_proxy 10.210.0.3:8080 {
import common_proxy
}
log
}
`,
},
}
ctx := context.Background()
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
validator := NewMockCaddyfileValidator(t)
generator := NewCaddyfileGenerator("test-machine-id", "test-machine", validator, nil)
caddyContainer := caddyContainerFromTestContainers(tt.containers)
config, err := generator.Generate(ctx, caddyContainer, tt.containers, true)
require.NoError(t, err)
assert.Equal(t, tt.want, normaliseGeneratedTimestamp(config), "Generated Caddyfile doesn't match")
})
}
}
func TestCaddyfileGenerator_ValidatorTransportErrorAbortsGeneration(t *testing.T) {
validator := NewMockCaddyfileValidator(t)
validator.EXPECT().Validate(mock.Anything, mock.Anything).Return(errors.New("socket disappeared"))
generator := NewCaddyfileGenerator("test-machine-id", "test-machine", validator, nil)
app := newContainerRecordWithCaddyConfig(
"web",
"10.210.0.2",
"web.example.com { respond ok }",
"test-machine-id",
time.Now(),
)
_, err := generator.Generate(context.Background(), caddyContainerFromTestContainers(nil),
[]store.ContainerRecord{app}, false)
require.ErrorContains(t, err, "socket disappeared")
}
func newContainer(ip string, ports ...string) api.ServiceContainer {
portsLabel := strings.Join(ports, ",")
return api.ServiceContainer{Container: api.Container{InspectResponse: container.InspectResponse{
ContainerJSONBase: &container.ContainerJSONBase{
State: &container.State{
Running: true,
},
},
NetworkSettings: &container.NetworkSettings{
Networks: map[string]*network.EndpointSettings{
docker.NetworkName: {
IPAddress: ip,
},
},
},
Config: &container.Config{
Labels: map[string]string{
api.LabelServicePorts: portsLabel,
},
},
}}}
}
func newContainerWithoutNetwork(ports ...string) api.ServiceContainer {
portsLabel := strings.Join(ports, ",")
return api.ServiceContainer{Container: api.Container{InspectResponse: container.InspectResponse{
ContainerJSONBase: &container.ContainerJSONBase{
State: &container.State{
Running: true,
},
},
NetworkSettings: &container.NetworkSettings{
Networks: map[string]*network.EndpointSettings{
"other-network": {
IPAddress: "172.17.0.2",
},
},
},
Config: &container.Config{
Labels: map[string]string{
api.LabelServicePorts: portsLabel,
},
},
}}}
}
func newContainerRecord(ctr api.ServiceContainer, machineID string) store.ContainerRecord { func newContainerRecord(ctr api.ServiceContainer, machineID string) store.ContainerRecord {
return store.ContainerRecord{ return store.ContainerRecord{
Container: ctr, Container: ctr,
@@ -911,100 +1061,6 @@ func newContainerRecordWithCaddyConfig(serviceName, ip, caddyConfig, machineID s
} }
} }
func TestCaddyfileGeneratorWithoutCustomConfigs(t *testing.T) {
// Test that when includeCustom is false (Caddy not available), x-caddy configs are skipped.
tests := []struct {
name string
containers []store.ContainerRecord
want string
}{
{
name: "x-caddy configs are skipped",
containers: []store.ContainerRecord{
newContainerRecordWithCaddyConfig(
"caddy",
"10.210.0.1",
`# Global config
{
global directive
}`,
"test-machine-id",
time.Now(),
),
newContainerRecordWithCaddyConfig(
"web",
"10.210.0.2",
`web.example.com {
reverse_proxy web:3000
}`,
"test-machine-id",
time.Now(),
),
newContainerRecordWithPorts(
"api",
"10.210.0.3",
[]string{"api.example.com:8080/http"},
"test-machine-id",
),
},
want: testCaddyfileHeader + `
# Sites generated from service ports.
http://api.example.com {
reverse_proxy 10.210.0.3:8080 {
import common_proxy
}
log
}
# NOTE: User-defined configs for services were skipped because Caddy is not running on this machine
# (not accessible via the shared admin socket /run/uncloud/caddy/admin.sock) or the latest
# generated config is invalid. Please check the service 'caddy' is running (uc inspect caddy)
# and its logs for more details (uc logs caddy).
`,
},
{
name: "no containers with x-caddy configs",
containers: []store.ContainerRecord{
newContainerRecordWithPorts(
"api",
"10.210.0.3",
[]string{"api.example.com:8080/http"},
"test-machine-id",
),
},
want: testCaddyfileHeader + `
# Sites generated from service ports.
http://api.example.com {
reverse_proxy 10.210.0.3:8080 {
import common_proxy
}
log
}
# NOTE: User-defined configs for services were skipped because Caddy is not running on this machine
# (not accessible via the shared admin socket /run/uncloud/caddy/admin.sock) or the latest
# generated config is invalid. Please check the service 'caddy' is running (uc inspect caddy)
# and its logs for more details (uc logs caddy).
`,
},
}
ctx := context.Background()
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Validator is not expected to be called in these tests.
generator := NewCaddyfileGenerator("test-machine-id", "test-machine", nil, nil)
config, err := generator.Generate(ctx, tt.containers, false)
require.NoError(t, err)
assert.Equal(t, tt.want, normaliseGeneratedTimestamp(config), "Generated Caddyfile doesn't match")
})
}
}
func newContainerRecordWithPorts(serviceName, ip string, ports []string, machineID string) store.ContainerRecord { func newContainerRecordWithPorts(serviceName, ip string, ports []string, machineID string) store.ContainerRecord {
portsLabel := strings.Join(ports, ",") portsLabel := strings.Join(ports, ",")
return store.ContainerRecord{ return store.ContainerRecord{
@@ -1037,3 +1093,18 @@ func newContainerRecordWithPorts(serviceName, ip string, ports []string, machine
MachineID: machineID, MachineID: machineID,
} }
} }
// caddyContainerFromTestContainers returns the local Caddy container from the provided test containers,
// or creates a default one if not found.
func caddyContainerFromTestContainers(records []store.ContainerRecord) api.ServiceContainer {
if caddyCtr := selectLocalCaddyContainer(records, "test-machine-id"); caddyCtr != nil {
return *caddyCtr
}
return newContainerRecordWithCaddyConfig(
"caddy",
"10.210.0.1",
"",
"test-machine-id",
time.Now(),
).Container
}
+12 -8
View File
@@ -3,7 +3,6 @@ package caddyconfig
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"io" "io"
"net" "net"
@@ -26,15 +25,15 @@ func NewCaddyAdminClient(socketPath string) *CaddyAdminClient {
client: &http.Client{ client: &http.Client{
Timeout: 5 * time.Second, Timeout: 5 * time.Second,
Transport: &http.Transport{ Transport: &http.Transport{
DialContext: func(_ context.Context, _, _ string) (net.Conn, error) { DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
return net.Dial("unix", socketPath) return (&net.Dialer{}).DialContext(ctx, "unix", socketPath)
}, },
}, },
}, },
} }
} }
// IsAvailable checks if the local Caddy instance is listening on the admin socket. // IsAvailable checks whether a Caddy process accepts connections at the admin socket.
func (c *CaddyAdminClient) IsAvailable() bool { func (c *CaddyAdminClient) IsAvailable() bool {
conn, err := net.DialTimeout("unix", c.socketPath, 1*time.Second) conn, err := net.DialTimeout("unix", c.socketPath, 1*time.Second)
// A stale socket file left over from a crashed Caddy container returns ECONNREFUSED so this is correctly handled // A stale socket file left over from a crashed Caddy container returns ECONNREFUSED so this is correctly handled
@@ -80,15 +79,16 @@ func (c *CaddyAdminClient) Adapt(ctx context.Context, caddyfile string) (string,
// If the response is a 400 Bad Request, try to parse the error message from it. // If the response is a 400 Bad Request, try to parse the error message from it.
if resp.StatusCode == http.StatusBadRequest { if resp.StatusCode == http.StatusBadRequest {
var apiError caddy.APIError var apiError caddy.APIError
if err = json.Unmarshal(body, &apiError); err == nil { if err = json.Unmarshal(body, &apiError); err == nil && apiError.Message != "" {
return "", errors.New(apiError.Message) return "", &InvalidCaddyfileError{Message: apiError.Message}
} }
return "", &InvalidCaddyfileError{Message: string(body)}
} }
return "", errors.New(string(body)) return "", fmt.Errorf("adapt request failed: HTTP %d: %s", resp.StatusCode, string(body))
} }
// Load loads a Caddyfile configuration into the Caddy instance running on the machine. // Load loads a Caddyfile configuration into the Caddy instance.
// Due to a Caddy bug (https://github.com/caddyserver/caddy/issues/7246), we first adapt the Caddyfile to JSON // Due to a Caddy bug (https://github.com/caddyserver/caddy/issues/7246), we first adapt the Caddyfile to JSON
// and then load the JSON config to get proper error handling. // and then load the JSON config to get proper error handling.
func (c *CaddyAdminClient) Load(ctx context.Context, caddyfile string) error { func (c *CaddyAdminClient) Load(ctx context.Context, caddyfile string) error {
@@ -96,7 +96,11 @@ func (c *CaddyAdminClient) Load(ctx context.Context, caddyfile string) error {
if err != nil { if err != nil {
return fmt.Errorf("adapt Caddyfile to JSON config: %w", err) return fmt.Errorf("adapt Caddyfile to JSON config: %w", err)
} }
return c.LoadJSON(ctx, jsonConfig)
}
// LoadJSON loads a JSON configuration into the Caddy instance.
func (c *CaddyAdminClient) LoadJSON(ctx context.Context, jsonConfig string) error {
req, err := http.NewRequestWithContext(ctx, "POST", "http://localhost/load", strings.NewReader(jsonConfig)) req, err := http.NewRequestWithContext(ctx, "POST", "http://localhost/load", strings.NewReader(jsonConfig))
if err != nil { if err != nil {
return fmt.Errorf("create load request: %w", err) return fmt.Errorf("create load request: %w", err)
@@ -0,0 +1,54 @@
package caddyconfig
import (
"context"
"net"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"sync/atomic"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func testAdminServer(t *testing.T, handler http.HandlerFunc) string {
t.Helper()
// macOS limits Unix socket paths to 104 bytes. t.TempDir can exceed that.
dir, err := os.MkdirTemp("/tmp", "uc-caddy-")
require.NoError(t, err)
t.Cleanup(func() {
_ = os.RemoveAll(dir)
})
socketPath := filepath.Join(dir, "admin.sock")
listener, err := net.Listen("unix", socketPath)
require.NoError(t, err)
server := httptest.NewUnstartedServer(handler)
_ = server.Listener.Close()
server.Listener = listener
server.Start()
t.Cleanup(server.Close)
return socketPath
}
func TestCaddyAdminClient_Validate(t *testing.T) {
var status atomic.Int64
status.Store(http.StatusBadRequest)
socket := testAdminServer(t, func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(int(status.Load()))
_, _ = w.Write([]byte(`{"message":"invalid config"}`))
})
client := NewCaddyAdminClient(socket)
err := client.Validate(context.Background(), "invalid")
var invalid *InvalidCaddyfileError
assert.ErrorAs(t, err, &invalid)
status.Store(http.StatusInternalServerError)
err = client.Validate(context.Background(), "invalid")
assert.Error(t, err)
assert.NotErrorAs(t, err, &invalid)
}
+199 -108
View File
@@ -2,7 +2,7 @@ package caddyconfig
import ( import (
"context" "context"
"encoding/json" "errors"
"fmt" "fmt"
"log/slog" "log/slog"
"net/netip" "net/netip"
@@ -10,6 +10,7 @@ import (
"path/filepath" "path/filepath"
"slices" "slices"
"strings" "strings"
"time"
"github.com/psviderski/uncloud/internal/fs" "github.com/psviderski/uncloud/internal/fs"
"github.com/psviderski/uncloud/internal/machine/store" "github.com/psviderski/uncloud/internal/machine/store"
@@ -20,27 +21,40 @@ const (
CaddyServiceName = "caddy" CaddyServiceName = "caddy"
CaddyGroup = "uncloud" CaddyGroup = "uncloud"
VerifyPath = "/.uncloud-verify" VerifyPath = "/.uncloud-verify"
// periodicReconciliationInterval is the interval at which the controller reconciles the Caddyfile
// even if no container changes are observed.
periodicReconciliationInterval = 30 * time.Second
) )
// Controller monitors container changes in the cluster store and generates a configuration file for Caddy reverse // Controller keeps the local Caddy reverse proxy in sync with container state from the cluster store. It writes a
// proxy. The generated configuration allows Caddy to route external traffic to service containers across the internal // Caddyfile that routes external traffic to healthy service containers across the internal network.
// network. //
// Current Caddy deployments share one Caddyfile and one admin socket per machine. The controller must preserve a
// saved full Caddyfile when Caddy is unavailable because that file may be needed to restart or roll back a container.
// It writes a reduced bootstrap config only when no full file exists, including when an older offline file must be
// replaced. The shared layout cannot give overlapping Caddy revisions separate configurations.
type Controller struct { type Controller struct {
machineID string machineID string
caddyfilePath string caddyfilePath string
service *Service
generator *CaddyfileGenerator generator *CaddyfileGenerator
client *CaddyAdminClient client *CaddyAdminClient
store *store.Store store *store.Store
log *slog.Logger log *slog.Logger
// lastFingerprint caches the fingerprint of the containers used to generate the latest successfully loaded // Tests use their own group so they can exercise real file publication without the daemon's uncloud group.
// Caddyfile. nil means it hasn't been loaded yet or the last load failed. fileGroup string
// lastFingerprint records the healthy application-container inputs used for the last full Caddyfile that was
// successfully loaded into Caddy and saved to disk. It excludes the Caddy container, whose identity, start time,
// and global config are tracked separately below.
lastFingerprint []containerFingerprint lastFingerprint []containerFingerprint
// lastCaddyfile caches the last generated Caddyfile. lastCaddyCtrID string
lastCaddyfile string lastStartedAt string
lastGlobal string
lastSavedBody string
} }
// containerFingerprint is the subset of container data that the Caddyfile generator depends on. // containerFingerprint contains container identity and routing inputs that can change the generated Caddyfile.
// Comparing fingerprints lets the controller skip no-op regenerations. // It excludes incidental Docker state so unrelated container updates do not cause a Caddy reload.
type containerFingerprint struct { type containerFingerprint struct {
ID string ID string
IP netip.Addr IP netip.Addr
@@ -56,7 +70,8 @@ func (f containerFingerprint) Equal(other containerFingerprint) bool {
f.CaddyConfig == other.CaddyConfig f.CaddyConfig == other.CaddyConfig
} }
func NewController(machineID, configDir, adminSock string, store *store.Store) (*Controller, error) { func NewController(machineID string, service *Service, adminSock string, store *store.Store) (*Controller, error) {
configDir := service.configDir
if err := os.MkdirAll(configDir, 0o750); err != nil { if err := os.MkdirAll(configDir, 0o750); err != nil {
return nil, fmt.Errorf("create directory for Caddy configuration '%s': %w", configDir, err) return nil, fmt.Errorf("create directory for Caddy configuration '%s': %w", configDir, err)
} }
@@ -67,16 +82,21 @@ func NewController(machineID, configDir, adminSock string, store *store.Store) (
log := slog.With("component", "caddy-controller") log := slog.With("component", "caddy-controller")
client := NewCaddyAdminClient(adminSock) client := NewCaddyAdminClient(adminSock)
// generator is initialised by Run() once the machine name is resolved from the store. // The generator is initialised by Run() after resolving the machine name from the store.
return &Controller{ return &Controller{
machineID: machineID, machineID: machineID,
caddyfilePath: filepath.Join(configDir, "Caddyfile"), caddyfilePath: filepath.Join(configDir, "Caddyfile"),
service: service,
client: client, client: client,
store: store, store: store,
log: log, log: log,
fileGroup: CaddyGroup,
}, nil }, nil
} }
// Run reconciles on container changes and every periodicReconciliationInterval. Failed work is retried every second,
// but repeated retry failures are logged only during the periodic check.
// The periodic check also covers missed events and outstanding failures after the daemon restarts.
func (c *Controller) Run(ctx context.Context) error { func (c *Controller) Run(ctx context.Context) error {
// Default the machine name to the machine ID so the Caddyfile header still carries a stable identifier if // Default the machine name to the machine ID so the Caddyfile header still carries a stable identifier if
// the store lookup fails. // the store lookup fails.
@@ -95,14 +115,35 @@ func (c *Controller) Run(ctx context.Context) error {
} }
c.log.Info("Subscribed to container changes in the cluster to generate Caddy configuration.") c.log.Info("Subscribed to container changes in the cluster to generate Caddy configuration.")
containers = filterHealthyContainers(containers) var retryC <-chan time.Time
c.generateAndLoadCaddyfile(ctx, containers) periodic := time.NewTicker(periodicReconciliationInterval)
defer periodic.Stop()
// TODO: left for backward compatibility, remove later. handleResult := func(err error, logFailure bool) {
if err = c.generateJSONConfig(containers); err != nil { c.service.setReconciliationResult(err)
c.log.Error("Failed to generate Caddy JSON configuration to disk.", "err", err) if err == nil {
retryC = nil
return
}
if logFailure {
c.log.Error("Failed to reconcile Caddy configuration, will retry.", "err", err)
}
retryC = time.After(time.Second)
} }
reconcile := func(logFailure bool) {
ctrs, err := c.store.ListContainers(ctx, store.ListOptions{})
if err != nil {
err = fmt.Errorf("list containers: %w", err)
} else {
err = c.generateAndLoadCaddyfile(ctx, ctrs)
}
handleResult(err, logFailure)
}
// The subscription supplies an initial snapshot, so the first attempt need not wait for a change event.
handleResult(c.generateAndLoadCaddyfile(ctx, containers), true)
for { for {
select { select {
case _, ok := <-changes: case _, ok := <-changes:
@@ -111,25 +152,18 @@ func (c *Controller) Run(ctx context.Context) error {
} }
c.log.Debug("Cluster containers changed, regenerating Caddy configuration.") c.log.Debug("Cluster containers changed, regenerating Caddy configuration.")
containers, err = c.store.ListContainers(ctx, store.ListOptions{}) reconcile(true)
if err != nil { case <-periodic.C:
c.log.Error("Failed to list containers.", "err", err) reconcile(true)
continue case <-retryC:
} reconcile(false)
containers = filterHealthyContainers(containers)
c.generateAndLoadCaddyfile(ctx, containers)
// TODO: left for backward compatibility, remove later.
if err = c.generateJSONConfig(containers); err != nil {
c.log.Error("Failed to generate Caddy JSON configuration to disk.", "err", err)
}
case <-ctx.Done(): case <-ctx.Done():
return nil return nil
} }
} }
} }
// filterHealthyContainers filters out unhealthy and hook containers. // filterHealthyContainers filters out unhealthy, hook, and caddy containers.
// TODO: Filters out containers from this machine that are likely unavailable. The availability can be determined // TODO: Filters out containers from this machine that are likely unavailable. The availability can be determined
// by the cluster membership state of the machine that the container is running on. Implement machine membership // by the cluster membership state of the machine that the container is running on. Implement machine membership
// check using Corrossion Admin client. // check using Corrossion Admin client.
@@ -139,6 +173,9 @@ func filterHealthyContainers(containers []store.ContainerRecord) []store.Contain
if cr.Container.IsHook() { if cr.Container.IsHook() {
continue continue
} }
if cr.Container.ServiceName() == CaddyServiceName {
continue
}
if cr.Container.Healthy() { if cr.Container.Healthy() {
healthy = append(healthy, cr) healthy = append(healthy, cr)
} }
@@ -146,65 +183,96 @@ func filterHealthyContainers(containers []store.ContainerRecord) []store.Contain
return healthy return healthy
} }
// generateAndLoadCaddyfile regenerates the Caddyfile from the given containers and loads it into the local Caddy // generateAndLoadCaddyfile reconciles the shared Caddyfile for the selected local Caddy container. A full candidate
// if available. // reaches disk only after Caddy accepts it. Without an admin endpoint, the controller keeps a saved full file intact
func (c *Controller) generateAndLoadCaddyfile(ctx context.Context, containers []store.ContainerRecord) { // and writes a bootstrap config only when the file is absent or already a bootstrap.
// Check if Caddy is available before attempting to generate and load config. func (c *Controller) generateAndLoadCaddyfile(ctx context.Context, containers []store.ContainerRecord) error {
caddyAvailable := c.client.IsAvailable() caddyCtr := selectLocalCaddyContainer(containers, c.machineID)
if caddyCtr == nil {
// Skip regeneration when Caddy is available and the containers since the last successful load haven't changed. // Caddy is not running locally which means there is no reliable source for the global config, skipping.
// When Caddy is unavailable we still regenerate to keep the Caddyfile on disk updated. return nil
fingerprint := fingerprintContainers(containers)
if caddyAvailable && slices.EqualFunc(fingerprint, c.lastFingerprint, containerFingerprint.Equal) {
c.log.Debug("Caddy configuration is unchanged.", "path", c.caddyfilePath)
return
} }
caddyfile, err := c.generator.Generate(ctx, containers, caddyAvailable) saved, readErr := os.ReadFile(c.caddyfilePath)
if err != nil { if readErr != nil && !errors.Is(readErr, os.ErrNotExist) {
c.log.Error("Failed to generate Caddyfile configuration.", "err", err) return fmt.Errorf("read saved Caddyfile: %w", readErr)
return
} }
haveSaved := readErr == nil
if !caddyAvailable { healthyCtrs := filterHealthyContainers(containers)
// Caddy is not running so the generated Caddyfile should not include user-defined configs thus must be valid. fingerprint := fingerprintContainers(healthyCtrs)
// It's safe to write the config to disk so that when Caddy is deployed on this machine, it can pick it up.
if err = c.writeCaddyfileIfChanged(caddyfile); err != nil { if !caddyCtr.State.Running || !c.client.IsAvailable() {
c.log.Error("Failed to write Caddyfile to disk.", "err", err) if haveSaved && !isBootstrapCaddyfile(string(saved)) {
return // Caddy may need this file to restart or roll back. A hosts-only replacement could drop
// storage or other global settings while the admin API is unavailable.
if caddyCtr.State.Running {
return fmt.Errorf("caddy admin socket unavailable, preserving saved Caddyfile")
}
return nil
} }
c.log.Debug("Caddy is not running on this machine, skipping configuration load.", "path", c.caddyfilePath)
return bootstrap, err := c.generator.Generate(ctx, *caddyCtr, healthyCtrs, true)
if err != nil {
return fmt.Errorf("generate Caddy bootstrap config: %w", err)
}
if err = c.writeCaddyfileIfChanged(bootstrap); err != nil {
return fmt.Errorf("save Caddy bootstrap config: %w", err)
}
if caddyCtr.State.Running {
return fmt.Errorf("caddy admin socket unavailable, bootstrap config saved")
}
return nil
} }
// Caddy is available, try to load the config which may fail if the config is invalid. Generally, a config can if haveSaved &&
// pass the adaptation/validation step but still fail to load, for example, if it references resources that are caddyfileBody(string(saved)) == c.lastSavedBody &&
// not available. !isBootstrapCaddyfile(string(saved)) &&
c.lastCaddyCtrID == caddyCtr.ID &&
c.lastStartedAt == caddyCtr.State.StartedAt &&
c.lastGlobal == caddyCtr.ServiceSpec.CaddyConfig() &&
slices.EqualFunc(fingerprint, c.lastFingerprint, containerFingerprint.Equal) {
// No changes to the inputs that affect the generated Caddyfile, so no reload is needed.
return nil
}
caddyfile, err := c.generator.Generate(ctx, *caddyCtr, healthyCtrs, false)
if err != nil {
return fmt.Errorf("generate Caddyfile: %w", err)
}
// Try to load the config which may fail if the config is invalid. Generally, a config can pass
// the adaptation/validation step but still fail to load, for example, if it references resources
// that are not available.
if err = c.client.Load(ctx, caddyfile); err != nil { if err = c.client.Load(ctx, caddyfile); err != nil {
c.log.Error("Failed to load new Caddy configuration into local Caddy instance.", return fmt.Errorf("load Caddyfile: %w", err)
"err", err, "path", c.caddyfilePath) }
// Mark the cache stale so the next container change retries the load even if the container set is unchanged. if err = c.writeCaddyfileIfChanged(caddyfile); err != nil {
c.lastFingerprint = nil return fmt.Errorf("save loaded Caddyfile: %w", err)
// Don't write invalid config to disk.
return
} }
c.lastFingerprint = fingerprint c.lastFingerprint = fingerprint
c.lastCaddyCtrID = caddyCtr.ID
// Config loaded successfully, now write it to disk. c.lastStartedAt = caddyCtr.State.StartedAt
if err = c.writeCaddyfileIfChanged(caddyfile); err != nil { c.lastGlobal = caddyCtr.ServiceSpec.CaddyConfig()
c.log.Error("Failed to write Caddyfile to disk after successful load.", "err", err) c.lastSavedBody = caddyfileBody(caddyfile)
// Config is already loaded in Caddy, so this is not critical. The next regeneration retries the disk write.
return
}
c.log.Info("New Caddy configuration loaded into local Caddy instance.", "path", c.caddyfilePath) c.log.Info("New Caddy configuration loaded into local Caddy instance.", "path", c.caddyfilePath)
return nil
}
// isBootstrapCaddyfile distinguishes a reduced startup file from a full file that must survive Caddy outage.
// It also recognizes the older daemon's offline file. That file omitted every x-caddy block, including globals,
// so treating it as a protected full file would preserve the configuration-loss bug:
// https://github.com/psviderski/uncloud/issues/412
func isBootstrapCaddyfile(caddyfile string) bool {
return strings.Contains(caddyfile, bootstrapMarker) || strings.Contains(caddyfile, legacyBootstrapMarker)
} }
// fingerprintContainers returns a fingerprint of containers that the Caddyfile generator depends on. // fingerprintContainers returns a fingerprint of containers that the Caddyfile generator depends on.
func fingerprintContainers(containers []store.ContainerRecord) []containerFingerprint { func fingerprintContainers(containers []store.ContainerRecord) []containerFingerprint {
fingerprints := make([]containerFingerprint, len(containers)) fingerprints := make([]containerFingerprint, len(containers))
for i, cr := range containers { for i, cr := range containers {
// Ignore ports parsing error as not much we can do about it. The generator just logs them and continues. // Ignore ports parsing error as not much we can do about it. The generator just logs them and skips
// the container.
ports, _ := cr.Container.ServicePorts() ports, _ := cr.Container.ServicePorts()
fingerprints[i] = containerFingerprint{ fingerprints[i] = containerFingerprint{
ID: cr.Container.ID, ID: cr.Container.ID,
@@ -220,21 +288,71 @@ func fingerprintContainers(containers []store.ContainerRecord) []containerFinger
return fingerprints return fingerprints
} }
// writeCaddyfileIfChanged writes the Caddyfile content to disk with proper permissions only if its body differs // selectLocalCaddyContainer chooses which local Caddy container supplies global Caddy config. A running container
// from the last successfully written content. The first line of the Caddyfile carries a generation timestamp that // takes precedence over a newer stopped replacement, even if the running container is unhealthy. If two revisions
// changes on every regeneration, so it's excluded from the comparison to avoid redundant writes. // run at once, the shared admin socket does not identify its owner, so this choice remains best effort.
func selectLocalCaddyContainer(records []store.ContainerRecord, machineID string) *api.ServiceContainer {
var selected *api.ServiceContainer
for _, cr := range records {
ctr := cr.Container
if cr.MachineID != machineID || ctr.ServiceName() != CaddyServiceName || ctr.IsHook() {
continue
}
if selected == nil {
selected = &ctr
continue
}
if ctr.State.Running != selected.State.Running {
if ctr.State.Running {
selected = &ctr
}
continue
}
if ctr.CreatedTime().Compare(selected.CreatedTime()) > 0 {
selected = &ctr
}
}
return selected
}
// writeCaddyfileIfChanged atomically replaces the saved Caddyfile only if its body differs from the new content.
func (c *Controller) writeCaddyfileIfChanged(caddyfile string) error { func (c *Controller) writeCaddyfileIfChanged(caddyfile string) error {
if caddyfileBody(caddyfile) == caddyfileBody(c.lastCaddyfile) { saved, err := os.ReadFile(c.caddyfilePath)
if err != nil && !errors.Is(err, os.ErrNotExist) {
return fmt.Errorf("read Caddyfile '%s': %w", c.caddyfilePath, err)
}
if err == nil && caddyfileBody(caddyfile) == caddyfileBody(string(saved)) {
return nil return nil
} }
if err := os.WriteFile(c.caddyfilePath, []byte(caddyfile), 0o640); err != nil { dir := filepath.Dir(c.caddyfilePath)
return fmt.Errorf("write Caddyfile to file '%s': %w", c.caddyfilePath, err) tmp, err := os.CreateTemp(dir, ".Caddyfile-*")
if err != nil {
return fmt.Errorf("create temporary Caddyfile: %w", err)
} }
if err := fs.Chown(c.caddyfilePath, "", CaddyGroup); err != nil { defer os.Remove(tmp.Name())
return fmt.Errorf("change owner of Caddyfile '%s': %w", c.caddyfilePath, err) defer tmp.Close()
if err = tmp.Chmod(0o640); err != nil {
return fmt.Errorf("set temporary Caddyfile permissions: %w", err)
}
if err = fs.Chown(tmp.Name(), "", c.fileGroup); err != nil {
return fmt.Errorf("change owner of temporary Caddyfile: %w", err)
}
if _, err = tmp.WriteString(caddyfile); err != nil {
return fmt.Errorf("write temporary Caddyfile: %w", err)
}
if err = tmp.Sync(); err != nil {
return fmt.Errorf("sync temporary Caddyfile: %w", err)
}
if err = tmp.Close(); err != nil {
return fmt.Errorf("close temporary Caddyfile: %w", err)
}
if err = os.Rename(tmp.Name(), c.caddyfilePath); err != nil {
return fmt.Errorf("replace Caddyfile '%s': %w", c.caddyfilePath, err)
} }
c.lastCaddyfile = caddyfile
return nil return nil
} }
@@ -247,30 +365,3 @@ func caddyfileBody(caddyfile string) string {
} }
return caddyfile return caddyfile
} }
func (c *Controller) generateJSONConfig(containers []store.ContainerRecord) error {
serviceContainers := make([]api.ServiceContainer, len(containers))
for i, cr := range containers {
serviceContainers[i] = cr.Container
}
config, err := GenerateJSONConfig(serviceContainers, c.machineID)
if err != nil {
return err
}
configBytes, err := json.MarshalIndent(config, "", " ")
if err != nil {
return fmt.Errorf("marshal Caddy configuration: %w", err)
}
configPath := filepath.Join(filepath.Dir(c.caddyfilePath), "caddy.json")
if err = os.WriteFile(configPath, configBytes, 0o640); err != nil {
return fmt.Errorf("write Caddy configuration to file '%s': %w", configPath, err)
}
if err = fs.Chown(configPath, "", CaddyGroup); err != nil {
return fmt.Errorf("change owner of Caddy configuration file '%s': %w", configPath, err)
}
return nil
}
@@ -1,14 +1,488 @@
package caddyconfig package caddyconfig
import ( import (
"context"
"io"
"log/slog"
"net/http"
"net/netip" "net/netip"
"os"
"os/user"
"path/filepath"
"reflect" "reflect"
"strconv"
"strings"
"sync"
"sync/atomic"
"testing" "testing"
"time"
"github.com/docker/docker/api/types/container"
"github.com/psviderski/uncloud/internal/machine/store"
"github.com/psviderski/uncloud/pkg/api" "github.com/psviderski/uncloud/pkg/api"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func testController(t *testing.T, socketPath string) *Controller {
t.Helper()
group, err := user.LookupGroupId(strconv.Itoa(os.Getegid()))
require.NoError(t, err)
path := filepath.Join(t.TempDir(), "Caddyfile")
client := NewCaddyAdminClient(socketPath)
return &Controller{
machineID: "test-machine-id",
caddyfilePath: path,
generator: NewCaddyfileGenerator("test-machine-id", "test-machine", client, nil),
client: client,
log: slog.Default(),
fileGroup: group.Name,
}
}
type testCaddyAdmin struct {
mu sync.Mutex
adaptRequests []string
loadCount int
rejectAdaptContaining string
rejectLoad bool
}
func (a *testCaddyAdmin) ServeHTTP(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/adapt":
body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
a.mu.Lock()
a.adaptRequests = append(a.adaptRequests, string(body))
reject := a.rejectAdaptContaining != "" && strings.Contains(string(body), a.rejectAdaptContaining)
a.mu.Unlock()
if reject {
http.Error(w, "invalid Caddyfile", http.StatusBadRequest)
return
}
_, _ = io.WriteString(w, `{"result":{}}`)
case "/load":
a.mu.Lock()
a.loadCount++
reject := a.rejectLoad
a.mu.Unlock()
if reject {
http.Error(w, "load rejected", http.StatusBadRequest)
}
default:
http.NotFound(w, r)
}
}
func (a *testCaddyAdmin) snapshot() ([]string, int) {
a.mu.Lock()
defer a.mu.Unlock()
return append([]string(nil), a.adaptRequests...), a.loadCount
}
func (a *testCaddyAdmin) setRejectLoad(reject bool) {
a.mu.Lock()
a.rejectLoad = reject
a.mu.Unlock()
}
func TestController_GenerateAndLoadCaddyfile(t *testing.T) {
t.Run("keeps the saved file when there is no local Caddy container", func(t *testing.T) {
controller := testController(t, filepath.Join(t.TempDir(), "missing.sock"))
saved := "# previous full Caddyfile\n"
require.NoError(t, os.WriteFile(controller.caddyfilePath, []byte(saved), 0o640))
remote := newContainerRecordWithCaddyConfig("caddy", "10.210.0.2", "{ remote }",
"other-machine", time.Now())
require.NoError(t, controller.generateAndLoadCaddyfile(context.Background(), []store.ContainerRecord{remote}))
got, err := os.ReadFile(controller.caddyfilePath)
require.NoError(t, err)
assert.Equal(t, saved, string(got))
})
t.Run("does not publish a bootstrap with an invalid global template", func(t *testing.T) {
controller := testController(t, filepath.Join(t.TempDir(), "missing.sock"))
caddyCtr := newContainerRecordWithCaddyConfig("caddy", "10.210.0.2", "{{upstreams",
"test-machine-id", time.Now())
require.ErrorContains(t, controller.generateAndLoadCaddyfile(context.Background(),
[]store.ContainerRecord{caddyCtr}), "render template")
_, err := os.Stat(controller.caddyfilePath)
assert.ErrorIs(t, err, os.ErrNotExist)
})
t.Run("bootstraps unhealthy Caddy with its globals and healthy routes", func(t *testing.T) {
controller := testController(t, filepath.Join(t.TempDir(), "missing.sock"))
caddyCtr := newContainerRecordWithCaddyConfig("caddy", "10.210.0.2", "{\n\tstorage uncloud\n}",
"test-machine-id", time.Now())
caddyCtr.Container.State.Health = &container.Health{Status: "unhealthy"}
newerStopped := newContainerRecordWithCaddyConfig("caddy", "10.210.0.6", "{\n\tstorage replacement\n}",
"test-machine-id", time.Now().Add(time.Minute))
newerStopped.Container.State.Running = false
app := newContainerRecordWithPorts("web", "10.210.0.3", []string{"app.example.com:8080/http"},
"test-machine-id")
app.Container.ServiceSpec.Caddy = &api.CaddySpec{Config: "custom.example.com { respond app }"}
unhealthy := newContainerRecordWithPorts("unhealthy", "10.210.0.4",
[]string{"unhealthy.example.com:8080/http"}, "test-machine-id")
unhealthy.Container.State.Health = &container.Health{Status: "unhealthy"}
hook := newContainerRecordWithPorts("hook", "10.210.0.5",
[]string{"hook.example.com:8080/http"}, "test-machine-id")
hook.Container.Config.Labels[api.LabelHook] = "pre-deploy"
err := controller.generateAndLoadCaddyfile(context.Background(),
[]store.ContainerRecord{newerStopped, caddyCtr, app, unhealthy, hook})
require.ErrorContains(t, err, "admin socket unavailable")
saved, err := os.ReadFile(controller.caddyfilePath)
require.NoError(t, err)
assert.Contains(t, string(saved), bootstrapMarker+caddyCtr.Container.ID)
assert.Contains(t, string(saved), "storage uncloud")
assert.NotContains(t, string(saved), "storage replacement")
assert.Contains(t, string(saved), "app.example.com")
assert.NotContains(t, string(saved), "custom.example.com")
assert.NotContains(t, string(saved), "unhealthy.example.com")
assert.NotContains(t, string(saved), "hook.example.com")
})
t.Run("replaces a legacy offline file with a bootstrap containing globals", func(t *testing.T) {
controller := testController(t, filepath.Join(t.TempDir(), "missing.sock"))
legacy := "# generated by older daemon\nhttp:// { respond ok }\n" + legacyBootstrapMarker + "\n"
require.NoError(t, os.WriteFile(controller.caddyfilePath, []byte(legacy), 0o640))
caddyCtr := newContainerRecordWithCaddyConfig("caddy", "10.210.0.2", "{\n\tstorage uncloud\n}",
"test-machine-id", time.Now())
err := controller.generateAndLoadCaddyfile(context.Background(), []store.ContainerRecord{caddyCtr})
require.ErrorContains(t, err, "admin socket unavailable")
saved, err := os.ReadFile(controller.caddyfilePath)
require.NoError(t, err)
assert.Contains(t, string(saved), "storage uncloud")
assert.Contains(t, string(saved), bootstrapMarker+caddyCtr.Container.ID)
assert.NotContains(t, string(saved), legacyBootstrapMarker)
})
t.Run("saves a bootstrap for a stopped Caddy container with no file", func(t *testing.T) {
controller := testController(t, filepath.Join(t.TempDir(), "missing.sock"))
caddyCtr := newContainerRecordWithCaddyConfig("caddy", "10.210.0.2", "{\n\tstorage uncloud\n}",
"test-machine-id", time.Now())
caddyCtr.Container.State.Running = false
require.NoError(t, controller.generateAndLoadCaddyfile(context.Background(), []store.ContainerRecord{caddyCtr}))
saved, err := os.ReadFile(controller.caddyfilePath)
require.NoError(t, err)
assert.Contains(t, string(saved), bootstrapMarker+caddyCtr.Container.ID)
assert.Contains(t, string(saved), "storage uncloud")
})
for _, tc := range []struct {
name string
running bool
}{
{name: "preserves a full file when the admin socket is unavailable", running: true},
{name: "preserves a stopped container's full file", running: false},
} {
t.Run(tc.name, func(t *testing.T) {
controller := testController(t, filepath.Join(t.TempDir(), "missing.sock"))
saved := "{\n\tstorage uncloud\n}\n\nold.example.com { respond old }\n"
require.NoError(t, os.WriteFile(controller.caddyfilePath, []byte(saved), 0o640))
caddyCtr := newContainerRecordWithCaddyConfig("caddy", "10.210.0.2", "{\n\tstorage new\n}",
"test-machine-id", time.Now())
caddyCtr.Container.State.Running = tc.running
app := newContainerRecordWithPorts("web", "10.210.0.3", []string{"new.example.com:8080/http"},
"test-machine-id")
err := controller.generateAndLoadCaddyfile(context.Background(), []store.ContainerRecord{caddyCtr, app})
if tc.running {
require.ErrorContains(t, err, "admin socket unavailable")
} else {
require.NoError(t, err)
}
got, err := os.ReadFile(controller.caddyfilePath)
require.NoError(t, err)
assert.Equal(t, saved, string(got))
})
}
t.Run("loads a full file and reloads after Caddy restarts", func(t *testing.T) {
admin := &testCaddyAdmin{}
controller := testController(t, testAdminServer(t, admin.ServeHTTP))
caddyCtr := newContainerRecordWithCaddyConfig("caddy", "10.210.0.2", "{\n\tstorage uncloud\n}",
"test-machine-id", time.Now())
caddyCtr.Container.State.Health = &container.Health{Status: "unhealthy"}
app := newContainerRecordWithCaddyConfig("web", "10.210.0.3", "app.example.com { respond app }",
"test-machine-id", time.Now())
records := []store.ContainerRecord{caddyCtr, app}
require.NoError(t, controller.generateAndLoadCaddyfile(context.Background(), records))
saved, err := os.ReadFile(controller.caddyfilePath)
require.NoError(t, err)
assert.Contains(t, string(saved), "storage uncloud")
assert.Contains(t, string(saved), "app.example.com")
assert.NotContains(t, string(saved), bootstrapMarker)
adapted, loads := admin.snapshot()
require.NotEmpty(t, adapted)
assert.Contains(t, adapted[0], "storage uncloud")
assert.Equal(t, 1, loads)
require.NoError(t, controller.generateAndLoadCaddyfile(context.Background(), records))
_, loads = admin.snapshot()
assert.Equal(t, 1, loads, "unchanged inputs should not reload Caddy")
records[0].Container.State.StartedAt = time.Now().UTC().Format(time.RFC3339Nano)
require.NoError(t, controller.generateAndLoadCaddyfile(context.Background(), records))
_, loads = admin.snapshot()
assert.Equal(t, 2, loads, "a restarted Caddy process needs the full configuration")
})
t.Run("reloads when an application route changes", func(t *testing.T) {
admin := &testCaddyAdmin{}
controller := testController(t, testAdminServer(t, admin.ServeHTTP))
caddyCtr := newContainerRecordWithCaddyConfig("caddy", "10.210.0.2", "{\n\tstorage uncloud\n}",
"test-machine-id", time.Now())
app := newContainerRecordWithPorts("web", "10.210.0.3", []string{"app.example.com:8080/http"},
"test-machine-id")
records := []store.ContainerRecord{caddyCtr, app}
require.NoError(t, controller.generateAndLoadCaddyfile(context.Background(), records))
_, loads := admin.snapshot()
assert.Equal(t, 1, loads)
records[1] = newContainerRecordWithPorts("web", "10.210.0.4",
[]string{"app.example.com:8080/http"}, "test-machine-id")
require.NoError(t, controller.generateAndLoadCaddyfile(context.Background(), records))
saved, err := os.ReadFile(controller.caddyfilePath)
require.NoError(t, err)
assert.Contains(t, string(saved), "10.210.0.4:8080")
assert.NotContains(t, string(saved), "10.210.0.3:8080")
_, loads = admin.snapshot()
assert.Equal(t, 2, loads)
})
t.Run("rejects invalid globals without loading or replacing the saved file", func(t *testing.T) {
admin := &testCaddyAdmin{rejectAdaptContaining: "storage rejected"}
controller := testController(t, testAdminServer(t, admin.ServeHTTP))
saved := "{\n\tstorage uncloud\n}\n\nold.example.com { respond old }\n"
require.NoError(t, os.WriteFile(controller.caddyfilePath, []byte(saved), 0o640))
caddyCtr := newContainerRecordWithCaddyConfig("caddy", "10.210.0.2", "{\n\tstorage rejected\n}",
"test-machine-id", time.Now())
err := controller.generateAndLoadCaddyfile(context.Background(), []store.ContainerRecord{caddyCtr})
require.ErrorContains(t, err, "validate user-defined global Caddy config")
got, err := os.ReadFile(controller.caddyfilePath)
require.NoError(t, err)
assert.Equal(t, saved, string(got))
_, loads := admin.snapshot()
assert.Zero(t, loads)
})
t.Run("skips invalid application custom config", func(t *testing.T) {
admin := &testCaddyAdmin{rejectAdaptContaining: "invalid.example.com"}
controller := testController(t, testAdminServer(t, admin.ServeHTTP))
caddyCtr := newContainerRecordWithCaddyConfig("caddy", "10.210.0.2", "{\n\tstorage uncloud\n}",
"test-machine-id", time.Now())
invalid := newContainerRecordWithCaddyConfig("invalid", "10.210.0.3",
"invalid.example.com { respond bad }", "test-machine-id", time.Now())
valid := newContainerRecordWithCaddyConfig("valid", "10.210.0.4",
"valid.example.com { respond good }", "test-machine-id", time.Now())
require.NoError(t, controller.generateAndLoadCaddyfile(context.Background(),
[]store.ContainerRecord{caddyCtr, invalid, valid}))
saved, err := os.ReadFile(controller.caddyfilePath)
require.NoError(t, err)
assert.Contains(t, string(saved), "storage uncloud")
assert.Contains(t, string(saved), "valid.example.com")
assert.NotContains(t, string(saved), "invalid.example.com")
assert.Contains(t, string(saved), "Skipped invalid user-defined configs")
_, loads := admin.snapshot()
assert.Equal(t, 1, loads)
})
t.Run("keeps the saved file when application validation cannot complete", func(t *testing.T) {
var loads atomic.Int64
socket := testAdminServer(t, func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/adapt":
http.Error(w, "adapter unavailable", http.StatusInternalServerError)
case "/load":
loads.Add(1)
default:
http.NotFound(w, r)
}
})
controller := testController(t, socket)
saved := "old.example.com { respond old }\n"
require.NoError(t, os.WriteFile(controller.caddyfilePath, []byte(saved), 0o640))
caddyCtr := newContainerRecordWithCaddyConfig("caddy", "10.210.0.2", "",
"test-machine-id", time.Now())
app := newContainerRecordWithCaddyConfig("web", "10.210.0.3", "new.example.com { respond new }",
"test-machine-id", time.Now())
err := controller.generateAndLoadCaddyfile(context.Background(), []store.ContainerRecord{caddyCtr, app})
require.ErrorContains(t, err, "validate Caddy config for service")
got, err := os.ReadFile(controller.caddyfilePath)
require.NoError(t, err)
assert.Equal(t, saved, string(got))
assert.Zero(t, loads.Load())
})
t.Run("retries a rejected load without changing inputs", func(t *testing.T) {
admin := &testCaddyAdmin{rejectLoad: true}
controller := testController(t, testAdminServer(t, admin.ServeHTTP))
saved := "{\n\tstorage old\n}\n\nold.example.com { respond old }\n"
require.NoError(t, os.WriteFile(controller.caddyfilePath, []byte(saved), 0o640))
caddyCtr := newContainerRecordWithCaddyConfig("caddy", "10.210.0.2", "{\n\tstorage uncloud\n}",
"test-machine-id", time.Now())
records := []store.ContainerRecord{caddyCtr}
require.ErrorContains(t, controller.generateAndLoadCaddyfile(context.Background(), records), "load Caddyfile")
got, err := os.ReadFile(controller.caddyfilePath)
require.NoError(t, err)
assert.Equal(t, saved, string(got))
admin.setRejectLoad(false)
require.NoError(t, controller.generateAndLoadCaddyfile(context.Background(), records))
got, err = os.ReadFile(controller.caddyfilePath)
require.NoError(t, err)
assert.Contains(t, string(got), "storage uncloud")
_, loads := admin.snapshot()
assert.Equal(t, 2, loads)
})
t.Run("retries saving after a successful load", func(t *testing.T) {
admin := &testCaddyAdmin{}
controller := testController(t, testAdminServer(t, admin.ServeHTTP))
group := controller.fileGroup
controller.fileGroup = "uncloud-test-group-that-does-not-exist"
saved := "{\n\tstorage old\n}\n\nold.example.com { respond old }\n"
require.NoError(t, os.WriteFile(controller.caddyfilePath, []byte(saved), 0o640))
caddyCtr := newContainerRecordWithCaddyConfig("caddy", "10.210.0.2", "{\n\tstorage uncloud\n}",
"test-machine-id", time.Now())
records := []store.ContainerRecord{caddyCtr}
require.ErrorContains(t, controller.generateAndLoadCaddyfile(context.Background(), records), "save loaded Caddyfile")
got, err := os.ReadFile(controller.caddyfilePath)
require.NoError(t, err)
assert.Equal(t, saved, string(got))
controller.fileGroup = group
require.NoError(t, controller.generateAndLoadCaddyfile(context.Background(), records))
got, err = os.ReadFile(controller.caddyfilePath)
require.NoError(t, err)
assert.Contains(t, string(got), "storage uncloud")
_, loads := admin.snapshot()
assert.Equal(t, 2, loads)
require.NoError(t, controller.generateAndLoadCaddyfile(context.Background(), records))
_, loads = admin.snapshot()
assert.Equal(t, 2, loads, "a loaded and saved revision should not reload")
})
t.Run("loads a saved bootstrap when the admin socket becomes available", func(t *testing.T) {
controller := testController(t, filepath.Join(t.TempDir(), "missing.sock"))
caddyCtr := newContainerRecordWithCaddyConfig("caddy", "10.210.0.2", "{\n\tstorage uncloud\n}",
"test-machine-id", time.Now())
records := []store.ContainerRecord{caddyCtr}
require.Error(t, controller.generateAndLoadCaddyfile(context.Background(), records))
bootstrap, err := os.ReadFile(controller.caddyfilePath)
require.NoError(t, err)
assert.Contains(t, string(bootstrap), bootstrapMarker)
admin := &testCaddyAdmin{}
socket := testAdminServer(t, admin.ServeHTTP)
controller.client = NewCaddyAdminClient(socket)
controller.generator = NewCaddyfileGenerator("test-machine-id", "test-machine", controller.client, nil)
require.NoError(t, controller.generateAndLoadCaddyfile(context.Background(), records))
full, err := os.ReadFile(controller.caddyfilePath)
require.NoError(t, err)
assert.NotContains(t, string(full), bootstrapMarker)
_, loads := admin.snapshot()
assert.Equal(t, 1, loads)
require.NoError(t, os.WriteFile(controller.caddyfilePath, bootstrap, 0o640))
require.NoError(t, controller.generateAndLoadCaddyfile(context.Background(), records))
_, loads = admin.snapshot()
assert.Equal(t, 2, loads, "a saved bootstrap is not an applied full configuration")
})
}
func TestController_WriteCaddyfileIfChanged(t *testing.T) {
t.Run("publishes a complete file with the configured group", func(t *testing.T) {
controller := testController(t, filepath.Join(t.TempDir(), "missing.sock"))
caddyfile := "# Generated now\n{\n\tstorage uncloud\n}\n"
require.NoError(t, controller.writeCaddyfileIfChanged(caddyfile))
got, err := os.ReadFile(controller.caddyfilePath)
require.NoError(t, err)
assert.Equal(t, caddyfile, string(got))
info, err := os.Stat(controller.caddyfilePath)
require.NoError(t, err)
assert.Equal(t, os.FileMode(0o640), info.Mode().Perm())
})
t.Run("does not replace a file when only the timestamp changes", func(t *testing.T) {
controller := testController(t, filepath.Join(t.TempDir(), "missing.sock"))
saved := "# Generated yesterday\nsite.example.com { respond ok }\n"
require.NoError(t, os.WriteFile(controller.caddyfilePath, []byte(saved), 0o640))
before, err := os.Stat(controller.caddyfilePath)
require.NoError(t, err)
require.NoError(t, controller.writeCaddyfileIfChanged(
"# Generated today\nsite.example.com { respond ok }\n"))
got, err := os.ReadFile(controller.caddyfilePath)
require.NoError(t, err)
assert.Equal(t, saved, string(got))
after, err := os.Stat(controller.caddyfilePath)
require.NoError(t, err)
assert.True(t, os.SameFile(before, after), "an unchanged body should not replace the file")
})
t.Run("preserves the previous file when publication fails", func(t *testing.T) {
controller := testController(t, filepath.Join(t.TempDir(), "missing.sock"))
controller.fileGroup = "uncloud-test-group-that-does-not-exist"
saved := "# previous\nsite.example.com { respond old }\n"
require.NoError(t, os.WriteFile(controller.caddyfilePath, []byte(saved), 0o640))
err := controller.writeCaddyfileIfChanged("# next\nsite.example.com { respond new }\n")
require.ErrorContains(t, err, "change owner of temporary Caddyfile")
got, err := os.ReadFile(controller.caddyfilePath)
require.NoError(t, err)
assert.Equal(t, saved, string(got))
tmpFiles, err := filepath.Glob(filepath.Join(filepath.Dir(controller.caddyfilePath), ".Caddyfile-*"))
require.NoError(t, err)
assert.Empty(t, tmpFiles)
})
}
func TestSelectLocalCaddyContainer(t *testing.T) {
t.Run("prefers a running unhealthy container over a newer stopped one", func(t *testing.T) {
older := newContainerRecordWithCaddyConfig("caddy", "10.210.0.2", "{ older }",
"test-machine-id", time.Now().Add(-time.Hour))
older.Container.State.Health = &container.Health{Status: "unhealthy"}
newer := newContainerRecordWithCaddyConfig("caddy", "10.210.0.3", "{ newer }",
"test-machine-id", time.Now())
newer.Container.State.Running = false
selected := selectLocalCaddyContainer([]store.ContainerRecord{newer, older}, "test-machine-id")
require.NotNil(t, selected)
assert.Equal(t, older.Container.ID, selected.ID)
})
t.Run("ignores remote and hook containers", func(t *testing.T) {
local := newContainerRecordWithCaddyConfig("caddy", "10.210.0.2", "{ local }",
"test-machine-id", time.Now().Add(-time.Hour))
remote := newContainerRecordWithCaddyConfig("caddy", "10.210.0.3", "{ remote }",
"other-machine", time.Now())
hook := newContainerRecordWithCaddyConfig("caddy", "10.210.0.4", "{ hook }",
"test-machine-id", time.Now())
hook.Container.Config.Labels[api.LabelHook] = "pre-deploy"
selected := selectLocalCaddyContainer([]store.ContainerRecord{remote, hook, local}, "test-machine-id")
require.NotNil(t, selected)
assert.Equal(t, local.Container.ID, selected.ID)
})
}
// TestContainerFingerprint_EqualCoversAllFields is a guard: when a field is added to containerFingerprint, // TestContainerFingerprint_EqualCoversAllFields is a guard: when a field is added to containerFingerprint,
// Equal must also compare it. A mutation of any single field should flip equality to false. If this test fails // Equal must also compare it. A mutation of any single field should flip equality to false. If this test fails
// after adding a field, update Equal to include it. // after adding a field, update Equal to include it.
-141
View File
@@ -1,141 +0,0 @@
package caddyconfig
import (
"encoding/json"
"errors"
"fmt"
"maps"
"net/http"
"slices"
"strconv"
"time"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
"github.com/caddyserver/caddy/v2/modules/caddyhttp/reverseproxy"
"github.com/psviderski/uncloud/pkg/api"
)
func GenerateJSONConfig(containers []api.ServiceContainer, verifyResponse string) (*caddy.Config, error) {
httpHostUpstreams, httpsHostUpstreams := httpUpstreamsFromPorts(containers)
var warnings []caddyconfig.Warning
servers := make(map[string]*caddyhttp.Server)
servers["http"] = &caddyhttp.Server{
Listen: []string{fmt.Sprintf(":%d", caddyhttp.DefaultHTTPPort)},
// All http requests to this server are logged to the default logger.
Logs: &caddyhttp.ServerLogConfig{},
Routes: append(
hostUpstreamsToRoutes(httpHostUpstreams, &warnings),
// Add a route to respond with a static verification response at the /.uncloud-verify path.
verificationRoute(verifyResponse, &warnings),
),
}
servers["https"] = &caddyhttp.Server{
Listen: []string{fmt.Sprintf(":%d", caddyhttp.DefaultHTTPSPort)},
// All https requests to this server are logged to the default logger.
Logs: &caddyhttp.ServerLogConfig{},
Routes: hostUpstreamsToRoutes(httpsHostUpstreams, &warnings),
}
httpApp := caddyhttp.App{
Servers: servers,
}
config := &caddy.Config{
AppsRaw: caddy.ModuleMap{
"http": caddyconfig.JSON(httpApp, &warnings),
},
}
var err error
if len(warnings) > 0 {
// warnings only contains errors from JSON marshaling, which are highly unlikely with correct code.
for _, w := range warnings {
err = errors.Join(err, errors.New(w.Message))
}
return nil, fmt.Errorf("marshal Caddy configuration: %w", err)
}
return config, nil
}
// hostUpstreamsToRoutes converts a map of hostnames to upstreams to a list of Caddy routes.
func hostUpstreamsToRoutes(hostUpstreams map[string][]string, warnings *[]caddyconfig.Warning) []caddyhttp.Route {
// Sort hostnames for deterministic output.
hostnames := slices.Collect(maps.Keys(hostUpstreams))
slices.Sort(hostnames)
routes := make([]caddyhttp.Route, 0, len(hostUpstreams))
for _, hostname := range hostnames {
upstreams := hostUpstreams[hostname]
upstreamPool := make([]*reverseproxy.Upstream, len(upstreams))
for i, upstream := range upstreams {
upstreamPool[i] = &reverseproxy.Upstream{
Dial: upstream,
}
}
handler := &reverseproxy.Handler{
HealthChecks: &reverseproxy.HealthChecks{
// Enable passive health checks to automatically detect unhealthy upstreams.
Passive: &reverseproxy.PassiveHealthChecks{
FailDuration: caddy.Duration(30 * time.Second),
},
},
LoadBalancing: &reverseproxy.LoadBalancing{
// Retry failed requests to skip over temporarily unavailable upstreams.
Retries: 3,
},
Upstreams: upstreamPool,
}
routes = append(routes, caddyhttp.Route{
MatcherSetsRaw: caddyhttp.RawMatcherSets{
{
"host": caddyconfig.JSON(caddyhttp.MatchHost{hostname}, warnings),
},
},
HandlersRaw: []json.RawMessage{
caddyconfig.JSONModuleObject(handler, "handler", "reverse_proxy", warnings),
},
})
}
return routes
}
// verificationRoute returns a Caddy route that responds with the given static response at the /.uncloud-verify path.
func verificationRoute(response string, warnings *[]caddyconfig.Warning) caddyhttp.Route {
// Return the following route:
// {
// "match": [
// {
// "path": [
// "/.uncloud-verify"
// ]
// }
// ],
// "handle": [
// {
// "handler": "static_response",
// "body": "<response>",
// "status_code": 200
// }
// ]
// }
staticResponse := caddyhttp.StaticResponse{
StatusCode: caddyhttp.WeakString(strconv.Itoa(http.StatusOK)),
Body: response,
}
return caddyhttp.Route{
MatcherSetsRaw: caddyhttp.RawMatcherSets{
{
"path": caddyconfig.JSON(caddyhttp.MatchPath{VerifyPath}, warnings),
},
},
HandlersRaw: []json.RawMessage{
caddyconfig.JSONModuleObject(staticResponse, "handler", "static_response", warnings),
},
}
}
@@ -1,378 +0,0 @@
package caddyconfig
import (
"strings"
"testing"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/network"
"github.com/psviderski/uncloud/internal/machine/docker"
"github.com/psviderski/uncloud/pkg/api"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestGenerateJSONConfig(t *testing.T) {
configWithoutServices := `{
"servers": {
"http": {
"listen": [":80"],
"routes": [{
"match": [{"path": ["/.uncloud-verify"]}],
"handle": [{
"body": "verification-response-body",
"handler": "static_response",
"status_code": 200
}]
}],
"logs": {}
},
"https": {
"listen": [":443"],
"logs": {}
}
}
}`
tests := []struct {
name string
containers []api.ServiceContainer
want string
wantErr bool
}{
{
name: "empty containers",
containers: []api.ServiceContainer{},
want: configWithoutServices,
wantErr: false,
},
{
name: "HTTP container",
containers: []api.ServiceContainer{
newContainer("10.210.0.2", "app.example.com:8080/http"),
},
want: `{
"servers": {
"http": {
"listen": [":80"],
"routes": [
{
"match": [{"host": ["app.example.com"]}],
"handle": [{
"handler": "reverse_proxy",
"health_checks": {
"passive": {
"fail_duration": 30000000000
}
},
"load_balancing": {
"retries": 3
},
"upstreams": [{"dial": "10.210.0.2:8080"}]
}]
},
{
"match": [{"path": ["/.uncloud-verify"]}],
"handle": [{
"body": "verification-response-body",
"handler": "static_response",
"status_code": 200
}]
}
],
"logs": {}
},
"https": {
"listen": [":443"],
"logs": {}
}
}
}`,
wantErr: false,
},
{
name: "load balancing multiple containers",
containers: []api.ServiceContainer{
newContainer("10.210.0.2", "app.example.com:8080/http"),
newContainer("10.210.0.3", "app.example.com:8080/http"),
},
want: `{
"servers": {
"http": {
"listen": [":80"],
"routes": [
{
"match": [{"host": ["app.example.com"]}],
"handle": [{
"handler": "reverse_proxy",
"health_checks": {
"passive": {
"fail_duration": 30000000000
}
},
"load_balancing": {
"retries": 3
},
"upstreams": [
{"dial": "10.210.0.2:8080"},
{"dial": "10.210.0.3:8080"}
]
}]
},
{
"match": [{"path": ["/.uncloud-verify"]}],
"handle": [{
"body": "verification-response-body",
"handler": "static_response",
"status_code": 200
}]
}
],
"logs": {}
},
"https": {
"listen": [":443"],
"logs": {}
}
}
}`,
wantErr: false,
},
{
name: "HTTPS container",
containers: []api.ServiceContainer{
newContainer("10.210.0.2", "secure.example.com:8000/https"),
},
want: `{
"servers": {
"http": {
"listen": [":80"],
"routes": [
{
"match": [{"path": ["/.uncloud-verify"]}],
"handle": [{
"body": "verification-response-body",
"handler": "static_response",
"status_code": 200
}]
}
],
"logs": {}
},
"https": {
"listen": [":443"],
"routes": [
{
"match": [{"host": ["secure.example.com"]}],
"handle": [{
"handler": "reverse_proxy",
"health_checks": {
"passive": {
"fail_duration": 30000000000
}
},
"load_balancing": {
"retries": 3
},
"upstreams": [{"dial": "10.210.0.2:8000"}]
}]
}
],
"logs": {}
}
}
}`,
wantErr: false,
},
{
name: "mixed HTTP and HTTPS",
containers: []api.ServiceContainer{
newContainer("10.210.0.2",
"app.example.com:8080/http",
"web.example.com:8000/http"),
newContainer("10.210.0.3",
"app.example.com:8080/http",
"secure.example.com:8888/https"),
newContainer("10.210.0.4",
"web.example.com:8000/http",
"secure.example.com:8888/https"),
newContainer("10.210.0.5",
"app.example.com:8080/http",
"web.example.com:8000/http",
"secure.example.com:8888/https"),
},
want: `{
"servers": {
"http": {
"listen": [":80"],
"routes": [
{
"match": [{"host": ["app.example.com"]}],
"handle": [{
"handler": "reverse_proxy",
"health_checks": {
"passive": {
"fail_duration": 30000000000
}
},
"load_balancing": {
"retries": 3
},
"upstreams": [
{"dial": "10.210.0.2:8080"},
{"dial": "10.210.0.3:8080"},
{"dial": "10.210.0.5:8080"}
]
}]
},
{
"match": [{"host": ["web.example.com"]}],
"handle": [{
"handler": "reverse_proxy",
"health_checks": {
"passive": {
"fail_duration": 30000000000
}
},
"load_balancing": {
"retries": 3
},
"upstreams": [
{"dial": "10.210.0.2:8000"},
{"dial": "10.210.0.4:8000"},
{"dial": "10.210.0.5:8000"}
]
}]
},
{
"match": [{"path": ["/.uncloud-verify"]}],
"handle": [{
"body": "verification-response-body",
"handler": "static_response",
"status_code": 200
}]
}
],
"logs": {}
},
"https": {
"listen": [":443"],
"routes": [
{
"match": [{"host": ["secure.example.com"]}],
"handle": [{
"handler": "reverse_proxy",
"health_checks": {
"passive": {
"fail_duration": 30000000000
}
},
"load_balancing": {
"retries": 3
},
"upstreams": [
{"dial": "10.210.0.3:8888"},
{"dial": "10.210.0.4:8888"},
{"dial": "10.210.0.5:8888"}
]
}]
}
],
"logs": {}
}
}
}`,
wantErr: false,
},
{
name: "container without uncloud network ignored",
containers: []api.ServiceContainer{
newContainerWithoutNetwork("ignored.example.com:8080/http"),
},
want: configWithoutServices,
wantErr: false,
},
{
name: "container with invalid port ignored",
containers: []api.ServiceContainer{
newContainer("10.210.0.2", "invalid-port"),
},
want: configWithoutServices,
wantErr: false,
},
{
name: "containers with unsupported protocols and host mode ignored",
containers: []api.ServiceContainer{
newContainer("10.210.0.2", "5000/tcp"),
newContainer("10.210.0.3", "5000/udp"),
newContainer("10.210.0.4", "80:8080/tcp@host"),
},
want: configWithoutServices,
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
config, err := GenerateJSONConfig(tt.containers, "verification-response-body")
if tt.wantErr {
assert.Error(t, err)
return
}
require.NoError(t, err)
require.Len(t, config.AppsRaw, 1, "Expected one http app")
require.Contains(t, config.AppsRaw, "http", "Expected http app")
assert.JSONEq(t, tt.want, string(config.AppsRaw["http"]), "Generated Caddy app config doesn't match")
})
}
}
func newContainer(ip string, ports ...string) api.ServiceContainer {
portsLabel := strings.Join(ports, ",")
return api.ServiceContainer{Container: api.Container{InspectResponse: container.InspectResponse{
ContainerJSONBase: &container.ContainerJSONBase{
State: &container.State{
Running: true,
},
},
NetworkSettings: &container.NetworkSettings{
Networks: map[string]*network.EndpointSettings{
docker.NetworkName: {
IPAddress: ip,
},
},
},
Config: &container.Config{
Labels: map[string]string{
api.LabelServicePorts: portsLabel,
},
},
}}}
}
func newContainerWithoutNetwork(ports ...string) api.ServiceContainer {
portsLabel := strings.Join(ports, ",")
return api.ServiceContainer{Container: api.Container{InspectResponse: container.InspectResponse{
ContainerJSONBase: &container.ContainerJSONBase{
State: &container.State{
Running: true,
},
},
NetworkSettings: &container.NetworkSettings{
Networks: map[string]*network.EndpointSettings{
"other-network": {
IPAddress: "172.17.0.2",
},
},
},
Config: &container.Config{
Labels: map[string]string{
api.LabelServicePorts: portsLabel,
},
},
}}}
}
+14 -5
View File
@@ -2,6 +2,7 @@ package caddyconfig
import ( import (
"context" "context"
"errors"
"os" "os"
"google.golang.org/grpc/codes" "google.golang.org/grpc/codes"
@@ -9,7 +10,7 @@ import (
"google.golang.org/protobuf/types/known/emptypb" "google.golang.org/protobuf/types/known/emptypb"
"google.golang.org/protobuf/types/known/timestamppb" "google.golang.org/protobuf/types/known/timestamppb"
"github.com/psviderski/uncloud/internal/machine/api/pb" "github.com/psviderski/uncloud/api/pb"
) )
// Server implements the gRPC Caddy service. // Server implements the gRPC Caddy service.
@@ -22,18 +23,26 @@ func NewServer(service *Service) *Server {
return &Server{service: service} return &Server{service: service}
} }
// GetConfig retrieves the current Caddy configuration from the machine. // GetConfig retrieves the saved Caddy configuration and the latest reconciliation error from the machine.
func (s *Server) GetConfig(ctx context.Context, _ *emptypb.Empty) (*pb.GetCaddyConfigResponse, error) { func (s *Server) GetConfig(ctx context.Context, _ *emptypb.Empty) (*pb.GetCaddyConfigResponse, error) {
caddyfile, modifiedAt, err := s.service.Caddyfile() caddyfile, modifiedAt, err := s.service.Caddyfile()
if err != nil { if err != nil {
if os.IsNotExist(err) { if errors.Is(err, os.ErrNotExist) {
if lastErr := s.service.LastReconciliationError(); lastErr != nil {
return nil, status.Errorf(codes.NotFound, "%v; last Caddy config load failed: %v", err, lastErr)
}
return nil, status.Error(codes.NotFound, err.Error()) return nil, status.Error(codes.NotFound, err.Error())
} }
return nil, status.Error(codes.Internal, err.Error()) return nil, status.Error(codes.Internal, err.Error())
} }
recErr := ""
if lastErr := s.service.LastReconciliationError(); lastErr != nil {
recErr = lastErr.Error()
}
return &pb.GetCaddyConfigResponse{ return &pb.GetCaddyConfigResponse{
Caddyfile: caddyfile, Caddyfile: caddyfile,
ModifiedAt: timestamppb.New(modifiedAt), ModifiedAt: timestamppb.New(modifiedAt),
LastReconciliationError: recErr,
}, nil }, nil
} }
@@ -0,0 +1,62 @@
package caddyconfig
import (
"context"
"errors"
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/emptypb"
)
func TestServer_GetConfig(t *testing.T) {
t.Run("returns saved Caddyfile and reconciliation error", func(t *testing.T) {
dir := t.TempDir()
caddyfile := "example.com { respond ok }\n"
path := filepath.Join(dir, "Caddyfile")
require.NoError(t, os.WriteFile(path, []byte(caddyfile), 0o640))
info, err := os.Stat(path)
require.NoError(t, err)
service := NewService(dir)
service.setReconciliationResult(errors.New("module not registered: caddy.storage.uncloud"))
config, err := NewServer(service).GetConfig(context.Background(), &emptypb.Empty{})
require.NoError(t, err)
assert.Equal(t, caddyfile, config.GetCaddyfile())
assert.True(t, info.ModTime().Equal(config.GetModifiedAt().AsTime()))
assert.Equal(t, "module not registered: caddy.storage.uncloud", config.GetLastReconciliationError())
service.setReconciliationResult(nil)
config, err = NewServer(service).GetConfig(context.Background(), &emptypb.Empty{})
require.NoError(t, err)
assert.Empty(t, config.GetLastReconciliationError())
})
t.Run("returns NotFound with reconciliation error when Caddyfile is absent", func(t *testing.T) {
service := NewService(t.TempDir())
service.setReconciliationResult(errors.New("module not registered: caddy.storage.uncloud"))
config, err := NewServer(service).GetConfig(context.Background(), &emptypb.Empty{})
assert.Nil(t, config)
assert.Equal(t, codes.NotFound, status.Code(err))
assert.ErrorContains(t, err, "Caddyfile")
assert.ErrorContains(t, err, "last Caddy config load failed: module not registered: caddy.storage.uncloud")
})
t.Run("returns NotFound without reconciliation error when Caddyfile is absent", func(t *testing.T) {
service := NewService(t.TempDir())
config, err := NewServer(service).GetConfig(context.Background(), &emptypb.Empty{})
assert.Nil(t, config)
assert.Equal(t, codes.NotFound, status.Code(err))
assert.NotContains(t, err.Error(), "last Caddy config load failed")
})
}
+20 -2
View File
@@ -4,12 +4,15 @@ import (
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
"sync"
"time" "time"
) )
// Service provides methods to interact with the Caddy configuration on the machine. // Service provides methods to interact with the Caddy configuration on the machine.
type Service struct { type Service struct {
configDir string configDir string
mu sync.RWMutex
lastReconciliationError error
} }
// NewService creates a new Service instance with the specified Caddy configuration directory. // NewService creates a new Service instance with the specified Caddy configuration directory.
@@ -17,7 +20,8 @@ func NewService(configDir string) *Service {
return &Service{configDir: configDir} return &Service{configDir: configDir}
} }
// Caddyfile retrieves the current Caddy configuration (Caddyfile) from the machine's config directory. // Caddyfile retrieves the saved Caddyfile from the machine's config directory. The saved file may differ from the
// running configuration if Caddy accepted a load but the subsequent write failed.
func (s *Service) Caddyfile() (string, time.Time, error) { func (s *Service) Caddyfile() (string, time.Time, error) {
path := filepath.Join(s.configDir, "Caddyfile") path := filepath.Join(s.configDir, "Caddyfile")
content, err := os.ReadFile(path) content, err := os.ReadFile(path)
@@ -33,3 +37,17 @@ func (s *Service) Caddyfile() (string, time.Time, error) {
return string(content), fileInfo.ModTime(), nil return string(content), fileInfo.ModTime(), nil
} }
// LastReconciliationError reports the most recent unsuccessful controller attempt, if any. A successful attempt
// clears it. An empty value does not prove that Caddy has loaded the saved Caddyfile.
func (s *Service) LastReconciliationError() error {
s.mu.RLock()
defer s.mu.RUnlock()
return s.lastReconciliationError
}
func (s *Service) setReconciliationResult(err error) {
s.mu.Lock()
defer s.mu.Unlock()
s.lastReconciliationError = err
}
@@ -0,0 +1,42 @@
package caddyconfig
import (
"errors"
"sync"
"testing"
"github.com/stretchr/testify/assert"
)
func TestService_LastReconciliationError(t *testing.T) {
service := NewService(t.TempDir())
assert.NoError(t, service.LastReconciliationError())
service.setReconciliationResult(errors.New("global Caddy config rejected"))
assert.EqualError(t, service.LastReconciliationError(), "global Caddy config rejected")
service.setReconciliationResult(nil)
assert.NoError(t, service.LastReconciliationError())
}
func TestService_LastReconciliationErrorConcurrent(t *testing.T) {
service := NewService(t.TempDir())
var wg sync.WaitGroup
for range 10 {
wg.Add(2)
go func() {
defer wg.Done()
for range 100 {
service.setReconciliationResult(errors.New("retry"))
service.setReconciliationResult(nil)
}
}()
go func() {
defer wg.Done()
for range 100 {
_ = service.LastReconciliationError()
}
}()
}
wg.Wait()
}
@@ -0,0 +1,79 @@
package caddystorage
import (
"context"
"encoding/json"
"fmt"
"log/slog"
"strings"
"github.com/caddyserver/certmagic"
"github.com/psviderski/uncloud/api/pb"
"github.com/psviderski/uncloud/internal/machine/store"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/emptypb"
)
// ListCertificates lists issued certificates from this machine's Caddy storage replica.
// Unreadable entries and invalid resource metadata are skipped. Chains and issuer data are returned as stored.
func (s *Server) ListCertificates(ctx context.Context, _ *emptypb.Empty) (*pb.ListCertificatesResponse, error) {
records, err := s.store.List(ctx, "certificates/", store.KeyspaceListOptions{KeysOnly: true})
if err != nil {
if ctx.Err() != nil {
return nil, status.FromContextError(ctx.Err()).Err()
}
return nil, status.Errorf(codes.Internal, "list Caddy certificates: %v", err)
}
resp := &pb.ListCertificatesResponse{}
for _, record := range records {
if err = ctx.Err(); err != nil {
return nil, status.FromContextError(err).Err()
}
if !isCertificateKey(record.Key) {
continue
}
cert, err := s.loadIssuedCertificate(ctx, record.Key)
if err != nil {
slog.Warn("Failed to load issued certificate from Caddy storage.", "key", record.Key, "err", err)
continue
}
resp.Certificates = append(resp.Certificates, cert)
}
return resp, nil
}
// isCertificateKey recognizes CertMagic's certificates/<issuer>/<name>/<name>.crt layout.
func isCertificateKey(key string) bool {
parts := strings.Split(key, "/")
return len(parts) == 4 && parts[0] == "certificates" && parts[3] == parts[2]+".crt"
}
func (s *Server) loadIssuedCertificate(ctx context.Context, key string) (*pb.IssuedCertificate, error) {
chain, err := s.store.Get(ctx, key)
if err != nil {
return nil, err
}
metaKey := strings.TrimSuffix(key, ".crt") + ".json"
meta, err := s.store.Get(ctx, metaKey)
if err != nil {
return nil, err
}
var resource certmagic.CertificateResource
if err = json.Unmarshal(meta.Value, &resource); err != nil {
return nil, err
}
if len(resource.SANs) == 0 || strings.TrimSpace(resource.SANs[0]) == "" {
return nil, fmt.Errorf("certificate metadata has no SAN")
}
return &pb.IssuedCertificate{
San: resource.SANs[0],
Chain: chain.Value,
IssuerData: resource.IssuerData,
}, nil
}
@@ -0,0 +1,233 @@
package caddystorage
import (
"context"
"crypto/ed25519"
"crypto/rand"
"crypto/x509"
"crypto/x509/pkix"
"encoding/json"
"encoding/pem"
"errors"
"math/big"
"net"
"slices"
"strings"
"testing"
"time"
"github.com/caddyserver/certmagic"
"github.com/psviderski/uncloud/internal/machine/store"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/emptypb"
)
func TestServerListCertificates(t *testing.T) {
for _, sans := range [][]string{
{"app.example.com"}, {"*.example.com"}, {"192.0.2.1"}, {"www.example.com", "example.com"},
} {
t.Run(sans[0], func(t *testing.T) {
bundle, _, _ := certificateFixture(t, &x509.Certificate{
DNSNames: []string{"example.com", "www.example.com"},
IPAddresses: []net.IP{net.ParseIP("192.0.2.1")},
NotAfter: time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC),
})
key := certmagic.StorageKeys.SiteCert("local", sans[0])
storage := newCertificateStorage(t)
raw := json.RawMessage(`{"unknown_issuer_field":true}`)
storage.add(t, key, bundle, sans, raw)
storage.keys = append(storage.keys, "certificates", "certificates/local", "certificates/local/app.crt",
"certificates/local/app/wrong.crt", "certificates/local/app/app.crt/extra.crt",
"pki/authorities/local/root.crt", "ocsp/certificate")
resp, err := NewServer(storage).ListCertificates(t.Context(), &emptypb.Empty{})
require.NoError(t, err)
require.Len(t, resp.Certificates, 1)
cert := resp.Certificates[0]
assert.Equal(t, sans[0], cert.San)
assert.Equal(t, bundle, cert.Chain, "PEM must be returned unchanged")
assert.Equal(t, []byte(raw), cert.IssuerData)
assert.Equal(t, []string{key, strings.TrimSuffix(key, ".crt") + ".json"}, storage.loads)
})
}
}
func TestServerListCertificates_PreservesIssuerData(t *testing.T) {
bundle, _, _ := certificateFixture(t, &x509.Certificate{})
for _, raw := range []string{
"", "null", `{"url":"https://ca/cert/1","ca":"https://ca/directory"}`,
`{"url":"https://ca/cert/1","ca":"https://ca/directory","renewal_info":{"_selectedTime":"invalid"}}`,
`"provider-record"`, "[1,2,3]",
} {
t.Run(raw, func(t *testing.T) {
storage := newCertificateStorage(t)
storage.add(t, "certificates/custom/app/app.crt", bundle, []string{"app"}, json.RawMessage(raw))
resp, err := NewServer(storage).ListCertificates(t.Context(), &emptypb.Empty{})
require.NoError(t, err)
require.Len(t, resp.Certificates, 1)
assert.Equal(t, raw, string(resp.Certificates[0].IssuerData))
})
}
}
func TestServerListCertificates_OrderingAndDuplicates(t *testing.T) {
bundle, _, _ := certificateFixture(t, &x509.Certificate{})
storage := newCertificateStorage(t)
storage.add(t, certmagic.StorageKeys.SiteCert("z-issuer", "a.example.com"), bundle, []string{"a.example.com"}, nil)
storage.add(t, certmagic.StorageKeys.SiteCert("a-issuer", "z.example.com"), bundle, []string{"z.example.com"}, nil)
storage.add(t, certmagic.StorageKeys.SiteCert("b-issuer", "z.example.com"), bundle, []string{"z.example.com"}, nil)
resp, err := NewServer(storage).ListCertificates(t.Context(), &emptypb.Empty{})
require.NoError(t, err)
require.Len(t, resp.Certificates, 3)
certs := resp.Certificates
assert.Equal(t, []string{"z.example.com", "z.example.com", "a.example.com"},
[]string{certs[0].San, certs[1].San, certs[2].San})
for _, cert := range certs {
assert.Equal(t, bundle, cert.Chain)
}
}
func TestServerListCertificates_SkipsUnreadableEntries(t *testing.T) {
bundle, _, _ := certificateFixture(t, &x509.Certificate{})
const badKey = "certificates/local/bad/bad.crt"
const metaKey = "certificates/local/bad/bad.json"
for _, tt := range []struct {
name string
change func(*certificateStorageStub)
}{
{"missing chain", func(s *certificateStorageStub) { delete(s.values, badKey) }},
{"chain read failure", func(s *certificateStorageStub) { s.loadErrors[badKey] = errors.New("offline") }},
{"missing metadata", func(s *certificateStorageStub) { delete(s.values, metaKey) }},
{"metadata read failure", func(s *certificateStorageStub) { s.loadErrors[metaKey] = errors.New("offline") }},
{"invalid JSON", func(s *certificateStorageStub) { s.values[metaKey] = []byte("{") }},
{"missing SAN", func(s *certificateStorageStub) { s.values[metaKey] = []byte(`{}`) }},
{"empty first SAN", func(s *certificateStorageStub) { s.values[metaKey] = []byte(`{"sans":["","app"]}`) }},
{"blank first SAN", func(s *certificateStorageStub) { s.values[metaKey] = []byte(`{"sans":[" "]}`) }},
} {
t.Run(tt.name, func(t *testing.T) {
storage := newCertificateStorage(t)
storage.add(t, badKey, bundle, []string{"bad"}, nil)
storage.add(t, "certificates/local/good/good.crt", bundle, []string{"good"}, nil)
tt.change(storage)
resp, err := NewServer(storage).ListCertificates(t.Context(), &emptypb.Empty{})
require.NoError(t, err)
require.Len(t, resp.Certificates, 1)
assert.Equal(t, "good", resp.Certificates[0].San)
})
}
t.Run("all entries unreadable", func(t *testing.T) {
storage := newCertificateStorage(t)
storage.keys = []string{badKey}
resp, err := NewServer(storage).ListCertificates(t.Context(), &emptypb.Empty{})
require.NoError(t, err)
assert.Empty(t, resp.Certificates)
})
}
func TestServerListCertificates_ListErrors(t *testing.T) {
for _, fail := range []bool{false, true} {
storage := newCertificateStorage(t)
if fail {
storage.listError = errors.New("offline")
}
resp, err := NewServer(storage).ListCertificates(t.Context(), &emptypb.Empty{})
assert.Empty(t, storage.loads)
if fail {
assert.Nil(t, resp)
assert.Equal(t, codes.Internal, status.Code(err))
} else {
require.NoError(t, err)
assert.Empty(t, resp.Certificates)
}
}
}
// Only reads are implemented. Unexpected writes panic through the nil embedded Keyspace.
type certificateStorageStub struct {
*store.Keyspace
t *testing.T
keys []string
values map[string][]byte
loads []string
listCalls int
listError error
loadErrors map[string]error
onLoad func(string)
}
func newCertificateStorage(t *testing.T) *certificateStorageStub {
return &certificateStorageStub{t: t, values: make(map[string][]byte), loadErrors: make(map[string]error)}
}
func (s *certificateStorageStub) add(t *testing.T, key string, bundle []byte, sans []string, issuerData json.RawMessage) {
t.Helper()
metaKey := strings.TrimSuffix(key, ".crt") + ".json"
keyKey := strings.TrimSuffix(key, ".crt") + ".key"
meta, err := json.Marshal(certmagic.CertificateResource{SANs: sans, IssuerData: issuerData})
require.NoError(t, err)
s.keys = append(s.keys, key, metaKey, keyKey)
s.values[key], s.values[metaKey], s.values[keyKey] = bundle, meta, []byte("must not read private keys")
}
func (s *certificateStorageStub) List(ctx context.Context, prefix string, opts store.KeyspaceListOptions) ([]store.Record, error) {
s.listCalls++
assert.Equal(s.t, "certificates/", prefix)
assert.True(s.t, opts.KeysOnly, "listing must not fetch private key values")
var records []store.Record
for _, key := range slices.Sorted(slices.Values(s.keys)) {
records = append(records, store.Record{Key: key})
}
return records, s.listError
}
func (s *certificateStorageStub) Get(ctx context.Context, key string) (store.Record, error) {
require.False(s.t, strings.HasSuffix(key, ".key"), "private keys must never be loaded")
s.loads = append(s.loads, key)
if s.onLoad != nil {
s.onLoad(key)
}
if err := s.loadErrors[key]; err != nil {
return store.Record{}, err
}
value, ok := s.values[key]
if !ok {
return store.Record{}, store.ErrKeyNotFound
}
return store.Record{Key: key, Value: value}, nil
}
func certificateFixture(t *testing.T, template *x509.Certificate) ([]byte, *x509.Certificate, *x509.Certificate) {
t.Helper()
publicKey, key, err := ed25519.GenerateKey(rand.Reader)
require.NoError(t, err)
root := &x509.Certificate{
SerialNumber: big.NewInt(1), Subject: pkix.Name{CommonName: "Test CA"}, IsCA: true,
BasicConstraintsValid: true, KeyUsage: x509.KeyUsageCertSign,
NotBefore: time.Date(1990, 1, 1, 0, 0, 0, 0, time.UTC),
NotAfter: time.Date(2040, 1, 1, 0, 0, 0, 0, time.UTC),
}
rootDER, err := x509.CreateCertificate(rand.Reader, root, root, publicKey, key)
require.NoError(t, err)
root, err = x509.ParseCertificate(rootDER)
require.NoError(t, err)
template.SerialNumber = big.NewInt(2)
template.NotBefore = root.NotBefore
if template.NotAfter.IsZero() {
template.NotAfter = root.NotAfter
}
leafDER, err := x509.CreateCertificate(rand.Reader, template, root, publicKey, key)
require.NoError(t, err)
leaf, err := x509.ParseCertificate(leafDER)
require.NoError(t, err)
bundle := append(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: leafDER}),
pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: rootDER})...)
return bundle, leaf, root
}
+188
View File
@@ -0,0 +1,188 @@
// Package caddystorage implements the machine-local Caddy storage API.
// See api/pb/caddy_storage.proto for the gRPC service definition.
package caddystorage
import (
"context"
"errors"
"fmt"
"maps"
"path"
"slices"
"strings"
"github.com/psviderski/uncloud/api/pb"
"github.com/psviderski/uncloud/internal/machine/store"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/emptypb"
"google.golang.org/protobuf/types/known/timestamppb"
)
// Namespace identifies Caddy storage records in the cluster key-value store.
const Namespace = "caddy_storage"
// Server implements the machine-local CaddyStorage gRPC service.
type Server struct {
pb.UnimplementedCaddyStorageServer
store keyspace
}
type keyspace interface {
Get(context.Context, string) (store.Record, error)
Put(context.Context, string, []byte) error
Delete(context.Context, string, store.KeyspaceDeleteOptions) error
List(context.Context, string, store.KeyspaceListOptions) ([]store.Record, error)
}
func NewServer(store keyspace) *Server {
return &Server{store: store}
}
func (s *Server) Store(ctx context.Context, req *pb.StoreCaddyStorageRequest) (*emptypb.Empty, error) {
if err := validateKey(req.Key); err != nil {
return nil, status.Error(codes.InvalidArgument, err.Error())
}
if err := s.store.Put(ctx, req.Key, req.Value); err != nil {
return nil, status.Errorf(codes.Internal, "put value: %v", err)
}
return &emptypb.Empty{}, nil
}
func (s *Server) Load(ctx context.Context, req *pb.LoadCaddyStorageRequest) (*pb.LoadCaddyStorageResponse, error) {
if err := validateKey(req.Key); err != nil {
return nil, status.Error(codes.InvalidArgument, err.Error())
}
record, err := s.store.Get(ctx, req.Key)
if errors.Is(err, store.ErrKeyNotFound) {
return nil, status.Errorf(codes.NotFound, "Caddy storage key %q not found", req.Key)
}
if err != nil {
return nil, status.Errorf(codes.Internal, "get value: %v", err)
}
return &pb.LoadCaddyStorageResponse{
Value: record.Value,
UpdatedAt: timestamppb.New(record.UpdatedAt),
}, nil
}
func (s *Server) Delete(ctx context.Context, req *pb.DeleteCaddyStorageRequest) (*emptypb.Empty, error) {
if err := validateKey(req.Key); err != nil {
return nil, status.Error(codes.InvalidArgument, err.Error())
}
// Delete the key and all keys prefixed by it in case it's a directory.
if err := errors.Join(
s.store.Delete(ctx, req.Key, store.KeyspaceDeleteOptions{}),
s.store.Delete(ctx, req.Key+"/", store.KeyspaceDeleteOptions{Prefix: true}),
); err != nil {
return nil, status.Errorf(codes.Internal, "delete key: %v", err)
}
return &emptypb.Empty{}, nil
}
func (s *Server) List(ctx context.Context, req *pb.ListCaddyStorageRequest) (*pb.ListCaddyStorageResponse, error) {
if req.Prefix != "" {
if err := validateKey(req.Prefix); err != nil {
return nil, status.Error(codes.InvalidArgument, err.Error())
}
}
storagePrefix := req.Prefix
if storagePrefix != "" {
storagePrefix += "/"
}
records, err := s.store.List(ctx, storagePrefix, store.KeyspaceListOptions{KeysOnly: true})
if err != nil {
return nil, status.Errorf(codes.Internal, "list keys: %v", err)
}
// No descendants can mean an existing terminal key or a missing path.
// Check the exact key for non-root prefixes. An empty prefix lists the storage root.
if len(records) == 0 && req.Prefix != "" {
if _, err = s.store.Get(ctx, req.Prefix); errors.Is(err, store.ErrKeyNotFound) {
return nil, status.Errorf(codes.NotFound, "Caddy storage prefix %q not found", req.Prefix)
} else if err != nil {
return nil, status.Errorf(codes.Internal, "get prefix value: %v", err)
}
}
return &pb.ListCaddyStorageResponse{
Keys: listKeys(records, req.Prefix, req.Recursive),
}, nil
}
func listKeys(records []store.Record, prefix string, recursive bool) []string {
keys := make(map[string]struct{})
for _, record := range records {
relative := record.Key
if prefix != "" {
relative = strings.TrimPrefix(record.Key, prefix+"/")
}
components := strings.Split(relative, "/")
if !recursive {
keys[path.Join(prefix, components[0])] = struct{}{}
continue
}
for i := range components {
keys[path.Join(prefix, path.Join(components[:i+1]...))] = struct{}{}
}
}
return slices.Sorted(maps.Keys(keys))
}
func (s *Server) Stat(ctx context.Context, req *pb.StatCaddyStorageRequest) (*pb.StatCaddyStorageResponse, error) {
if err := validateKey(req.Key); err != nil {
return nil, status.Error(codes.InvalidArgument, err.Error())
}
record, err := s.store.Get(ctx, req.Key)
if err == nil {
return &pb.StatCaddyStorageResponse{
Key: req.Key,
UpdatedAt: timestamppb.New(record.UpdatedAt),
Size: int64(len(record.Value)),
IsTerminal: true,
}, nil
}
if !errors.Is(err, store.ErrKeyNotFound) {
return nil, status.Errorf(codes.Internal, "get value: %v", err)
}
// A terminal key is not found. Check if there are any keys prefixed by the key to determine if it's a directory.
records, err := s.store.List(ctx, req.Key+"/", store.KeyspaceListOptions{KeysOnly: true})
if err != nil {
return nil, status.Errorf(codes.Internal, "list keys: %v", err)
}
if len(records) == 0 {
return nil, status.Errorf(codes.NotFound, "Caddy storage key %q not found", req.Key)
}
return &pb.StatCaddyStorageResponse{
Key: req.Key,
IsTerminal: false,
}, nil
}
func validateKey(key string) error {
if key == "" {
return fmt.Errorf("key is empty")
}
if strings.HasPrefix(key, "/") || strings.HasSuffix(key, "/") {
return fmt.Errorf("key %q must not have a leading or trailing slash", key)
}
if strings.Contains(key, "\\") {
return fmt.Errorf("key %q must use forward slashes", key)
}
for component := range strings.SplitSeq(key, "/") {
if component == "" || component == "." || component == ".." {
return fmt.Errorf("key %q contains an invalid path component", key)
}
}
return nil
}
@@ -0,0 +1,106 @@
package caddystorage
import (
"testing"
"github.com/psviderski/uncloud/internal/machine/store"
"github.com/stretchr/testify/require"
)
func TestListKeys(t *testing.T) {
tests := []struct {
name string
records []store.Record
prefix string
recursive bool
want []string
}{
{
name: "empty root",
recursive: true,
},
{
name: "root immediate children",
records: []store.Record{
{Key: "ocsp/example.com"},
{Key: "certificates/issuer/example.com/example.com.key"},
{Key: "certificates/issuer/example.com/example.com.crt"},
},
want: []string{
"certificates",
"ocsp",
},
},
{
name: "root recursive",
records: []store.Record{
{Key: "ocsp/example.com"},
{Key: "certificates/issuer/example.com/example.com.key"},
{Key: "certificates/issuer/example.com/example.com.crt"},
},
recursive: true,
want: []string{
"certificates",
"certificates/issuer",
"certificates/issuer/example.com",
"certificates/issuer/example.com/example.com.crt",
"certificates/issuer/example.com/example.com.key",
"ocsp",
"ocsp/example.com",
},
},
{
name: "nested immediate children",
records: []store.Record{
{Key: "certificates/issuer/example.net/example.net.crt"},
{Key: "certificates/issuer/example.com/example.com.key"},
{Key: "certificates/issuer/example.com/example.com.crt"},
},
prefix: "certificates/issuer",
want: []string{
"certificates/issuer/example.com",
"certificates/issuer/example.net",
},
},
{
name: "nested terminal children",
records: []store.Record{
{Key: "ocsp/example.net"},
{Key: "ocsp/example.com"},
},
prefix: "ocsp",
want: []string{
"ocsp/example.com",
"ocsp/example.net",
},
},
{
name: "nested recursive",
records: []store.Record{
{Key: "certificates/issuer/example.net/example.net.crt"},
{Key: "certificates/issuer/example.com/example.com.key"},
{Key: "certificates/issuer/example.com/example.com.crt"},
},
prefix: "certificates/issuer",
recursive: true,
want: []string{
"certificates/issuer/example.com",
"certificates/issuer/example.com/example.com.crt",
"certificates/issuer/example.com/example.com.key",
"certificates/issuer/example.net",
"certificates/issuer/example.net/example.net.crt",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := listKeys(tt.records, tt.prefix, tt.recursive)
if len(tt.want) == 0 {
require.Empty(t, got)
return
}
require.Equal(t, tt.want, got)
})
}
}
+51 -103
View File
@@ -12,7 +12,7 @@ import (
"time" "time"
"github.com/cenkalti/backoff/v4" "github.com/cenkalti/backoff/v4"
"github.com/psviderski/uncloud/internal/machine/api/pb" "github.com/psviderski/uncloud/api/pb"
"github.com/psviderski/uncloud/internal/machine/caddyconfig" "github.com/psviderski/uncloud/internal/machine/caddyconfig"
"github.com/psviderski/uncloud/internal/machine/constants" "github.com/psviderski/uncloud/internal/machine/constants"
"github.com/psviderski/uncloud/internal/machine/corromigrate" "github.com/psviderski/uncloud/internal/machine/corromigrate"
@@ -176,7 +176,7 @@ func (cc *clusterController) Run(ctx context.Context) error {
// Start the network API server before waiting for the store sync so the machine is reachable on the mesh // Start the network API server before waiting for the store sync so the machine is reachable on the mesh
// during the sync and can serve requests that don't depend on the store. // during the sync and can serve requests that don't depend on the store.
// Assume the management IP can't be changed when the network is running. // Assume the management IP can't be changed when the network is running.
apiAddr := net.JoinHostPort(cc.state.Network.ManagementIP.String(), strconv.Itoa(constants.MachineAPIPort)) apiAddr := net.JoinHostPort(cc.state.Network.ManagementIP.String(), strconv.Itoa(constants.UncloudAPIPort))
listener, err := net.Listen("tcp", apiAddr) listener, err := net.Listen("tcp", apiAddr)
if err != nil { if err != nil {
return fmt.Errorf("listen API port: %w", err) return fmt.Errorf("listen API port: %w", err)
@@ -189,9 +189,10 @@ func (cc *clusterController) Run(ctx context.Context) error {
return nil return nil
}) })
// Wait for the store database to sync to the minimum version before starting store-dependent components. // Wait for initial replication before starting store-dependent components, such as WireGuard peer reconciliation.
// This prevents issues with using partially replicated data when the machine just joined the cluster, // This prevents issues with using partially replicated data when the machine just joined the cluster, e.g.,
// e.g., an empty machine list causing WireGuard peer misconfiguration. // an empty machine list causing WireGuard peer misconfiguration.
// This uses Store.WaitForVersion's replication-progress semantics, not an exact snapshot of the join-time data.
if err = cc.waitStoreSync(ctx); err != nil { if err = cc.waitStoreSync(ctx); err != nil {
return fmt.Errorf("wait initial cluster store sync: %w", err) return fmt.Errorf("wait initial cluster store sync: %w", err)
} }
@@ -366,120 +367,67 @@ func (cc *clusterController) handleEndpointChanges(ctx context.Context) {
} }
} }
// waitStoreSync blocks until the local store version >= state.MinStoreVersion and any known gaps are synced. // waitStoreSync waits for initial replication using [store.Store.WaitForVersion].
// No-op when MinStoreVersion is empty. Clears state.MinStoreVersion when reached. // No-op when MinStoreVersion is empty. Retries until cancellation and clears state.MinStoreVersion after success.
func (cc *clusterController) waitStoreSync(ctx context.Context) error { func (cc *clusterController) waitStoreSync(ctx context.Context) error {
target := cc.state.MinStoreVersion minVersion := cc.state.MinStoreVersion
if len(target) == 0 { if len(minVersion) == 0 {
return nil return nil
} }
slog.Info("Waiting for the initial cluster store sync.", "actors", len(target)) slog.Info("Waiting for the initial cluster store sync.", "min_version", minVersion)
ticker := time.NewTicker(500 * time.Millisecond) // Periodic deadlines surface stuck NAT/connectivity issues without aborting startup.
defer ticker.Stop()
// Periodic warning to surface stuck NAT/connectivity issues without aborting.
warnInterval := 5 * time.Minute warnInterval := 5 * time.Minute
warnTimer := time.NewTimer(warnInterval) nextWarning := time.Now().Add(warnInterval)
defer warnTimer.Stop() var lastErrLogTime time.Time
var (
lastLagging int
lastErrLogTime time.Time
)
for { for {
waitCtx, cancel := context.WithDeadline(ctx, nextWarning)
err := cc.store.WaitForVersion(waitCtx, minVersion)
cancel()
if ctx.Err() != nil {
return nil
}
if err == nil {
break
}
if errors.Is(err, store.ErrInvalidStoreVersion) {
return fmt.Errorf("wait for minimum cluster store version: %w", err)
}
if !time.Now().Before(nextWarning) {
slog.Warn("Cluster store sync still pending. Check connectivity to peers.")
nextWarning = time.Now().Add(warnInterval)
continue
}
// Retry store errors, throttling logs to once every 5 seconds.
if time.Since(lastErrLogTime) >= 5*time.Second {
slog.Error("Failed to check cluster store replication, retrying.", "err", err)
lastErrLogTime = time.Now()
}
select { select {
case <-ctx.Done(): case <-ctx.Done():
return nil return nil
case <-warnTimer.C: case <-time.After(500 * time.Millisecond):
local, err := cc.store.Version(ctx)
if err == nil {
slog.Error("Cluster store sync still pending. Check connectivity to peers.",
"lagging_actors", laggingActors(local, target))
} else {
slog.Error("Cluster store sync still pending. Check connectivity to peers.", "err", err)
}
warnTimer.Reset(warnInterval)
case <-ticker.C:
local, err := cc.store.Version(ctx)
if err != nil {
// Throttle error logs to once every 5 seconds.
if time.Since(lastErrLogTime) >= 5*time.Second {
slog.Error("Failed to get the cluster store version, retrying.", "err", err)
lastErrLogTime = time.Now()
}
continue
}
lagging := laggingActors(local, target)
if len(lagging) == 0 {
// Per-actor max doesn't imply contiguous apply: corrosion can buffer X:N before
// X:N-1 arrives and track the gap separately. Wait for any remaining gaps to be synced.
if err := cc.waitKnownMissingChanges(ctx); err != nil {
return fmt.Errorf("wait for known missing changes: %w", err)
}
// If the context was cancelled mid-gap-fill, don't persist a "synced" state.
if ctx.Err() != nil {
return nil
}
// Clear MinStoreVersion so next restart doesn't wait for sync.
cc.state.mu.Lock()
cc.state.MinStoreVersion = nil
err = cc.state.Save()
cc.state.mu.Unlock()
if err != nil {
return fmt.Errorf("save machine state after the initial cluster store sync: %w", err)
}
slog.Info("Cluster store completed the initial sync.", "actors", len(target))
return nil
}
if len(lagging) != lastLagging {
slog.Info("Syncing cluster store.", "lagging_actors", lagging)
lastLagging = len(lagging)
}
} }
} }
}
// laggingActors returns target actors whose local version is below the required value, as [have, need]. // Clear MinStoreVersion so the next restart doesn't wait for sync.
func laggingActors(local, target map[string]int64) map[string][2]int64 { cc.state.mu.Lock()
lagging := make(map[string][2]int64) cc.state.MinStoreVersion = nil
for actor, need := range target { err := cc.state.Save()
if have := local[actor]; have < need { if err != nil {
lagging[actor] = [2]int64{have, need} cc.state.MinStoreVersion = minVersion
}
} }
return lagging cc.state.mu.Unlock()
} if err != nil {
return fmt.Errorf("save machine state after the initial cluster store sync: %w", err)
// waitKnownMissingChanges polls the store until all known missing changes have been synced.
func (cc *clusterController) waitKnownMissingChanges(ctx context.Context) error {
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return nil
case <-ticker.C:
changes, err := cc.store.KnownMissingChanges(ctx)
if err != nil {
return fmt.Errorf("query known missing changes from cluster store: %w", err)
}
if len(changes) == 0 {
slog.Debug("All known missing changes have been synced to the cluster store.")
return nil
}
slog.Debug("Waiting for known missing changes to be synced to the cluster store.", "remaining",
len(changes))
}
} }
slog.Info("Cluster store completed the initial sync.", "min_version", minVersion)
return nil
} }
// runMachineSync keeps this machine's info in the cluster store in sync with the local state and the Docker engine // runMachineSync keeps this machine's info in the cluster store in sync with the local state and the Docker engine
+1 -1
View File
@@ -9,8 +9,8 @@ import (
"net/netip" "net/netip"
"time" "time"
"github.com/psviderski/uncloud/api/pb"
"github.com/psviderski/uncloud/internal/corrosion" "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/network"
"github.com/psviderski/uncloud/internal/machine/store" "github.com/psviderski/uncloud/internal/machine/store"
"github.com/psviderski/uncloud/internal/secret" "github.com/psviderski/uncloud/internal/secret"
+72 -14
View File
@@ -4,20 +4,24 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"errors" "errors"
"strings"
"github.com/psviderski/uncloud/internal/dns" "github.com/miekg/dns"
"github.com/psviderski/uncloud/internal/machine/api/pb" "github.com/psviderski/uncloud/api/pb"
undns "github.com/psviderski/uncloud/internal/dns"
"github.com/psviderski/uncloud/internal/machine/store" "github.com/psviderski/uncloud/internal/machine/store"
"google.golang.org/grpc/codes" "google.golang.org/grpc/codes"
"google.golang.org/grpc/status" "google.golang.org/grpc/status"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/emptypb" "google.golang.org/protobuf/types/known/emptypb"
) )
// uncloudDNSKey is the key used to store the details of the reserved domain in the store. // uncloudDNSKey stores the cluster domain and, when reserved, its Uncloud DNS credentials.
const uncloudDNSKey = "uncloud_dns" const uncloudDNSKey = "uncloud_dns"
type uncloudDNSDomain struct { type uncloudDNSDomain struct {
// Endpoint is the API endpoint of the Uncloud DNS service where the domain is reserved. // Endpoint is the API endpoint of the Uncloud DNS service where the domain is reserved.
// An empty endpoint means the domain is managed externally.
Endpoint string Endpoint string
Name string Name string
// TODO: encrypt the token in the store. // TODO: encrypt the token in the store.
@@ -34,14 +38,14 @@ func (c *Cluster) ReserveDomain(ctx context.Context, req *pb.ReserveDomainReques
} }
if _, err := c.storedDomain(ctx); err == nil { if _, err := c.storedDomain(ctx); err == nil {
return nil, status.Errorf(codes.AlreadyExists, "domain already reserved") return nil, status.Error(codes.AlreadyExists, "cluster domain already configured")
} else { } else {
if s := status.Convert(err); s.Code() != codes.NotFound { if s := status.Convert(err); s.Code() != codes.NotFound {
return nil, err return nil, err
} }
} }
dnsClient := dns.NewClient() dnsClient := undns.NewClient()
name, token, err := dnsClient.ReserveDomain(req.Endpoint) name, token, err := dnsClient.ReserveDomain(req.Endpoint)
if err != nil { if err != nil {
return nil, status.Error(codes.Internal, err.Error()) return nil, status.Error(codes.Internal, err.Error())
@@ -60,7 +64,7 @@ func (c *Cluster) ReserveDomain(ctx context.Context, req *pb.ReserveDomainReques
return nil, status.Errorf(codes.Internal, "store reserved domain: %v", err) return nil, status.Errorf(codes.Internal, "store reserved domain: %v", err)
} }
return &pb.Domain{Name: name}, nil return &pb.Domain{Name: name, Reserved: proto.Bool(true)}, nil
} }
func (c *Cluster) GetDomain(ctx context.Context, _ *emptypb.Empty) (*pb.Domain, error) { func (c *Cluster) GetDomain(ctx context.Context, _ *emptypb.Empty) (*pb.Domain, error) {
@@ -73,7 +77,7 @@ func (c *Cluster) GetDomain(ctx context.Context, _ *emptypb.Empty) (*pb.Domain,
return nil, err return nil, err
} }
return &pb.Domain{Name: domain.Name}, nil return &pb.Domain{Name: domain.Name, Reserved: proto.Bool(domain.Endpoint != "")}, nil
} }
func (c *Cluster) storedDomain(ctx context.Context) (uncloudDNSDomain, error) { func (c *Cluster) storedDomain(ctx context.Context) (uncloudDNSDomain, error) {
@@ -103,13 +107,63 @@ func (c *Cluster) ReleaseDomain(ctx context.Context, _ *emptypb.Empty) (*pb.Doma
if err != nil { if err != nil {
return nil, err return nil, err
} }
if domain.Endpoint == "" {
return nil, status.Error(codes.FailedPrecondition,
"cluster domain is set manually, use 'uc dns set \"\"' to unset it")
}
if err = c.store.Delete(ctx, uncloudDNSKey); err != nil { if err = c.store.Delete(ctx, uncloudDNSKey); err != nil {
return nil, status.Errorf(codes.Internal, "delete domain from store: %v", err) return nil, status.Errorf(codes.Internal, "delete domain from store: %v", err)
} }
// TODO: implement and call Uncloud DNS endpoint to release/delete the domain. // TODO: implement and call Uncloud DNS endpoint to release/delete the domain.
return &pb.Domain{Name: domain.Name}, nil return &pb.Domain{Name: domain.Name, Reserved: proto.Bool(true)}, nil
}
func (c *Cluster) SetDomain(ctx context.Context, req *pb.SetDomainRequest) (*emptypb.Empty, error) {
if err := c.checkReady(); err != nil {
return nil, err
}
name := req.GetName()
if name != "" {
if labels, ok := dns.IsDomainName(name); !ok || labels < 2 {
return nil, status.Errorf(codes.InvalidArgument,
"invalid cluster domain '%s': must be a valid domain name with at least two labels", name)
}
}
name = strings.ToLower(strings.TrimSuffix(name, "."))
existing, err := c.storedDomain(ctx)
if err != nil && status.Code(err) != codes.NotFound {
return nil, err
}
if name == "" {
if err == nil {
if existing.Endpoint != "" {
return nil, status.Error(codes.FailedPrecondition,
"cluster domain is reserved in Uncloud DNS, use 'uc dns release' to release it")
}
if err := c.store.Delete(ctx, uncloudDNSKey); err != nil {
return nil, status.Errorf(codes.Internal, "unset cluster domain: %v", err)
}
}
return &emptypb.Empty{}, nil
}
if err == nil {
return nil, status.Error(codes.AlreadyExists, "cluster domain already configured")
}
domain := uncloudDNSDomain{Name: name}
domainJSON, err := json.Marshal(domain)
if err != nil {
return nil, status.Errorf(codes.Internal, "marshal set domain for store: %v", err)
}
if err = c.store.Put(ctx, uncloudDNSKey, domainJSON); err != nil {
return nil, status.Errorf(codes.Internal, "store set domain: %v", err)
}
return &emptypb.Empty{}, nil
} }
func (c *Cluster) CreateDomainRecords( func (c *Cluster) CreateDomainRecords(
@@ -124,12 +178,16 @@ func (c *Cluster) CreateDomainRecords(
return nil, err return nil, err
} }
dnsClient := dns.NewClient() if domain.Endpoint == "" {
recordsReq := make([]dns.RecordRequest, len(req.Records)) return nil, status.Error(codes.FailedPrecondition, "cluster domain is not reserved in Uncloud DNS")
}
dnsClient := undns.NewClient()
recordsReq := make([]undns.RecordRequest, len(req.Records))
for i, r := range req.Records { for i, r := range req.Records {
recordsReq[i] = dns.RecordRequest{ recordsReq[i] = undns.RecordRequest{
Name: r.Name, Name: r.Name,
Type: dns.RecordType(r.Type.String()), Type: undns.RecordType(r.Type.String()),
Values: r.Values, Values: r.Values,
} }
} }
@@ -149,9 +207,9 @@ func (c *Cluster) CreateDomainRecords(
} }
switch r.Type { switch r.Type {
case dns.RecordTypeA: case undns.RecordTypeA:
resp.Records[i].Type = pb.DNSRecord_A resp.Records[i].Type = pb.DNSRecord_A
case dns.RecordTypeAAAA: case undns.RecordTypeAAAA:
resp.Records[i].Type = pb.DNSRecord_AAAA resp.Records[i].Type = pb.DNSRecord_AAAA
} }
} }
+4 -3
View File
@@ -1,8 +1,9 @@
package constants package constants
const ( const (
// MachineAPIPort is the port for the Machine API service on the management WireGuard network. // UncloudAPIPort is the TCP port on which each machine serves the Uncloud API over the management WireGuard
MachineAPIPort = 51000 // network.
// UnregistryPort is the port for the embedded container registry listening on the machine IP. UncloudAPIPort = 51000
// UnregistryPort is the TCP port for the embedded container registry listening on the machine IP.
UnregistryPort = 51500 UnregistryPort = 51500
) )
+1 -1
View File
@@ -15,8 +15,8 @@ import (
"path/filepath" "path/filepath"
"time" "time"
"github.com/psviderski/uncloud/api/pb"
"github.com/psviderski/uncloud/internal/fs" "github.com/psviderski/uncloud/internal/fs"
"github.com/psviderski/uncloud/internal/machine/api/pb"
"github.com/psviderski/uncloud/internal/machine/store" "github.com/psviderski/uncloud/internal/machine/store"
"google.golang.org/protobuf/encoding/protojson" "google.golang.org/protobuf/encoding/protojson"
_ "modernc.org/sqlite" _ "modernc.org/sqlite"
+52 -1
View File
@@ -9,6 +9,7 @@ import (
"time" "time"
"github.com/containerd/errdefs" "github.com/containerd/errdefs"
systemd "github.com/coreos/go-systemd/daemon"
"github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/image" "github.com/docker/docker/api/types/image"
"github.com/docker/docker/api/types/mount" "github.com/docker/docker/api/types/mount"
@@ -22,6 +23,13 @@ const (
Image = "ghcr.io/unlabs-dev/corrosion:2026.6.15" Image = "ghcr.io/unlabs-dev/corrosion:2026.6.15"
// ContainerName is the name of the managed Corrosion container. // ContainerName is the name of the managed Corrosion container.
ContainerName = "uncloud-corrosion" ContainerName = "uncloud-corrosion"
// systemdStartTimeoutExtension is the duration of each systemd service start timeout extension that prevents
// a slow image pull from exceeding the start timeout (TimeoutStartSec). Extensions are a no-op if Corrosion
// is not starting during a systemd service startup.
systemdStartTimeoutExtension = 30 * time.Second
// systemdStartTimeoutExtendMax caps the total duration the start timeout can be extended for.
systemdStartTimeoutExtendMax = 5 * time.Minute
) )
type DockerService struct { type DockerService struct {
@@ -175,8 +183,14 @@ func (s *DockerService) createAndStart(ctx context.Context) error {
} }
defer respBody.Close() defer respBody.Close()
// The pull on the first daemon start may take longer than the systemd service start timeout (TimeoutStartSec)
// on a slow connection. Keep extending the timeout while the pull is in progress so systemd doesn't kill
// the daemon before it reports readiness. This is a no-op if not running under systemd.
stopExtending := extendSystemdStartTimeout(ctx)
// Wait for pull to complete. // Wait for pull to complete.
if _, err := io.Copy(io.Discard, respBody); err != nil { _, err = io.Copy(io.Discard, respBody)
stopExtending()
if err != nil {
return fmt.Errorf("read pull response: %w", err) return fmt.Errorf("read pull response: %w", err)
} }
slog.Info("Docker image pulled.", "image", s.Image, "duration", time.Since(start).String()) slog.Info("Docker image pulled.", "image", s.Image, "duration", time.Since(start).String())
@@ -192,3 +206,40 @@ func (s *DockerService) createAndStart(ctx context.Context) error {
} }
return nil return nil
} }
// extendSystemdStartTimeout periodically asks systemd to extend the service start timeout, for up to
// systemdStartTimeoutExtendMax. The returned function stops the extensions. It is a no-op when the service
// is not running under systemd (NOTIFY_SOCKET is not set).
func extendSystemdStartTimeout(ctx context.Context) (stop context.CancelFunc) {
ctx, cancel := context.WithCancel(ctx)
go func() {
// Send extensions more frequently than they expire so a single missed tick doesn't time out the start.
ticker := time.NewTicker(systemdStartTimeoutExtension / 3)
defer ticker.Stop()
deadline := time.Now().Add(systemdStartTimeoutExtendMax)
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
if time.Now().After(deadline) {
slog.Warn("Stopped extending the systemd service start timeout: corrosion service is taking "+
"too long to start.", "timeout", systemdStartTimeoutExtendMax)
return
}
msg := fmt.Sprintf("EXTEND_TIMEOUT_USEC=%d", systemdStartTimeoutExtension.Microseconds())
if _, err := systemd.SdNotify(false, msg); err != nil {
slog.Warn("Failed to extend the systemd service start timeout when starting corrosion service.",
"err", err)
return
}
slog.Info("Extended systemd service start timeout while corrosion service is starting.",
"duration", systemdStartTimeoutExtension)
}
}
}()
return cancel
}
Loaded 100 of 228 files, more files were not shown because too many files have changed in this diff. Show more