Compare commits

..
160 Commits
Author SHA1 Message Date
Pasha Sviderski 55773e92ce fix: allow direct routing from WireGuard mesh to containers for Docker 28.2.0+ 2025-07-08 21:20:49 +10:00
Pasha SviderskiandGitHub 37f543b905 docs: fix suddenly exploded width of uc logo in README.md 2025-07-07 11:36:55 +10:00
Dario GriffoandGitHub e5f96509bf docs: add unofficial Debian package repository (#84) 2025-07-05 19:40:36 +10:00
Anton Ovchinnikov 2f35ac6498 chore: Add .editorconfig 2025-07-04 19:16:40 +02:00
Pasha Sviderski f75afa0cec Merge branch 'pasha/machine-rm' 2025-07-03 21:30:46 +10:00
Pasha Sviderski 0f38b128fa chore: fail 'machine rm' as not fully implemented 2025-07-03 21:30:09 +10:00
Pasha Sviderski 535b91fe52 feat: add stub for Reset method in machine API 2025-07-03 21:28:05 +10:00
Pasha Sviderski 5c8beb8bd3 chore: tidy up go.mod, remove toolchain 2025-07-03 20:57:56 +10:00
Pasha Sviderski 2b4da1e6fe chore: remove anonymous volumes created by service containers when removing them 2025-07-02 11:16:00 +10:00
Pasha Sviderski be8b4f079f feat: remove containers on machine before removing it 2025-07-02 11:14:31 +10:00
Pasha Sviderski 59b96cc01f feat: init 'machine rm' command that outputs service containers that will be removed 2025-07-01 19:51:57 +10:00
Pasha Sviderski 4c77fe2cfd chore: tidy up 2025-06-30 17:53:34 +10:00
Pavel Sviderski 3b89af4a11 fix: e2e compose build test on macOS with Docker Desktop 2025-06-27 17:54:02 +10:00
Zasda Yusuf MikailandGitHub 1abadaef50 feat: set default log driver for service containers to 'local' (#83) 2025-06-27 16:42:26 +10:00
Krishna SantoshandGitHub 3b1ce42dd7 fix(docs): add missing word to complete the sentence (#82) 2025-06-24 16:49:07 +10:00
Pavel Sviderski 1640b4c433 docs: mention github discussions for help in README 2025-06-24 11:40:06 +10:00
Anton OvchinnikovandGitHub 44549c00fd feat: Label new volumes as managed by uncloud (#79) 2025-06-23 22:05:05 +02:00
Anton Ovchinnikov 26f34df3e6 fix: Improve output for build operations 2025-06-22 19:26:25 +02:00
Anton Ovchinnikov 89fb9efcfa fix: Use GITHUB_TOKEN to increase the rate limit for mise action 2025-06-22 11:50:17 +02:00
Anton OvchinnikovandGitHub 638f320959 build: Install protobuf tools via mise (#77) 2025-06-22 11:23:56 +02:00
Pavel Sviderski 3f2e60ea74 docs: mention unregistry as comming soon feature 2025-06-22 09:38:15 +10:00
Pavel Sviderski 8e0a24e781 chore: add donate button in README 2025-06-20 13:37:50 +10:00
Pasha SviderskiandGitHub e3e39dde32 chore: create FUNDING.yml 2025-06-20 12:36:58 +10:00
Pavel Sviderski fc61e2fc07 feat: enable passive health checks and upstream retries in Caddy 2025-06-12 18:38:28 +10:00
Anton Ovchinnikov 2a0d31cec0 chore: Helper make target to install protobuf/grpc plugins 2025-06-11 23:57:50 +02:00
Anton OvchinnikovandGitHub 2f55af0cab test: Add an e2e test for build&push (#69)
* test: Add an e2e test for build&push

* test: Add image cleanup

* test: Switch to assert.NoError
2025-06-09 22:10:53 +02:00
Anton OvchinnikovandGitHub 971b21d6c1 feat: Implement basic build & push capabilities for services (#68)
* wip: Build experiments

* wip: overall structure for build/push

* ref: Split method, add test fixture

* feat: Basic push functionality

* ref: Better structure and output

* feat: Handle registry auth

* feat: Integrate with deploy

* ref: Split into files

* fix: Modernize push options
2025-06-03 21:35:54 +02:00
Pavel Sviderski b651d95183 docs: demo app - Compose and custom domain 2025-06-02 20:03:19 +10:00
Pavel Sviderski d45c1b6869 docs: add Deploy demo app page (partial), change prism theme to palenight 2025-05-30 20:00:04 +10:00
Pavel Sviderski a5fe9fbfb2 chore: update -p flag help for run command 2025-05-29 21:47:50 +10:00
Pavel Sviderski 4590a516ad chore: set default ssh key path to ~/.ssh/id_ed25519 and handle home expansion 2025-05-29 15:00:33 +10:00
Pavel Sviderski 5f9ed6dacf docs: fix links 2025-05-28 18:39:08 +10:00
Pavel Sviderski e97497d737 docs: refine overview 2025-05-28 18:35:23 +10:00
Pavel Sviderski b4bf1d17d0 docs: add note for Windows installation, enable tabs support 2025-05-28 11:14:27 +10:00
Pavel Sviderski cfb4693286 docs: installation page 2025-05-28 10:28:33 +10:00
Pavel Sviderski fd3facb6b0 chore: minor README change 2025-05-27 16:16:26 +10:00
Pavel Sviderski bd03bb9bfa chore: minor README change 2025-05-27 16:12:32 +10:00
Pavel Sviderski eec728620b chore: minor README change 2025-05-27 16:09:20 +10:00
Pavel Sviderski d2dd5e3c89 chore: add inspiration & acknowledgements section to README 2025-05-27 16:07:30 +10:00
Pavel Sviderski 2bfcce7803 chore: add uncloud-docs deploy from compose to demo section in README 2025-05-27 13:41:30 +10:00
Pavel Sviderski 1ec5184f21 chore: self-hosting focused front page (first screen) 2025-05-26 18:50:24 +10:00
Pavel Sviderski 23dfe45b2f chore: add docs badge to github README 2025-05-26 12:29:51 +10:00
Anton Ovchinnikov d9e906a77e test: Disable test caching 2025-05-25 19:53:43 +02:00
Anton Ovchinnikov 51f7ebf3bf test: Add cleanup target that removes dangling test containers 2025-05-25 19:46:13 +02:00
Pavel Sviderski ae04dea808 chore: add Docs to website navbar and footer, add other resources to footer 2025-05-23 20:11:11 +10:00
Pavel Sviderski 009c0e1b4c chore: scale docs service to 2 replicas in different regions 2025-05-23 18:09:54 +10:00
Pavel Sviderski 18d38e96b4 docs: minor overview polish 2025-05-23 16:31:29 +10:00
Pavel Sviderski 12c86f1dd4 fix: use existing cluster client when deploying caddy after adding new machine #65 2025-05-23 16:31:07 +10:00
Anton Ovchinnikov 4df1820f96 fix(doc): Tiny fixes 2025-05-22 22:19:02 +02:00
Pavel Sviderski 6f9ac1fcc7 chore: add Docker image for docs and Uncloud Compose deployment (dogfooding!) 2025-05-22 21:01:52 +10:00
Pavel Sviderski 6a98acbe79 docs: add Overview page and Getting started stubs 2025-05-22 18:37:10 +10:00
Pavel Sviderski b0059af9b1 docs: change color palette for links to blue 2025-05-22 18:36:35 +10:00
Pavel Sviderski 7edd8f2976 chore: add m alias for machine command 2025-05-21 15:46:31 +10:00
Pavel Sviderski 735e36012a chore: delete unused ucind-image-push target in Makefile 2025-05-21 15:46:09 +10:00
69ebce57e9 test: Add basic Github Actions config (#63)
* test: Add basic Github Actions config

* fix(e2e-tests): handle asynchronous port publishing for ucind container

* fix(e2e-tests): increase machine ready wait timeout 1->10s

* fix: Pin GHA versions

---------

Co-authored-by: Pavel Sviderski <me@psviderski.name>
2025-05-20 23:59:02 +02:00
Pavel Sviderski 365a5fc0b5 chore: build multiarch images for corrosion and ucind to support e2e tests on amd64 2025-05-20 12:47:27 +10:00
Pavel Sviderski 624caf037b chore(docs): init overview page 2025-05-19 22:42:04 +10:00
Pavel Sviderski ff1a8fbcf9 chore: fix comment 2025-05-16 21:35:21 +10:00
Pavel Sviderski 3aa8fead76 chore: configure docs custom styles, init intro page 2025-05-16 21:35:04 +10:00
Pavel Sviderski 4075f85b72 chore: minor website update 2025-05-15 20:46:02 +10:00
Pavel Sviderski 59f6fa75ca chore: init docusaurus docs 2025-05-15 20:45:46 +10:00
Pavel Sviderski 8b66a11394 chore: update hero 2025-05-14 22:06:31 +10:00
Pavel Sviderski fa3d992dbc chore: period 2025-05-13 18:27:55 +10:00
Pavel Sviderski a7d8afb052 chore: update value section wording 2025-05-13 18:26:37 +10:00
Pavel Sviderski 0d820bc75a chore: replace github discussions with discord in subscribe section 2025-05-13 18:23:36 +10:00
Pavel Sviderski 6a4e63dbd4 chore: add Discord to landing, remove crossed out Kubernetes 2025-05-13 18:21:33 +10:00
Pavel Sviderski 7699b530c2 chore: refine links 2025-05-13 17:59:41 +10:00
Pavel Sviderski f1abbbea6d chore: add discord and X badges to README 2025-05-13 17:53:57 +10:00
Pavel Sviderski 683ad22358 chore: remove extra [FLAGS] from volume commands help, add SERVICE for inspect 2025-05-13 10:24:16 +10:00
Pavel Sviderski 8f00e0082e chore: add social links in the footer 2025-05-10 14:36:05 +10:00
Pavel Sviderski c34542a9d8 chore: update website title 2025-05-10 09:11:16 +10:00
Pavel Sviderski 88db1cb6a9 fix: hero svg on safari 2025-05-10 07:48:20 +10:00
Pavel Sviderski dfa6e001c5 chore: cross over kubernetes on landing hero 2025-05-09 22:56:07 +10:00
Pavel Sviderski f49ec465d0 chore: bring terminal on landing back, update hero, move demo to bottom 2025-05-09 22:38:58 +10:00
Pavel Sviderski 1ffe9acdd7 chore: minor README rearrangement 2025-05-09 21:49:11 +10:00
Pavel Sviderski e35e69b4be fix: check latest Caddy image only if not specified for 'uc caddy deploy' 2025-05-07 16:03:07 +10:00
Pasha SviderskiandGitHub 11c2e641f1 Merge pull request #58 from Tova-Rozovsky/valid_DNS 2025-05-07 16:02:55 +10:00
Pasha SviderskiandGitHub bb97537df7 fix: format error message 2025-05-07 15:59:53 +10:00
Tova-Rozovsky 9f4b8179b5 valid service name 2025-05-06 17:43:05 +03:00
Pavel Sviderski f7b079c4f2 chore: collapse how it works section in readme 2025-05-06 19:00:54 +10:00
Pavel Sviderski 3648187219 chore: allow DNS queries from Uncloud containers to the embedded DNS server 2025-05-05 20:00:34 +10:00
Pavel Sviderski 93346301fb chore: clarify zero-downtime deployments feature 2025-05-05 17:15:41 +10:00
Pavel Sviderski cc86c8660c chore: rearrange features in README 2025-05-05 17:11:49 +10:00
Pavel Sviderski 2963436538 chore: update available features in README 2025-05-05 17:09:02 +10:00
Pavel Sviderski 6e326277bb chore: set up UNCLOUD-INPUT iptables chain and allow WireGuard traffic to the machine 2025-05-05 14:50:33 +10:00
Tova-Rozovsky 469d6fadcd Fix: restrict validation to RFC1123 label name 2025-05-04 16:14:17 +03:00
Tova-Rozovsky 5f214247ca added validition for service name 2025-05-02 10:00:16 +03:00
Pavel Sviderski 0b8495e964 feat: make all confirmation forms accessible 2025-05-01 14:12:12 +10:00
Pavel Sviderski 6068e24f4f fix(ctx): panic: index out of range when only one context in uncloud config (closes #52) 2025-05-01 14:01:29 +10:00
Pasha SviderskiandGitHub 7861d56dff Merge pull request #54 from cedws/profiles
feat: add support for Compose profiles
2025-05-01 13:54:25 +10:00
Connor Edwards 7bc69b9a9e feat: add support for Compose profiles 2025-05-01 12:23:56 +09:00
Pavel Sviderski 38800924ed fix(caddy): proxy to only healthy containers 2025-05-01 11:47:05 +10:00
Pavel Sviderski cf7e333579 feat(dns-server): use embedded DNS server for all service containers 2025-05-01 11:27:27 +10:00
Pavel Sviderski 769f5e47c3 feat(dns-server): check system prerequisites when init/add machine: DNS port is not in use by another DNS server 2025-04-30 20:46:10 +10:00
Pavel Sviderski 6e1165ec57 chore(dns-server): make TCP server non-critical; do not exit if it fails to start 2025-04-30 17:46:44 +10:00
Pavel Sviderski 8d16da596c feat(dns-server): run embedded internal DNS server on machine IP in uncloudd daemon process 2025-04-30 17:24:52 +10:00
Pavel Sviderski d6c06828b3 fix(dns-server): fix constructor and log messages 2025-04-30 17:20:48 +10:00
Pavel Sviderski fe93d91894 fix: attributes handling in SlogTextHandler for child loggers 2025-04-30 16:56:32 +10:00
Pavel Sviderski 3f70018e7c fix: import cycle in pkg/api 2025-04-30 14:18:28 +10:00
Pavel Sviderski 70e9385032 chore(dns-server): implement resolver for service records that watches container changes 2025-04-30 14:13:48 +10:00
Pavel Sviderski db3016fe89 fix: --file flag description for uc deploy cmd 2025-04-30 11:45:25 +10:00
Pavel Sviderski c56385602e chore(dns-server): authoritative responses for internal DNS queries and forwarding to ustream servers 2025-04-29 20:20:55 +10:00
Pavel Sviderski fbbf839258 chore: init embedded DNS server 2025-04-28 21:36:04 +10:00
Pavel Sviderski eee28e2ead feat: add version flag to uncloudd binary 2025-04-26 19:17:26 +10:00
Pasha SviderskiandGitHub ef57b62625 Merge pull request #50 from cedws/version
feat: add version flag to main cmd
2025-04-26 19:17:01 +10:00
Connor Edwards e854bbb3e1 feat: add version flag to main cmd 2025-04-25 19:55:37 +09:00
Pavel Sviderski 938e7d05c0 feat: add support for entrypoint, cpus, mem_limit, mem_reservation, priviliged, user to Compose deployment 2025-04-24 22:40:46 +10:00
Pavel Sviderski 6c2759f315 feat: add --cpu, --memory, --privileged, --user options for 'service run' 2025-04-24 20:30:48 +10:00
Pavel Sviderski 72bf34632f chore: map new container properties to Docker container configs 2025-04-24 17:24:58 +10:00
Pavel Sviderski 7db8bba6ea refactor: cpu and memory under Resources struct 2025-04-24 16:40:26 +10:00
Pavel Sviderski badb8ecd3c chore: implement EvalContainerSpecChange for new container properties 2025-04-24 15:51:47 +10:00
Pavel Sviderski 9413f1c0b1 feat: define essential container spec parameters 2025-04-24 13:37:25 +10:00
Pavel Sviderski d8818ba770 chore: remove unnecessary fields from containers table in machine.db 2025-04-23 16:11:29 +10:00
Pavel Sviderski 440215b811 fix: TestInstallCmd test 2025-04-23 16:10:40 +10:00
Pavel Sviderski 02a418aafb chore: add --version flag for 'machine init|add' to specify uncloud daemon version 2025-04-23 14:40:04 +10:00
Pavel Sviderski 33550be65e fix: compose deployment plan preview 2025-04-23 12:04:04 +10:00
Pavel Sviderski edad2429c5 chore: verify idempotency for compose deploy with volumes 2025-04-23 12:03:23 +10:00
Pavel Sviderski b605a64fbe chore: add e2e compose test with volumes 2025-04-23 10:41:24 +10:00
Pavel Sviderski 16c876d83f fix: compare container specs with equate empty option (nil map == {} map) 2025-04-22 23:31:45 +10:00
Pavel Sviderski d6ff62159d fix: compose deployment: use mutex to correctly collect service specs 2025-04-22 22:48:06 +10:00
Pavel Sviderski fcf164dda1 chore: refactor schedulers to work with cluster state snapshot, VolumeScheduler updates state with scheduled volumes 2025-04-22 20:34:54 +10:00
Pavel Sviderski e5957d9d6b chore: map compose volumes to server spec, initial volume scheduling 2025-04-21 21:25:26 +10:00
Pavel Sviderski 143e68cc95 chore: do not set default volume driver to local, distinguish behaviour when it's set or not 2025-04-21 16:53:43 +10:00
Pavel Sviderski 835c310987 chore: delete old basic-compose.yaml fixture 2025-04-21 12:50:13 +10:00
Pavel Sviderski 5fce84d33c chore: cleanup compose-basic test 2025-04-21 12:49:44 +10:00
Pavel Sviderski 45497fd20c chore: update api.ServiceClient interface to include service creation and removal 2025-04-21 12:48:21 +10:00
Pavel Sviderski f08d9fe405 refactor: 'service run' create missing volumes satisfying all placement constraints 2025-04-20 19:57:30 +10:00
Pavel Sviderski 565a8d8366 chore: implement VolumeScheduler to determine what missing volumes should be created and where 2025-04-20 18:57:26 +10:00
Pavel Sviderski 343357adf9 find optimal solution in VolumeScheduler 2025-04-20 18:53:18 +10:00
Pavel Sviderski 8ee551ca8f cleanup 2025-04-20 11:15:07 +10:00
Pavel Sviderski 948201cb6f add VolumeScheduler with one flaky test 2025-04-20 11:13:30 +10:00
Pavel Sviderski f71fc57f34 chore: e2e tests for replicated and global deployments with volumes 2025-04-18 17:27:34 +10:00
Pavel Sviderski 1acb0ff110 chore: implement VolumesConstraint to only schedule on machines with required volumes 2025-04-17 21:34:52 +10:00
Pavel Sviderski 03c2681792 chore: cleanup unused machine filter struct 2025-04-16 22:06:11 +10:00
Pavel Sviderski f3cb6657ae chore: replace machine filter with placement constraint in service spec 2025-04-16 22:04:37 +10:00
Pavel Sviderski 4532b985d4 chore: populate ucind cluster machines with IDs 2025-04-16 22:01:33 +10:00
Pavel Sviderski 000a8a1d3e chore: init scheduler entities 2025-04-15 22:30:03 +10:00
Pavel Sviderski 7705db2c4f refactor: machine listing for replicated plan 2025-04-14 22:04:39 +10:00
Pavel Sviderski 590a1b231f chore(volumes): e2e test when volume doesn't exist 2025-04-13 12:43:23 +10:00
Pavel Sviderski 8709ac0ed5 chore: fix Errorf arguments 2025-04-11 21:34:07 +10:00
Pavel Sviderski f8ebd94a79 feat(volumes): create and mount data volumes when running service with 'uc run' command 2025-04-11 21:28:36 +10:00
Pavel Sviderski 87331a9265 chore: add filter to ListMachines 2025-04-11 21:26:40 +10:00
Pavel Sviderski 2f5a2c6344 feat(volumes): new 'volume create|inspect|ls|rm' CLI commands to manage volumes 2025-04-10 20:33:03 +10:00
Pavel Sviderski 97e032cb7b chore: update ListVolumes to accept a filter 2025-04-10 20:31:52 +10:00
Pavel Sviderski 410a95ec88 chroe: add TODO to fix empty context in output message when adding machines 2025-04-10 20:29:52 +10:00
Pavel Sviderski df4ab40e12 chore(volumes): add client methods for volume management, e2e test 2025-04-10 13:50:08 +10:00
Pavel Sviderski c6205d4f57 chore(volumes): map volume specs to container config, diff specs with volumes 2025-04-09 21:51:20 +10:00
Pavel Sviderski 13dea92311 chore(volumes): gRPC methods for managing Docker volumes 2025-04-08 16:33:35 +10:00
Pavel Sviderski 9100c22e7f chore: new specs for defining service volumes 2025-04-07 21:00:26 +10:00
Pavel Sviderski 6125553a97 chore: introduce VolumeSpec supporting different volume types (bind, volume, tmpfs) 2025-04-04 21:22:54 +10:00
Pavel Sviderski c9452f1509 chore: ignore uc binary in the repo root 2025-04-03 18:19:06 +10:00
Pavel Sviderski 25b173128a feat(env): support env vars for services (both run command and compose) 2025-04-03 18:14:17 +10:00
Pavel Sviderski 4e40a93991 chore: remove anonymous volumes for ucind containers 2025-04-03 16:35:48 +10:00
Pavel Sviderski 9fc54c9d64 fix: format config.Path method 2025-04-03 16:35:00 +10:00
Pavel Sviderski cacfca7a88 chore: print message about changed context for 'ctx use' command 2025-04-03 16:21:04 +10:00
Pavel Sviderski b3885b0888 chore: activate the context of a newly initialised cluster 2025-04-03 13:38:48 +10:00
Pavel Sviderski 3ada89c4f8 fix: adding ucind cluster with tcp connections to config 2025-04-02 19:45:15 +10:00
Pavel Sviderski 20b2ca2ea5 feat(ctx): interactive ctx command to switch between cluster contexts 2025-04-02 16:31:26 +10:00
Pavel Sviderski 1bbefdd0d2 feat: add 'ctx ls' command to list available cluster contexts in config 2025-04-02 09:25:12 +10:00
Pavel Sviderski f467fad84c chore: rename cluster term to context in CLI and config, convert config toml -> yaml 2025-04-01 21:29:32 +10:00
152 changed files with 30142 additions and 1635 deletions
+20
View File
@@ -0,0 +1,20 @@
# EditorConfig is awesome: https://editorconfig.org
root = true
# Default settings for all files
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
# Settings for Go files
[*.go]
indent_style = tab
indent_size = 4
# Settings for Bash scripts
[*.sh]
indent_style = space
indent_size = 4
+1
View File
@@ -0,0 +1 @@
github: [psviderski]
Binary file not shown.

After

Width:  |  Height:  |  Size: 525 KiB

+67
View File
@@ -0,0 +1,67 @@
name: Go Tests
on:
push:
branches:
- "main"
- "test/**"
- "release/**"
pull_request:
branches:
- main
paths:
- ".github/**"
- "**.go"
- "**.proto"
- "go.*"
- "Makefile"
- "scripts/**"
- "test/**"
permissions:
contents: read
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Set up Go
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
with:
go-version: "1.23.2"
- name: Install dependencies
run: |
go mod tidy
git diff --exit-code ||
(echo "go.mod or go.sum has changed. Please run 'go mod tidy' and commit the changes." && exit 1)
- name: Run tests
run: make test
timeout-minutes: 10
check-protobuf:
runs-on: ${{ matrix.os }}
timeout-minutes: 10
strategy:
matrix:
# Run on more platforms so that mise lockfile checksums are checked/generated for all platforms.
# See https://github.com/jdx/mise/issues/4276 for details.
os: [ubuntu-latest, macos-latest]
steps:
- name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Install Mise
uses: jdx/mise-action@13abe502c30c1559a5c37dff303831bab82c9402 # v2.2.3
with:
version: "2025.6.5"
env:
GITHUB_TOKEN: ${{ github.token }}
- name: Generate Protobuf files
run: |
make proto
# check if the generated files are up to date
git diff --exit-code ||
(echo "Generated protobuf files are not up to date. Please run 'make proto' and commit the changes." && exit 1)
+4 -2
View File
@@ -27,8 +27,10 @@ go.work.sum
.env
.idea/
data
dist/
/data/
/dist/
/uc
/tmp
# Web
node_modules/
+4
View File
@@ -24,6 +24,8 @@ builds:
goarch:
- amd64
- arm64
ldflags:
- -s -w -X github.com/psviderski/uncloud/internal/version.version={{ .Version }}
- id: uncloudd
main: ./cmd/uncloudd
@@ -35,6 +37,8 @@ builds:
goarch:
- amd64
- arm64
ldflags:
- -s -w -X github.com/psviderski/uncloud/internal/version.version={{ .Version }}
archives:
- id: uncloud
+31
View File
@@ -0,0 +1,31 @@
[tools.go]
version = "1.23.10"
backend = "core:go"
[tools.go.checksums]
"go1.23.10.darwin-arm64.tar.gz" = "sha256:25c64bfa8a8fd8e7f62fb54afa4354af8409a4bb2358c2699a1003b733e6fce5"
"go1.23.10.linux-amd64.tar.gz" = "sha256:535f9f81802499f2a7dbfa70abb8fda3793725fcc29460f719815f6e10b5fd60"
[tools.protoc]
version = "27.3"
backend = "aqua:protocolbuffers/protobuf/protoc"
[tools.protoc.checksums]
"protoc-27.3-linux-x86_64.zip" = "sha256:6dab2adab83f915126cab53540d48957c40e9e9023969c3e84d44bfb936c7741"
"protoc-27.3-osx-aarch_64.zip" = "sha256:b22116bd97cdbd7ea25346abe635a9df268515fe5ef5afa93cd9a68fc2513f84"
[tools.protoc-gen-go]
version = "1.34.2"
backend = "aqua:protocolbuffers/protobuf-go/protoc-gen-go"
[tools.protoc-gen-go.checksums]
"protoc-gen-go.v1.34.2.darwin.arm64.tar.gz" = "sha256:17aca7f948dbb624049030cf841e35895cf34183ba006e721247fdeb95ff2780"
"protoc-gen-go.v1.34.2.linux.amd64.tar.gz" = "sha256:b87bc134dee55576a842141bf0ed27761c635d746780fce5dee038c6dd16554f"
[tools.protoc-gen-go-grpc]
version = "1.5.1"
backend = "aqua:grpc/grpc-go/protoc-gen-go-grpc"
[tools.protoc-gen-go-grpc.checksums]
"protoc-gen-go-grpc.v1.5.1.darwin.arm64.tar.gz" = "sha256:d6083feb51dcfe59f26793e99ef01ef5eac68b64294ec2546711f614ac5878f3"
"protoc-gen-go-grpc.v1.5.1.linux.amd64.tar.gz" = "sha256:a6cac4ea731e54aea304ad44d704a69d1cdc82997084b33637e21a89dc9229d6"
+6
View File
@@ -1,5 +1,11 @@
[settings]
experimental = true
[tools]
go = "1.23"
protoc = "27.3"
protoc-gen-go = "1.34.2"
protoc-gen-go-grpc = "1.5.1"
[env]
_.file = ".env"
+12 -3
View File
@@ -17,13 +17,22 @@ RUN GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -o uncloudd cmd/uncloudd/main
FROM alpine:${ALPINE_VERSION} AS corrosion-download
RUN wget -q -O /tmp/corrosion.tar.gz \
https://github.com/psviderski/corrosion/releases/latest/download/corrosion-aarch64-unknown-linux-gnu.tar.gz \
ARG TARGETARCH
RUN CORROSION_ARCH=$(case "${TARGETARCH}" in \
"amd64") echo "x86_64" ;; \
"arm64") echo "aarch64" ;; \
*) echo "Architecture '${TARGETARCH}' not supported" >&2 && exit 1 ;; \
esac) \
&& wget -q -O /tmp/corrosion.tar.gz \
"https://github.com/psviderski/corrosion/releases/latest/download/corrosion-${CORROSION_ARCH}-unknown-linux-gnu.tar.gz" \
&& tar -xzf /tmp/corrosion.tar.gz -C /tmp \
&& install /tmp/corrosion /usr/local/bin/corrosion \
&& rm /tmp/corrosion.tar.gz /tmp/corrosion
FROM chainguard/wolfi-base:latest AS corrosion
# Beware that more modern images like chainguard/wolfi-base build glibc with flags that require newer CPU features,
# e.g. x86-64-v2. So such image may fail with "Fatal glibc error: CPU does not support x86-64-v2" on older CPUs.
FROM gcr.io/distroless/cc-debian12:latest AS corrosion
COPY --from=corrosion-download /usr/local/bin/corrosion /usr/local/bin/corrosion
CMD ["corrosion", "agent"]
+43 -7
View File
@@ -1,5 +1,6 @@
CORROSION_IMAGE ?= ghcr.io/psviderski/corrosion:latest
UCIND_IMAGE ?= ghcr.io/psviderski/ucind:latest
DOCS_IMAGE ?= ghcr.io/psviderski/uncloud-docs:latest
update-dev:
GOOS=linux GOARCH=amd64 go build -o uncloudd-linux-amd64 ./cmd/uncloudd && \
@@ -31,11 +32,15 @@ reset-dev:
ssh ubuntu@152.67.101.197 "sudo systemctl stop uncloud && sudo rm -rf /var/lib/uncloud"
demo-reset:
rm -fv ~/.config/uncloud/config.toml
rm -fv ~/.config/uncloud/config.yaml
ssh ubuntu@152.67.101.197 "AUTO_CONFIRM=true sudo -E uncloud-uninstall && docker rmi caddy:2.9.1"
ssh root@5.223.45.199 "AUTO_CONFIRM=true sudo -E uncloud-uninstall"
ssh spy@192.168.40.243 "AUTO_CONFIRM=true sudo -E uncloud-uninstall"
.PHONY: ucind-cluster
ucind-cluster:
go run ./cmd/ucind cluster rm && go run ./cmd/ucind cluster create -m 3
.PHONY: proto
proto:
protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative \
@@ -45,14 +50,45 @@ proto:
corrosion-image:
docker build -t "$(CORROSION_IMAGE)" --target corrosion .
.PHONY: corrosion-image-push
corrosion-image-push: corrosion-image
docker push "$(CORROSION_IMAGE)"
.PHONY: corrosion-multiarch-image-push
corrosion-multiarch-image-push:
docker buildx build --push --platform linux/amd64,linux/arm64 -t "$(CORROSION_IMAGE)" --target corrosion .
.PHONY: ucind-image
ucind-image:
docker build -t "$(UCIND_IMAGE)" --target ucind .
.PHONY: ucind-image-push
ucind-image-push: ucind-image
docker push "$(UCIND_IMAGE)"
.PHONY: ucind-multiarch-image-push
ucind-multiarch-image-push:
docker buildx build --push --platform linux/amd64,linux/arm64 -t "$(UCIND_IMAGE)" --target ucind .
.PHONY: test
test:
ifeq ($(TEST_NAME),)
go test -count=1 -v ./...
else
go test -count=1 -v -run ^$(TEST_NAME)$$ ./...
endif
.PHONY: test-clean
test-clean:
@CONTAINERS=$$(docker ps --filter "name=ucind-test" -q); \
if [ -n "$$CONTAINERS" ]; then \
echo "Killing containers..."; \
docker kill $$CONTAINERS; \
fi; \
CONTAINERS_STOPPED=$$(docker ps -a --filter "name=ucind-test" -q); \
if [ -n "$$CONTAINERS_STOPPED" ]; then \
echo "Removing stopped containers..."; \
docker rm $$CONTAINERS_STOPPED; \
else \
echo "Nothing to clean."; \
fi
.PHONY: vet
vet:
go vet ./...
.PHONY: docs-image-push
docs-image:
docker buildx build --push --platform linux/amd64,linux/arm64 -t "$(DOCS_IMAGE)" ./docs
+89 -38
View File
@@ -1,40 +1,43 @@
<div align="center">
<img src="./website/images/logo.svg" height="100" alt="Uncloud logo"/>
<img src="./website/images/logo.svg" height="100" width="100" alt="Uncloud logo"/>
<h1>Uncloud</h1>
<p><strong>Docker simplicity. Multi-machine power.</strong></p>
<p>
<a href="https://docs.uncloud.run"><img src="https://img.shields.io/badge/Docs-blue.svg?style=for-the-badge&logo=gitbook&logoColor=white" alt="Documentation"></a>
<a href="https://discord.gg/eR35KQJhPu"><img src="https://img.shields.io/badge/discord-5865F2.svg?style=for-the-badge&logo=discord&logoColor=white" alt="Join Discord"></a>
<a href="https://x.com/psviderski"><img src="https://img.shields.io/badge/follow-black?style=for-the-badge&logo=X&logoColor=while" alt="Follow on X"></a>
<a href="https://github.com/sponsors/psviderski"><img src="https://img.shields.io/badge/Donate-EA4AAA.svg?style=for-the-badge&logo=githubsponsors&logoColor=white" alt="Donate"></a>
</p>
</div>
Uncloud is a lightweight clustering and container orchestration tool that lets you deploy and manage web apps across
cloud VMs and bare metal with minimized cluster management overhead. It creates a secure WireGuard mesh network between
cloud VMs and bare metal with minimised cluster management overhead. It creates a secure WireGuard mesh network between
your Docker hosts and provides automatic service discovery, load balancing, ingress with HTTPS, and simple CLI commands
to manage your apps.
Unlike traditional orchestrators, there's no central control plane and quorum to maintain. Each machine maintains a
synchronized copy of the cluster state through peer-to-peer communication, keeping cluster operations functional even if
synchronised copy of the cluster state through peer-to-peer communication, keeping cluster operations functional even if
some machines go offline.
Uncloud aims to be the solution for developers who want the flexibility of self-hosted infrastructure without the
operational complexity of Kubernetes.
## 🎬 Quick demo
Deploy a highly available web app with automatic HTTPS across multiple regions and on-premises in just a couple minutes.
<a href="https://uncloud.wistia.com/medias/k47uwt9uau?wvideo=k47uwt9uau">
<img src="https://embed-ssl.wistia.com/deliveries/3cf7014a48b93afc556444bed3e39a8c.jpg?image_crop_resized=900x526&image_play_button_rounded=true&image_play_button_size=2x&image_play_button_color=18181Be0" alt="Uncloud demo" width="450" height="263" />
</a>
Uncloud is the solution for developers who want the flexibility of self-hosted infrastructure without the operational
complexity of Kubernetes.
## ✨ Features
* **Deploy anywhere**: Combine cloud VMs, dedicated servers, and bare metal into a unified computing environment —
regardless of location or provider.
* **Docker Compose**: Familiar [Docker Compose](https://compose-spec.io/) format for defining services and volumes. No
need to learn a new bespoke DSL.
* **Zero-downtime deployments**: Rolling updates without service interruption. Automatic rollback on failure is coming
soon.
* **Service discovery**: Built-in DNS server resolves service names to container IPs.
* **Persistent storage**: Run stateful services with Docker volumes managed across machines.
* **Zero-config private network**: Automatic WireGuard mesh with peer discovery and NAT traversal. Containers get unique
IPs for direct cross-machine communication.
* **No control plane**: Fully decentralized design eliminates single points of failure and reduces operational overhead.
* **No control plane**: Fully decentralised design eliminates single points of failure and reduces operational overhead.
* **Imperative over declarative**: Favoring imperative operations over state reconciliation simplifies both the mental
model and troubleshooting.
* **Zero-downtime deployments**: Rolling updates with health checks and automatic rollback on failure.
* **Service discovery**: Built-in DNS server resolves service names to container IPs.
* **Managed DNS**: Automatic DNS records `*.<id>.cluster.uncloud.run` for services with public access via managed
[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
@@ -42,17 +45,27 @@ Deploy a highly available web app with automatic HTTPS across multiple regions a
* **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.
Coming soon:
### 🚀 Coming soon
* Infrastructure as Code using Docker Compose format
* Project isolation through environments/namespaces
* Persistent volumes and secrets management
* Monitoring and log aggregation
* Database deployment and management
* **[Unregistry](https://github.com/psviderski/unregistry) integration**: Push your Docker images directly to your
machines without an external registry. It will transfer only the missing layers, making it fast and efficient.
Here is a diagram of an Uncloud multi-provider cluster of 3 machines:
## 🎬 Quick demo
![Diagram: multi-provider cluster of 3 machines](website/images/diagram.webp)
The screenshot below demonstrates how I use Uncloud to deploy the [Uncloud Documentation](https://docs.uncloud.run)
website to 2 remote machines (why not?) from the [`compose.yaml`](docs/compose.yaml) file on my local machine.
It exposes the container port `8000/tcp` as HTTPS on the domain `docs.uncloud.run`, served by the Caddy reverse proxy on
the remote machines. All managed by Uncloud.
![Uncloud compose deployment demo](.github/images/compose-deploy.jpg)
Here is a more advanced use case. Deploy a highly available web app with automatic HTTPS across multiple regions and
on-premises in just a couple minutes.
<a href="https://uncloud.wistia.com/medias/k47uwt9uau?wvideo=k47uwt9uau">
<img src="https://embed-ssl.wistia.com/deliveries/3cf7014a48b93afc556444bed3e39a8c.jpg?image_crop_resized=900x526&image_play_button_rounded=true&image_play_button_size=2x&image_play_button_color=18181Be0" alt="Uncloud demo" width="450" height="263" />
</a>
## 💫 Why Uncloud?
@@ -62,14 +75,15 @@ believe there's a sweet spot in between — a pragmatic solution for the majorit
scale. You should be able to:
* **Own your infrastructure and data**: Whether driven by costs, compliance, or flexibility, run applications on any
combination of cloud VMs and personal hardware while maintaining the cloud-like experience you love.
* **Stay simple**: Don't worry about control planes, highly-available clusters, or complex YAML configurations for
common use cases.
combination of cloud VMs and personal hardware while controlling your data and maintaining the cloud-like experience
you love.
* **Stay simple as you grow**: Start with a single machine and add more whenever you need without changing your
workflow. No worrying about highly-available control planes or complex YAML configurations.
* **Build with proven primitives**: Get production-grade networking, deployment primitives, service discovery, load
balancing, and ingress with HTTPS out of the box without becoming a distributed systems expert.
* **Support sustainable computing** 🌿: Minimize system overhead to maximize resources available for your applications.
* **Support sustainable computing** 🌿: Minimise system overhead to maximise resources available for your applications.
Uncloud's goal is to make deployment and management of containerized applications feel as seamless as using a cloud
Uncloud's goal is to make deployment and management of containerised applications feel as seamless as using a cloud
platform, whether you're running on a $5 VPS, a spare Mac mini, or a rack of bare metal servers.
## 🚀 Quick start
@@ -82,8 +96,10 @@ platform, whether you're running on a $5 VPS, a spare Mac mini, or a rack of bar
# or using curl (macOS/Linux)
curl -fsS https://get.uncloud.run/install.sh | sh
```
See [Installation](https://docs.uncloud.run/getting-started/install-cli) for more options.
2. Initialize your first machine:
2. Initialise your first machine:
```bash
uc machine init root@your-server-ip
@@ -114,14 +130,20 @@ platform, whether you're running on a $5 VPS, a spare Mac mini, or a rack of bar
uncloud-uninstall
```
View the [user guide](docs/user_guide.md) for more information.
View the [Documentation](https://docs.uncloud.run) for more information.
## ⚙️ How it works
Check out the [design document](docs/design.md) to understand Uncloud's design philosophy and goals. Here, let's peek
under the hood to see what happens when you run certain commands.
Check out the [design document](docs/design.md) to understand Uncloud's design philosophy and goals.
**When you initialize a new cluster on a machine:**
Here is a diagram of an Uncloud multi-provider cluster of 3 machines:
![Diagram: multi-provider cluster of 3 machines](website/images/diagram.webp)
<details>
<summary>Peek under the hood to see what happens when you run certain commands.</summary>
**When you initialise a new cluster on a machine:**
```bash
$ uc machine init --name oracle-vm ubuntu@152.67.101.197
@@ -254,23 +276,52 @@ Look ma, no control plane or master nodes to maintain! Just a simple overlay net
sync that lets machines work together. Want to check on things or make changes? Connect to any machine either implicitly
using the CLI or directly over SSH. They all have the complete cluster state and can control everything. It's like each
machine is a full backup of your control plane.
</details>
## 🏗 Project status
Uncloud is currently in active development and is **not ready for production use**. Features may change significantly
and there may be breaking changes between releases.
I'd love your input! Here's how you can contribute:
We'd love your input! Here's how you can contribute:
* 🐛 Found a bug? [Open an issue](https://github.com/psviderski/uncloud/issues)
* 💡 Have ideas? [Join the discussion](https://github.com/psviderski/uncloud/discussions)
* 💡 Have questions, ideas, or need help?
* Start a discussion or join an existing one in
the [Discussions](https://github.com/psviderski/uncloud/discussions).
* Join our [Discord community](https://discord.gg/eR35KQJhPu) where we discuss features, roadmap, implementation
details, and help each other out.
## 🙏 Inspiration & Acknowledgements
I'm grateful to the following projects that inspired Uncloud's design and implementation:
* [Kamal](https://kamal-deploy.org/) — for proving that even in the declarative era of Kubernetes there is a place for
simple deployment tools that use imperative commands without complex orchestration. Kamal powers the multi-billion
dollar company [37signals](https://37signals.com/) where it was created, and that's truly inspiring!
* [Fly.io](https://fly.io/) — for inspiring my vision for what self-hosted infrastructure should feel like, proving that
developer experience and powerful infrastructure can coexist beautifully.
* [Tailscale](https://tailscale.com/) — for pioneering the vision of decentralised flat mesh networking with an amazing
user experience that feels like magic.
* [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
[grpc-proxy](https://github.com/siderolabs/grpc-proxy) and for its elegant approach to secure WireGuard-based overlay
networking with zero configuration.
* [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/)
— for showing the power of simplicity and pragmatism in container orchestration and that not every problem needs the
complexity of Kubernetes.
Special thanks to the [Corrosion](https://github.com/superfly/corrosion) project by Fly.io for providing the distributed
SQLite database used to share Uncloud's cluster state.
## 📫 Stay updated
* [Subscribe](https://uncloud.run/#subscribe) to my newsletter to follow the progress, get early insights into new
* Join our [Discord server](https://discord.gg/eR35KQJhPu) for real-time discussions, support, and updates.
* Follow [@psviderski](https://x.com/psviderski) on X/Twitter.
* Subscribe to [my newsletter](https://uncloud.run/#subscribe) to follow the progress, get early insights into new
features, and be the first to know when it's ready for production use.
* Watch this repository for releases.
* Follow [@psviderski](https://github.com/psviderski) on GitHub.
## ❤️ Contributors
+5 -4
View File
@@ -3,12 +3,13 @@ package main
import (
"context"
"fmt"
"github.com/docker/docker/client"
"github.com/spf13/cobra"
"os"
"strings"
"github.com/docker/docker/client"
"github.com/psviderski/uncloud/cmd/ucind/cluster"
"github.com/psviderski/uncloud/internal/ucind"
"github.com/spf13/cobra"
)
func main() {
@@ -44,9 +45,9 @@ func main() {
}
// TODO: allow to override using UNCLOUD_CONFIG env var.
cmd.PersistentFlags().StringVar(&configPath, "uncloud-config", "~/.config/uncloud/config.toml",
cmd.PersistentFlags().StringVar(&configPath, "uncloud-config", "~/.config/uncloud/config.yaml",
"path to the Uncloud configuration file.")
_ = cmd.MarkPersistentFlagFilename("uncloud-config", "toml")
_ = cmd.MarkPersistentFlagFilename("uncloud-config", "yaml", "yml")
cmd.AddCommand(
cluster.NewRootCommand(),
+77
View File
@@ -0,0 +1,77 @@
package main
import (
"context"
"fmt"
composecli "github.com/compose-spec/compose-go/v2/cli"
"github.com/psviderski/uncloud/internal/cli"
"github.com/psviderski/uncloud/pkg/client/compose"
"github.com/spf13/cobra"
)
// NewBuildCommand creates a new command to build services from a Compose file.
func NewBuildCommand() *cobra.Command {
opts := cli.BuildOptions{}
cmd := &cobra.Command{
Use: "build [FLAGS] [SERVICE...]",
Short: "Build services from a Compose file.",
RunE: func(cmd *cobra.Command, args []string) error {
uncli := cmd.Context().Value("cli").(*cli.CLI)
if len(args) > 0 {
opts.Services = args
}
return runBuild(cmd.Context(), uncli, opts)
},
}
cmd.Flags().StringSliceVarP(&opts.Files, "file", "f", nil,
"One or more Compose files to build (default compose.yaml)")
cmd.Flags().StringSliceVarP(&opts.Profiles, "profile", "p", nil,
"One or more Compose profiles to enable.")
cmd.Flags().BoolVarP(&opts.Push, "push", "P", false,
"Push built images to the registry after building. (default false)")
cmd.Flags().BoolVarP(&opts.NoCache, "no-cache", "n", false,
"Do not use cache when building images. (default false)")
return cmd
}
// TODO: deduplicate with a similar functino for deploy options
// projectOpts returns the project options for the Compose file(s).
func projectOptsFromBuildOpts(opts cli.BuildOptions) []composecli.ProjectOptionsFn {
projectOpts := []composecli.ProjectOptionsFn{}
if len(opts.Profiles) > 0 {
projectOpts = append(projectOpts, composecli.WithDefaultProfiles(opts.Profiles...))
}
return projectOpts
}
// runBuild parses the Compose file(s), builds the services, and pushes them if requested.
func runBuild(ctx context.Context, uncli *cli.CLI, opts cli.BuildOptions) error {
projectOpts := projectOptsFromBuildOpts(opts)
project, err := compose.LoadProject(ctx, opts.Files, projectOpts...)
if err != nil {
return fmt.Errorf("load compose file(s): %w", err)
}
if len(opts.Services) > 0 {
project, err = project.WithSelectedServices(opts.Services)
if err != nil {
return fmt.Errorf("select services: %w", err)
}
}
servicesToBuild := cli.GetServicesThatNeedBuild(project)
if len(servicesToBuild) == 0 {
fmt.Println("No services to build.")
return nil
}
return cli.BuildServices(ctx, servicesToBuild, opts)
}
+28 -53
View File
@@ -4,23 +4,23 @@ import (
"context"
"errors"
"fmt"
"maps"
"slices"
"strings"
"github.com/docker/cli/cli/streams"
"github.com/docker/compose/v2/pkg/progress"
"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/client"
"github.com/psviderski/uncloud/pkg/client/deploy"
"github.com/spf13/cobra"
"maps"
"slices"
"strings"
)
type deployOptions struct {
image string
machine string
cluster string
image string
machines []string
context string
}
func NewDeployCommand() *cobra.Command {
@@ -39,18 +39,19 @@ func NewDeployCommand() *cobra.Command {
cmd.Flags().StringVar(&opts.image, "image", "",
"Caddy Docker image to deploy. (default caddy:LATEST_VERSION)")
cmd.Flags().StringVarP(&opts.machine, "machine", "m", "",
"Machine names to deploy to (comma-separated). (default is all machines)")
cmd.Flags().StringSliceVarP(&opts.machines, "machine", "m", nil,
"Machine names to deploy to. Can be specified multiple times or as a comma-separated "+
"list of machine names. (default is all machines)")
cmd.Flags().StringVarP(
&opts.cluster, "cluster", "c", "",
"Name of the cluster to deploy to. (default is the current cluster)",
&opts.context, "context", "c", "",
"Name of the cluster context to deploy to. (default is the current context)",
)
return cmd
}
func runDeploy(ctx context.Context, uncli *cli.CLI, opts deployOptions) error {
clusterClient, err := uncli.ConnectCluster(ctx, opts.cluster)
clusterClient, err := uncli.ConnectCluster(ctx, opts.context)
if err != nil {
return fmt.Errorf("connect to cluster: %w", err)
}
@@ -87,16 +88,10 @@ func runDeploy(ctx context.Context, uncli *cli.CLI, opts deployOptions) error {
fmt.Println()
fmt.Println("Preparing a deployment plan...")
var filter deploy.MachineFilter
if opts.machine != "" {
machines := strings.Split(opts.machine, ",")
for i, m := range machines {
machines[i] = strings.TrimSpace(m)
}
filter = machineFilter(machines)
placement := api.Placement{
Machines: cli.ExpandCommaSeparatedValues(opts.machines),
}
d, err := clusterClient.NewCaddyDeployment(opts.image, filter)
d, err := clusterClient.NewCaddyDeployment(opts.image, placement)
if err != nil {
return fmt.Errorf("create caddy deployment: %w", err)
}
@@ -107,31 +102,18 @@ func runDeploy(ctx context.Context, uncli *cli.CLI, opts deployOptions) error {
plan, err := d.Plan(ctx)
if err != nil {
if errors.Is(err, deploy.ErrNoMatchingMachines) {
return fmt.Errorf("no machines found matching: %s", opts.machine)
}
return fmt.Errorf("plan caddy deployment: %w", err)
}
if len(plan.Operations) == 0 {
if opts.machine != "" {
fmt.Printf("%s service is up to date on selected machines.\n", client.CaddyServiceName)
} else {
fmt.Printf("%s service is up to date.\n", client.CaddyServiceName)
}
fmt.Printf("%s service is up to date.\n", client.CaddyServiceName)
} else {
if svc.ID == "" {
if opts.machine != "" {
fmt.Println("This will run a Caddy container on selected machines.")
if len(opts.machines) > 0 {
fmt.Println("This will run a Caddy container on each selected machine.")
} else {
fmt.Println("This will run a Caddy container on each machine.")
}
} else {
if opts.machine != "" {
fmt.Println("This will perform a rolling update of Caddy containers on selected machines.")
} else {
fmt.Println("This will perform a rolling update of Caddy containers on each machine.")
}
}
// Initialise a machine and container name resolver to properly format the plan output.
@@ -170,7 +152,8 @@ func runDeploy(ctx context.Context, uncli *cli.CLI, opts deployOptions) error {
}
func UpdateDomainRecords(ctx context.Context, clusterClient *client.Client, progressOut *streams.Out) error {
if _, err := clusterClient.GetDomain(ctx); err != nil {
domain, err := clusterClient.GetDomain(ctx)
if err != nil {
if errors.Is(err, api.ErrNotFound) {
fmt.Println("Skipping DNS records update as no cluster domain is reserved (see 'uc dns').")
return nil
@@ -182,7 +165,7 @@ func UpdateDomainRecords(ctx context.Context, clusterClient *client.Client, prog
// TODO: split the method into two: one to get the records and one to update them to ask for update confirmation.
var records []*pb.DNSRecord
err := progress.RunWithTitle(ctx, func(ctx context.Context) error {
err = progress.RunWithTitle(ctx, func(ctx context.Context) error {
var err error
records, err = clusterClient.CreateIngressRecords(ctx, client.CaddyServiceName)
return err
@@ -190,18 +173,19 @@ func UpdateDomainRecords(ctx context.Context, clusterClient *client.Client, prog
if err != nil {
if errors.Is(err, client.ErrNoReachableMachines) {
fmt.Println()
fmt.Println("DNS records could not be updated as there are no internet-reachable machines running " +
"caddy containers.")
fmt.Printf("DNS records for domain '%s' could not be updated as there are no internet-reachable "+
"machines running caddy containers.\n", domain)
fmt.Println()
fmt.Println("Possible solutions:")
fmt.Println("- Ensure your machines have public IP addresses")
fmt.Println("- Use --public-ip flag when adding machines to override the automatically detected IPs")
fmt.Println("- Check firewall settings on your machines")
fmt.Println("- Configure port forwarding if behind NAT")
fmt.Println("- Retry Caddy deployment after resolving connectivity issues with 'uc caddy deploy'")
fmt.Println("- Retry Caddy deployment with 'uc caddy deploy' after resolving connectivity issues")
fmt.Println()
fmt.Println("Your services will not be accessible from the internet until at least one machine " +
"becomes reachable.")
fmt.Println("Your services won't be accessible from the internet until at least one machine " +
"becomes reachable. If you aren't planning to expose any services publicly, you can release " +
"the domain by running 'uc dns release'.")
}
return fmt.Errorf("failed to update DNS records pointing to caddy service: %w", err)
}
@@ -214,12 +198,3 @@ func UpdateDomainRecords(ctx context.Context, clusterClient *client.Client, prog
return nil
}
func machineFilter(machines []string) deploy.MachineFilter {
if len(machines) == 0 {
return nil
}
return func(m *pb.MachineInfo) bool {
return slices.Contains(machines, m.Name)
}
}
+61
View File
@@ -0,0 +1,61 @@
package context
import (
"fmt"
"maps"
"os"
"slices"
"text/tabwriter"
"github.com/psviderski/uncloud/internal/cli"
"github.com/spf13/cobra"
)
func NewListCommand() *cobra.Command {
var clusterContext string
cmd := &cobra.Command{
Use: "ls",
Aliases: []string{"list"},
Short: "List available cluster contexts.",
RunE: func(cmd *cobra.Command, args []string) error {
uncli := cmd.Context().Value("cli").(*cli.CLI)
return list(uncli)
},
}
cmd.Flags().StringVarP(
&clusterContext, "context", "c", "",
"Name of the cluster context. (default is the current context)",
)
return cmd
}
func list(uncli *cli.CLI) error {
if uncli.Config == nil {
return fmt.Errorf("context management is not available: Uncloud configuration file is not being used")
}
if len(uncli.Config.Contexts) == 0 {
fmt.Println("No contexts found")
return nil
}
contextNames := slices.Sorted(maps.Keys(uncli.Config.Contexts))
currentContext := uncli.Config.CurrentContext
tw := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', 0)
fmt.Fprintln(tw, "NAME\tCURRENT\tCONNECTIONS")
for _, name := range contextNames {
current := ""
if name == currentContext {
current = "*"
}
connCount := len(uncli.Config.Contexts[name].Connections)
fmt.Fprintf(tw, "%s\t%s\t%d\n", name, current, connCount)
}
return tw.Flush()
}
+25
View File
@@ -0,0 +1,25 @@
package context
import (
"github.com/psviderski/uncloud/internal/cli"
"github.com/spf13/cobra"
)
func NewRootCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "ctx",
Aliases: []string{"context"},
Short: "Switch between different cluster contexts. Contains subcommands to manage contexts.",
RunE: func(cmd *cobra.Command, args []string) error {
uncli := cmd.Context().Value("cli").(*cli.CLI)
return selectContext(uncli)
},
}
cmd.AddCommand(
NewListCommand(),
NewUseCommand(),
)
return cmd
}
+82
View File
@@ -0,0 +1,82 @@
package context
import (
"fmt"
"maps"
"slices"
"github.com/charmbracelet/huh"
"github.com/psviderski/uncloud/internal/cli"
"github.com/spf13/cobra"
)
func NewUseCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "use [CONTEXT]",
Short: "Switch to a different cluster context.",
Long: "Switch to a different cluster context. If no context is provided, " +
"a list of available contexts will be displayed for selection.",
RunE: func(cmd *cobra.Command, args []string) error {
uncli := cmd.Context().Value("cli").(*cli.CLI)
if len(args) == 1 {
if err := uncli.SetCurrentContext(args[0]); err != nil {
return fmt.Errorf("failed to set the current cluster context to '%s': %w", args[0], err)
}
fmt.Printf("Current cluster context is now '%s'.\n", args[0])
return nil
}
return selectContext(uncli)
},
}
return cmd
}
func selectContext(uncli *cli.CLI) error {
if uncli.Config == nil {
return fmt.Errorf("context management is not available: Uncloud configuration file is not being used")
}
if len(uncli.Config.Contexts) == 0 {
return fmt.Errorf("no contexts found in Uncloud config (%s)", uncli.Config.Path())
}
contextNames := slices.Sorted(maps.Keys(uncli.Config.Contexts))
var selected string
form := huh.NewForm(
huh.NewGroup(
huh.NewSelect[string]().
Title("Select a cluster context").
Options(buildContextOptions(contextNames, uncli.Config.CurrentContext)...).
Value(&selected),
),
)
if err := form.Run(); err != nil {
return fmt.Errorf("select cluster context: %w", err)
}
if err := uncli.SetCurrentContext(selected); err != nil {
return fmt.Errorf("set current cluster context: %w", err)
}
fmt.Printf("Current cluster context is now '%s'.\n", selected)
return nil
}
func buildContextOptions(contexts []string, current string) []huh.Option[string] {
options := make([]huh.Option[string], len(contexts))
for i, ctx := range contexts {
opt := huh.NewOption(ctx, ctx)
if ctx == current {
opt.Key += " (current)"
opt = opt.Selected(true)
}
options[i] = opt
}
return options
}
+80 -37
View File
@@ -4,30 +4,33 @@ import (
"context"
"errors"
"fmt"
"github.com/compose-spec/compose-go/v2/types"
"strings"
composecli "github.com/compose-spec/compose-go/v2/cli"
"github.com/docker/compose/v2/pkg/progress"
"github.com/psviderski/uncloud/internal/cli"
"github.com/psviderski/uncloud/pkg/api"
"github.com/psviderski/uncloud/pkg/client"
"github.com/psviderski/uncloud/pkg/client/compose"
"github.com/psviderski/uncloud/pkg/client/deploy"
"github.com/spf13/cobra"
"strings"
)
type deployOptions struct {
files []string
profiles []string
services []string
noBuild bool
cluster string
context string
}
// NewDeployCommand creates a new command to deploy services from a Compose file.
func NewDeployCommand() *cobra.Command {
opts := deployOptions{}
cmd := &cobra.Command{
Use: "deploy [FLAGS] [SERVICE...]",
// TODO: remove WIP when the command is fully implemented.
Short: "WIP: Deploy services from a Compose file.",
Use: "deploy [FLAGS] [SERVICE...]",
Short: "Deploy services from a Compose file.",
RunE: func(cmd *cobra.Command, args []string) error {
uncli := cmd.Context().Value("cli").(*cli.CLI)
@@ -40,29 +43,66 @@ func NewDeployCommand() *cobra.Command {
}
cmd.Flags().StringSliceVarP(&opts.files, "file", "f", nil,
"One or more Compose files to deploy services from. (default compose-ports-long.yaml)")
cmd.Flags().StringVarP(&opts.cluster, "cluster", "c", "",
"Name of the cluster to deploy to (default is the current cluster)")
"One or more Compose files to deploy services from. (default compose.yaml)")
cmd.Flags().StringSliceVarP(&opts.profiles, "profile", "p", nil,
"One or more Compose profiles to enable.")
cmd.Flags().StringVarP(&opts.context, "context", "c", "",
"Name of the cluster context to deploy to (default is the current context)")
cmd.Flags().BoolVarP(&opts.noBuild, "no-build", "n", false,
"Do not build images before deploying services. (default false)")
// TODO: Consider adding a filter flag to specify which machines to deploy to but keep the rest running.
// Could be useful to test a new version on a subset of machines before rolling out to all.
return cmd
}
// projectOpts returns the project options for the Compose file(s).
func projectOpts(opts deployOptions) []composecli.ProjectOptionsFn {
projectOpts := []composecli.ProjectOptionsFn{}
if len(opts.profiles) > 0 {
projectOpts = append(projectOpts, composecli.WithDefaultProfiles(opts.profiles...))
}
return projectOpts
}
// runDeploy parses the Compose file(s) and deploys the services.
func runDeploy(ctx context.Context, uncli *cli.CLI, opts deployOptions) error {
project, err := compose.LoadProject(ctx, opts.files)
projectOpts := projectOpts(opts)
project, err := compose.LoadProject(ctx, opts.files, projectOpts...)
if err != nil {
return fmt.Errorf("load compose file(s): %w", err)
}
if len(opts.services) > 0 {
// TODO: handle dependencies properly.
project, err = project.WithSelectedServices(opts.services, types.IgnoreDependencies)
// Includes service dependencies by default. This is the default docker compose behavior.
project, err = project.WithSelectedServices(opts.services)
if err != nil {
return fmt.Errorf("select services: %w", err)
}
}
clusterClient, err := uncli.ConnectCluster(ctx, opts.cluster)
servicesToBuild := cli.GetServicesThatNeedBuild(project)
if len(servicesToBuild) > 0 {
if opts.noBuild {
fmt.Println("Not building services as requested.")
} else {
buildOpts := cli.BuildOptions{
Push: true,
NoCache: false,
}
if err := cli.BuildServices(ctx, servicesToBuild, buildOpts); err != nil {
return fmt.Errorf("build services: %w", err)
}
}
}
clusterClient, err := uncli.ConnectCluster(ctx, opts.context)
if err != nil {
return fmt.Errorf("connect to cluster: %w", err)
}
@@ -84,30 +124,8 @@ func runDeploy(ctx context.Context, uncli *cli.CLI, opts deployOptions) error {
}
fmt.Println("Deployment plan:")
for _, op := range plan.Operations {
svcPlan, ok := op.(*deploy.Plan)
if !ok {
return fmt.Errorf("expected service Plan, got: %T", op)
}
svc, err := clusterClient.InspectService(ctx, svcPlan.ServiceID)
if err != nil {
if !errors.Is(err, api.ErrNotFound) {
return fmt.Errorf("inspect service: %w", err)
}
fmt.Printf("- Run service [name=%s]\n", svcPlan.ServiceName)
} else {
fmt.Printf("- Update service [name=%s]\n", svc.Name)
}
// Initialise a machine and container name resolver to properly format the service plan output.
resolver, err := clusterClient.ServiceOperationNameResolver(ctx, svc)
if err != nil {
return fmt.Errorf("create machine and container name resolver for service operations: %w", err)
}
fmt.Println(indent(svcPlan.Format(resolver), " "))
if err = printPlan(ctx, clusterClient, plan); err != nil {
return fmt.Errorf("print deployment plan: %w", err)
}
fmt.Println()
@@ -128,6 +146,31 @@ func runDeploy(ctx context.Context, uncli *cli.CLI, opts deployOptions) error {
}, uncli.ProgressOut(), "Deploying services")
}
func printPlan(ctx context.Context, cli *client.Client, plan deploy.SequenceOperation) error {
for _, op := range plan.Operations {
svcPlan, ok := op.(*deploy.Plan)
if !ok {
fmt.Println("- " + op.Format(nil))
continue
}
svc, err := cli.InspectService(ctx, svcPlan.ServiceID)
if err != nil && !errors.Is(err, api.ErrNotFound) {
return fmt.Errorf("inspect service: %w", err)
}
// Initialise a machine and container name resolver to properly format the service plan output.
resolver, err := cli.ServiceOperationNameResolver(ctx, svc)
if err != nil {
return fmt.Errorf("create machine and container name resolver for service operations: %w", err)
}
fmt.Printf("- Deploy service [name=%s]\n", svcPlan.ServiceName)
fmt.Println(indent(svcPlan.Format(resolver), " "))
}
return nil
}
func indent(text, prefix string) string {
lines := strings.Split(text, "\n")
for i, line := range lines {
+6 -5
View File
@@ -4,15 +4,16 @@ import (
"context"
"errors"
"fmt"
"github.com/psviderski/uncloud/internal/cli"
"github.com/spf13/cobra"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/emptypb"
"github.com/psviderski/uncloud/internal/cli"
)
type releaseOptions struct {
cluster string
context string
}
func NewReleaseCommand() *cobra.Command {
@@ -28,15 +29,15 @@ func NewReleaseCommand() *cobra.Command {
}
cmd.Flags().StringVarP(
&opts.cluster, "cluster", "c", "",
"Name of the cluster. (default is the current cluster)",
&opts.context, "context", "c", "",
"Name of the cluster context. (default is the current context)",
)
return cmd
}
func release(ctx context.Context, uncli *cli.CLI, opts releaseOptions) error {
client, err := uncli.ConnectCluster(ctx, opts.cluster)
client, err := uncli.ConnectCluster(ctx, opts.context)
if err != nil {
return fmt.Errorf("connect to cluster: %w", err)
}
+5 -4
View File
@@ -4,6 +4,7 @@ import (
"context"
"errors"
"fmt"
"github.com/psviderski/uncloud/cmd/uncloud/caddy"
"github.com/psviderski/uncloud/internal/cli"
"github.com/psviderski/uncloud/internal/machine/api/pb"
@@ -18,7 +19,7 @@ const DefaultUncloudDNSAPIEndpoint = "https://dns.uncloud.run/v1"
type reserveOptions struct {
endpoint string
cluster string
context string
}
func NewReserveCommand() *cobra.Command {
@@ -36,15 +37,15 @@ func NewReserveCommand() *cobra.Command {
cmd.Flags().StringVar(&opts.endpoint, "endpoint", DefaultUncloudDNSAPIEndpoint,
"API endpoint for the Uncloud DNS service.")
cmd.Flags().StringVarP(
&opts.cluster, "cluster", "c", "",
"Name of the cluster. (default is the current cluster)",
&opts.context, "context", "c", "",
"Name of the cluster context. (default is the current context)",
)
return cmd
}
func reserve(ctx context.Context, uncli *cli.CLI, opts reserveOptions) error {
clusterClient, err := uncli.ConnectCluster(ctx, opts.cluster)
clusterClient, err := uncli.ConnectCluster(ctx, opts.context)
if err != nil {
return fmt.Errorf("connect to cluster: %w", err)
}
+5 -4
View File
@@ -4,13 +4,14 @@ import (
"context"
"errors"
"fmt"
"github.com/psviderski/uncloud/internal/cli"
"github.com/psviderski/uncloud/pkg/api"
"github.com/spf13/cobra"
)
type showOptions struct {
cluster string
context string
}
func NewShowCommand() *cobra.Command {
@@ -26,15 +27,15 @@ func NewShowCommand() *cobra.Command {
}
cmd.Flags().StringVarP(
&opts.cluster, "cluster", "c", "",
"Name of the cluster. (default is the current cluster)",
&opts.context, "context", "c", "",
"Name of the cluster context. (default is the current context)",
)
return cmd
}
func show(ctx context.Context, uncli *cli.CLI, opts showOptions) error {
clusterClient, err := uncli.ConnectCluster(ctx, opts.cluster)
clusterClient, err := uncli.ConnectCluster(ctx, opts.context)
if err != nil {
return fmt.Errorf("connect to cluster: %w", err)
}
+30 -21
View File
@@ -4,20 +4,19 @@ import (
"context"
"errors"
"fmt"
"net/netip"
"time"
"github.com/cenkalti/backoff/v4"
"github.com/docker/compose/v2/pkg/progress"
"github.com/psviderski/uncloud/cmd/uncloud/caddy"
"github.com/psviderski/uncloud/internal/cli"
"github.com/psviderski/uncloud/internal/cli/config"
"github.com/psviderski/uncloud/internal/machine/api/pb"
"github.com/psviderski/uncloud/pkg/api"
"github.com/psviderski/uncloud/pkg/client"
"github.com/spf13/cobra"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/emptypb"
"net/netip"
"time"
)
type addOptions struct {
@@ -25,7 +24,8 @@ type addOptions struct {
noCaddy bool
publicIP string
sshKey string
cluster string
context string
version string
}
func NewAddCommand() *cobra.Command {
@@ -62,13 +62,18 @@ func NewAddCommand() *cobra.Command {
"blank '' or 'none' to disable ingress on this machine, or specify an IP address.",
)
cmd.Flags().StringVarP(
&opts.sshKey, "ssh-key", "i", "",
"path to SSH private key for SSH remote login. (default ~/.ssh/id_*)",
&opts.sshKey, "ssh-key", "i", "~/.ssh/id_ed25519",
"Path to SSH private key for remote login (if not already added to SSH agent).",
)
cmd.Flags().StringVar(
&opts.version, "version", "latest",
"Version of the Uncloud daemon to install on the machine.",
)
cmd.Flags().StringVarP(
&opts.cluster, "cluster", "c", "",
"Name of the cluster to add the machine to. (default is the current cluster)",
&opts.context, "context", "c", "",
"Name of the cluster context to add the machine to. (default is the current context)",
)
return cmd
}
@@ -87,10 +92,17 @@ func add(ctx context.Context, uncli *cli.CLI, remoteMachine cli.RemoteMachine, o
publicIP = &ip
}
machineClient, err := uncli.AddMachine(ctx, remoteMachine, opts.cluster, opts.name, publicIP)
clusterClient, machineClient, err := uncli.AddMachine(ctx, cli.AddMachineOptions{
Context: opts.context,
MachineName: opts.name,
PublicIP: publicIP,
RemoteMachine: remoteMachine,
Version: opts.version,
})
if err != nil {
return err
}
defer clusterClient.Close()
defer machineClient.Close()
if opts.noCaddy {
@@ -104,18 +116,13 @@ func add(ctx context.Context, uncli *cli.CLI, remoteMachine cli.RemoteMachine, o
return fmt.Errorf("wait for cluster to be initialised on machine: %w", err)
}
// Inspect the added machine to get its ID to create a filter for the Caddy deployment.
minfo, err := machineClient.Inspect(ctx, &emptypb.Empty{})
if err != nil {
return fmt.Errorf("inspect machine: %w", err)
}
filter := func(m *pb.MachineInfo) bool {
return m.Id == minfo.Id
}
// Deploy a Caddy service container to the added machine. If caddy service is already deployed on other machines,
// use the deployed image version. Otherwise, use the latest version.
// 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.
// See the issue for more details: https://github.com/psviderski/uncloud/issues/65.
caddyImage := ""
caddySvc, err := machineClient.InspectService(ctx, client.CaddyServiceName)
caddySvc, err := clusterClient.InspectService(ctx, client.CaddyServiceName)
if err != nil {
if !errors.Is(err, api.ErrNotFound) {
return fmt.Errorf("inspect caddy service: %w", err)
@@ -136,7 +143,9 @@ func add(ctx context.Context, uncli *cli.CLI, remoteMachine cli.RemoteMachine, o
}
}
d, err := machineClient.NewCaddyDeployment(caddyImage, filter)
// TODO: scale the existing Caddy service to the new machine instead of running a new deployment
// that may cause a small downtime.
d, err := clusterClient.NewCaddyDeployment(caddyImage, api.Placement{})
if err != nil {
return fmt.Errorf("create caddy deployment: %w", err)
}
@@ -162,7 +171,7 @@ func waitClusterInitialised(ctx context.Context, client *client.Client) error {
), ctx)
check := func() error {
_, err := client.ListMachines(ctx)
_, err := client.ListMachines(ctx, nil)
if err == nil {
return nil
}
+25 -11
View File
@@ -3,6 +3,8 @@ package machine
import (
"context"
"fmt"
"net/netip"
"github.com/docker/compose/v2/pkg/progress"
"github.com/psviderski/uncloud/cmd/uncloud/caddy"
"github.com/psviderski/uncloud/cmd/uncloud/dns"
@@ -10,8 +12,8 @@ import (
"github.com/psviderski/uncloud/internal/cli/config"
"github.com/psviderski/uncloud/internal/machine/api/pb"
"github.com/psviderski/uncloud/internal/machine/cluster"
"github.com/psviderski/uncloud/pkg/api"
"github.com/spf13/cobra"
"net/netip"
)
type initOptions struct {
@@ -22,15 +24,17 @@ type initOptions struct {
noDNS bool
publicIP string
sshKey string
cluster string
version string
context string
}
func NewInitCommand() *cobra.Command {
opts := initOptions{}
cmd := &cobra.Command{
Use: "init [USER@HOST:PORT]",
Short: "Initialise a new cluster that consists of the local or remote machine.",
// TODO: include usage examples of initialising a local and remote machine.
Short: "Initialise a new cluster with a remote machine as the first member.",
// TODO: include usage examples of initialising a remote machine.
// TODO: support initialising a cluster on the local machine.
Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
uncli := cmd.Context().Value("cli").(*cli.CLI)
@@ -76,12 +80,16 @@ func NewInitCommand() *cobra.Command {
"blank '' or 'none' to disable ingress on this machine, or specify an IP address.",
)
cmd.Flags().StringVarP(
&opts.sshKey, "ssh-key", "i", "",
"Path to SSH private key for SSH remote login. (default ~/.ssh/id_*)",
&opts.sshKey, "ssh-key", "i", "~/.ssh/id_ed25519",
"Path to SSH private key for remote login (if not already added to SSH agent).",
)
cmd.Flags().StringVar(
&opts.version, "version", "latest",
"Version of the Uncloud daemon to install on the machine.",
)
cmd.Flags().StringVarP(
&opts.cluster, "cluster", "c", "",
"Name of the cluster in the local config if initialising a remote machine.",
&opts.context, "context", "c", "default",
"Name of the created context for the initialised cluster in the Uncloud config.",
)
return cmd
@@ -106,8 +114,14 @@ func initCluster(ctx context.Context, uncli *cli.CLI, remoteMachine *cli.RemoteM
}
publicIP = &ip
}
client, err := uncli.InitCluster(ctx, remoteMachine, opts.cluster, opts.name, netPrefix, publicIP)
client, err := uncli.InitCluster(ctx, cli.InitClusterOptions{
Context: opts.context,
MachineName: opts.name,
Network: netPrefix,
PublicIP: publicIP,
RemoteMachine: remoteMachine,
Version: opts.version,
})
if err != nil {
return err
}
@@ -132,7 +146,7 @@ func initCluster(ctx context.Context, uncli *cli.CLI, remoteMachine *cli.RemoteM
}
if !opts.noCaddy {
d, err := client.NewCaddyDeployment("", nil)
d, err := client.NewCaddyDeployment("", api.Placement{})
if err != nil {
return fmt.Errorf("create caddy deployment: %w", err)
}
@@ -3,29 +3,30 @@ package machine
import (
"context"
"fmt"
"github.com/spf13/cobra"
"net/netip"
"os"
"strings"
"text/tabwriter"
"github.com/psviderski/uncloud/internal/cli"
"github.com/psviderski/uncloud/internal/machine/network"
"github.com/spf13/cobra"
)
func NewListCommand() *cobra.Command {
var cluster string
var contextName string
cmd := &cobra.Command{
Use: "ls",
Aliases: []string{"list"},
Short: "List machines in a cluster.",
RunE: func(cmd *cobra.Command, args []string) error {
uncli := cmd.Context().Value("cli").(*cli.CLI)
return list(cmd.Context(), uncli, cluster)
return list(cmd.Context(), uncli, contextName)
},
}
cmd.Flags().StringVarP(
&cluster, "cluster", "c", "",
"Name of the cluster. (default is the current cluster)",
&contextName, "context", "c", "",
"Name of the cluster context. (default is the current context)",
)
return cmd
}
@@ -37,7 +38,7 @@ func list(ctx context.Context, uncli *cli.CLI, clusterName string) error {
}
defer client.Close()
machines, err := client.ListMachines(ctx)
machines, err := client.ListMachines(ctx, nil)
if err != nil {
return fmt.Errorf("list machines: %w", err)
}
@@ -67,7 +68,8 @@ func list(ctx context.Context, uncli *cli.CLI, clusterName string) error {
}
if _, err = fmt.Fprintf(
tw, "%s\t%s\t%s\t%s\t%s\n", m.Name, capitalise(member.State.String()), subnet, publicIP, strings.Join(endpoints, ", "),
tw, "%s\t%s\t%s\t%s\t%s\n", m.Name, capitalise(member.State.String()), subnet, publicIP,
strings.Join(endpoints, ", "),
); err != nil {
return fmt.Errorf("write row: %w", err)
}
+200
View File
@@ -0,0 +1,200 @@
package machine
import (
"context"
"errors"
"fmt"
"maps"
"slices"
"strings"
"sync"
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/lipgloss/tree"
"github.com/docker/compose/v2/pkg/progress"
"github.com/docker/docker/api/types/container"
"github.com/psviderski/uncloud/internal/cli"
"github.com/psviderski/uncloud/pkg/api"
"github.com/spf13/cobra"
)
type removeOptions struct {
force bool
yes bool
context string
}
func NewRmCommand() *cobra.Command {
opts := removeOptions{}
cmd := &cobra.Command{
Use: "rm MACHINE",
Aliases: []string{"remove", "delete"},
Short: "Remove a machine from a cluster.",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
uncli := cmd.Context().Value("cli").(*cli.CLI)
return remove(cmd.Context(), uncli, args[0], opts)
},
}
cmd.Flags().StringVarP(&opts.context, "context", "c", "",
"Name of the cluster context. (default is the current context)")
cmd.Flags().BoolVarP(&opts.yes, "yes", "y", false,
"Do not prompt for confirmation before removing the machine.")
return cmd
}
func remove(ctx context.Context, uncli *cli.CLI, machineName string, opts removeOptions) error {
client, err := uncli.ConnectCluster(ctx, opts.context)
if err != nil {
return fmt.Errorf("connect to cluster: %w", err)
}
defer client.Close()
// Verify the machine exists and list all service containers on it including stopped ones.
listCtx, machines, err := api.ProxyMachinesContext(ctx, client, []string{machineName})
if err != nil {
return err
}
if len(machines) == 0 {
return fmt.Errorf("machine '%s' not found in the cluster", machineName)
}
m := machines[0].Machine
listOpts := container.ListOptions{All: true}
machineContainers, err := client.Docker.ListServiceContainers(listCtx, "", listOpts)
if err != nil {
return fmt.Errorf("list containers: %w", err)
}
containers := machineContainers[0].Containers
if len(containers) > 0 {
plural := ""
if len(containers) > 1 {
plural = "s"
}
fmt.Printf("Found %d service container%s on machine '%s':\n", len(containers), plural, m.Name)
fmt.Println(formatContainerTree(containers))
fmt.Println()
fmt.Println("This will remove all service containers on the machine, reset it to the uninitialised state, " +
"and remove it from the cluster.")
} else {
fmt.Printf("No service containers found on machine '%s'.\n", m.Name)
fmt.Println("This will reset the machine to the uninitialised state and remove it from the cluster.")
}
if !opts.yes {
confirmed, err := cli.Confirm()
if err != nil {
return fmt.Errorf("confirm removal: %w", err)
}
if !confirmed {
fmt.Println("Cancelled. Machine was not removed.")
return nil
}
}
if len(containers) > 0 {
err = progress.RunWithTitle(ctx, func(ctx context.Context) error {
return removeContainers(ctx, client, containers)
}, uncli.ProgressOut(), "Removing containers")
if err != nil {
return fmt.Errorf("remove containers: %w", err)
}
fmt.Println()
}
// TODO: 4. Implement and call Reset via Machine API to reset the machine state to uninitialised.
// TODO: 5. Remove the machine from the cluster store.
return fmt.Errorf("resetting machine is not fully implemented yet")
//fmt.Printf("Machine '%s' removed from the cluster.\n", m.Name)
//return nil
}
// formatContainerTree formats a list of containers grouped by service as a tree structure.
func formatContainerTree(containers []api.ServiceContainer) string {
if len(containers) == 0 {
return ""
}
// Group containers by service.
serviceContainers := make(map[string][]api.ServiceContainer)
for _, ctr := range containers {
serviceName := ctr.ServiceName()
serviceContainers[serviceName] = append(serviceContainers[serviceName], ctr)
}
// Build tree output.
var output []string
serviceNames := slices.Sorted(maps.Keys(serviceContainers))
for _, serviceName := range serviceNames {
ctrs := serviceContainers[serviceName]
mode := ctrs[0].ServiceMode()
// Format a tree for the service with its containers.
plural := ""
if len(ctrs) > 1 {
plural = "s"
}
t := tree.Root(fmt.Sprintf("• %s (%s, %d container%s)", serviceName, mode, len(ctrs), plural)).
EnumeratorStyle(lipgloss.NewStyle().MarginLeft(2).MarginRight(1))
// Add containers as children.
for _, ctr := range ctrs {
state, _ := ctr.HumanState()
info := fmt.Sprintf("%s • %s • %s", ctr.Name, ctr.Config.Image, state)
t.Child(info)
}
output = append(output, t.String())
}
return strings.Join(output, "\n")
}
// removeContainers removes the given service containers from the machine.
func removeContainers(ctx context.Context, client api.Client, containers []api.ServiceContainer) error {
if len(containers) == 0 {
return nil
}
wg := sync.WaitGroup{}
errCh := make(chan error)
for _, ctr := range containers {
wg.Add(1)
go func(c api.ServiceContainer) {
defer wg.Done()
// Gracefully stop the container before removing it.
err := client.StopContainer(ctx, c.ServiceID(), c.ID, container.StopOptions{})
if err != nil && !errors.Is(err, api.ErrNotFound) {
errCh <- fmt.Errorf("stop container '%s': %w", c.ID, err)
}
err = client.RemoveContainer(ctx, c.ServiceID(), c.ID, container.RemoveOptions{
// Remove anonymous volumes created by the container.
RemoveVolumes: true,
})
if err != nil && !errors.Is(err, api.ErrNotFound) {
errCh <- fmt.Errorf("remove container '%s': %w", c.ID, err)
}
}(ctr)
}
go func() {
wg.Wait()
close(errCh)
}()
var err error
for e := range errCh {
err = errors.Join(err, e)
}
return err
}
+4 -2
View File
@@ -6,13 +6,15 @@ import (
func NewRootCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "machine",
Short: "Manage machines in an Uncloud cluster.",
Use: "machine",
Aliases: []string{"m"},
Short: "Manage machines in an Uncloud cluster.",
}
cmd.AddCommand(
NewAddCommand(),
NewInitCommand(),
NewListCommand(),
NewRmCommand(),
NewTokenCommand(),
)
return cmd
+12 -4
View File
@@ -7,12 +7,15 @@ import (
"strings"
"github.com/psviderski/uncloud/cmd/uncloud/caddy"
cmdcontext "github.com/psviderski/uncloud/cmd/uncloud/context"
"github.com/psviderski/uncloud/cmd/uncloud/dns"
"github.com/psviderski/uncloud/cmd/uncloud/machine"
"github.com/psviderski/uncloud/cmd/uncloud/service"
"github.com/psviderski/uncloud/cmd/uncloud/volume"
"github.com/psviderski/uncloud/internal/cli"
"github.com/psviderski/uncloud/internal/cli/config"
"github.com/psviderski/uncloud/internal/fs"
"github.com/psviderski/uncloud/internal/version"
"github.com/spf13/cobra"
)
@@ -26,6 +29,7 @@ func main() {
cmd := &cobra.Command{
Use: "uncloud",
Short: "A CLI tool for managing Uncloud resources such as clusters, machines, and services.",
Version: version.String(),
SilenceUsage: true,
SilenceErrors: true,
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
@@ -37,7 +41,7 @@ func main() {
return fmt.Errorf("parse TCP address: %w", err)
}
conn = &config.MachineConnection{
TCP: addrPort,
TCP: &addrPort,
}
} else {
dest := opts.connect
@@ -53,7 +57,7 @@ func main() {
configPath := fs.ExpandHomeDir(opts.configPath)
uncli, err := cli.New(configPath, conn)
if err != nil {
return fmt.Errorf("initialize CLI: %w", err)
return fmt.Errorf("initialise CLI: %w", err)
}
cmd.SetContext(context.WithValue(cmd.Context(), "cli", uncli))
return nil
@@ -64,13 +68,16 @@ func main() {
"Connect to a remote cluster machine without using the Uncloud configuration file.\n"+
"Format: [ssh://]user@host[:port] or tcp://host:port")
// TODO: allow to override using UNCLOUD_CONFIG env var.
cmd.PersistentFlags().StringVar(&opts.configPath, "uncloud-config", "~/.config/uncloud/config.toml",
cmd.PersistentFlags().StringVar(&opts.configPath, "uncloud-config", "~/.config/uncloud/config.yaml",
"Path to the Uncloud configuration file.")
_ = cmd.MarkPersistentFlagFilename("uncloud-config", "toml")
_ = cmd.MarkPersistentFlagFilename("uncloud-config", "yaml", "yml")
// TODO: make --context a global flag and pass it as a value of the command context.
cmd.AddCommand(
NewDeployCommand(),
NewBuildCommand(),
caddy.NewRootCommand(),
cmdcontext.NewRootCommand(),
dns.NewRootCommand(),
machine.NewRootCommand(),
service.NewRootCommand(),
@@ -79,6 +86,7 @@ func main() {
service.NewRmCommand(),
service.NewRunCommand(),
service.NewScaleCommand(),
volume.NewRootCommand(),
)
cobra.CheckErr(cmd.Execute())
}
+8 -7
View File
@@ -3,14 +3,15 @@ package service
import (
"context"
"fmt"
"github.com/docker/docker/pkg/stringid"
"github.com/docker/go-units"
"os"
"text/tabwriter"
"time"
"github.com/spf13/cobra"
"github.com/docker/docker/pkg/stringid"
"github.com/docker/go-units"
"github.com/psviderski/uncloud/internal/cli"
"github.com/spf13/cobra"
)
type inspectOptions struct {
@@ -21,7 +22,7 @@ type inspectOptions struct {
func NewInspectCommand() *cobra.Command {
opts := inspectOptions{}
cmd := &cobra.Command{
Use: "inspect",
Use: "inspect SERVICE",
Short: "Display detailed information on a service.",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
@@ -31,8 +32,8 @@ func NewInspectCommand() *cobra.Command {
},
}
cmd.Flags().StringVarP(
&opts.cluster, "cluster", "c", "",
"Name of the cluster. (default is the current cluster)",
&opts.cluster, "context", "c", "",
"Name of the cluster context. (default is the current context)",
)
return cmd
}
@@ -49,7 +50,7 @@ func inspect(ctx context.Context, uncli *cli.CLI, opts inspectOptions) error {
return fmt.Errorf("inspect service: %w", err)
}
machines, err := client.ListMachines(ctx)
machines, err := client.ListMachines(ctx, nil)
if err != nil {
return fmt.Errorf("list machines: %w", err)
}
@@ -3,14 +3,16 @@ package service
import (
"context"
"fmt"
"github.com/psviderski/uncloud/internal/cli"
"github.com/spf13/cobra"
"os"
"strings"
"text/tabwriter"
"github.com/psviderski/uncloud/internal/cli"
"github.com/spf13/cobra"
)
func NewListCommand() *cobra.Command {
// TODO(lhf): rename to context
var cluster string
cmd := &cobra.Command{
Use: "ls",
@@ -22,8 +24,8 @@ func NewListCommand() *cobra.Command {
},
}
cmd.Flags().StringVarP(
&cluster, "cluster", "c", "",
"Name of the cluster. (default is the current cluster)",
&cluster, "context", "c", "",
"Name of the cluster context. (default is the current context)",
)
return cmd
}
+4 -5
View File
@@ -3,11 +3,10 @@ package service
import (
"context"
"fmt"
"github.com/docker/cli/cli/streams"
"github.com/docker/compose/v2/pkg/progress"
"github.com/psviderski/uncloud/internal/cli"
"github.com/spf13/cobra"
"os"
)
type rmOptions struct {
@@ -29,8 +28,8 @@ func NewRmCommand() *cobra.Command {
},
}
cmd.Flags().StringVarP(
&opts.cluster, "cluster", "c", "",
"Name of the cluster. (default is the current cluster)",
&opts.cluster, "context", "c", "",
"Name of the cluster context. (default is the current context)",
)
return cmd
}
@@ -48,7 +47,7 @@ func rm(ctx context.Context, uncli *cli.CLI, opts rmOptions) error {
return fmt.Errorf("remove service '%s': %w", s, err)
}
return nil
}, streams.NewOut(os.Stdout), "Removing service "+s)
}, uncli.ProgressOut(), "Removing service "+s)
}
return nil
+3 -2
View File
@@ -6,8 +6,9 @@ import (
func NewRootCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "service",
Short: "Manage services in an Uncloud cluster.",
Use: "service",
Aliases: []string{"svc"},
Short: "Manage services in an Uncloud cluster.",
}
cmd.AddCommand(
NewInspectCommand(),
+260 -82
View File
@@ -3,26 +3,35 @@ package service
import (
"context"
"fmt"
"os"
"strings"
dockeropts "github.com/docker/cli/opts"
"github.com/docker/compose/v2/pkg/progress"
"github.com/docker/docker/daemon/names"
"github.com/psviderski/uncloud/internal/cli"
"github.com/psviderski/uncloud/internal/machine/api/pb"
"github.com/psviderski/uncloud/internal/secret"
"github.com/psviderski/uncloud/pkg/api"
"github.com/psviderski/uncloud/pkg/client/deploy"
"github.com/spf13/cobra"
"slices"
"strings"
)
type runOptions struct {
command []string
cpu dockeropts.NanoCPUs
entrypoint string
entrypointChanged bool
env []string
image string
machines []string
memory dockeropts.MemBytes
mode string
name string
privileged bool
publish []string
pull string
replicas uint
user string
volumes []string
cluster string
@@ -48,112 +57,69 @@ func NewRunCommand() *cobra.Command {
},
}
cmd.Flags().VarP(&opts.cpu, "cpu", "",
"Maximum number of CPU cores a service container can use. Fractional values are allowed: "+
"0.5 for half a core or 2.25 for two and a quarter cores.")
cmd.Flags().StringVar(&opts.entrypoint, "entrypoint", "",
"Overwrite the default ENTRYPOINT of the image. Pass an empty string \"\" to reset it.")
cmd.Flags().StringSliceVarP(&opts.env, "env", "e", nil,
"Set an environment variable for service containers. Can be specified multiple times.\n"+
"Format: VAR=value or just VAR to use the value from the local environment.")
cmd.Flags().StringVar(&opts.mode, "mode", api.ServiceModeReplicated,
fmt.Sprintf("Replication mode of the service: either '%s' (a specified number of containers across "+
"the machines) or '%s' (one container on every machine).",
api.ServiceModeReplicated, api.ServiceModeGlobal))
cmd.Flags().StringSliceVarP(&opts.machines, "machine", "m", nil,
"Placement constraint by machine name, limiting which machines the service can run on. Can be specified "+
"Placement constraint by machine names, limiting which machines the service can run on. Can be specified "+
"multiple times or as a comma-separated list of machine names. (default is any suitable machine)")
cmd.Flags().VarP(&opts.memory, "memory", "",
"Maximum amount of memory a service container can use. Value is a positive integer with optional unit suffix "+
"(b, k, m, g). Default unit is bytes if no suffix specified.\n"+
"Examples: 1073741824, 1024m, 1g (all equal 1 gibibyte)")
cmd.Flags().StringVarP(&opts.name, "name", "n", "",
"Assign a name to the service. A random name is generated if not specified.")
cmd.Flags().BoolVar(&opts.privileged, "privileged", false,
"Give extended privileges to service containers. This is a security risk and should be used with caution.")
cmd.Flags().StringSliceVarP(&opts.publish, "publish", "p", nil,
"Publish a service port to make it accessible outside the cluster. Can be specified multiple times.\n"+
"Format: [hostname:][load_balancer_port:]container_port[/protocol] or [host_ip:]:host_port:container_port[/protocol]@host\n"+
"Supported protocols: tcp, udp, http, https (default is tcp). If a hostname for http(s) port is not specified,\n"+
"service-name.cluster-domain will be used as the hostname.\n"+
"Supported protocols: tcp, udp, http, https (default is tcp). If a hostname for http(s) port is not specified\n"+
"and a cluster domain is reserved, service-name.cluster-domain will be used as the hostname.\n"+
"Examples:\n"+
" -p 8080/https Publish port 8080 as HTTPS via load balancer with default service-name.cluster-domain hostname\n"+
" -p app.example.com:8080/https Publish port 8080 as HTTPS via load balancer with custom hostname\n"+
" -p 9000:8080 Publish port 8080 as TCP port 9000 via load balancer\n"+
" -p 8080/https Publish port 8080 as HTTPS via reverse proxy with default service-name.cluster-domain hostname\n"+
" -p app.example.com:8080/https Publish port 8080 as HTTPS via reverse proxy with custom hostname\n"+
// TODO: add support for publishing L4 tcp/udp ports.
//" -p 9000:8080 Publish port 8080 as TCP port 9000 via reverse proxy\n"+
" -p 53:5353/udp@host Bind UDP port 5353 to host port 53")
cmd.Flags().StringVar(&opts.pull, "pull", api.PullPolicyMissing,
fmt.Sprintf("Pull image from the registry before running service containers ('%s', '%s', '%s').",
api.PullPolicyAlways, api.PullPolicyMissing, api.PullPolicyNever))
cmd.Flags().UintVar(&opts.replicas, "replicas", 1,
"Number of containers to run for the service. Only valid for a replicated service.")
cmd.Flags().StringVarP(&opts.user, "user", "u", "",
"User name or UID and optionally group name or GID used for running the command inside service containers.\n"+
"Format: USER[:GROUP] or UID[:GID]. If not specified, the user is set to the default user of the image.")
cmd.Flags().StringSliceVarP(&opts.volumes, "volume", "v", nil,
"Bind mount a host file or directory into a service container using the format "+
"/host/path:/container/path[:ro]. Can be specified multiple times.")
"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"+
"Format: volume_name:/container/path[:ro|volume-nocopy] or /host/path:/container/path[:ro]\n"+
"Examples:\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 /host/path:/container/path:ro Bind mount a host directory or file as read-only")
cmd.Flags().StringVarP(
&opts.cluster, "cluster", "c", "",
"Name of the cluster to run the service in. (default is the current cluster)",
&opts.cluster, "context", "c", "",
"Name of the cluster context to run the service in. (default is the current context)",
)
return cmd
}
func run(ctx context.Context, uncli *cli.CLI, opts runOptions) error {
switch opts.mode {
case api.ServiceModeReplicated, api.ServiceModeGlobal:
default:
return fmt.Errorf("invalid replication mode: '%s'", opts.mode)
}
switch opts.pull {
case api.PullPolicyAlways, api.PullPolicyMissing, api.PullPolicyNever:
default:
return fmt.Errorf("invalid pull policy: '%s'", opts.pull)
}
var machineFilter deploy.MachineFilter
if len(opts.machines) > 0 {
var machines []string
for _, value := range opts.machines {
if value == "" {
continue
}
mlist := strings.Split(value, ",")
for _, m := range mlist {
if m = strings.TrimSpace(m); m != "" {
machines = append(machines, m)
}
}
}
if len(machines) > 0 {
machineFilter = func(m *pb.MachineInfo) bool {
return slices.Contains(machines, m.Name)
}
}
}
ports := make([]api.PortSpec, len(opts.publish))
for i, publishPort := range opts.publish {
port, err := api.ParsePortSpec(publishPort)
if err != nil {
return fmt.Errorf("invalid service port '%s': %w", publishPort, err)
}
ports[i] = port
}
// TODO: parse and validate opts.volumes to fail fast if invalid.
spec := api.ServiceSpec{
Container: api.ContainerSpec{
Command: opts.command,
Image: opts.image,
PullPolicy: opts.pull,
Volumes: opts.volumes,
},
Mode: opts.mode,
Name: opts.name,
Ports: ports,
Replicas: opts.replicas,
}
// Overwrite the default ENTRYPOINT of the image or reset it if an empty string is passed.
if opts.entrypoint != "" {
spec.Container.Entrypoint = []string{opts.entrypoint}
} else if opts.entrypointChanged {
spec.Container.Entrypoint = []string{""}
}
if err := spec.Validate(); err != nil {
return fmt.Errorf("invalid service configuration: %w", err)
spec, err := prepareServiceSpec(opts)
if err != nil {
return err
}
clusterClient, err := uncli.ConnectCluster(ctx, opts.cluster)
@@ -162,9 +128,17 @@ func run(ctx context.Context, uncli *cli.CLI, opts runOptions) error {
}
defer clusterClient.Close()
resp, err := clusterClient.RunService(ctx, spec, machineFilter)
var resp api.RunServiceResponse
err = progress.RunWithTitle(ctx, func(ctx context.Context) error {
resp, err = clusterClient.RunService(ctx, spec)
if err != nil {
return fmt.Errorf("run service: %w", err)
}
return nil
}, uncli.ProgressOut(), fmt.Sprintf("Running service %s (%s mode)", spec.Name, spec.Mode))
if err != nil {
return fmt.Errorf("run service: %w", err)
return err
}
svc, err := clusterClient.InspectService(ctx, resp.ID)
@@ -183,3 +157,207 @@ func run(ctx context.Context, uncli *cli.CLI, opts runOptions) error {
return nil
}
func prepareServiceSpec(opts runOptions) (api.ServiceSpec, error) {
var spec api.ServiceSpec
env, err := parseEnv(opts.env)
if err != nil {
return spec, err
}
switch opts.mode {
case api.ServiceModeReplicated, api.ServiceModeGlobal:
default:
return spec, fmt.Errorf("invalid replication mode: '%s'", opts.mode)
}
switch opts.pull {
case api.PullPolicyAlways, api.PullPolicyMissing, api.PullPolicyNever:
default:
return spec, fmt.Errorf("invalid pull policy: '%s'", opts.pull)
}
ports := make([]api.PortSpec, len(opts.publish))
for i, publishPort := range opts.publish {
port, err := api.ParsePortSpec(publishPort)
if err != nil {
return spec, fmt.Errorf("invalid service port '%s': %w", publishPort, err)
}
ports[i] = port
}
volumes, mounts, err := parseVolumeFlags(opts.volumes)
if err != nil {
return spec, err
}
placement := api.Placement{
Machines: cli.ExpandCommaSeparatedValues(opts.machines),
}
spec = api.ServiceSpec{
Container: api.ContainerSpec{
Command: opts.command,
Env: env,
Image: opts.image,
Privileged: opts.privileged,
PullPolicy: opts.pull,
Resources: api.ContainerResources{
CPU: opts.cpu.Value(),
Memory: opts.memory.Value(),
},
User: opts.user,
VolumeMounts: mounts,
},
Mode: opts.mode,
Name: opts.name,
Placement: placement,
Ports: ports,
Replicas: opts.replicas,
Volumes: volumes,
}
// Overwrite the default ENTRYPOINT of the image or reset it if an empty string is passed.
if opts.entrypoint != "" {
spec.Container.Entrypoint = []string{opts.entrypoint}
} else if opts.entrypointChanged {
spec.Container.Entrypoint = []string{""}
}
if err = spec.Validate(); err != nil {
return spec, fmt.Errorf("invalid service configuration: %w", err)
}
// Generate a service name if not specified to be able to include it in the progress title.
if spec.Name == "" {
spec.Name, err = deploy.GenerateServiceName(spec.Container.Image)
if err != nil {
return spec, fmt.Errorf("generate service name: %w", err)
}
}
return spec, err
}
// parseEnv parses the environment variables from the command line arguments.
// It supports two formats: "VAR=value" or just "VAR" to use the value from the local environment.
func parseEnv(env []string) (api.EnvVars, error) {
envVars := make(api.EnvVars)
for _, e := range env {
key, value, hasValue := strings.Cut(e, "=")
if key == "" {
return nil, fmt.Errorf("invalid environment variable: '%s'", e)
}
if hasValue {
envVars[key] = value
} else {
if localEnvValue, ok := os.LookupEnv(key); ok {
envVars[key] = localEnvValue
}
}
}
return envVars, nil
}
// parseVolumeFlags parses volume flag values in Docker CLI format and returns VolumeSpecs and VolumeMounts.
// It handles both named volumes (volume_name:/container/path[:ro|volume-nocopy])
// and bind mounts (/host/path:/container/path[:ro]).
func parseVolumeFlags(volumes []string) ([]api.VolumeSpec, []api.VolumeMount, error) {
specs := make([]api.VolumeSpec, 0, len(volumes))
mounts := make([]api.VolumeMount, 0, len(volumes))
// Track volume names to avoid duplicate specs.
seenVolumes := make(map[string]struct{})
for _, vol := range volumes {
spec, mount, err := parseVolumeFlagValue(vol)
if err != nil {
return nil, nil, fmt.Errorf("invalid volume mount '%s': %w", vol, err)
}
if _, ok := seenVolumes[spec.Name]; !ok {
specs = append(specs, spec)
seenVolumes[spec.Name] = struct{}{}
}
mounts = append(mounts, mount)
}
return specs, mounts, nil
}
func parseVolumeFlagValue(volume string) (api.VolumeSpec, api.VolumeMount, error) {
var spec api.VolumeSpec
var mount api.VolumeMount
parts := strings.Split(volume, ":")
switch len(parts) {
case 1:
return spec, mount, fmt.Errorf("invalid format, must contain at least one separator ':'")
case 2, 3:
// Format: (volume_name|/host/path):/container/path[:opts]
if !strings.HasPrefix(parts[1], "/") {
return spec, mount, fmt.Errorf("invalid container mount path: '%s', must be absolute path", parts[1])
}
mount.ContainerPath = parts[1]
volumeNoCopy := false
if len(parts) == 3 {
opts := strings.Split(parts[2], ",")
for _, opt := range opts {
switch opt {
case "ro", "readonly":
mount.ReadOnly = true
case "volume-nocopy":
volumeNoCopy = true
default:
return spec, mount, fmt.Errorf("invalid option: '%s'", opt)
}
}
}
if strings.HasPrefix(parts[0], "/") {
// Host path bind mount: /host/path:/container/path
suffix, err := secret.RandomAlphaNumeric(4)
if err != nil {
return spec, mount, fmt.Errorf("generate random suffix: %w", err)
}
spec = api.VolumeSpec{
Name: "bind-" + suffix,
Type: api.VolumeTypeBind,
BindOptions: &api.BindOptions{
HostPath: parts[0],
CreateHostPath: true,
},
}
} else {
// Named volume mount: volume_name:/container/path
volumeName := parts[0]
if !names.RestrictedNamePattern.MatchString(volumeName) {
return spec, mount, fmt.Errorf("volume name '%s' includes invalid characters, only '%s' are allowed. "+
"If you intended to pass a host directory or file, use absolute path",
volumeName, names.RestrictedNameChars)
}
spec = api.VolumeSpec{
Name: volumeName,
Type: api.VolumeTypeVolume,
VolumeOptions: &api.VolumeOptions{
Name: volumeName,
NoCopy: volumeNoCopy,
},
}
}
mount.VolumeName = spec.Name
default:
return spec, mount, fmt.Errorf("invalid format, must container at most 2 separators ':'")
}
return spec, mount, nil
}
+3 -3
View File
@@ -40,8 +40,8 @@ func NewScaleCommand() *cobra.Command {
}
cmd.Flags().StringVarP(
&opts.cluster, "cluster", "c", "",
"Name of the cluster. (default is the current cluster)",
&opts.cluster, "context", "c", "",
"Name of the cluster context. (default is the current context)",
)
return cmd
@@ -143,7 +143,7 @@ func confirm() (bool, error) {
Negative("No").
Value(&confirmed),
),
)
).WithAccessible(true)
if err := form.Run(); err != nil {
return false, err
}
+148
View File
@@ -0,0 +1,148 @@
package volume
import (
"context"
"fmt"
"slices"
"strings"
"github.com/charmbracelet/huh"
"github.com/docker/docker/api/types/volume"
"github.com/psviderski/uncloud/internal/cli"
"github.com/psviderski/uncloud/internal/machine/api/pb"
"github.com/psviderski/uncloud/pkg/api"
"github.com/spf13/cobra"
)
type createOptions struct {
driver string
driverOpts []string
labels []string
machine string
context string
}
func NewCreateCommand() *cobra.Command {
opts := createOptions{}
cmd := &cobra.Command{
Use: "create VOLUME_NAME",
Short: "Create a volume on a specific machine.",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
uncli := cmd.Context().Value("cli").(*cli.CLI)
opts.driver = strings.TrimSpace(opts.driver)
volumeName := args[0]
if volumeName == "" {
return fmt.Errorf("volume name is required")
}
return create(cmd.Context(), uncli, volumeName, opts)
},
}
cmd.Flags().StringVarP(&opts.driver, "driver", "d", "local",
"Volume driver to use.")
cmd.Flags().StringSliceVarP(&opts.driverOpts, "opt", "o", nil,
"Driver specific options in the form of 'key=value' pairs. Can be specified multiple times.")
cmd.Flags().StringSliceVarP(&opts.labels, "label", "l", nil,
"Labels to assign to the volume in the form of 'key=value' pairs. Can be specified multiple times.")
cmd.Flags().StringVarP(&opts.machine, "machine", "m", "",
"Name or ID of the machine to create the volume on.")
cmd.Flags().StringVarP(&opts.context, "context", "c", "",
"Name of the cluster context. (default is the current context)")
return cmd
}
func create(ctx context.Context, uncli *cli.CLI, name string, opts createOptions) error {
client, err := uncli.ConnectCluster(ctx, opts.context)
if err != nil {
return fmt.Errorf("connect to cluster: %w", err)
}
defer client.Close()
// Parse driver options.
driverOpts := make(map[string]string)
for _, opt := range opts.driverOpts {
k, v, ok := strings.Cut(opt, "=")
if !ok {
return fmt.Errorf("invalid driver option format: '%s' (expected key=value)", opt)
}
driverOpts[k] = v
}
// Parse labels.
labels := make(map[string]string)
for _, label := range opts.labels {
k, v, ok := strings.Cut(label, "=")
if !ok {
return fmt.Errorf("invalid label format: '%s' (expected key=value)", label)
}
labels[k] = v
}
// List machines and filter by the specified machine name or ID.
// If no machine is specified, prompt the user to select one.
machines, err := client.ListMachines(ctx, nil)
if err != nil {
return fmt.Errorf("list machines: %w", err)
}
var selectedMachine *pb.MachineInfo
if opts.machine == "" {
if len(machines) == 1 {
selectedMachine = machines[0].Machine
} else {
if selectedMachine, err = promptSelectMachine(ctx, machines); err != nil {
return fmt.Errorf("select machine: %w", err)
}
}
} else {
m := machines.FindByNameOrID(opts.machine)
if m == nil {
return fmt.Errorf("machine '%s' not found", opts.machine)
}
selectedMachine = m.Machine
}
createOpts := volume.CreateOptions{
Name: name,
Driver: opts.driver,
DriverOpts: driverOpts,
Labels: labels,
}
vol, err := client.CreateVolume(ctx, selectedMachine.Id, createOpts)
if err != nil {
return fmt.Errorf("create volume '%s' on machine '%s': %w", name, selectedMachine.Name, err)
}
fmt.Printf("Volume '%s' created on machine '%s'.\n", vol.Volume.Name, vol.MachineName)
return nil
}
func promptSelectMachine(ctx context.Context, machines api.MachineMembersList) (*pb.MachineInfo, error) {
options := make([]huh.Option[*pb.MachineInfo], len(machines))
for i, m := range machines {
options[i] = huh.NewOption(m.Machine.Name, m.Machine)
}
slices.SortFunc(options, func(a, b huh.Option[*pb.MachineInfo]) int {
return strings.Compare(a.Key, b.Key)
})
var selected *pb.MachineInfo
form := huh.NewForm(
huh.NewGroup(
huh.NewSelect[*pb.MachineInfo]().
Title("Select a machine to create the volume on (or specify with --machine flag)").
Options(options...).
Value(&selected),
),
)
if err := form.RunWithContext(ctx); err != nil {
return nil, err
}
return selected, nil
}
+81
View File
@@ -0,0 +1,81 @@
package volume
import (
"context"
"encoding/json"
"errors"
"fmt"
"github.com/psviderski/uncloud/internal/cli"
"github.com/psviderski/uncloud/pkg/api"
"github.com/spf13/cobra"
)
type inspectOptions struct {
machine string
context string
}
func NewInspectCommand() *cobra.Command {
opts := inspectOptions{}
cmd := &cobra.Command{
Use: "inspect VOLUME_NAME",
Short: "Display detailed information on a volume.",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
uncli := cmd.Context().Value("cli").(*cli.CLI)
return inspect(cmd.Context(), uncli, args[0], opts)
},
}
cmd.Flags().StringVarP(&opts.machine, "machine", "m", "",
"Name or ID of the machine where the volume is located. "+
"If not specified, the volume will be searched across all machines.")
cmd.Flags().StringVarP(&opts.context, "context", "c", "",
"Name of the cluster context. (default is the current context)")
return cmd
}
func inspect(ctx context.Context, uncli *cli.CLI, name string, opts inspectOptions) error {
client, err := uncli.ConnectCluster(ctx, opts.context)
if err != nil {
return fmt.Errorf("connect to cluster: %w", err)
}
defer client.Close()
filter := &api.VolumeFilter{
Names: []string{name},
}
if opts.machine != "" {
filter.Machines = []string{opts.machine}
}
volumes, err := client.ListVolumes(ctx, filter)
if err != nil {
return fmt.Errorf("list volumes: %w", err)
}
if len(volumes) == 0 {
if opts.machine != "" {
return fmt.Errorf("volume '%s' not found on machine '%s'", name, opts.machine)
}
return fmt.Errorf("volume '%s' not found on any machine", name)
}
if len(volumes) > 1 {
fmt.Printf("Volume '%s' found on multiple machines:\n", name)
for _, v := range volumes {
fmt.Printf(" • %s\n", v.MachineName)
}
return errors.New("specify --machine flag to choose which machine to use")
}
data, err := json.MarshalIndent(volumes[0], "", " ")
if err != nil {
return fmt.Errorf("marshal volume: %w", err)
}
fmt.Println(string(data))
return nil
}
+105
View File
@@ -0,0 +1,105 @@
package volume
import (
"context"
"fmt"
"os"
"slices"
"strings"
"text/tabwriter"
"github.com/psviderski/uncloud/internal/cli"
"github.com/psviderski/uncloud/pkg/api"
"github.com/spf13/cobra"
)
type listOptions struct {
machines []string
quiet bool
context string
}
func NewListCommand() *cobra.Command {
opts := listOptions{}
cmd := &cobra.Command{
Use: "ls",
Aliases: []string{"list"},
Short: "List volumes across all machines in the cluster.",
RunE: func(cmd *cobra.Command, args []string) error {
uncli := cmd.Context().Value("cli").(*cli.CLI)
return list(cmd.Context(), uncli, opts)
},
}
cmd.Flags().StringSliceVarP(&opts.machines, "machine", "m", nil,
"Filter volumes by machine name or ID. Can be specified multiple times or as a comma-separated list. "+
"(default is include all machines)")
cmd.Flags().BoolVarP(&opts.quiet, "quiet", "q", false,
"Only display volume names.")
cmd.Flags().StringVarP(&opts.context, "context", "c", "",
"Name of the cluster context. (default is the current context)")
return cmd
}
func list(ctx context.Context, uncli *cli.CLI, opts listOptions) error {
client, err := uncli.ConnectCluster(ctx, opts.context)
if err != nil {
return fmt.Errorf("connect to cluster: %w", err)
}
defer client.Close()
// Apply machine filter if specified.
var filter *api.VolumeFilter
if len(opts.machines) > 0 {
machines := cli.ExpandCommaSeparatedValues(opts.machines)
filter = &api.VolumeFilter{
Machines: machines,
}
}
volumes, err := client.ListVolumes(ctx, filter)
if err != nil {
return fmt.Errorf("list volumes: %w", err)
}
if len(volumes) == 0 {
if !opts.quiet {
fmt.Println("No volumes found.")
}
return nil
}
// Sort the volumes by name first, then by machine name.
slices.SortFunc(volumes, func(a, b api.MachineVolume) int {
cmp := strings.Compare(a.Volume.Name, b.Volume.Name)
if cmp != 0 {
return cmp
}
return strings.Compare(a.MachineName, b.MachineName)
})
// If quiet mode, just print volume names.
if opts.quiet {
for _, v := range volumes {
fmt.Println(v.Volume.Name)
}
return nil
}
// Print the volumes in a table format.
tw := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', 0)
fmt.Fprintln(tw, "NAME\tDRIVER\tMACHINE")
for _, v := range volumes {
fmt.Fprintf(tw, "%s\t%s\t%s\n",
v.Volume.Name,
v.Volume.Driver,
v.MachineName,
)
}
return tw.Flush()
}
+111
View File
@@ -0,0 +1,111 @@
package volume
import (
"context"
"errors"
"fmt"
"github.com/psviderski/uncloud/internal/cli"
"github.com/psviderski/uncloud/pkg/api"
"github.com/spf13/cobra"
)
type removeOptions struct {
force bool
machines []string
yes bool
context string
}
func NewRemoveCommand() *cobra.Command {
opts := removeOptions{}
cmd := &cobra.Command{
Use: "rm VOLUME_NAME [VOLUME_NAME...]",
Aliases: []string{"remove", "delete"},
Short: "Remove one or more volumes.",
Long: "Remove one or more volumes. You cannot remove a volume that is in use by a container.",
Args: cobra.MinimumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
uncli := cmd.Context().Value("cli").(*cli.CLI)
return remove(cmd.Context(), uncli, args, opts)
},
}
cmd.Flags().BoolVarP(&opts.force, "force", "f", false,
"Force the removal of one or more volumes.")
cmd.Flags().StringSliceVarP(&opts.machines, "machine", "m", nil,
"Name or ID of the machine to remove one or more volumes from. "+
"Can be specified multiple times or as a comma-separated list.\n"+
"If not specified, the found volume(s) will be removed from all machines.")
cmd.Flags().BoolVarP(&opts.yes, "yes", "y", false,
"Do not prompt for confirmation before removing the volume(s).")
cmd.Flags().StringVarP(&opts.context, "context", "c", "",
"Name of the cluster context. (default is the current context)")
return cmd
}
func remove(ctx context.Context, uncli *cli.CLI, names []string, opts removeOptions) error {
client, err := uncli.ConnectCluster(ctx, opts.context)
if err != nil {
return fmt.Errorf("connect to cluster: %w", err)
}
defer client.Close()
filter := &api.VolumeFilter{
Names: names,
}
if len(opts.machines) > 0 {
machines := cli.ExpandCommaSeparatedValues(opts.machines)
filter.Machines = machines
}
volumes, err := client.ListVolumes(ctx, filter)
if err != nil {
return fmt.Errorf("list volumes: %w", err)
}
if len(volumes) == 0 {
if len(names) == 1 {
return fmt.Errorf("volume '%s' not found", names[0])
}
return fmt.Errorf("no volumes found matching the specified names")
}
// Confirm removal if not using --yes flag.
if !opts.yes {
fmt.Println("The following volumes will be removed:")
for _, v := range volumes {
fmt.Printf(" • '%s' on machine '%s'\n", v.Volume.Name, v.MachineName)
}
fmt.Println()
confirmed, err := cli.Confirm()
if err != nil {
return fmt.Errorf("confirm removal: %w", err)
}
if !confirmed {
fmt.Println("Cancelled. No volumes were removed.")
return nil
}
}
// Remove the volumes one by one collecting errors.
var removeErr error
for _, v := range volumes {
if err = client.RemoveVolume(ctx, v.MachineID, v.Volume.Name, opts.force); err != nil {
if !errors.Is(err, api.ErrNotFound) {
removeErr = errors.Join(removeErr, fmt.Errorf("failed to remove volume '%s' on machine '%s': %w",
v.Volume.Name, v.MachineName, err))
}
continue
}
fmt.Printf("Volume '%s' removed from machine '%s'.\n", v.Volume.Name, v.MachineName)
}
return removeErr
}
+19
View File
@@ -0,0 +1,19 @@
package volume
import (
"github.com/spf13/cobra"
)
func NewRootCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "volume",
Short: "Manage volumes in an Uncloud cluster.",
}
cmd.AddCommand(
NewCreateCommand(),
NewInspectCommand(),
NewListCommand(),
NewRemoveCommand(),
)
return cmd
}
+4 -2
View File
@@ -2,14 +2,16 @@ package main
import (
"context"
"github.com/spf13/cobra"
"log/slog"
"os"
"os/signal"
"syscall"
"github.com/psviderski/uncloud/internal/daemon"
"github.com/psviderski/uncloud/internal/log"
"github.com/psviderski/uncloud/internal/machine"
"github.com/psviderski/uncloud/internal/version"
"github.com/spf13/cobra"
)
func main() {
@@ -22,6 +24,7 @@ func main() {
cmd := &cobra.Command{
Use: "uncloudd",
Short: "Uncloud machine daemon.",
Version: version.String(),
SilenceUsage: true,
SilenceErrors: true,
RunE: func(cmd *cobra.Command, args []string) error {
@@ -52,5 +55,4 @@ func main() {
}()
cobra.CheckErr(cmd.ExecuteContext(ctx))
}
+14
View File
@@ -0,0 +1,14 @@
# Ignore everything and use an allowlist of what is allowed to not package any secrets by accident.
*
# Allow files and directories.
!docs/
!src/
!static/
!pkg/
!Caddyfile
!*.js
!*.json
# Ignore unnecessary files inside allowed directories.
**/.DS_Store
+20
View File
@@ -0,0 +1,20 @@
# Dependencies
/node_modules
# Production
/build
# Generated files
.docusaurus
.cache-loader
# Misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
+9
View File
@@ -0,0 +1,9 @@
{
admin off
}
:8000 {
root * /usr/share/caddy
file_server
try_files {path} /index.html
}
+27
View File
@@ -0,0 +1,27 @@
# Stage 1: Base image.
## Start with a base image containing NodeJS so we can build Docusaurus.
FROM node:lts-alpine AS base
## Disable colour output from yarn to make logs easier to read.
ENV FORCE_COLOR=0
## Enable corepack.
RUN corepack enable
## Set the working directory to `/opt/docusaurus`.
WORKDIR /opt/docusaurus
# Stage 2: Production build.
FROM base AS prod
## Set the working directory to `/opt/docusaurus`.
WORKDIR /opt/docusaurus
## Copy over the source code.
COPY . /opt/docusaurus/
## Install dependencies with `--immutable` to ensure reproducibility.
RUN npm ci
## Build the static site.
RUN npm run build
# Stage 3: Serve static site with Caddy.
FROM caddy:2.10.0-alpine AS caddy
## Copy the Caddyfile.
COPY ./Caddyfile /etc/caddy/Caddyfile
## Copy the Docusaurus build output.
COPY --from=prod /opt/docusaurus/build /usr/share/caddy
+41
View File
@@ -0,0 +1,41 @@
# Website
This website is built using [Docusaurus](https://docusaurus.io/), a modern static website generator.
### Installation
```
$ yarn
```
### Local Development
```
$ yarn start
```
This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server.
### Build
```
$ yarn build
```
This command generates static content into the `build` directory and can be served using any static contents hosting service.
### Deployment
Using SSH:
```
$ USE_SSH=true yarn deploy
```
Not using SSH:
```
$ GIT_USER=<Your GitHub username> yarn deploy
```
If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch.
+8
View File
@@ -0,0 +1,8 @@
services:
uncloud-docs:
image: ghcr.io/psviderski/uncloud-docs:latest
pull_policy: always
user: nobody
x-ports:
- docs.uncloud.run:8000/https
scale: 2
+115
View File
@@ -0,0 +1,115 @@
---
slug: /
---
# Overview
Uncloud makes **self-hosting web applications** across multiple machines in production dead simple.
You can connect any machines — from cloud VMs to bare metal servers (no matter where they're located) — into a secure
private network. Then run and scale multi-service and multi-container web apps and databases across your machines using
simple Docker-like commands and [Docker Compose](https://docs.docker.com/reference/compose-file/) files.
Uncloud covers all the essentials for operating apps in production without overwhelming you with the complexity of
traditional container orchestrators like Kubernetes or Swarm:
* Initial machine and network setup
* Zero-downtime rolling deployments
* Health checks and automatic restarts
* Automatic HTTPS and reverse proxy configuration
* Scaling services across multiple machines
* Cross-machine service communication without exposing ports to the internet
* DNS-based service discovery
* Load balancing
* Persistent storage
## Use cases
Some of the common use cases Uncloud is a great fit for:
- **Self-hosting and Homelabs**: Run your self-hosted apps on your own hardware. Start with a single machine and add
more as your needs grow.
- **Outgrowing Docker Compose**: Level up your Docker Compose setup with zero-downtime deployments, replicas across
multiple machines for improved reliability, cross-machine service communication, automated reverse proxy management,
and more using the same Compose file.
- **Small to medium web applications**: Deploy your SaaS product, websites, or personal projects with redundancy across
multiple machines for better reliability and your peace of mind.
- **Hybrid setups (cloud + on-prem)**: Combine cloud VMs with on-premise for cost savings and data sovereignty — all
managed through the same interface.
- **Agencies and freelancers**: Host multiple client projects with proper isolation on shared infrastructure, optimising
costs and resources.
- **Edge computing**: Deploy applications closer to your users for lower latency and better performance.
- **Dev/staging environments**: Spin up additional environments for development and testing that mirror production
reusing the same Compose configuration.
## What makes Uncloud different
Here are the design decisions that make Uncloud stand out:
### Decentralised design
You can think of an Uncloud cluster as a **network of Docker hosts** (machines) that are all aware of each other. All
machines in the cluster are equal. You can connect to any of them to manage containers on any other machine in the
cluster. If a machine or part of the network goes down, the rest of the cluster keeps running.
There is no centralised control plane, so no need to worry about maintaining a quorum of machines for it. The time saved
can be better spent developing and deploying your apps instead.
### Zero-config overlay network
Uncloud automatically configures and maintains a secure **WireGuard mesh network** across your machines. It handles key
management, peer discovery, and NAT traversal without any manual configuration. This makes it easy to connect machines
from different networks and locations, such as cloud VMs, on-premise servers, or your Raspberry Pi at home.
Docker containers running on different machines get **unique IP addresses** from the cluster network so they can
**communicate directly** as if they were on a single machine without opening up any host ports to the internet.
The design and implementation were highly inspired by
Talos [KubeSpan](https://www.talos.dev/v1.10/talos-guides/network/kubespan/).
### Managed DNS service
Uncloud can provide **managed DNS records** like `<service-name>.<cluster-id>.cluster.uncloud.run` for your public
services through free [Uncloud DNS](https://github.com/psviderski/uncloud-dns) service. You can deploy a service and
instantly access it from anywhere with a proper DNS name and HTTPS without any manual DNS configuration. This makes
self-hosting much more accessible and simplifies the process of adding your own domain later.
### No complex orchestration
Uncloud operations are done using **imperative CLI commands** that have the taste of Docker and Docker Compose. The
deployment and scaling commands can output an execution plan that describes what exactly will be changed on your cluster
once you approve it. For example, what containers and volumes will be created or removed, and on which machines.
This gives you full visibility and control over every change with **immediate feedback** when something goes wrong.
### Minimal resource footprint
The Uncloud daemon consists of a couple Go and Rust binaries running alongside the Docker daemon on each machine. It
needs no more than **150 MB of RAM** and a few percent of a CPU core in small setups. This minimal overhead maximises
the system resources available for your apps.
You can run Uncloud on machines with as little as 512 MB of RAM, assuming you also need some RAM for the OS and Docker,
as well as the apps you want to run.
### Troubleshooting-friendly
When something goes wrong, you can dive straight into standard Docker containers without layers of abstraction in your
way. You can also SSH into any machine and use the regular Linux troubleshooting tools. For example, `ping` service
containers by their service names, `curl` service endpoints, or analyse traffic between containers using `wireshark`.
## Getting started
Install Uncloud CLI and deploy your first app in minutes:
* [Install Uncloud CLI](./2-getting-started/1-install-cli.md)
* [Deploy demo app](./2-getting-started/2-deploy-demo-app.md)
## Getting help
* **Discord community**: Join our [Discord server](https://discord.gg/eR35KQJhPu) for real-time discussions, support,
and updates.
* **GitHub issues**: Report bugs or request features on our [GitHub repository](https://github.com/psviderski/uncloud).
* **Documentation**: Browse the full documentation (this website you're on) for detailed guides and references. Use
search to find what you need quickly.
* **Newsletter**: Subscribe to the [newsletter](https://uncloud.run/#subscribe) for development updates and early
insights.
@@ -0,0 +1,120 @@
# Install CLI
Install the Uncloud command-line utility to manage your machines and deploy apps using `uc` commands. You will run `uc`
locally so choose the appropriate installation method for your operating system.
:::info NOTE
Windows is not natively supported yet, but you can install and run `uc` in a
[WSL](https://learn.microsoft.com/en-us/windows/wsl/) terminal by following the instructions for Linux.
:::
## Homebrew (macOS, Linux)
If you have [Homebrew](https://brew.sh/) package manager installed, this is the recommended installation method on macOS
and Linux:
```shell
brew install psviderski/tap/uncloud
```
To upgrade to the latest version:
```shell
brew upgrade uncloud
```
## Install script (macOS, Linux)
For a quick automated installation, use the install script:
```shell
curl -fsS https://get.uncloud.run/install.sh | sh
```
The script will:
- Detect your operating system and architecture
- Download the appropriate latest binary from [GitHub releases](https://github.com/psviderski/uncloud/releases)
- Install it to `/usr/local/bin/uncloud` using `sudo` (you may need to enter your user password)
- Create a shortcut `uc` in `/usr/local/bin` for convenience
Don't like `curl | sh`? You can download and review the [install script](https://get.uncloud.run/install.sh) first and
then run it:
```shell
curl -fsSO https://get.uncloud.run/install.sh
cat install.sh
sh install.sh
```
## GitHub download (macOS, Linux)
You can manually download and use a pre-built binary from the
[latest release](https://github.com/psviderski/uncloud/releases/latest) on GitHub.
<Tabs>
<TabItem value="macOS (Apple Silicon)">
```shell
curl -L https://github.com/psviderski/uncloud/releases/latest/download/uncloud_macos_arm64.tar.gz | tar xz
mv uncloud uc
```
</TabItem>
<TabItem value="macOS (Intel)">
```shell
curl -L https://github.com/psviderski/uncloud/releases/latest/download/uncloud_macos_amd64.tar.gz | tar xz
mv uncloud uc
```
</TabItem>
<TabItem value="Linux (AMD 64-bit)">
```shell
curl -L https://github.com/psviderski/uncloud/releases/latest/download/uncloud_linux_amd64.tar.gz | tar xz
mv uncloud uc
```
</TabItem>
<TabItem value="Linux (ARM 64-bit)">
```shell
curl -L https://github.com/psviderski/uncloud/releases/latest/download/uncloud_linux_arm64.tar.gz | tar xz
mv uncloud uc
```
</TabItem>
</Tabs>
You can use the `./uc` binary directly from the current directory, or move it to a directory in your system's `PATH`
to run it as `uc` from any location.
For example, move it to `/usr/local/bin` which is a common location for user-installed binaries:
```shell
sudo mv ./uc /usr/local/bin
```
## Verify installation
After installation, verify that `uc` command is working:
```shell
uc --version
```
## Linux (via package managers)
### Debian
Via unofficial repository packages created and maintained at [uncloud-debian](https://github.com/dariogriffo/uncloud-debian/) by @dariogriffo
You can install uncloud the debian way by running:
```sh
curl -sS https://debian.griffo.io/EA0F721D231FDD3A0A17B9AC7808B4DD62C41256.asc | sudo gpg --dearmor --yes -o /etc/apt/trusted.gpg.d/debian.griffo.io.gpg
echo "deb https://debian.griffo.io/apt $(lsb_release -sc 2>/dev/null) main" | sudo tee /etc/apt/sources.list.d/debian.griffo.io.list
apt install -y uncloud
```
or in the releases page of the repository [here](https://github.com/dariogriffo/uncloud-debian/releases)
## Next steps
Now that you have `uc` installed, you're ready to:
- [Deploy demo app](./2-deploy-demo-app.md)
@@ -0,0 +1,315 @@
# Deploy demo app
In this guide, we'll deploy [Excalidraw](https://excalidraw.com) — a popular sketching and diagramming tool — to your
Linux server. You'll learn the **basics of Uncloud** and see how simple it is to **run web apps** on your own
infrastructure with secure internet access.
## Prerequisites
Before you begin, you'll need:
- **Uncloud CLI** [installed](1-install-cli.md) on your local machine
- A **Ubuntu or Debian server** with **public IP address** and **SSH access** (as `root` or a user with `sudo`
privileges) using a **private key**.
:::tip Need a server?
A small Virtual Private Server (VPS) or dedicated server from providers like [Hetzner](https://www.hetzner.com) or
[DigitalOcean](https://www.digitalocean.com) is a great choice for learning Uncloud and running lightweight services. We
recommend using a freshly installed server as existing services on ports 80 and 443 can cause conflicts.
**Minimum requirements:** 1 vCPU, 512 MB RAM, Ubuntu 22.04 or Debian 11, AMD64 (recommended) or ARM64 architecture.
Other Linux distributions may work, but haven't been tested yet.
:::
## Set up your server
First, let's turn your server into an Uncloud **machine**. This simply means setting it up so you can deploy and manage
services on it using `uc`.
```shell
uc machine init root@<your-server-ip>
```
If the SSH key to access your server isn't added to your [SSH agent](https://www.ssh.com/academy/ssh/agent), specify it
with the `-i` flag:
```shell
uc machine init root@<your-server-ip> -i ~/.ssh/id_xxx
```
This command will:
- Install the latest stable Docker version on your server if it's not already installed
- Install the Uncloud daemon on your server
- Create a Docker network for Uncloud-managed containers
- Deploy [Caddy](https://caddyserver.com/) as your reverse proxy listening on host ports 80 and 443
- Reserve a free `xxxxxx.cluster.uncloud.run` subdomain via the Uncloud managed DNS service and point it to your
server's IP
All in about a minute!
<details>
<summary>💡 Expand to see example output</summary>
```
$ uc machine init root@157.180.72.195
Downloading Uncloud install script: https://raw.githubusercontent.com/psviderski/uncloud/refs/heads/main/scripts/install.sh
⏳ Running Uncloud install script...
⏳ Installing Docker...
# Executing docker install script, commit: 53a22f61c0628e58e1d6680b49e82993d304b449
+ sh -c apt-get -qq update >/dev/null
+ sh -c DEBIAN_FRONTEND=noninteractive apt-get -y -qq install ca-certificates curl >/dev/null
+ sh -c install -m 0755 -d /etc/apt/keyrings
+ sh -c curl -fsSL "https://download.docker.com/linux/ubuntu/gpg" -o /etc/apt/keyrings/docker.asc
+ sh -c chmod a+r /etc/apt/keyrings/docker.asc
+ sh -c echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu noble stable" > /etc/apt/sources.list.d/docker.list
+ sh -c apt-get -qq update >/dev/null
+ sh -c DEBIAN_FRONTEND=noninteractive apt-get -y -qq install docker-ce docker-ce-cli containerd.io docker-compose-plugin docker-ce-rootless-extras docker-buildx-plugin >/dev/null
Running kernel seems to be up-to-date.
No services need to be restarted.
No containers need to be restarted.
No user sessions are running outdated binaries.
No VM guests are running outdated hypervisor (qemu) binaries on this host.
+ sh -c docker version
Client: Docker Engine - Community
Version: 28.2.1
API version: 1.50
Go version: go1.24.3
Git commit: 879ac3f
Built: Wed May 28 19:25:01 2025
OS/Arch: linux/amd64
Context: default
Server: Docker Engine - Community
Engine:
Version: 28.2.1
API version: 1.50 (minimum version 1.24)
Go version: go1.24.3
Git commit: 0e2cc22
Built: Wed May 28 19:25:01 2025
OS/Arch: linux/amd64
Experimental: false
containerd:
Version: 1.7.27
GitCommit: 05044ec0a9a75232cad458027ca83437aae3f4da
runc:
Version: 1.2.5
GitCommit: v1.2.5-0-g59923ef
docker-init:
Version: 0.19.0
GitCommit: de40ad0
================================================================================
To run Docker as a non-privileged user, consider setting up the
Docker daemon in rootless mode for your user:
dockerd-rootless-setuptool.sh install
Visit https://docs.docker.com/go/rootless/ to learn about rootless mode.
To run the Docker daemon as a fully privileged service, but granting non-root
users access, refer to https://docs.docker.com/go/daemon-access/
WARNING: Access to the remote API on a privileged Docker daemon is equivalent
to root access on the host. Refer to the 'Docker daemon attack surface'
documentation for details: https://docs.docker.com/go/attack-surface/
================================================================================
✓ Docker installed successfully.
✓ Linux user and group 'uncloud' created.
⏳ Installing Uncloud binaries...
⏳ Downloading uncloudd binary: https://github.com/psviderski/uncloud/releases/latest/download/uncloudd_linux_amd64.tar.gz
✓ uncloudd binary installed: /usr/local/bin/uncloudd
⏳ Downloading uninstall script: https://raw.githubusercontent.com/psviderski/uncloud/refs/heads/main/scripts/uninstall.sh
✓ uncloud-uninstall script installed: /usr/local/bin/uncloud-uninstall
✓ Systemd unit file created: /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)...
✓ Uncloud machine daemon started.
✓ Uncloud installed on the machine successfully! 🎉
Cluster initialised with machine 'machine-dc3c' and saved as context 'default' in your local config (/Users/spy/.config/uncloud/config.yaml)
Current cluster context is now 'default'.
Waiting for the machine to be ready...
Reserved cluster domain: 7za6s7.cluster.uncloud.run
[+] Deploying service caddy 7/2
✔ Container caddy-d7uk on machine-dc3c Started 6.1s
✔ Image caddy:2.10.0 on machine-dc3c Pulled 3.7s
Updating cluster domain records in Uncloud DNS to point to machines running caddy service...
[+] Verifying internet access to caddy service 1/1
✔ Machine machine-dc3c (157.180.72.195) Reachable 0.7s
DNS records updated to use only the internet-reachable machines running caddy service:
*.7za6s7.cluster.uncloud.run A → 157.180.72.195
```
</details>
## Deploy Excalidraw
Now that your machine is set up, let's deploy `excalidraw` service from the
[official Docker image](https://hub.docker.com/r/excalidraw/excalidraw). The service will publish the container port 80
as HTTPS endpoint on the previously reserved domain via Caddy.
```shell
uc run --name excalidraw --publish 80/https excalidraw/excalidraw
```
You'll see the progress of the deployment and the public URL where you can access the service:
```
[+] Running service excalidraw (replicated mode) 2/2
✔ Container excalidraw-azpc on machine-dc3c Started 8.9s
✔ Image excalidraw/excalidraw on machine-dc3c Pulled 4.7s
excalidraw endpoints:
• https://excalidraw.7za6s7.cluster.uncloud.run → :80
```
## Verify your deployment
After the service is deployed, use the `uc inspect` command to check its status and details:
```shell
uc inspect excalidraw
```
```
ID: 4d2de1600b6ada221a03896cd388836c
Name: excalidraw
Mode: replicated
CONTAINER ID IMAGE CREATED STATUS MACHINE
fde7ac7f11ad excalidraw/excalidraw About a minute ago Up About a minute (healthy) machine-dc3c
```
In this example, the service has one container running on the machine `machine-dc3c` (our server). The container is up
and healthy.
You can also list all deployed services and their public endpoints using the `uc ls` command:
```shell
uc ls
```
```
NAME MODE REPLICAS ENDPOINTS
caddy global 1
excalidraw replicated 1 https://excalidraw.7za6s7.cluster.uncloud.run → :80
```
You can see `caddy` service listed here. That's your reverse proxy, running as a regular Uncloud service.
## It's live! Start drawing! ✨
Open your browser and navigate to the URL shown in the endpoints. It may take a moment for Caddy to obtain a TLS
certificate from Let's Encrypt. If it doesn't load immediately, wait a few seconds and try again.
![Excalidraw running on Uncloud](./img/excalidraw-browser.png)
You now have:
- Your **own Excalidraw instance** running on your server
- A **public URL** with **automatic HTTPS** you can share with your team and friends
- **Full control over your data** — no analytics or tracking
## Convert to Docker Compose format
Uncloud supports the [Compose file format](https://docs.docker.com/reference/compose-file/) for defining services. This
allows you to version control your deployments, share configurations with your team, and deploy complex multi-service
applications with a single command.
Let's create a `compose.yaml` file in your current directory for the `excalidraw` service we just deployed.
```yaml title="compose.yaml"
services:
excalidraw:
image: excalidraw/excalidraw
x-ports:
- 80/https
```
:::info note
The `x-ports` key is an Uncloud-specific extension to the Compose file format. It allows you to specify ports that
should be published as HTTP(S) endpoints. Uncloud automatically configures the reverse proxy (Caddy) to route traffic to
these ports.
:::
Now deploy it:
```shell
uc deploy
```
```
Services are up to date.
```
Since `excalidraw` service is already running with the same configuration, Uncloud recognises there's nothing to change.
We've successfully converted our deployment created with `uc run` to a Compose file.
## Use your own domain
Want to use your own domain, for example, `excalidraw.example.com` instead of `excalidraw.7za6s7.cluster.uncloud.run`?
Add a CNAME record `excalidraw.example.com` in your DNS provider (Cloudflare, Namecheap, etc.) pointing to
`excalidraw.7za6s7.cluster.uncloud.run`. Alternatively, you can add an A record pointing to your server's IP.
Then update the published port `80/https` in `compose.yaml` to use your domain:
```yaml title="compose.yaml"
...
x-ports:
- excalidraw.example.com:80/https
```
Finally, deploy the changes:
```shell
uc deploy
```
```
Deployment plan:
- Deploy service [name=excalidraw]
- machine-dc3c: Run container [image=excalidraw/excalidraw]
- machine-dc3c: Remove container [name=excalidraw-azpc]
Do you want to continue?
Choose [y/N]: y
Chose: Yes!
[+] Deploying services 2/2
✔ Container excalidraw-0z12 on machine-dc3c Started 3.5s
✔ Container excalidraw-azpc on machine-dc3c Removed 3.4s
```
Notice how Uncloud performed a **zero-downtime deployment** — it started the new container with the updated
configuration before removing the old one. Your service stayed available throughout the update.
Give it a moment for Caddy to obtain a TLS certificate, then visit https://excalidraw.example.com.
## Clean up
TBD
## Next steps
TBD
@@ -0,0 +1,4 @@
label: Getting started
collapsed: false # keep the category open by default
link:
type: generated-index
Binary file not shown.

After

Width:  |  Height:  |  Size: 389 KiB

+153
View File
@@ -0,0 +1,153 @@
// @ts-check
// `@type` JSDoc annotations allow editor autocompletion and type checking
// (when paired with `@ts-check`).
// There are various equivalent ways to declare your Docusaurus config.
// See: https://docusaurus.io/docs/api/docusaurus-config
import {themes as prismThemes} from 'prism-react-renderer';
// This runs in Node.js - Don't use client-side code here (browser APIs, JSX...)
/** @type {import('@docusaurus/types').Config} */
const config = {
title: 'Uncloud Docs',
tagline: 'Dinosaurs are cool',
favicon: 'img/favicon.png',
// Set the production url of your site here
url: 'https://docs.uncloud.run',
// Set the /<baseUrl>/ pathname under which your site is served
// For GitHub pages deployment, it is often '/<projectName>/'
baseUrl: '/',
// GitHub pages deployment config.
// If you aren't using GitHub pages, you don't need these.
organizationName: '', // Usually your GitHub org/user name.
projectName: '', // Usually your repo name.
onBrokenLinks: 'throw',
onBrokenMarkdownLinks: 'warn',
// Even if you don't use internationalization, you can use this field to set
// useful metadata like html lang. For example, if your site is Chinese, you
// may want to replace "en" with "zh-Hans".
i18n: {
defaultLocale: 'en',
locales: ['en'],
},
themes: [
[
'@easyops-cn/docusaurus-search-local',
/** @type {import("@easyops-cn/docusaurus-search-local").PluginOptions} */
({
docsRouteBasePath: '/',
hashed: true,
highlightSearchTermsOnTargetPage: true,
}),
],
],
presets: [
[
'classic',
/** @type {import('@docusaurus/preset-classic').Options} */
({
docs: {
// Remove this to remove the "edit this page" links.
editUrl: 'https://github.com/psviderski/uncloud/edit/main/docs/',
// Serve the docs at the site's root.
routeBasePath: '/',
showLastUpdateTime: true,
sidebarPath: './sidebars.js',
},
// blog: {
// showReadingTime: true,
// feedOptions: {
// type: ['rss', 'atom'],
// xslt: true,
// },
// // Please change this to your repo.
// // Remove this to remove the "edit this page" links.
// editUrl:
// 'https://github.com/facebook/docusaurus/tree/main/packages/create-docusaurus/templates/shared/',
// // Useful options to enforce blogging best practices
// onInlineTags: 'warn',
// onInlineAuthors: 'warn',
// onUntruncatedBlogPosts: 'warn',
// },
theme: {
customCss: './src/css/custom.css',
},
}),
],
],
themeConfig:
/** @type {import('@docusaurus/preset-classic').ThemeConfig} */
({
colorMode: {
// The color mode when user first visits the site.
defaultMode: "light",
// Whether to use the prefers-color-scheme media-query, using user system preferences,
// instead of the hardcoded defaultMode.
respectPrefersColorScheme: true,
},
// The meta image URL for the site (og:image and twitter:image meta tags).
// Relative to your site's "static" directory. Cannot be SVGs. Can be external URLs too.
image: 'img/social-card.png',
navbar: {
title: 'Uncloud Docs',
logo: {
alt: 'Uncloud Logo',
src: 'img/logo.svg',
},
items: [
{
type: 'search',
position: 'right',
},
{
href: 'https://github.com/psviderski/uncloud',
label: 'GitHub',
position: 'right',
},
],
},
footer: {
style: 'light',
links: [
{
title: 'Uncloud',
items: [
{
html: 'Made with ❤️ by <a href="https://github.com/psviderski">@psviderski</a> in Australia'
},
],
},
{
title: 'Community',
items: [
{
label: 'Discord',
href: 'https://discord.gg/eR35KQJhPu',
},
{
label: 'X',
href: 'https://x.com/psviderski',
},
{
label: 'GitHub',
href: 'https://github.com/psviderski/uncloud',
},
],
}
]
},
prism: {
theme: prismThemes.palenight,
//darkTheme: prismThemes.dracula,
},
}),
};
export default config;
+18434
View File
File diff suppressed because it is too large Load Diff
+45
View File
@@ -0,0 +1,45 @@
{
"name": "docs",
"version": "0.0.0",
"private": true,
"scripts": {
"docusaurus": "docusaurus",
"start": "docusaurus start",
"build": "docusaurus build",
"swizzle": "docusaurus swizzle",
"deploy": "docusaurus deploy",
"clear": "docusaurus clear",
"serve": "docusaurus serve",
"write-translations": "docusaurus write-translations",
"write-heading-ids": "docusaurus write-heading-ids"
},
"dependencies": {
"@docusaurus/core": "3.7.0",
"@docusaurus/preset-classic": "3.7.0",
"@easyops-cn/docusaurus-search-local": "^0.49.2",
"@mdx-js/react": "^3.0.0",
"clsx": "^2.0.0",
"prism-react-renderer": "^2.3.0",
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"@docusaurus/module-type-aliases": "3.7.0",
"@docusaurus/types": "3.7.0"
},
"browserslist": {
"production": [
">0.5%",
"not dead",
"not op_mini all"
],
"development": [
"last 3 chrome version",
"last 3 firefox version",
"last 5 safari version"
]
},
"engines": {
"node": ">=18.0"
}
}
+35
View File
@@ -0,0 +1,35 @@
// @ts-check
// This runs in Node.js - Don't use client-side code here (browser APIs, JSX...)
/**
* Creating a sidebar enables you to:
- create an ordered group of docs
- render a sidebar for each doc of that group
- provide next/previous navigation
The sidebars can be generated from the filesystem, or explicitly defined here.
Create as many sidebars as you want.
@type {import('@docusaurus/plugin-content-docs').SidebarsConfig}
*/
const sidebars = {
// By default, Docusaurus generates a sidebar from the docs folder structure
sidebar: [{type: 'autogenerated', dirName: '.'}],
// But you can create a sidebar manually
/*
tutorialSidebar: [
'intro',
'hello',
{
type: 'category',
label: 'Tutorial',
items: ['tutorial-basics/create-a-document'],
},
],
*/
};
export default sidebars;
+40
View File
@@ -0,0 +1,40 @@
/**
* Any CSS included here will be global. The classic template
* bundles Infima by default. Infima is a CSS framework designed to
* work well for content-centric websites.
*/
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;700&display=swap');
/* You can override the default Infima variables here. */
:root {
--ifm-font-family-base: 'Inter', sans-serif;
--ifm-heading-font-family: 'Inter', sans-serif;
--ifm-color-primary: #006be6;
--ifm-color-primary-dark: #0060cf;
--ifm-color-primary-darker: #005bc4;
--ifm-color-primary-darkest: #004ba1;
--ifm-color-primary-light: #0076fd;
--ifm-color-primary-lighter: #0a7cff;
--ifm-color-primary-lightest: #2c8eff;
--ifm-code-font-size: 95%;
--docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.1);
}
/* For readability concerns, you should choose a lighter palette in dark mode. */
[data-theme='dark'] {
--ifm-color-primary: #3391ee;
--ifm-color-primary-dark: #1883ec;
--ifm-color-primary-darker: #137be3;
--ifm-color-primary-darkest: #1066bb;
--ifm-color-primary-light: #4e9ff0;
--ifm-color-primary-lighter: #5ba7f1;
--ifm-color-primary-lightest: #83bcf5;
--docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3);
}
.menu {
font-size: 0.875rem;
font-weight: var(--ifm-font-weight-normal);
}
+13
View File
@@ -0,0 +1,13 @@
import React from 'react';
// Import the original mapper
import MDXComponents from '@theme-original/MDXComponents';
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
export default {
// Re-use the default mapping
...MDXComponents,
// Global import the <Tabs> and <TabItem> components
Tabs,
TabItem,
};
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

+7
View File
@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 80 80">
<g>
<rect x="0" y="0" width="80" height="80" rx="10" fill="#18181B"/>
<path d="M20 32 L20 48 L36 48 L36 32 M60 32 L45 32 L45 48 L60 48" stroke="white" stroke-width="6" fill="none"
stroke-linecap="square" stroke-linejoin="miter"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 341 B

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

+1 -1
View File
@@ -16,7 +16,7 @@ In addition, any ports for any workloads you want to expose will need to be open
### Configuration
Uncloud stores its configuration in `~/.config/uncloud/config.toml`. If you wish to reinitialise a cluster, simply remove it from this config.
Uncloud stores its configuration in `~/.config/uncloud/config.yaml`. If you wish to reinitialise a cluster, simply remove it from this config.
### Initialisation
+14 -13
View File
@@ -2,8 +2,6 @@ module github.com/psviderski/uncloud
go 1.23.0
toolchain go1.23.2
require (
github.com/BurntSushi/toml v1.4.0
github.com/Masterminds/semver v1.5.0
@@ -11,6 +9,7 @@ require (
github.com/caddyserver/caddy/v2 v2.8.4
github.com/cenkalti/backoff/v4 v4.3.0
github.com/charmbracelet/huh v0.6.0
github.com/charmbracelet/lipgloss v0.13.0
github.com/compose-spec/compose-go/v2 v2.4.5
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf
github.com/deckarep/golang-set/v2 v2.8.0
@@ -21,6 +20,8 @@ require (
github.com/docker/docker v27.4.0-rc.2+incompatible
github.com/docker/go-connections v0.5.0
github.com/docker/go-units v0.5.0
github.com/goccy/go-yaml v1.17.1
github.com/google/go-cmp v0.7.0
github.com/google/go-containerregistry v0.20.2
github.com/hashicorp/memberlist v0.5.1
github.com/hashicorp/serf v0.10.1
@@ -33,6 +34,8 @@ require (
github.com/ipfs/go-log/v2 v2.5.1
github.com/jmoiron/sqlx v1.4.0
github.com/lmittmann/tint v1.0.5
github.com/miekg/dns v1.1.65
github.com/moby/term v0.5.0
github.com/opencontainers/go-digest v1.0.0
github.com/opencontainers/image-spec v1.1.0
github.com/siderolabs/discovery-api v0.1.4
@@ -43,9 +46,9 @@ require (
github.com/vishvananda/netlink v1.3.0
go.uber.org/zap v1.27.0
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba
golang.org/x/crypto v0.32.0
golang.org/x/net v0.34.0
golang.org/x/sync v0.10.0
golang.org/x/crypto v0.33.0
golang.org/x/net v0.35.0
golang.org/x/sync v0.11.0
golang.org/x/sys v0.31.0
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6
@@ -82,7 +85,6 @@ require (
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/charmbracelet/bubbles v0.20.0 // indirect
github.com/charmbracelet/bubbletea v1.1.1 // indirect
github.com/charmbracelet/lipgloss v0.13.0 // indirect
github.com/charmbracelet/x/ansi v0.3.2 // indirect
github.com/charmbracelet/x/exp/strings v0.0.0-20240919170804-a4978c8e603a // indirect
github.com/charmbracelet/x/term v0.2.0 // indirect
@@ -135,7 +137,6 @@ require (
github.com/google/btree v1.1.2 // indirect
github.com/google/cel-go v0.20.1 // indirect
github.com/google/flatbuffers v24.3.25+incompatible // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/google/uuid v1.6.0 // indirect
@@ -196,7 +197,6 @@ require (
github.com/mdlayher/socket v0.5.1 // indirect
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
github.com/mholt/acmez/v2 v2.0.3 // indirect
github.com/miekg/dns v1.1.62 // indirect
github.com/miekg/pkcs11 v1.1.1 // indirect
github.com/minio/sha256-simd v1.0.1 // indirect
github.com/mitchellh/cli v1.1.5 // indirect
@@ -212,7 +212,8 @@ require (
github.com/moby/patternmatcher v0.6.0 // indirect
github.com/moby/sys/sequential v0.6.0 // indirect
github.com/moby/sys/signal v0.7.1 // indirect
github.com/moby/term v0.5.0 // indirect
github.com/moby/sys/user v0.3.0 // indirect
github.com/moby/sys/userns v0.1.0 // indirect
github.com/morikuni/aec v1.0.0 // indirect
github.com/mr-tron/base58 v1.2.0 // indirect
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
@@ -302,11 +303,11 @@ require (
go.uber.org/zap/exp v0.2.0 // indirect
golang.org/x/crypto/x509roots/fallback v0.0.0-20240507223354-67b13616a595 // indirect
golang.org/x/exp v0.0.0-20241215155358-4a5509556b9e // indirect
golang.org/x/mod v0.22.0 // indirect
golang.org/x/term v0.28.0 // indirect
golang.org/x/text v0.21.0 // indirect
golang.org/x/mod v0.23.0 // indirect
golang.org/x/term v0.29.0 // indirect
golang.org/x/text v0.22.0 // indirect
golang.org/x/time v0.8.0 // indirect
golang.org/x/tools v0.29.0 // indirect
golang.org/x/tools v0.30.0 // indirect
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
google.golang.org/genproto v0.0.0-20240401170217-c3f982113cda // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 // indirect
+24 -18
View File
@@ -105,6 +105,8 @@ github.com/aws/smithy-go v1.20.2 h1:tbp628ireGtzcHDDmLT/6ADHidqnwgF57XOXZe6tp4Q=
github.com/aws/smithy-go v1.20.2/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8=
github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA=
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o=
github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
@@ -153,6 +155,8 @@ github.com/charmbracelet/lipgloss v0.13.0 h1:4X3PPeoWEDCMvzDvGmTajSyYPcZM4+y8sCA
github.com/charmbracelet/lipgloss v0.13.0/go.mod h1:nw4zy0SBX/F/eAO1cWdcvy6qnkDUxr8Lw7dvFrAIbbY=
github.com/charmbracelet/x/ansi v0.3.2 h1:wsEwgAN+C9U06l9dCVMX0/L3x7ptvY1qmjMwyfE6USY=
github.com/charmbracelet/x/ansi v0.3.2/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw=
github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b h1:MnAMdlwSltxJyULnrYbkZpp4k58Co7Tah3ciKhSNo0Q=
github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
github.com/charmbracelet/x/exp/strings v0.0.0-20240919170804-a4978c8e603a h1:JMdM89Udp/cOl5tC3MuUJXTPE/nAdU1oyt9jRU44qq8=
github.com/charmbracelet/x/exp/strings v0.0.0-20240919170804-a4978c8e603a/go.mod h1:pBhA0ybfXv6hDjQUZ7hk1lVxBiUbupdw5R31yPUViVQ=
github.com/charmbracelet/x/term v0.2.0 h1:cNB9Ot9q8I711MyZ7myUR5HFWL/lc3OpU8jZ4hwm0x0=
@@ -343,6 +347,8 @@ github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZ
github.com/go-viper/mapstructure/v2 v2.0.0 h1:dhn8MZ1gZ0mzeodTG3jt5Vj/o87xZKuNAprG2mQfMfc=
github.com/go-viper/mapstructure/v2 v2.0.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0=
github.com/goccy/go-yaml v1.17.1 h1:LI34wktB2xEE3ONG/2Ar54+/HJVBriAGJ55PHls4YuY=
github.com/goccy/go-yaml v1.17.1/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E=
@@ -397,8 +403,8 @@ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/go-containerregistry v0.20.2 h1:B1wPJ1SN/S7pB+ZAimcciVD+r+yV/l/DSArMxlbwseo=
github.com/google/go-containerregistry v0.20.2/go.mod h1:z38EKdKh4h7IP2gSfUUqEvalZBqs6AoLeWfUy34nQC8=
github.com/google/go-tpm v0.9.0 h1:sQF6YqWMi+SCXpsmS3fd21oPy/vSddwZry4JnmltHVk=
@@ -719,8 +725,8 @@ github.com/mholt/acmez/v2 v2.0.3 h1:CgDBlEwg3QBp6s45tPQmFIBrkRIkBT4rW4orMM6p4sw=
github.com/mholt/acmez/v2 v2.0.3/go.mod h1:pQ1ysaDeGrIMvJ9dfJMk5kJNkn7L2sb3UhyrX6Q91cw=
github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI=
github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ=
github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ=
github.com/miekg/dns v1.1.65 h1:0+tIPHzUW0GCge7IiK3guGP57VAw7hoPDfApjkMD1Fc=
github.com/miekg/dns v1.1.65/go.mod h1:Dzw9769uoKVaLuODMDZz9M6ynFU6Em65csPuoi8G0ck=
github.com/miekg/pkcs11 v1.0.2/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs=
github.com/miekg/pkcs11 v1.1.1 h1:Ugu9pdy6vAYku5DEpVWVFPYnzV+bxB+iRdbuFSu7TvU=
github.com/miekg/pkcs11 v1.1.1/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs=
@@ -1194,8 +1200,8 @@ golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5y
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
golang.org/x/crypto/x509roots/fallback v0.0.0-20240507223354-67b13616a595 h1:TgSqweA595vD0Zt86JzLv3Pb/syKg8gd5KMGGbJPYFw=
golang.org/x/crypto/x509roots/fallback v0.0.0-20240507223354-67b13616a595/go.mod h1:kNa9WdvYnzFwC79zRpLRMJbdEFlhyM5RPFBBZp/wWH8=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@@ -1212,8 +1218,8 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
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.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4=
golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM=
golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
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-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -1235,8 +1241,8 @@ 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.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE=
golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
@@ -1250,8 +1256,8 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ
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.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
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-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -1308,8 +1314,8 @@ golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg=
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
@@ -1320,8 +1326,8 @@ golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg=
golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -1343,8 +1349,8 @@ golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4f
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.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.29.0 h1:Xx0h3TtM9rzQpQuR4dKLrdglAmCEN5Oi+P74JdhdzXE=
golang.org/x/tools v0.29.0/go.mod h1:KMQVMRsVxU6nHCFXrBPhDB8XncLNLM0lIy/F14RP588=
golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY=
golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY=
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+176
View File
@@ -0,0 +1,176 @@
package cli
import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"os"
composetypes "github.com/compose-spec/compose-go/v2/types"
"github.com/distribution/reference"
"github.com/docker/cli/cli/config"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/image"
dockerclient "github.com/docker/docker/client"
"github.com/docker/docker/pkg/archive"
"github.com/docker/docker/pkg/jsonmessage"
"github.com/docker/docker/registry"
"github.com/moby/term"
)
type BuildOptions struct {
Files []string
Profiles []string
Services []string
Push bool
NoCache bool
}
// GetServicesThatNeedBuild returns a map of services that require building
func GetServicesThatNeedBuild(project *composetypes.Project) map[string]composetypes.ServiceConfig {
servicesToBuild := make(map[string]composetypes.ServiceConfig, len(project.Services))
for serviceName, service := range project.Services {
if service.Build == nil {
continue
}
servicesToBuild[serviceName] = service
}
return servicesToBuild
}
// BuildServices builds the services defined in the provided map.
func BuildServices(ctx context.Context, servicesToBuild map[string]composetypes.ServiceConfig, opts BuildOptions) error {
fmt.Println("Building services...")
// Init docker client (can be local or remote, depending on DOCKER_HOST environment variable)
dockerCli, err := dockerclient.NewClientWithOpts(dockerclient.FromEnv, dockerclient.WithAPIVersionNegotiation())
if err != nil {
return err
}
defer dockerCli.Close()
serviceImages := make(map[string]string, len(servicesToBuild))
// Build the services using the local docker client and compose libraries
for _, service := range servicesToBuild {
fmt.Printf("Building service: %s\n", service.Name)
imageName, err := buildSingleService(ctx, dockerCli, service, opts)
if err != nil {
return fmt.Errorf("build service %s: %w", service.Name, err)
}
serviceImages[service.Name] = imageName
}
fmt.Printf("Service images are built.\n")
if opts.Push {
err = pushServiceImages(ctx, dockerCli, serviceImages)
}
return err
}
// buildSingleService builds a single service using the Docker client and Compose libraries.
func buildSingleService(ctx context.Context, dockerCli *dockerclient.Client, service composetypes.ServiceConfig, opts BuildOptions) (string, error) {
if service.Build == nil {
return "", fmt.Errorf("service %s has no build configuration", service.Name)
}
if service.Image == "" {
return "", fmt.Errorf("service %s has no image specified; building services without image is not supported yet", service.Name)
}
buildContextPath := service.Build.Context
imageName := service.Image
// Create a tar archive of the build context
buildContext, err := archive.TarWithOptions(buildContextPath, &archive.TarOptions{})
if err != nil {
return "", fmt.Errorf("failed to create build context for service %s: %w", service.Name, err)
}
buildOptions := types.ImageBuildOptions{
// TODO: Support Dockerfiles outside the build context
// See https://github.com/docker/compose/blob/cf89fd1aa1328d5af77658ccc5a1e1b29981ae80/pkg/compose/build_classic.go#L92
Dockerfile: service.Build.Dockerfile,
Tags: []string{imageName},
Remove: true, // Remove intermediate containers
NoCache: opts.NoCache,
}
buildResponse, err := dockerCli.ImageBuild(ctx, buildContext, buildOptions)
if err != nil {
return "", fmt.Errorf("failed to build image for service %s: %w", service.Name, err)
}
defer buildResponse.Body.Close()
// Display the build response
fd, isTerminal := term.GetFdInfo(os.Stdout)
if err := jsonmessage.DisplayJSONMessagesStream(buildResponse.Body, os.Stdout, fd, isTerminal, nil); err != nil {
return "", fmt.Errorf("failed to display build response for service %s: %w", service.Name, err)
}
return imageName, nil
}
// pushSingleServiceImage pushes a single service image.
func pushSingleServiceImage(ctx context.Context, dockerCli *dockerclient.Client, serviceName string, imageName string) error {
ref, err := reference.ParseNormalizedNamed(imageName)
if err != nil {
return err
}
repoInfo, err := registry.ParseRepositoryInfo(ref)
if err != nil {
return err
}
registryKey := repoInfo.Index.Name
if repoInfo.Index.Official {
registryKey = registry.IndexServer
}
// Load the Docker config file with auth details, if available
configFile := config.LoadDefaultConfigFile(os.Stderr)
authConfig, err := configFile.GetAuthConfig(registryKey)
if err != nil {
return err
}
authJSON, err := json.Marshal(authConfig)
if err != nil {
return fmt.Errorf("failed to marshal auth config for registry %s: %w", registryKey, err)
}
authStr := base64.URLEncoding.EncodeToString(authJSON)
pushOptions := image.PushOptions{
RegistryAuth: authStr,
}
pushResponse, err := dockerCli.ImagePush(ctx, imageName, pushOptions)
if err != nil {
return fmt.Errorf("failed to push image %s: %w", imageName, err)
}
defer pushResponse.Close()
fmt.Printf("Pushing image %s for service %s...\n", imageName, serviceName)
fd, isTerminal := term.GetFdInfo(os.Stdout)
if err := jsonmessage.DisplayJSONMessagesStream(pushResponse, os.Stdout, fd, isTerminal, nil); err != nil {
return fmt.Errorf("failed to display push response for image %s: %w", imageName, err)
}
fmt.Printf("Image %s pushed successfully.\n", imageName)
return nil
}
// pushServiceImages pushes all built service images to the registry.
func pushServiceImages(ctx context.Context, dockerCli *dockerclient.Client, serviceImages map[string]string) error {
fmt.Printf("Pushing images...\n")
for serviceName, imageName := range serviceImages {
if err := pushSingleServiceImage(ctx, dockerCli, serviceName, imageName); err != nil {
return fmt.Errorf("push image for service %s: %w", serviceName, err)
}
}
return nil
}
+167 -121
View File
@@ -4,6 +4,10 @@ import (
"context"
"errors"
"fmt"
"net/netip"
"os"
"github.com/charmbracelet/huh"
"github.com/docker/cli/cli/streams"
"github.com/psviderski/uncloud/internal/cli/config"
"github.com/psviderski/uncloud/internal/fs"
@@ -13,17 +17,15 @@ import (
"github.com/psviderski/uncloud/pkg/api"
"github.com/psviderski/uncloud/pkg/client"
"github.com/psviderski/uncloud/pkg/client/connector"
"net/netip"
"os"
"github.com/charmbracelet/huh"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/emptypb"
)
const defaultClusterName = "default"
const defaultContextName = "default"
type CLI struct {
config *config.Config
Config *config.Config
conn *config.MachineConnection
}
@@ -41,65 +43,72 @@ func New(configPath string, conn *config.MachineConnection) (*CLI, error) {
}
return &CLI{
config: cfg,
Config: cfg,
}, nil
}
func (cli *CLI) CreateCluster(name string) error {
if _, ok := cli.config.Clusters[name]; ok {
return fmt.Errorf("cluster %q already exists", name)
func (cli *CLI) CreateContext(name string) error {
if _, ok := cli.Config.Contexts[name]; ok {
return fmt.Errorf("context '%s' already exists", name)
}
cli.config.Clusters[name] = &config.Cluster{
cli.Config.Contexts[name] = &config.Context{
Name: name,
}
return cli.config.Save()
return cli.Config.Save()
}
func (cli *CLI) SetCurrentCluster(name string) error {
if _, ok := cli.config.Clusters[name]; !ok {
func (cli *CLI) SetCurrentContext(name string) error {
if _, ok := cli.Config.Contexts[name]; !ok {
return api.ErrNotFound
}
cli.config.CurrentCluster = name
return cli.config.Save()
cli.Config.CurrentContext = name
return cli.Config.Save()
}
// ConnectCluster connects to a cluster using the given cluster name or the current cluster if not specified.
// ConnectCluster connects to a cluster using the given context name or the current context if not specified.
// If the CLI was initialised with a machine connection, the config is ignored and the connection is used instead.
func (cli *CLI) ConnectCluster(ctx context.Context, clusterName string) (*client.Client, error) {
func (cli *CLI) ConnectCluster(ctx context.Context, contextName string) (*client.Client, error) {
if cli.conn != nil {
return connectCluster(ctx, *cli.conn)
}
if len(cli.config.Clusters) == 0 {
return nil, errors.New(
"no clusters found in the Uncloud config. " +
"Please initialise a cluster with `uncloud machine init` first",
if len(cli.Config.Contexts) == 0 {
return nil, fmt.Errorf(
"no cluster contexts found in the Uncloud config (%s). "+
"Please initialise a cluster with 'uncloud machine init' first",
cli.Config.Path(),
)
}
if clusterName == "" {
if contextName == "" {
// If the cluster is not specified, use the current cluster if set.
if cli.config.CurrentCluster == "" {
return nil, errors.New(
"the current cluster is not set in the Uncloud config. " +
"Please specify a cluster with the --cluster flag or set current_cluster in the config",
)
}
if _, ok := cli.config.Clusters[cli.config.CurrentCluster]; !ok {
if cli.Config.CurrentContext == "" {
return nil, fmt.Errorf(
"current cluster %q not found in the config. "+
"Please specify a cluster with the --cluster flag or update current_cluster in the config",
cli.config.CurrentCluster,
"the current cluster context is not set in the Uncloud config (%s). "+
"Please specify the context with the '--context' flag or set 'current_context' in the config",
cli.Config.Path(),
)
}
clusterName = cli.config.CurrentCluster
if _, ok := cli.Config.Contexts[cli.Config.CurrentContext]; !ok {
return nil, fmt.Errorf(
"current cluster context '%s' not found in the Uncloud config (%s). "+
"Please specify the context with the '--context' flag or update 'current_context' in the config",
cli.Config.CurrentContext,
cli.Config.Path(),
)
}
contextName = cli.Config.CurrentContext
}
cfg, ok := cli.config.Clusters[clusterName]
cfg, ok := cli.Config.Contexts[contextName]
if !ok {
return nil, fmt.Errorf("cluster %q not found in the config", clusterName)
return nil, fmt.Errorf("cluster context '%s' not found in the Uncloud config (%s)",
contextName, cli.Config.Path())
}
if len(cfg.Connections) == 0 {
return nil, fmt.Errorf("no connection configurations found for cluster %q in the config", clusterName)
return nil, fmt.Errorf(
"no connection configurations found for cluster context '%s' in the Uncloud config (%s)",
contextName, cli.Config.Path(),
)
}
// TODO: iterate over all connections and try to connect to the cluster using the first successful connection.
@@ -107,7 +116,7 @@ func (cli *CLI) ConnectCluster(ctx context.Context, clusterName string) (*client
c, err := connectCluster(ctx, conn)
if err != nil {
return nil, errors.New("no valid connection configuration found for the cluster")
return nil, fmt.Errorf("connect to cluster (context '%s'): %w", contextName, err)
}
return c, nil
@@ -129,46 +138,42 @@ func connectCluster(ctx context.Context, conn config.MachineConnection) (*client
KeyPath: keyPath,
}
return client.New(ctx, connector.NewSSHConnector(sshConfig))
} else if conn.TCP.IsValid() {
return client.New(ctx, connector.NewTCPConnector(conn.TCP))
} else if conn.TCP != nil && conn.TCP.IsValid() {
return client.New(ctx, connector.NewTCPConnector(*conn.TCP))
}
return nil, errors.New("connection configuration is invalid")
}
type InitClusterOptions struct {
Context string
MachineName string
Network netip.Prefix
PublicIP *netip.Addr
RemoteMachine *RemoteMachine
Version string
}
// InitCluster initialises a new cluster on a remote machine and returns a client to interact with the cluster.
// The client should be closed after use by the caller.
func (cli *CLI) InitCluster(
ctx context.Context,
remoteMachine *RemoteMachine,
clusterName,
machineName string,
netPrefix netip.Prefix,
publicIP *netip.Addr,
) (*client.Client, error) {
if remoteMachine != nil {
return cli.initRemoteMachine(ctx, *remoteMachine, clusterName, machineName, netPrefix, publicIP)
func (cli *CLI) InitCluster(ctx context.Context, opts InitClusterOptions) (*client.Client, error) {
if opts.RemoteMachine != nil {
return cli.initRemoteMachine(ctx, opts)
}
// TODO: implement local machine initialisation
return nil, fmt.Errorf("local machine initialisation is not implemented yet")
}
func (cli *CLI) initRemoteMachine(
ctx context.Context,
remoteMachine RemoteMachine,
clusterName,
machineName string,
netPrefix netip.Prefix,
publicIP *netip.Addr,
) (*client.Client, error) {
if clusterName == "" {
clusterName = defaultClusterName
func (cli *CLI) initRemoteMachine(ctx context.Context, opts InitClusterOptions) (*client.Client, error) {
contextName := opts.Context
if contextName == "" {
contextName = defaultContextName
}
if _, ok := cli.config.Clusters[clusterName]; ok {
return nil, fmt.Errorf("cluster %q already exists", clusterName)
if _, ok := cli.Config.Contexts[contextName]; ok {
return nil, fmt.Errorf("cluster context '%s' already exists", contextName)
}
machineClient, err := cli.provisionRemoteMachine(ctx, remoteMachine)
machineClient, err := cli.provisionRemoteMachine(ctx, *opts.RemoteMachine, opts.Version)
if err != nil {
return nil, err
}
@@ -190,13 +195,24 @@ func (cli *CLI) initRemoteMachine(
}
}
req := &pb.InitClusterRequest{
MachineName: machineName,
Network: pb.NewIPPrefix(netPrefix),
// Check machine meets all necessary system requirements before proceeding.
checkResp, err := machineClient.CheckPrerequisites(ctx, &emptypb.Empty{})
// TODO(lhf): remove Unimplemented check when v0.9.0 is released.
if err != nil {
if status.Convert(err).Code() != codes.Unimplemented {
return nil, fmt.Errorf("check machine prerequisites: %w", err)
}
} else if !checkResp.Satisfied {
return nil, fmt.Errorf("machine prerequisites not satisfied: %s", checkResp.Error)
}
if publicIP != nil {
if publicIP.IsValid() {
req.PublicIpConfig = &pb.InitClusterRequest_PublicIp{PublicIp: pb.NewIP(*publicIP)}
req := &pb.InitClusterRequest{
MachineName: opts.MachineName,
Network: pb.NewIPPrefix(opts.Network),
}
if opts.PublicIP != nil {
if opts.PublicIP.IsValid() {
req.PublicIpConfig = &pb.InitClusterRequest_PublicIp{PublicIp: pb.NewIP(*opts.PublicIP)}
} else {
// Invalid or in other words zero IP means to automatically detect the public IP.
req.PublicIpConfig = &pb.InitClusterRequest_PublicIpAuto{PublicIpAuto: true}
@@ -207,44 +223,58 @@ func (cli *CLI) initRemoteMachine(
if err != nil {
return nil, fmt.Errorf("init cluster: %w", err)
}
fmt.Printf("Cluster %q initialised with machine %q\n", clusterName, resp.Machine.Name)
if err = cli.CreateCluster(clusterName); err != nil {
return nil, fmt.Errorf("save cluster to config: %w", err)
fmt.Printf("Cluster initialised with machine '%s' and saved as context '%s' in your local config (%s)\n",
resp.Machine.Name, contextName, cli.Config.Path())
if err = cli.CreateContext(contextName); err != nil {
return nil, fmt.Errorf("save cluster context to config: %w", err)
}
// Set the current cluster to the just created one if it is the only cluster in the config.
if len(cli.config.Clusters) == 1 {
if err = cli.SetCurrentCluster(clusterName); err != nil {
return nil, fmt.Errorf("set current cluster: %w", err)
}
if err = cli.SetCurrentContext(contextName); err != nil {
return nil, fmt.Errorf("set current cluster context: %w", err)
}
fmt.Printf("Current cluster context is now '%s'.\n", contextName)
// Save the machine's SSH connection details in the cluster config.
// Save the machine's SSH connection details in the context config.
connCfg := config.MachineConnection{
SSH: config.NewSSHDestination(remoteMachine.User, remoteMachine.Host, remoteMachine.Port),
SSHKeyFile: remoteMachine.KeyPath,
SSH: config.NewSSHDestination(opts.RemoteMachine.User, opts.RemoteMachine.Host, opts.RemoteMachine.Port),
SSHKeyFile: opts.RemoteMachine.KeyPath,
}
cli.config.Clusters[clusterName].Connections = append(cli.config.Clusters[clusterName].Connections, connCfg)
if err = cli.config.Save(); err != nil {
cli.Config.Contexts[contextName].Connections = append(cli.Config.Contexts[contextName].Connections, connCfg)
if err = cli.Config.Save(); err != nil {
return nil, fmt.Errorf("save config: %w", err)
}
return machineClient, nil
}
// AddMachine provisions a remote machine and adds it to the cluster. It returns a client to interact with the machine
// which should be closed after use by the caller.
func (cli *CLI) AddMachine(
ctx context.Context, remoteMachine RemoteMachine, clusterName, machineName string, publicIP *netip.Addr,
) (*client.Client, error) {
c, err := cli.ConnectCluster(ctx, clusterName)
if err != nil {
return nil, fmt.Errorf("connect to cluster: %w", err)
}
defer c.Close()
type AddMachineOptions struct {
Context string
MachineName string
PublicIP *netip.Addr
RemoteMachine RemoteMachine
Version string
}
machineClient, err := cli.provisionRemoteMachine(ctx, remoteMachine)
// AddMachine provisions a remote machine and adds it to the cluster. It returns a cluster client and a machine client.
// The cluster client is connected to the existing machine in the cluster. It was used to add the new machine to the
// cluster. The machine client is connected to the new machine and can be used to interact with it.
// Both client should be closed after use by the caller.
func (cli *CLI) AddMachine(ctx context.Context, opts AddMachineOptions) (*client.Client, *client.Client, error) {
contextName := opts.Context
if contextName == "" {
contextName = cli.Config.CurrentContext
}
c, err := cli.ConnectCluster(ctx, contextName)
if err != nil {
return nil, err
return nil, nil, fmt.Errorf("connect to cluster (context '%s'): %w", contextName, err)
}
defer func() {
if err != nil {
c.Close()
}
}()
machineClient, err := cli.provisionRemoteMachine(ctx, opts.RemoteMachine, opts.Version)
if err != nil {
return nil, nil, err
}
defer func() {
if err != nil {
@@ -255,21 +285,32 @@ func (cli *CLI) AddMachine(
// Check if the machine is already initialised as a cluster member and prompt the user to reset it first.
minfo, err := machineClient.Inspect(ctx, &emptypb.Empty{})
if err != nil {
return nil, fmt.Errorf("inspect machine: %w", err)
return nil, nil, fmt.Errorf("inspect machine: %w", err)
}
if minfo.Id != "" {
if err = cli.promptResetMachine(); err != nil {
return nil, err
return nil, nil, err
}
}
// Check machine meets all necessary system requirements before proceeding.
checkResp, err := machineClient.CheckPrerequisites(ctx, &emptypb.Empty{})
// TODO(lhf): remove Unimplemented check when v0.9.0 is released.
if err != nil {
if status.Convert(err).Code() != codes.Unimplemented {
return nil, nil, fmt.Errorf("check machine prerequisites: %w", err)
}
} else if !checkResp.Satisfied {
return nil, nil, fmt.Errorf("machine prerequisites not satisfied: %s", checkResp.Error)
}
tokenResp, err := machineClient.Token(ctx, &emptypb.Empty{})
if err != nil {
return nil, fmt.Errorf("get remote machine token: %w", err)
return nil, nil, fmt.Errorf("get remote machine token: %w", err)
}
token, err := machine.ParseToken(tokenResp.Token)
if err != nil {
return nil, fmt.Errorf("parse remote machine token: %w", err)
return nil, nil, fmt.Errorf("parse remote machine token: %w", err)
}
// Register the machine in the cluster using its public key and endpoints from the token.
@@ -278,15 +319,15 @@ func (cli *CLI) AddMachine(
endpoints[i] = pb.NewIPPort(addrPort)
}
addReq := &pb.AddMachineRequest{
Name: machineName,
Name: opts.MachineName,
Network: &pb.NetworkConfig{
Endpoints: endpoints,
PublicKey: token.PublicKey,
},
}
if publicIP != nil {
if publicIP.IsValid() {
addReq.PublicIp = pb.NewIP(*publicIP)
if opts.PublicIP != nil {
if opts.PublicIP.IsValid() {
addReq.PublicIp = pb.NewIP(*opts.PublicIP)
} else if token.PublicIP.IsValid() {
// Invalid or in other words zero IP means to use an automatically detected public IP from the token.
addReq.PublicIp = pb.NewIP(token.PublicIP)
@@ -295,13 +336,13 @@ func (cli *CLI) AddMachine(
addResp, err := c.AddMachine(ctx, addReq)
if err != nil {
return nil, fmt.Errorf("add machine to cluster: %w", err)
return nil, nil, fmt.Errorf("add machine to cluster (context '%s'): %w", contextName, err)
}
// List other machines in the cluster to include them in the join request.
machines, err := c.ListMachines(ctx)
machines, err := c.ListMachines(ctx, nil)
if err != nil {
return nil, fmt.Errorf("list cluster machines: %w", err)
return nil, nil, fmt.Errorf("list cluster machines: %w", err)
}
otherMachines := make([]*pb.MachineInfo, 0, len(machines)-1)
for _, m := range machines {
@@ -316,30 +357,34 @@ func (cli *CLI) AddMachine(
OtherMachines: otherMachines,
}
if _, err = machineClient.JoinCluster(ctx, joinReq); err != nil {
return nil, fmt.Errorf("join cluster: %w", err)
return nil, nil, fmt.Errorf("join cluster: %w", err)
}
fmt.Printf("Machine %q added to cluster\n", addResp.Machine.Name)
// TODO: fix empty context name when using the current context (contextName == "").
fmt.Printf("Machine '%s' added to the cluster (context '%s').\n", addResp.Machine.Name, contextName)
// Save the machine's SSH connection details in the cluster config.
// Save the machine's SSH connection details in the context config.
connCfg := config.MachineConnection{
SSH: config.NewSSHDestination(remoteMachine.User, remoteMachine.Host, remoteMachine.Port),
SSHKeyFile: remoteMachine.KeyPath,
SSH: config.NewSSHDestination(opts.RemoteMachine.User, opts.RemoteMachine.Host, opts.RemoteMachine.Port),
SSHKeyFile: opts.RemoteMachine.KeyPath,
}
if clusterName == "" {
clusterName = cli.config.CurrentCluster
if contextName == "" {
contextName = cli.Config.CurrentContext
}
cli.config.Clusters[clusterName].Connections = append(cli.config.Clusters[clusterName].Connections, connCfg)
if err = cli.config.Save(); err != nil {
return nil, fmt.Errorf("save config: %w", err)
cli.Config.Contexts[contextName].Connections = append(cli.Config.Contexts[contextName].Connections, connCfg)
if err = cli.Config.Save(); err != nil {
return nil, nil, fmt.Errorf("save config: %w", err)
}
return machineClient, nil
return c, machineClient, nil
}
// provisionRemoteMachine installs the Uncloud daemon and dependencies on the remote machine over SSH and returns
// a machine API client to interact with the machine. The client should be closed after use by the caller.
func (cli *CLI) provisionRemoteMachine(ctx context.Context, remoteMachine RemoteMachine) (*client.Client, error) {
// The version parameter specifies the version of the Uncloud daemon to install. If empty, the latest version is used.
func (cli *CLI) provisionRemoteMachine(
ctx context.Context, remoteMachine RemoteMachine, version string,
) (*client.Client, error) {
// Provision the remote machine by installing the Uncloud daemon and dependencies over SSH.
sshClient, err := sshexec.Connect(remoteMachine.User, remoteMachine.Host, remoteMachine.Port, remoteMachine.KeyPath)
if err != nil {
@@ -350,7 +395,7 @@ func (cli *CLI) provisionRemoteMachine(ctx context.Context, remoteMachine Remote
}
exec := sshexec.NewRemote(sshClient)
// Install and run the Uncloud daemon and dependencies on the remote machine.
if err = provisionMachine(ctx, exec); err != nil {
if err = provisionMachine(ctx, exec, version); err != nil {
return nil, fmt.Errorf("provision machine: %w", err)
}
@@ -387,7 +432,7 @@ func (cli *CLI) promptResetMachine() error {
Negative("No").
Value(&confirm),
),
)
).WithAccessible(true)
if err := form.Run(); err != nil {
return fmt.Errorf("prompt user to confirm: %w", err)
}
@@ -396,7 +441,8 @@ func (cli *CLI) promptResetMachine() error {
return fmt.Errorf("remote machine is already initialised as a cluster member")
}
// TODO: implement resetting the remote machine.
return fmt.Errorf("resetting the remote machine is not implemented yet")
return fmt.Errorf("resetting the remote machine is not implemented yet. " +
"Please manually run 'uncloud-uninstall' on the remote machine to fully uninstall Uncloud from it")
}
// ProgressOut returns an output stream for progress writer.
-6
View File
@@ -1,6 +0,0 @@
package config
type Cluster struct {
Name string `toml:"-"`
Connections []MachineConnection `toml:"connections"`
}
+20 -12
View File
@@ -2,14 +2,15 @@ package config
import (
"fmt"
"github.com/BurntSushi/toml"
"os"
"path/filepath"
"github.com/goccy/go-yaml"
)
type Config struct {
Clusters map[string]*Cluster `toml:"clusters"`
CurrentCluster string `toml:"current_cluster"`
CurrentContext string `yaml:"current_context"`
Contexts map[string]*Context `yaml:"contexts"`
// path is the file path config is read from.
path string
@@ -18,10 +19,10 @@ type Config struct {
func NewFromFile(path string) (*Config, error) {
_, err := os.Stat(path)
if err != nil && !os.IsNotExist(err) {
return nil, fmt.Errorf("check file permissions %q: %w", path, err)
return nil, fmt.Errorf("check file permissions '%s': %w", path, err)
}
c := &Config{
Clusters: map[string]*Cluster{},
Contexts: map[string]*Context{},
path: path,
}
if os.IsNotExist(err) {
@@ -34,30 +35,37 @@ func NewFromFile(path string) (*Config, error) {
return c, nil
}
func (c *Config) Path() string {
return c.path
}
func (c *Config) Read() error {
_, err := toml.DecodeFile(c.path, c)
data, err := os.ReadFile(c.path)
if err != nil {
return fmt.Errorf("read config file %q: %w", c.path, err)
return fmt.Errorf("read config file '%s': %w", c.path, err)
}
if err = yaml.Unmarshal(data, c); err != nil {
return fmt.Errorf("parse config file '%s': %s", c.path, yaml.FormatError(err, true, true))
}
return nil
}
func (c *Config) Save() error {
dir, _ := filepath.Split(c.path)
if err := os.MkdirAll(dir, 0700); err != nil {
return fmt.Errorf("create config directory %q: %w", dir, err)
return fmt.Errorf("create config directory '%s': %w", dir, err)
}
f, err := os.OpenFile(c.path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil {
return fmt.Errorf("write config file %q: %w", c.path, err)
return fmt.Errorf("write config file '%s': %w", c.path, err)
}
encoder := toml.NewEncoder(f)
encoder.Indent = ""
encoder := yaml.NewEncoder(f, yaml.Indent(2), yaml.IndentSequence(true))
if err = encoder.Encode(c); err != nil {
_ = f.Close()
return fmt.Errorf("encode config file %q: %w", c.path, err)
return fmt.Errorf("encode config file '%s': %w", c.path, err)
}
return f.Close()
}
+8 -5
View File
@@ -5,6 +5,7 @@ import (
"net/netip"
"strconv"
"strings"
"github.com/psviderski/uncloud/internal/secret"
)
@@ -14,11 +15,13 @@ const (
)
type MachineConnection struct {
SSH SSHDestination `toml:"ssh,omitempty"`
SSHKeyFile string `toml:"ssh_key_file,omitempty"`
TCP netip.AddrPort `toml:"tcp,omitempty"`
Host string `toml:"host,omitempty"`
PublicKey secret.Secret `toml:"public_key,omitempty"`
SSH SSHDestination `yaml:"ssh,omitempty"`
SSHKeyFile string `yaml:"ssh_key_file,omitempty"`
// TCP is the address and port of the machine's API server.
// The pointer is used to omit the field when not set. Otherwise, yaml marshalling includes an empty object.
TCP *netip.AddrPort `yaml:"tcp,omitempty"`
Host string `yaml:"host,omitempty"`
PublicKey secret.Secret `yaml:"public_key,omitempty"`
}
// SSHDestination represents an SSH destination string in the canonical form of "user@host:port".
+6
View File
@@ -0,0 +1,6 @@
package config
type Context struct {
Name string `yaml:"-"`
Connections []MachineConnection `yaml:"connections"`
}
+23
View File
@@ -0,0 +1,23 @@
package cli
import (
"strings"
)
// ExpandCommaSeparatedValues takes a slice of strings and expands any comma-separated values into individual elements.
func ExpandCommaSeparatedValues(values []string) []string {
if len(values) == 0 {
return nil
}
var expanded []string
for _, value := range values {
for _, v := range strings.Split(value, ",") {
if v = strings.TrimSpace(v); v != "" {
expanded = append(expanded, v)
}
}
}
return expanded
}
+19 -15
View File
@@ -4,6 +4,8 @@ import (
"context"
"fmt"
"os"
"strings"
"github.com/psviderski/uncloud/internal/sshexec"
)
@@ -17,36 +19,38 @@ type RemoteMachine struct {
KeyPath string
}
func installCmd(user string) string {
sudoPrefix := "sudo"
func installCmd(user string, version string) string {
sudoPrefix := ""
var env []string
// Add the SSH user (non-root) to the uncloud group to allow access to the Uncloud daemon unix socket.
env := "UNCLOUD_GROUP_ADD_USER=" + user
curlBashCmd := fmt.Sprintf(
"curl -fsSL %s | %s %s bash", sshexec.Quote(installScriptURL), sudoPrefix, sshexec.Quote(env),
)
if user == "root" {
curlBashCmd = fmt.Sprintf(
"curl -fsSL %s | bash", sshexec.Quote(installScriptURL),
)
if user != "root" {
sudoPrefix = "sudo"
env = append(env, "UNCLOUD_GROUP_ADD_USER="+sshexec.Quote(user))
}
if version != "" {
env = append(env, "UNCLOUD_VERSION="+sshexec.Quote(version))
}
envCmd := strings.Join(env, " ")
curlBashCmd := fmt.Sprintf("curl -fsSL %s | %s %s bash", sshexec.Quote(installScriptURL), sudoPrefix, envCmd)
return curlBashCmd
}
// provisionMachine provisions the remote machine by downloading the Uncloud install script from GitHub and running it.
func provisionMachine(ctx context.Context, exec sshexec.Executor) error {
// If version is specified, it will be passed to the install script as UNCLOUD_VERSION environment variable.
func provisionMachine(ctx context.Context, exec sshexec.Executor, version string) error {
user, err := exec.Run(ctx, "whoami")
if err != nil {
return fmt.Errorf("run whoami: %w", err)
}
installCmd := installCmd(user)
cmd := installCmd(user, version)
fmt.Println("Downloading Uncloud install script:", installScriptURL)
cmd := sshexec.QuoteCommand("bash", "-c", "set -o pipefail; "+installCmd)
cmd = sshexec.QuoteCommand("bash", "-c", "set -o pipefail; "+cmd)
if err = exec.Stream(ctx, cmd, os.Stdout, os.Stderr); err != nil {
return fmt.Errorf("download and run install script: %w", err)
}
+19 -2
View File
@@ -8,12 +8,29 @@ import (
func TestInstallCmd(t *testing.T) {
t.Run("root", func(t *testing.T) {
cmd := installCmd("root")
cmd := installCmd("root", "")
assert.NotContains(t, cmd, "sudo")
assert.NotContains(t, cmd, "UNCLOUD_GROUP_ADD_USER")
})
// Test with version
t.Run("root with version", func(t *testing.T) {
cmd := installCmd("root", "v1.2.3")
assert.NotContains(t, cmd, "sudo")
assert.NotContains(t, cmd, "UNCLOUD_GROUP_ADD_USER")
assert.Contains(t, cmd, "UNCLOUD_VERSION=v1.2.3")
})
t.Run("nonroot", func(t *testing.T) {
cmd := installCmd("nonroot")
cmd := installCmd("nonroot", "")
assert.Contains(t, cmd, "sudo")
assert.Contains(t, cmd, "UNCLOUD_GROUP_ADD_USER=nonroot")
})
t.Run("nonroot with version", func(t *testing.T) {
cmd := installCmd("nonroot", "v1.2.3")
assert.Contains(t, cmd, "sudo")
assert.Contains(t, cmd, "UNCLOUD_GROUP_ADD_USER=nonroot")
assert.Contains(t, cmd, "UNCLOUD_VERSION=v1.2.3")
})
}
+1 -1
View File
@@ -14,7 +14,7 @@ func Confirm() (bool, error) {
Negative("No").
Value(&confirmed),
),
)
).WithAccessible(true)
if err := form.Run(); err != nil {
return false, err
}
+10 -2
View File
@@ -45,11 +45,19 @@ func (h *SlogTextHandler) Enabled(ctx context.Context, level slog.Level) bool {
}
func (h *SlogTextHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
return h.TextHandler.WithAttrs(attrs)
return &SlogTextHandler{
TextHandler: h.TextHandler.WithAttrs(attrs).(*slog.TextHandler),
opts: h.opts,
w: h.w,
}
}
func (h *SlogTextHandler) WithGroup(name string) slog.Handler {
return h.TextHandler.WithGroup(name)
return &SlogTextHandler{
TextHandler: h.TextHandler.WithGroup(name).(*slog.TextHandler),
opts: h.opts,
w: h.w,
}
}
func (h *SlogTextHandler) Handle(ctx context.Context, r slog.Record) error {
+588 -164
View File
@@ -982,6 +982,309 @@ func (x *RemoteImage) GetManifest() []byte {
return nil
}
type CreateVolumeRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
// JSON serialised volume.CreateOptions.
Options []byte `protobuf:"bytes,1,opt,name=options,proto3" json:"options,omitempty"`
}
func (x *CreateVolumeRequest) Reset() {
*x = CreateVolumeRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[18]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *CreateVolumeRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*CreateVolumeRequest) ProtoMessage() {}
func (x *CreateVolumeRequest) ProtoReflect() protoreflect.Message {
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[18]
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 CreateVolumeRequest.ProtoReflect.Descriptor instead.
func (*CreateVolumeRequest) Descriptor() ([]byte, []int) {
return file_internal_machine_api_pb_docker_proto_rawDescGZIP(), []int{18}
}
func (x *CreateVolumeRequest) GetOptions() []byte {
if x != nil {
return x.Options
}
return nil
}
type CreateVolumeResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
// JSON serialised volume.Volume.
Volume []byte `protobuf:"bytes,1,opt,name=volume,proto3" json:"volume,omitempty"`
}
func (x *CreateVolumeResponse) Reset() {
*x = CreateVolumeResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[19]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *CreateVolumeResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*CreateVolumeResponse) ProtoMessage() {}
func (x *CreateVolumeResponse) ProtoReflect() protoreflect.Message {
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[19]
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 CreateVolumeResponse.ProtoReflect.Descriptor instead.
func (*CreateVolumeResponse) Descriptor() ([]byte, []int) {
return file_internal_machine_api_pb_docker_proto_rawDescGZIP(), []int{19}
}
func (x *CreateVolumeResponse) GetVolume() []byte {
if x != nil {
return x.Volume
}
return nil
}
type ListVolumesRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
// JSON serialised volume.ListOptions.
Options []byte `protobuf:"bytes,1,opt,name=options,proto3" json:"options,omitempty"`
}
func (x *ListVolumesRequest) Reset() {
*x = ListVolumesRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[20]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *ListVolumesRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ListVolumesRequest) ProtoMessage() {}
func (x *ListVolumesRequest) ProtoReflect() protoreflect.Message {
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[20]
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 ListVolumesRequest.ProtoReflect.Descriptor instead.
func (*ListVolumesRequest) Descriptor() ([]byte, []int) {
return file_internal_machine_api_pb_docker_proto_rawDescGZIP(), []int{20}
}
func (x *ListVolumesRequest) GetOptions() []byte {
if x != nil {
return x.Options
}
return nil
}
type ListVolumesResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
// Must contain only one repeated messages field to allow broadcasting ListVolumes requests to multiple machines.
Messages []*MachineVolumes `protobuf:"bytes,1,rep,name=messages,proto3" json:"messages,omitempty"`
}
func (x *ListVolumesResponse) Reset() {
*x = ListVolumesResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[21]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *ListVolumesResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ListVolumesResponse) ProtoMessage() {}
func (x *ListVolumesResponse) ProtoReflect() protoreflect.Message {
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[21]
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 ListVolumesResponse.ProtoReflect.Descriptor instead.
func (*ListVolumesResponse) Descriptor() ([]byte, []int) {
return file_internal_machine_api_pb_docker_proto_rawDescGZIP(), []int{21}
}
func (x *ListVolumesResponse) GetMessages() []*MachineVolumes {
if x != nil {
return x.Messages
}
return nil
}
type MachineVolumes struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Metadata *Metadata `protobuf:"bytes,1,opt,name=metadata,proto3" json:"metadata,omitempty"`
// JSON serialised volume.ListResponse.
Response []byte `protobuf:"bytes,2,opt,name=response,proto3" json:"response,omitempty"`
}
func (x *MachineVolumes) Reset() {
*x = MachineVolumes{}
if protoimpl.UnsafeEnabled {
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[22]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *MachineVolumes) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*MachineVolumes) ProtoMessage() {}
func (x *MachineVolumes) ProtoReflect() protoreflect.Message {
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[22]
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 MachineVolumes.ProtoReflect.Descriptor instead.
func (*MachineVolumes) Descriptor() ([]byte, []int) {
return file_internal_machine_api_pb_docker_proto_rawDescGZIP(), []int{22}
}
func (x *MachineVolumes) GetMetadata() *Metadata {
if x != nil {
return x.Metadata
}
return nil
}
func (x *MachineVolumes) GetResponse() []byte {
if x != nil {
return x.Response
}
return nil
}
type RemoveVolumeRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
Force bool `protobuf:"varint,2,opt,name=force,proto3" json:"force,omitempty"`
}
func (x *RemoveVolumeRequest) Reset() {
*x = RemoveVolumeRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[23]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *RemoveVolumeRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*RemoveVolumeRequest) ProtoMessage() {}
func (x *RemoveVolumeRequest) ProtoReflect() protoreflect.Message {
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[23]
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 RemoveVolumeRequest.ProtoReflect.Descriptor instead.
func (*RemoveVolumeRequest) Descriptor() ([]byte, []int) {
return file_internal_machine_api_pb_docker_proto_rawDescGZIP(), []int{23}
}
func (x *RemoveVolumeRequest) GetId() string {
if x != nil {
return x.Id
}
return ""
}
func (x *RemoveVolumeRequest) GetForce() bool {
if x != nil {
return x.Force
}
return false
}
type CreateServiceContainerRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
@@ -996,7 +1299,7 @@ type CreateServiceContainerRequest struct {
func (x *CreateServiceContainerRequest) Reset() {
*x = CreateServiceContainerRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[18]
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[24]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -1009,7 +1312,7 @@ func (x *CreateServiceContainerRequest) String() string {
func (*CreateServiceContainerRequest) ProtoMessage() {}
func (x *CreateServiceContainerRequest) ProtoReflect() protoreflect.Message {
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[18]
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[24]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -1022,7 +1325,7 @@ func (x *CreateServiceContainerRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use CreateServiceContainerRequest.ProtoReflect.Descriptor instead.
func (*CreateServiceContainerRequest) Descriptor() ([]byte, []int) {
return file_internal_machine_api_pb_docker_proto_rawDescGZIP(), []int{18}
return file_internal_machine_api_pb_docker_proto_rawDescGZIP(), []int{24}
}
func (x *CreateServiceContainerRequest) GetServiceId() string {
@@ -1060,7 +1363,7 @@ type ServiceContainer struct {
func (x *ServiceContainer) Reset() {
*x = ServiceContainer{}
if protoimpl.UnsafeEnabled {
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[19]
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[25]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -1073,7 +1376,7 @@ func (x *ServiceContainer) String() string {
func (*ServiceContainer) ProtoMessage() {}
func (x *ServiceContainer) ProtoReflect() protoreflect.Message {
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[19]
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[25]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -1086,7 +1389,7 @@ func (x *ServiceContainer) ProtoReflect() protoreflect.Message {
// Deprecated: Use ServiceContainer.ProtoReflect.Descriptor instead.
func (*ServiceContainer) Descriptor() ([]byte, []int) {
return file_internal_machine_api_pb_docker_proto_rawDescGZIP(), []int{19}
return file_internal_machine_api_pb_docker_proto_rawDescGZIP(), []int{25}
}
func (x *ServiceContainer) GetContainer() []byte {
@@ -1116,7 +1419,7 @@ type ListServiceContainersRequest struct {
func (x *ListServiceContainersRequest) Reset() {
*x = ListServiceContainersRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[20]
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[26]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -1129,7 +1432,7 @@ func (x *ListServiceContainersRequest) String() string {
func (*ListServiceContainersRequest) ProtoMessage() {}
func (x *ListServiceContainersRequest) ProtoReflect() protoreflect.Message {
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[20]
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[26]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -1142,7 +1445,7 @@ func (x *ListServiceContainersRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use ListServiceContainersRequest.ProtoReflect.Descriptor instead.
func (*ListServiceContainersRequest) Descriptor() ([]byte, []int) {
return file_internal_machine_api_pb_docker_proto_rawDescGZIP(), []int{20}
return file_internal_machine_api_pb_docker_proto_rawDescGZIP(), []int{26}
}
func (x *ListServiceContainersRequest) GetServiceId() string {
@@ -1172,7 +1475,7 @@ type ListServiceContainersResponse struct {
func (x *ListServiceContainersResponse) Reset() {
*x = ListServiceContainersResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[21]
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[27]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -1185,7 +1488,7 @@ func (x *ListServiceContainersResponse) String() string {
func (*ListServiceContainersResponse) ProtoMessage() {}
func (x *ListServiceContainersResponse) ProtoReflect() protoreflect.Message {
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[21]
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[27]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -1198,7 +1501,7 @@ func (x *ListServiceContainersResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use ListServiceContainersResponse.ProtoReflect.Descriptor instead.
func (*ListServiceContainersResponse) Descriptor() ([]byte, []int) {
return file_internal_machine_api_pb_docker_proto_rawDescGZIP(), []int{21}
return file_internal_machine_api_pb_docker_proto_rawDescGZIP(), []int{27}
}
func (x *ListServiceContainersResponse) GetMessages() []*MachineServiceContainers {
@@ -1220,7 +1523,7 @@ type MachineServiceContainers struct {
func (x *MachineServiceContainers) Reset() {
*x = MachineServiceContainers{}
if protoimpl.UnsafeEnabled {
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[22]
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[28]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -1233,7 +1536,7 @@ func (x *MachineServiceContainers) String() string {
func (*MachineServiceContainers) ProtoMessage() {}
func (x *MachineServiceContainers) ProtoReflect() protoreflect.Message {
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[22]
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[28]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -1246,7 +1549,7 @@ func (x *MachineServiceContainers) ProtoReflect() protoreflect.Message {
// Deprecated: Use MachineServiceContainers.ProtoReflect.Descriptor instead.
func (*MachineServiceContainers) Descriptor() ([]byte, []int) {
return file_internal_machine_api_pb_docker_proto_rawDescGZIP(), []int{22}
return file_internal_machine_api_pb_docker_proto_rawDescGZIP(), []int{28}
}
func (x *MachineServiceContainers) GetMetadata() *Metadata {
@@ -1352,108 +1655,143 @@ var file_internal_machine_api_pb_docker_proto_rawDesc = []byte{
0x0a, 0x09, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28,
0x09, 0x52, 0x09, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x12, 0x1a, 0x0a, 0x08,
0x6d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08,
0x6d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x22, 0x88, 0x01, 0x0a, 0x1d, 0x43, 0x72, 0x65,
0x61, 0x74, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69,
0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x65,
0x72, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09,
0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x49, 0x64, 0x12, 0x21, 0x0a, 0x0c, 0x73, 0x65, 0x72,
0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x70, 0x65, 0x63, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52,
0x0b, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x53, 0x70, 0x65, 0x63, 0x12, 0x25, 0x0a, 0x0e,
0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03,
0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x4e,
0x61, 0x6d, 0x65, 0x22, 0x53, 0x0a, 0x10, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f,
0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x1c, 0x0a, 0x09, 0x63, 0x6f, 0x6e, 0x74, 0x61,
0x69, 0x6e, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x63, 0x6f, 0x6e, 0x74,
0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x21, 0x0a, 0x0c, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65,
0x5f, 0x73, 0x70, 0x65, 0x63, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0b, 0x73, 0x65, 0x72,
0x76, 0x69, 0x63, 0x65, 0x53, 0x70, 0x65, 0x63, 0x22, 0x57, 0x0a, 0x1c, 0x4c, 0x69, 0x73, 0x74,
0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72,
0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x65, 0x72, 0x76,
0x69, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x65,
0x72, 0x76, 0x69, 0x63, 0x65, 0x49, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f,
0x6e, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e,
0x73, 0x22, 0x5a, 0x0a, 0x1d, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65,
0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
0x73, 0x65, 0x12, 0x39, 0x0a, 0x08, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x18, 0x01,
0x20, 0x03, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69,
0x6e, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e,
0x65, 0x72, 0x73, 0x52, 0x08, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x22, 0x7c, 0x0a,
0x18, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43,
0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x12, 0x29, 0x0a, 0x08, 0x6d, 0x65, 0x74,
0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x61, 0x70,
0x69, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61,
0x64, 0x61, 0x74, 0x61, 0x12, 0x35, 0x0a, 0x0a, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65,
0x72, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x53,
0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52,
0x0a, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x32, 0xf3, 0x07, 0x0a, 0x06,
0x44, 0x6f, 0x63, 0x6b, 0x65, 0x72, 0x12, 0x4c, 0x0a, 0x0f, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65,
0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x1b, 0x2e, 0x61, 0x70, 0x69, 0x2e,
0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52,
0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x72, 0x65,
0x61, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70,
0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4f, 0x0a, 0x10, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x43,
0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x1c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49,
0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52,
0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x73,
0x70, 0x65, 0x63, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x73,
0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x44, 0x0a, 0x0e, 0x53, 0x74, 0x61, 0x72, 0x74, 0x43, 0x6f,
0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x1a, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x53, 0x74,
0x61, 0x72, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75,
0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f,
0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x42, 0x0a, 0x0d, 0x53,
0x74, 0x6f, 0x70, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x19, 0x2e, 0x61,
0x70, 0x69, 0x2e, 0x53, 0x74, 0x6f, 0x70, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72,
0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65,
0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12,
0x49, 0x0a, 0x0e, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72,
0x73, 0x12, 0x1a, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x74,
0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e,
0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65,
0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x46, 0x0a, 0x0f, 0x52, 0x65,
0x6d, 0x6f, 0x76, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x1b, 0x2e,
0x61, 0x70, 0x69, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69,
0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f,
0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70,
0x74, 0x79, 0x12, 0x36, 0x0a, 0x09, 0x50, 0x75, 0x6c, 0x6c, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x12,
0x15, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x50, 0x75, 0x6c, 0x6c, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x52,
0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x10, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4a, 0x53, 0x4f,
0x4e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x30, 0x01, 0x12, 0x43, 0x0a, 0x0c, 0x49, 0x6e,
0x73, 0x70, 0x65, 0x63, 0x74, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x12, 0x18, 0x2e, 0x61, 0x70, 0x69,
0x2e, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71,
0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x73, 0x70, 0x65,
0x63, 0x74, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12,
0x55, 0x0a, 0x12, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65,
0x49, 0x6d, 0x61, 0x67, 0x65, 0x12, 0x1e, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x73, 0x70,
0x65, 0x63, 0x74, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x52, 0x65,
0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x73, 0x70,
0x65, 0x63, 0x74, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x52, 0x65,
0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x5a, 0x0a, 0x16, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65,
0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72,
0x12, 0x22, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x53, 0x65, 0x72,
0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71,
0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74,
0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
0x73, 0x65, 0x12, 0x4e, 0x0a, 0x17, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72,
0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x1c, 0x2e,
0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61,
0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x15, 0x2e, 0x61, 0x70,
0x69, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e,
0x65, 0x72, 0x12, 0x5e, 0x0a, 0x15, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63,
0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x12, 0x21, 0x2e, 0x61, 0x70,
0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e,
0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22,
0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65,
0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
0x73, 0x65, 0x12, 0x4d, 0x0a, 0x16, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x53, 0x65, 0x72, 0x76,
0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x1b, 0x2e, 0x61,
0x6d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x22, 0x2f, 0x0a, 0x13, 0x43, 0x72, 0x65, 0x61,
0x74, 0x65, 0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12,
0x18, 0x0a, 0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c,
0x52, 0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x2e, 0x0a, 0x14, 0x43, 0x72, 0x65,
0x61, 0x74, 0x65, 0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
0x65, 0x12, 0x16, 0x0a, 0x06, 0x76, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28,
0x0c, 0x52, 0x06, 0x76, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x22, 0x2e, 0x0a, 0x12, 0x4c, 0x69, 0x73,
0x74, 0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12,
0x18, 0x0a, 0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c,
0x52, 0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x46, 0x0a, 0x13, 0x4c, 0x69, 0x73,
0x74, 0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
0x12, 0x2f, 0x0a, 0x08, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03,
0x28, 0x0b, 0x32, 0x13, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65,
0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x73, 0x52, 0x08, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65,
0x73, 0x22, 0x57, 0x0a, 0x0e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x56, 0x6f, 0x6c, 0x75,
0x6d, 0x65, 0x73, 0x12, 0x29, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18,
0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d, 0x65, 0x74, 0x61,
0x64, 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x1a,
0x0a, 0x08, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c,
0x52, 0x08, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x3b, 0x0a, 0x13, 0x52, 0x65,
0x6d, 0x6f, 0x76, 0x65, 0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69,
0x64, 0x12, 0x14, 0x0a, 0x05, 0x66, 0x6f, 0x72, 0x63, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08,
0x52, 0x05, 0x66, 0x6f, 0x72, 0x63, 0x65, 0x22, 0x88, 0x01, 0x0a, 0x1d, 0x43, 0x72, 0x65, 0x61,
0x74, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e,
0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x65, 0x72,
0x76, 0x69, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73,
0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x49, 0x64, 0x12, 0x21, 0x0a, 0x0c, 0x73, 0x65, 0x72, 0x76,
0x69, 0x63, 0x65, 0x5f, 0x73, 0x70, 0x65, 0x63, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0b,
0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x53, 0x70, 0x65, 0x63, 0x12, 0x25, 0x0a, 0x0e, 0x63,
0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20,
0x01, 0x28, 0x09, 0x52, 0x0d, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x4e, 0x61,
0x6d, 0x65, 0x22, 0x53, 0x0a, 0x10, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e,
0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x1c, 0x0a, 0x09, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69,
0x6e, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x63, 0x6f, 0x6e, 0x74, 0x61,
0x69, 0x6e, 0x65, 0x72, 0x12, 0x21, 0x0a, 0x0c, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x5f,
0x73, 0x70, 0x65, 0x63, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0b, 0x73, 0x65, 0x72, 0x76,
0x69, 0x63, 0x65, 0x53, 0x70, 0x65, 0x63, 0x22, 0x57, 0x0a, 0x1c, 0x4c, 0x69, 0x73, 0x74, 0x53,
0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73,
0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x65, 0x72, 0x76, 0x69,
0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x65, 0x72,
0x76, 0x69, 0x63, 0x65, 0x49, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e,
0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73,
0x22, 0x5a, 0x0a, 0x1d, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43,
0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
0x65, 0x12, 0x39, 0x0a, 0x08, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x18, 0x01, 0x20,
0x03, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e,
0x65, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65,
0x72, 0x73, 0x52, 0x08, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x22, 0x7c, 0x0a, 0x18,
0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f,
0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x12, 0x29, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61,
0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x61, 0x70, 0x69,
0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64,
0x61, 0x74, 0x61, 0x12, 0x35, 0x0a, 0x0a, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72,
0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x53, 0x65,
0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x0a,
0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x32, 0xbc, 0x09, 0x0a, 0x06, 0x44,
0x6f, 0x63, 0x6b, 0x65, 0x72, 0x12, 0x4c, 0x0a, 0x0f, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x43,
0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x1b, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43,
0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65,
0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x72, 0x65, 0x61,
0x74, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f,
0x6e, 0x73, 0x65, 0x12, 0x4f, 0x0a, 0x10, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x43, 0x6f,
0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x1c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e,
0x73, 0x70, 0x65, 0x63, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65,
0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x73, 0x70,
0x65, 0x63, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70,
0x6f, 0x6e, 0x73, 0x65, 0x12, 0x44, 0x0a, 0x0e, 0x53, 0x74, 0x61, 0x72, 0x74, 0x43, 0x6f, 0x6e,
0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x1a, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x53, 0x74, 0x61,
0x72, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65,
0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74,
0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x42, 0x0a, 0x0d, 0x53, 0x74,
0x6f, 0x70, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x19, 0x2e, 0x61, 0x70,
0x69, 0x2e, 0x53, 0x74, 0x6f, 0x70, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52,
0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e,
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x49,
0x0a, 0x0e, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73,
0x12, 0x1a, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61,
0x69, 0x6e, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x61,
0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72,
0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x46, 0x0a, 0x0f, 0x52, 0x65, 0x6d,
0x6f, 0x76, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x1b, 0x2e, 0x61,
0x70, 0x69, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e,
0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67,
0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74,
0x79, 0x42, 0x37, 0x5a, 0x35, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f,
0x70, 0x73, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x6b, 0x69, 0x2f, 0x75, 0x6e, 0x63, 0x6c, 0x6f,
0x75, 0x64, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x6d, 0x61, 0x63, 0x68,
0x69, 0x6e, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74,
0x6f, 0x33,
0x79, 0x12, 0x36, 0x0a, 0x09, 0x50, 0x75, 0x6c, 0x6c, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x12, 0x15,
0x2e, 0x61, 0x70, 0x69, 0x2e, 0x50, 0x75, 0x6c, 0x6c, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x52, 0x65,
0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x10, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4a, 0x53, 0x4f, 0x4e,
0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x30, 0x01, 0x12, 0x43, 0x0a, 0x0c, 0x49, 0x6e, 0x73,
0x70, 0x65, 0x63, 0x74, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x12, 0x18, 0x2e, 0x61, 0x70, 0x69, 0x2e,
0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75,
0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63,
0x74, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x55,
0x0a, 0x12, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x49,
0x6d, 0x61, 0x67, 0x65, 0x12, 0x1e, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x73, 0x70, 0x65,
0x63, 0x74, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71,
0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x73, 0x70, 0x65,
0x63, 0x74, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73,
0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x43, 0x0a, 0x0c, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x56,
0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x12, 0x18, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x72, 0x65, 0x61,
0x74, 0x65, 0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a,
0x19, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x56, 0x6f, 0x6c, 0x75,
0x6d, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x40, 0x0a, 0x0b, 0x4c, 0x69,
0x73, 0x74, 0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x73, 0x12, 0x17, 0x2e, 0x61, 0x70, 0x69, 0x2e,
0x4c, 0x69, 0x73, 0x74, 0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65,
0x73, 0x74, 0x1a, 0x18, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x56, 0x6f, 0x6c,
0x75, 0x6d, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x40, 0x0a, 0x0c,
0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x12, 0x18, 0x2e, 0x61,
0x70, 0x69, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x56, 0x6f, 0x6c, 0x75, 0x6d, 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, 0x5a,
0x0a, 0x16, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43,
0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x22, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43,
0x72, 0x65, 0x61, 0x74, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x74,
0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x61,
0x70, 0x69, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e,
0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4e, 0x0a, 0x17, 0x49, 0x6e,
0x73, 0x70, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x74,
0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x1c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x73, 0x70,
0x65, 0x63, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75,
0x65, 0x73, 0x74, 0x1a, 0x15, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63,
0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x5e, 0x0a, 0x15, 0x4c, 0x69,
0x73, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e,
0x65, 0x72, 0x73, 0x12, 0x21, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x65,
0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x52,
0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73,
0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65,
0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4d, 0x0a, 0x16, 0x52, 0x65,
0x6d, 0x6f, 0x76, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61,
0x69, 0x6e, 0x65, 0x72, 0x12, 0x1b, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x76,
0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f,
0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x42, 0x37, 0x5a, 0x35, 0x67, 0x69, 0x74,
0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x70, 0x73, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73,
0x6b, 0x69, 0x2f, 0x75, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72,
0x6e, 0x61, 0x6c, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f,
0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
@@ -1468,7 +1806,7 @@ func file_internal_machine_api_pb_docker_proto_rawDescGZIP() []byte {
return file_internal_machine_api_pb_docker_proto_rawDescData
}
var file_internal_machine_api_pb_docker_proto_msgTypes = make([]protoimpl.MessageInfo, 23)
var file_internal_machine_api_pb_docker_proto_msgTypes = make([]protoimpl.MessageInfo, 29)
var file_internal_machine_api_pb_docker_proto_goTypes = []any{
(*CreateContainerRequest)(nil), // 0: api.CreateContainerRequest
(*CreateContainerResponse)(nil), // 1: api.CreateContainerResponse
@@ -1488,55 +1826,69 @@ var file_internal_machine_api_pb_docker_proto_goTypes = []any{
(*InspectRemoteImageRequest)(nil), // 15: api.InspectRemoteImageRequest
(*InspectRemoteImageResponse)(nil), // 16: api.InspectRemoteImageResponse
(*RemoteImage)(nil), // 17: api.RemoteImage
(*CreateServiceContainerRequest)(nil), // 18: api.CreateServiceContainerRequest
(*ServiceContainer)(nil), // 19: api.ServiceContainer
(*ListServiceContainersRequest)(nil), // 20: api.ListServiceContainersRequest
(*ListServiceContainersResponse)(nil), // 21: api.ListServiceContainersResponse
(*MachineServiceContainers)(nil), // 22: api.MachineServiceContainers
(*Metadata)(nil), // 23: api.Metadata
(*emptypb.Empty)(nil), // 24: google.protobuf.Empty
(*CreateVolumeRequest)(nil), // 18: api.CreateVolumeRequest
(*CreateVolumeResponse)(nil), // 19: api.CreateVolumeResponse
(*ListVolumesRequest)(nil), // 20: api.ListVolumesRequest
(*ListVolumesResponse)(nil), // 21: api.ListVolumesResponse
(*MachineVolumes)(nil), // 22: api.MachineVolumes
(*RemoveVolumeRequest)(nil), // 23: api.RemoveVolumeRequest
(*CreateServiceContainerRequest)(nil), // 24: api.CreateServiceContainerRequest
(*ServiceContainer)(nil), // 25: api.ServiceContainer
(*ListServiceContainersRequest)(nil), // 26: api.ListServiceContainersRequest
(*ListServiceContainersResponse)(nil), // 27: api.ListServiceContainersResponse
(*MachineServiceContainers)(nil), // 28: api.MachineServiceContainers
(*Metadata)(nil), // 29: api.Metadata
(*emptypb.Empty)(nil), // 30: google.protobuf.Empty
}
var file_internal_machine_api_pb_docker_proto_depIdxs = []int32{
8, // 0: api.ListContainersResponse.messages:type_name -> api.MachineContainers
23, // 1: api.MachineContainers.metadata:type_name -> api.Metadata
29, // 1: api.MachineContainers.metadata:type_name -> api.Metadata
14, // 2: api.InspectImageResponse.messages:type_name -> api.Image
23, // 3: api.Image.metadata:type_name -> api.Metadata
29, // 3: api.Image.metadata:type_name -> api.Metadata
17, // 4: api.InspectRemoteImageResponse.messages:type_name -> api.RemoteImage
23, // 5: api.RemoteImage.metadata:type_name -> api.Metadata
22, // 6: api.ListServiceContainersResponse.messages:type_name -> api.MachineServiceContainers
23, // 7: api.MachineServiceContainers.metadata:type_name -> api.Metadata
19, // 8: api.MachineServiceContainers.containers:type_name -> api.ServiceContainer
0, // 9: api.Docker.CreateContainer:input_type -> api.CreateContainerRequest
2, // 10: api.Docker.InspectContainer:input_type -> api.InspectContainerRequest
4, // 11: api.Docker.StartContainer:input_type -> api.StartContainerRequest
5, // 12: api.Docker.StopContainer:input_type -> api.StopContainerRequest
6, // 13: api.Docker.ListContainers:input_type -> api.ListContainersRequest
9, // 14: api.Docker.RemoveContainer:input_type -> api.RemoveContainerRequest
10, // 15: api.Docker.PullImage:input_type -> api.PullImageRequest
12, // 16: api.Docker.InspectImage:input_type -> api.InspectImageRequest
15, // 17: api.Docker.InspectRemoteImage:input_type -> api.InspectRemoteImageRequest
18, // 18: api.Docker.CreateServiceContainer:input_type -> api.CreateServiceContainerRequest
2, // 19: api.Docker.InspectServiceContainer:input_type -> api.InspectContainerRequest
20, // 20: api.Docker.ListServiceContainers:input_type -> api.ListServiceContainersRequest
9, // 21: api.Docker.RemoveServiceContainer:input_type -> api.RemoveContainerRequest
1, // 22: api.Docker.CreateContainer:output_type -> api.CreateContainerResponse
3, // 23: api.Docker.InspectContainer:output_type -> api.InspectContainerResponse
24, // 24: api.Docker.StartContainer:output_type -> google.protobuf.Empty
24, // 25: api.Docker.StopContainer:output_type -> google.protobuf.Empty
7, // 26: api.Docker.ListContainers:output_type -> api.ListContainersResponse
24, // 27: api.Docker.RemoveContainer:output_type -> google.protobuf.Empty
11, // 28: api.Docker.PullImage:output_type -> api.JSONMessage
13, // 29: api.Docker.InspectImage:output_type -> api.InspectImageResponse
16, // 30: api.Docker.InspectRemoteImage:output_type -> api.InspectRemoteImageResponse
1, // 31: api.Docker.CreateServiceContainer:output_type -> api.CreateContainerResponse
19, // 32: api.Docker.InspectServiceContainer:output_type -> api.ServiceContainer
21, // 33: api.Docker.ListServiceContainers:output_type -> api.ListServiceContainersResponse
24, // 34: api.Docker.RemoveServiceContainer:output_type -> google.protobuf.Empty
22, // [22:35] is the sub-list for method output_type
9, // [9:22] is the sub-list for method input_type
9, // [9:9] is the sub-list for extension type_name
9, // [9:9] is the sub-list for extension extendee
0, // [0:9] is the sub-list for field type_name
29, // 5: api.RemoteImage.metadata:type_name -> api.Metadata
22, // 6: api.ListVolumesResponse.messages:type_name -> api.MachineVolumes
29, // 7: api.MachineVolumes.metadata:type_name -> api.Metadata
28, // 8: api.ListServiceContainersResponse.messages:type_name -> api.MachineServiceContainers
29, // 9: api.MachineServiceContainers.metadata:type_name -> api.Metadata
25, // 10: api.MachineServiceContainers.containers:type_name -> api.ServiceContainer
0, // 11: api.Docker.CreateContainer:input_type -> api.CreateContainerRequest
2, // 12: api.Docker.InspectContainer:input_type -> api.InspectContainerRequest
4, // 13: api.Docker.StartContainer:input_type -> api.StartContainerRequest
5, // 14: api.Docker.StopContainer:input_type -> api.StopContainerRequest
6, // 15: api.Docker.ListContainers:input_type -> api.ListContainersRequest
9, // 16: api.Docker.RemoveContainer:input_type -> api.RemoveContainerRequest
10, // 17: api.Docker.PullImage:input_type -> api.PullImageRequest
12, // 18: api.Docker.InspectImage:input_type -> api.InspectImageRequest
15, // 19: api.Docker.InspectRemoteImage:input_type -> api.InspectRemoteImageRequest
18, // 20: api.Docker.CreateVolume:input_type -> api.CreateVolumeRequest
20, // 21: api.Docker.ListVolumes:input_type -> api.ListVolumesRequest
23, // 22: api.Docker.RemoveVolume:input_type -> api.RemoveVolumeRequest
24, // 23: api.Docker.CreateServiceContainer:input_type -> api.CreateServiceContainerRequest
2, // 24: api.Docker.InspectServiceContainer:input_type -> api.InspectContainerRequest
26, // 25: api.Docker.ListServiceContainers:input_type -> api.ListServiceContainersRequest
9, // 26: api.Docker.RemoveServiceContainer:input_type -> api.RemoveContainerRequest
1, // 27: api.Docker.CreateContainer:output_type -> api.CreateContainerResponse
3, // 28: api.Docker.InspectContainer:output_type -> api.InspectContainerResponse
30, // 29: api.Docker.StartContainer:output_type -> google.protobuf.Empty
30, // 30: api.Docker.StopContainer:output_type -> google.protobuf.Empty
7, // 31: api.Docker.ListContainers:output_type -> api.ListContainersResponse
30, // 32: api.Docker.RemoveContainer:output_type -> google.protobuf.Empty
11, // 33: api.Docker.PullImage:output_type -> api.JSONMessage
13, // 34: api.Docker.InspectImage:output_type -> api.InspectImageResponse
16, // 35: api.Docker.InspectRemoteImage:output_type -> api.InspectRemoteImageResponse
19, // 36: api.Docker.CreateVolume:output_type -> api.CreateVolumeResponse
21, // 37: api.Docker.ListVolumes:output_type -> api.ListVolumesResponse
30, // 38: api.Docker.RemoveVolume:output_type -> google.protobuf.Empty
1, // 39: api.Docker.CreateServiceContainer:output_type -> api.CreateContainerResponse
25, // 40: api.Docker.InspectServiceContainer:output_type -> api.ServiceContainer
27, // 41: api.Docker.ListServiceContainers:output_type -> api.ListServiceContainersResponse
30, // 42: api.Docker.RemoveServiceContainer:output_type -> google.protobuf.Empty
27, // [27:43] is the sub-list for method output_type
11, // [11:27] is the sub-list for method input_type
11, // [11:11] is the sub-list for extension type_name
11, // [11:11] is the sub-list for extension extendee
0, // [0:11] is the sub-list for field type_name
}
func init() { file_internal_machine_api_pb_docker_proto_init() }
@@ -1763,7 +2115,7 @@ func file_internal_machine_api_pb_docker_proto_init() {
}
}
file_internal_machine_api_pb_docker_proto_msgTypes[18].Exporter = func(v any, i int) any {
switch v := v.(*CreateServiceContainerRequest); i {
switch v := v.(*CreateVolumeRequest); i {
case 0:
return &v.state
case 1:
@@ -1775,7 +2127,7 @@ func file_internal_machine_api_pb_docker_proto_init() {
}
}
file_internal_machine_api_pb_docker_proto_msgTypes[19].Exporter = func(v any, i int) any {
switch v := v.(*ServiceContainer); i {
switch v := v.(*CreateVolumeResponse); i {
case 0:
return &v.state
case 1:
@@ -1787,7 +2139,7 @@ func file_internal_machine_api_pb_docker_proto_init() {
}
}
file_internal_machine_api_pb_docker_proto_msgTypes[20].Exporter = func(v any, i int) any {
switch v := v.(*ListServiceContainersRequest); i {
switch v := v.(*ListVolumesRequest); i {
case 0:
return &v.state
case 1:
@@ -1799,7 +2151,7 @@ func file_internal_machine_api_pb_docker_proto_init() {
}
}
file_internal_machine_api_pb_docker_proto_msgTypes[21].Exporter = func(v any, i int) any {
switch v := v.(*ListServiceContainersResponse); i {
switch v := v.(*ListVolumesResponse); i {
case 0:
return &v.state
case 1:
@@ -1811,6 +2163,78 @@ func file_internal_machine_api_pb_docker_proto_init() {
}
}
file_internal_machine_api_pb_docker_proto_msgTypes[22].Exporter = func(v any, i int) any {
switch v := v.(*MachineVolumes); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_internal_machine_api_pb_docker_proto_msgTypes[23].Exporter = func(v any, i int) any {
switch v := v.(*RemoveVolumeRequest); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_internal_machine_api_pb_docker_proto_msgTypes[24].Exporter = func(v any, i int) any {
switch v := v.(*CreateServiceContainerRequest); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_internal_machine_api_pb_docker_proto_msgTypes[25].Exporter = func(v any, i int) any {
switch v := v.(*ServiceContainer); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_internal_machine_api_pb_docker_proto_msgTypes[26].Exporter = func(v any, i int) any {
switch v := v.(*ListServiceContainersRequest); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_internal_machine_api_pb_docker_proto_msgTypes[27].Exporter = func(v any, i int) any {
switch v := v.(*ListServiceContainersResponse); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_internal_machine_api_pb_docker_proto_msgTypes[28].Exporter = func(v any, i int) any {
switch v := v.(*MachineServiceContainers); i {
case 0:
return &v.state
@@ -1829,7 +2253,7 @@ func file_internal_machine_api_pb_docker_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_internal_machine_api_pb_docker_proto_rawDesc,
NumEnums: 0,
NumMessages: 23,
NumMessages: 29,
NumExtensions: 0,
NumServices: 1,
},
+36
View File
@@ -14,12 +14,17 @@ service Docker {
rpc StopContainer(StopContainerRequest) returns (google.protobuf.Empty);
rpc ListContainers(ListContainersRequest) returns (ListContainersResponse);
rpc RemoveContainer(RemoveContainerRequest) returns (google.protobuf.Empty);
rpc PullImage(PullImageRequest) returns (stream JSONMessage);
rpc InspectImage(InspectImageRequest) returns (InspectImageResponse);
// InspectRemoteImage returns the image metadata for an image in a remote registry using the machine's
// Docker auth credentials if necessary.
rpc InspectRemoteImage(InspectRemoteImageRequest) returns (InspectRemoteImageResponse);
rpc CreateVolume(CreateVolumeRequest) returns (CreateVolumeResponse);
rpc ListVolumes(ListVolumesRequest) returns (ListVolumesResponse);
rpc RemoveVolume(RemoveVolumeRequest) returns (google.protobuf.Empty);
rpc CreateServiceContainer(CreateServiceContainerRequest) returns (CreateContainerResponse);
rpc InspectServiceContainer(InspectContainerRequest) returns (ServiceContainer);
rpc ListServiceContainers(ListServiceContainersRequest) returns (ListServiceContainersResponse);
@@ -129,6 +134,37 @@ message RemoteImage {
bytes manifest = 3;
}
message CreateVolumeRequest {
// JSON serialised volume.CreateOptions.
bytes options = 1;
}
message CreateVolumeResponse {
// JSON serialised volume.Volume.
bytes volume = 1;
}
message ListVolumesRequest {
// JSON serialised volume.ListOptions.
bytes options = 1;
}
message ListVolumesResponse {
// Must contain only one repeated messages field to allow broadcasting ListVolumes requests to multiple machines.
repeated MachineVolumes messages = 1;
}
message MachineVolumes {
Metadata metadata = 1;
// JSON serialised volume.ListResponse.
bytes response = 2;
}
message RemoveVolumeRequest {
string id = 1;
bool force = 2;
}
message CreateServiceContainerRequest {
string service_id = 1;
// JSON serialised api.ServiceSpec.
+114
View File
@@ -29,6 +29,9 @@ const (
Docker_PullImage_FullMethodName = "/api.Docker/PullImage"
Docker_InspectImage_FullMethodName = "/api.Docker/InspectImage"
Docker_InspectRemoteImage_FullMethodName = "/api.Docker/InspectRemoteImage"
Docker_CreateVolume_FullMethodName = "/api.Docker/CreateVolume"
Docker_ListVolumes_FullMethodName = "/api.Docker/ListVolumes"
Docker_RemoveVolume_FullMethodName = "/api.Docker/RemoveVolume"
Docker_CreateServiceContainer_FullMethodName = "/api.Docker/CreateServiceContainer"
Docker_InspectServiceContainer_FullMethodName = "/api.Docker/InspectServiceContainer"
Docker_ListServiceContainers_FullMethodName = "/api.Docker/ListServiceContainers"
@@ -50,6 +53,9 @@ type DockerClient interface {
// InspectRemoteImage returns the image metadata for an image in a remote registry using the machine's
// Docker auth credentials if necessary.
InspectRemoteImage(ctx context.Context, in *InspectRemoteImageRequest, opts ...grpc.CallOption) (*InspectRemoteImageResponse, error)
CreateVolume(ctx context.Context, in *CreateVolumeRequest, opts ...grpc.CallOption) (*CreateVolumeResponse, error)
ListVolumes(ctx context.Context, in *ListVolumesRequest, opts ...grpc.CallOption) (*ListVolumesResponse, error)
RemoveVolume(ctx context.Context, in *RemoveVolumeRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
CreateServiceContainer(ctx context.Context, in *CreateServiceContainerRequest, opts ...grpc.CallOption) (*CreateContainerResponse, error)
InspectServiceContainer(ctx context.Context, in *InspectContainerRequest, opts ...grpc.CallOption) (*ServiceContainer, error)
ListServiceContainers(ctx context.Context, in *ListServiceContainersRequest, opts ...grpc.CallOption) (*ListServiceContainersResponse, error)
@@ -163,6 +169,36 @@ func (c *dockerClient) InspectRemoteImage(ctx context.Context, in *InspectRemote
return out, nil
}
func (c *dockerClient) CreateVolume(ctx context.Context, in *CreateVolumeRequest, opts ...grpc.CallOption) (*CreateVolumeResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(CreateVolumeResponse)
err := c.cc.Invoke(ctx, Docker_CreateVolume_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *dockerClient) ListVolumes(ctx context.Context, in *ListVolumesRequest, opts ...grpc.CallOption) (*ListVolumesResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ListVolumesResponse)
err := c.cc.Invoke(ctx, Docker_ListVolumes_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *dockerClient) RemoveVolume(ctx context.Context, in *RemoveVolumeRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(emptypb.Empty)
err := c.cc.Invoke(ctx, Docker_RemoveVolume_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *dockerClient) CreateServiceContainer(ctx context.Context, in *CreateServiceContainerRequest, opts ...grpc.CallOption) (*CreateContainerResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(CreateContainerResponse)
@@ -218,6 +254,9 @@ type DockerServer interface {
// InspectRemoteImage returns the image metadata for an image in a remote registry using the machine's
// Docker auth credentials if necessary.
InspectRemoteImage(context.Context, *InspectRemoteImageRequest) (*InspectRemoteImageResponse, error)
CreateVolume(context.Context, *CreateVolumeRequest) (*CreateVolumeResponse, error)
ListVolumes(context.Context, *ListVolumesRequest) (*ListVolumesResponse, error)
RemoveVolume(context.Context, *RemoveVolumeRequest) (*emptypb.Empty, error)
CreateServiceContainer(context.Context, *CreateServiceContainerRequest) (*CreateContainerResponse, error)
InspectServiceContainer(context.Context, *InspectContainerRequest) (*ServiceContainer, error)
ListServiceContainers(context.Context, *ListServiceContainersRequest) (*ListServiceContainersResponse, error)
@@ -259,6 +298,15 @@ func (UnimplementedDockerServer) InspectImage(context.Context, *InspectImageRequ
func (UnimplementedDockerServer) InspectRemoteImage(context.Context, *InspectRemoteImageRequest) (*InspectRemoteImageResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method InspectRemoteImage not implemented")
}
func (UnimplementedDockerServer) CreateVolume(context.Context, *CreateVolumeRequest) (*CreateVolumeResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method CreateVolume not implemented")
}
func (UnimplementedDockerServer) ListVolumes(context.Context, *ListVolumesRequest) (*ListVolumesResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ListVolumes not implemented")
}
func (UnimplementedDockerServer) RemoveVolume(context.Context, *RemoveVolumeRequest) (*emptypb.Empty, error) {
return nil, status.Errorf(codes.Unimplemented, "method RemoveVolume not implemented")
}
func (UnimplementedDockerServer) CreateServiceContainer(context.Context, *CreateServiceContainerRequest) (*CreateContainerResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method CreateServiceContainer not implemented")
}
@@ -447,6 +495,60 @@ func _Docker_InspectRemoteImage_Handler(srv interface{}, ctx context.Context, de
return interceptor(ctx, in, info, handler)
}
func _Docker_CreateVolume_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(CreateVolumeRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(DockerServer).CreateVolume(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: Docker_CreateVolume_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(DockerServer).CreateVolume(ctx, req.(*CreateVolumeRequest))
}
return interceptor(ctx, in, info, handler)
}
func _Docker_ListVolumes_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ListVolumesRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(DockerServer).ListVolumes(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: Docker_ListVolumes_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(DockerServer).ListVolumes(ctx, req.(*ListVolumesRequest))
}
return interceptor(ctx, in, info, handler)
}
func _Docker_RemoveVolume_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(RemoveVolumeRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(DockerServer).RemoveVolume(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: Docker_RemoveVolume_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(DockerServer).RemoveVolume(ctx, req.(*RemoveVolumeRequest))
}
return interceptor(ctx, in, info, handler)
}
func _Docker_CreateServiceContainer_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(CreateServiceContainerRequest)
if err := dec(in); err != nil {
@@ -558,6 +660,18 @@ var Docker_ServiceDesc = grpc.ServiceDesc{
MethodName: "InspectRemoteImage",
Handler: _Docker_InspectRemoteImage_Handler,
},
{
MethodName: "CreateVolume",
Handler: _Docker_CreateVolume_Handler,
},
{
MethodName: "ListVolumes",
Handler: _Docker_ListVolumes_Handler,
},
{
MethodName: "RemoveVolume",
Handler: _Docker_RemoveVolume_Handler,
},
{
MethodName: "CreateServiceContainer",
Handler: _Docker_CreateServiceContainer_Handler,
+275 -136
View File
@@ -163,6 +163,63 @@ func (x *NetworkConfig) GetPublicKey() []byte {
return nil
}
type CheckPrerequisitesResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
// Overall status of the checks.
Satisfied bool `protobuf:"varint,1,opt,name=satisfied,proto3" json:"satisfied,omitempty"`
// Error message if not satisfied (empty if all checks pass).
Error string `protobuf:"bytes,2,opt,name=error,proto3" json:"error,omitempty"`
}
func (x *CheckPrerequisitesResponse) Reset() {
*x = CheckPrerequisitesResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *CheckPrerequisitesResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*CheckPrerequisitesResponse) ProtoMessage() {}
func (x *CheckPrerequisitesResponse) ProtoReflect() protoreflect.Message {
mi := &file_internal_machine_api_pb_machine_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 CheckPrerequisitesResponse.ProtoReflect.Descriptor instead.
func (*CheckPrerequisitesResponse) Descriptor() ([]byte, []int) {
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{2}
}
func (x *CheckPrerequisitesResponse) GetSatisfied() bool {
if x != nil {
return x.Satisfied
}
return false
}
func (x *CheckPrerequisitesResponse) GetError() string {
if x != nil {
return x.Error
}
return ""
}
type InitClusterRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
@@ -180,7 +237,7 @@ type InitClusterRequest struct {
func (x *InitClusterRequest) Reset() {
*x = InitClusterRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[2]
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -193,7 +250,7 @@ func (x *InitClusterRequest) String() string {
func (*InitClusterRequest) ProtoMessage() {}
func (x *InitClusterRequest) ProtoReflect() protoreflect.Message {
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[2]
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[3]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -206,7 +263,7 @@ func (x *InitClusterRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use InitClusterRequest.ProtoReflect.Descriptor instead.
func (*InitClusterRequest) Descriptor() ([]byte, []int) {
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{2}
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{3}
}
func (x *InitClusterRequest) GetMachineName() string {
@@ -271,7 +328,7 @@ type InitClusterResponse struct {
func (x *InitClusterResponse) Reset() {
*x = InitClusterResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[3]
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -284,7 +341,7 @@ func (x *InitClusterResponse) String() string {
func (*InitClusterResponse) ProtoMessage() {}
func (x *InitClusterResponse) ProtoReflect() protoreflect.Message {
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[3]
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[4]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -297,7 +354,7 @@ func (x *InitClusterResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use InitClusterResponse.ProtoReflect.Descriptor instead.
func (*InitClusterResponse) Descriptor() ([]byte, []int) {
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{3}
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{4}
}
func (x *InitClusterResponse) GetMachine() *MachineInfo {
@@ -319,7 +376,7 @@ type JoinClusterRequest struct {
func (x *JoinClusterRequest) Reset() {
*x = JoinClusterRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[4]
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[5]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -332,7 +389,7 @@ func (x *JoinClusterRequest) String() string {
func (*JoinClusterRequest) ProtoMessage() {}
func (x *JoinClusterRequest) ProtoReflect() protoreflect.Message {
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[4]
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[5]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -345,7 +402,7 @@ func (x *JoinClusterRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use JoinClusterRequest.ProtoReflect.Descriptor instead.
func (*JoinClusterRequest) Descriptor() ([]byte, []int) {
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{4}
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{5}
}
func (x *JoinClusterRequest) GetMachine() *MachineInfo {
@@ -373,7 +430,7 @@ type TokenResponse struct {
func (x *TokenResponse) Reset() {
*x = TokenResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[5]
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[6]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -386,7 +443,7 @@ func (x *TokenResponse) String() string {
func (*TokenResponse) ProtoMessage() {}
func (x *TokenResponse) ProtoReflect() protoreflect.Message {
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[5]
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[6]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -399,7 +456,7 @@ func (x *TokenResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use TokenResponse.ProtoReflect.Descriptor instead.
func (*TokenResponse) Descriptor() ([]byte, []int) {
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{5}
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{6}
}
func (x *TokenResponse) GetToken() string {
@@ -409,6 +466,44 @@ func (x *TokenResponse) GetToken() string {
return ""
}
type ResetRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
}
func (x *ResetRequest) Reset() {
*x = ResetRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[7]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *ResetRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ResetRequest) ProtoMessage() {}
func (x *ResetRequest) ProtoReflect() protoreflect.Message {
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[7]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ResetRequest.ProtoReflect.Descriptor instead.
func (*ResetRequest) Descriptor() ([]byte, []int) {
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{7}
}
type Service struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
@@ -423,7 +518,7 @@ type Service struct {
func (x *Service) Reset() {
*x = Service{}
if protoimpl.UnsafeEnabled {
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[6]
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[8]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -436,7 +531,7 @@ func (x *Service) String() string {
func (*Service) ProtoMessage() {}
func (x *Service) ProtoReflect() protoreflect.Message {
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[6]
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[8]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -449,7 +544,7 @@ func (x *Service) ProtoReflect() protoreflect.Message {
// Deprecated: Use Service.ProtoReflect.Descriptor instead.
func (*Service) Descriptor() ([]byte, []int) {
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{6}
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{8}
}
func (x *Service) GetId() string {
@@ -491,7 +586,7 @@ type InspectServiceRequest struct {
func (x *InspectServiceRequest) Reset() {
*x = InspectServiceRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[7]
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[9]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -504,7 +599,7 @@ func (x *InspectServiceRequest) String() string {
func (*InspectServiceRequest) ProtoMessage() {}
func (x *InspectServiceRequest) ProtoReflect() protoreflect.Message {
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[7]
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[9]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -517,7 +612,7 @@ func (x *InspectServiceRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use InspectServiceRequest.ProtoReflect.Descriptor instead.
func (*InspectServiceRequest) Descriptor() ([]byte, []int) {
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{7}
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{9}
}
func (x *InspectServiceRequest) GetId() string {
@@ -538,7 +633,7 @@ type InspectServiceResponse struct {
func (x *InspectServiceResponse) Reset() {
*x = InspectServiceResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[8]
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[10]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -551,7 +646,7 @@ func (x *InspectServiceResponse) String() string {
func (*InspectServiceResponse) ProtoMessage() {}
func (x *InspectServiceResponse) ProtoReflect() protoreflect.Message {
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[8]
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[10]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -564,7 +659,7 @@ func (x *InspectServiceResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use InspectServiceResponse.ProtoReflect.Descriptor instead.
func (*InspectServiceResponse) Descriptor() ([]byte, []int) {
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{8}
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{10}
}
func (x *InspectServiceResponse) GetService() *Service {
@@ -587,7 +682,7 @@ type Service_Container struct {
func (x *Service_Container) Reset() {
*x = Service_Container{}
if protoimpl.UnsafeEnabled {
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[9]
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[11]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -600,7 +695,7 @@ func (x *Service_Container) String() string {
func (*Service_Container) ProtoMessage() {}
func (x *Service_Container) ProtoReflect() protoreflect.Message {
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[9]
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[11]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -613,7 +708,7 @@ func (x *Service_Container) ProtoReflect() protoreflect.Message {
// Deprecated: Use Service_Container.ProtoReflect.Descriptor instead.
func (*Service_Container) Descriptor() ([]byte, []int) {
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{6, 0}
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{8, 0}
}
func (x *Service_Container) GetMachineId() string {
@@ -659,77 +754,91 @@ var file_internal_machine_api_pb_machine_proto_rawDesc = []byte{
0x0b, 0x32, 0x0b, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x50, 0x50, 0x6f, 0x72, 0x74, 0x52, 0x09,
0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x70, 0x75, 0x62,
0x6c, 0x69, 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x70,
0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x22, 0xc3, 0x01, 0x0a, 0x12, 0x49, 0x6e, 0x69,
0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12,
0x20, 0x0a, 0x0b, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x01,
0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x4e, 0x61, 0x6d,
0x65, 0x12, 0x27, 0x0a, 0x07, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x18, 0x02, 0x20, 0x01,
0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x50, 0x50, 0x72, 0x65, 0x66, 0x69,
0x78, 0x52, 0x07, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x12, 0x26, 0x0a, 0x09, 0x70, 0x75,
0x62, 0x6c, 0x69, 0x63, 0x5f, 0x69, 0x70, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x07, 0x2e,
0x61, 0x70, 0x69, 0x2e, 0x49, 0x50, 0x48, 0x00, 0x52, 0x08, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63,
0x49, 0x70, 0x12, 0x26, 0x0a, 0x0e, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x69, 0x70, 0x5f,
0x61, 0x75, 0x74, 0x6f, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x48, 0x00, 0x52, 0x0c, 0x70, 0x75,
0x62, 0x6c, 0x69, 0x63, 0x49, 0x70, 0x41, 0x75, 0x74, 0x6f, 0x42, 0x12, 0x0a, 0x10, 0x70, 0x75,
0x62, 0x6c, 0x69, 0x63, 0x5f, 0x69, 0x70, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0x41,
0x0a, 0x13, 0x49, 0x6e, 0x69, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x73,
0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2a, 0x0a, 0x07, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65,
0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d, 0x61, 0x63,
0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x07, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e,
0x65, 0x22, 0x79, 0x0a, 0x12, 0x4a, 0x6f, 0x69, 0x6e, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72,
0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2a, 0x0a, 0x07, 0x6d, 0x61, 0x63, 0x68, 0x69,
0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x22, 0x50, 0x0a, 0x1a, 0x43, 0x68, 0x65, 0x63,
0x6b, 0x50, 0x72, 0x65, 0x72, 0x65, 0x71, 0x75, 0x69, 0x73, 0x69, 0x74, 0x65, 0x73, 0x52, 0x65,
0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x61, 0x74, 0x69, 0x73, 0x66,
0x69, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x73, 0x61, 0x74, 0x69, 0x73,
0x66, 0x69, 0x65, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x02, 0x20,
0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x22, 0xc3, 0x01, 0x0a, 0x12, 0x49,
0x6e, 0x69, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
0x74, 0x12, 0x20, 0x0a, 0x0b, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x4e, 0x61, 0x6d, 0x65,
0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x4e,
0x61, 0x6d, 0x65, 0x12, 0x27, 0x0a, 0x07, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x18, 0x02,
0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x50, 0x50, 0x72, 0x65,
0x66, 0x69, 0x78, 0x52, 0x07, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x12, 0x26, 0x0a, 0x09,
0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x69, 0x70, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32,
0x07, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x50, 0x48, 0x00, 0x52, 0x08, 0x70, 0x75, 0x62, 0x6c,
0x69, 0x63, 0x49, 0x70, 0x12, 0x26, 0x0a, 0x0e, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x69,
0x70, 0x5f, 0x61, 0x75, 0x74, 0x6f, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x48, 0x00, 0x52, 0x0c,
0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x49, 0x70, 0x41, 0x75, 0x74, 0x6f, 0x42, 0x12, 0x0a, 0x10,
0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x69, 0x70, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67,
0x22, 0x41, 0x0a, 0x13, 0x49, 0x6e, 0x69, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x52,
0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2a, 0x0a, 0x07, 0x6d, 0x61, 0x63, 0x68, 0x69,
0x6e, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d,
0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x07, 0x6d, 0x61, 0x63, 0x68,
0x69, 0x6e, 0x65, 0x12, 0x37, 0x0a, 0x0e, 0x6f, 0x74, 0x68, 0x65, 0x72, 0x5f, 0x6d, 0x61, 0x63,
0x68, 0x69, 0x6e, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x61, 0x70,
0x69, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x0d, 0x6f,
0x74, 0x68, 0x65, 0x72, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x22, 0x25, 0x0a, 0x0d,
0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x14, 0x0a,
0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x6f,
0x6b, 0x65, 0x6e, 0x22, 0xc3, 0x01, 0x0a, 0x07, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12,
0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12,
0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e,
0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28,
0x09, 0x52, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x12, 0x36, 0x0a, 0x0a, 0x63, 0x6f, 0x6e, 0x74, 0x61,
0x69, 0x6e, 0x65, 0x72, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x61, 0x70,
0x69, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69,
0x6e, 0x65, 0x72, 0x52, 0x0a, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x1a,
0x48, 0x0a, 0x09, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x1d, 0x0a, 0x0a,
0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
0x52, 0x09, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x64, 0x12, 0x1c, 0x0a, 0x09, 0x63,
0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09,
0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x22, 0x27, 0x0a, 0x15, 0x49, 0x6e, 0x73,
0x70, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65,
0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02,
0x69, 0x64, 0x22, 0x40, 0x0a, 0x16, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72,
0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x26, 0x0a, 0x07,
0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0c, 0x2e,
0x61, 0x70, 0x69, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x07, 0x73, 0x65, 0x72,
0x76, 0x69, 0x63, 0x65, 0x32, 0xc0, 0x02, 0x0a, 0x07, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65,
0x12, 0x40, 0x0a, 0x0b, 0x49, 0x6e, 0x69, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x12,
0x17, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x69, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65,
0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49,
0x6e, 0x69, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
0x73, 0x65, 0x12, 0x3e, 0x0a, 0x0b, 0x4a, 0x6f, 0x69, 0x6e, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65,
0x72, 0x12, 0x17, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4a, 0x6f, 0x69, 0x6e, 0x43, 0x6c, 0x75, 0x73,
0x74, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f,
0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70,
0x74, 0x79, 0x12, 0x33, 0x0a, 0x05, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x16, 0x2e, 0x67, 0x6f,
0x69, 0x6e, 0x65, 0x22, 0x79, 0x0a, 0x12, 0x4a, 0x6f, 0x69, 0x6e, 0x43, 0x6c, 0x75, 0x73, 0x74,
0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2a, 0x0a, 0x07, 0x6d, 0x61, 0x63,
0x68, 0x69, 0x6e, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x61, 0x70, 0x69,
0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x07, 0x6d, 0x61,
0x63, 0x68, 0x69, 0x6e, 0x65, 0x12, 0x37, 0x0a, 0x0e, 0x6f, 0x74, 0x68, 0x65, 0x72, 0x5f, 0x6d,
0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x10, 0x2e,
0x61, 0x70, 0x69, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52,
0x0d, 0x6f, 0x74, 0x68, 0x65, 0x72, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x22, 0x25,
0x0a, 0x0d, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12,
0x14, 0x0a, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05,
0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0x0e, 0x0a, 0x0c, 0x52, 0x65, 0x73, 0x65, 0x74, 0x52, 0x65,
0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0xc3, 0x01, 0x0a, 0x07, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63,
0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69,
0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52,
0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x18, 0x03, 0x20,
0x01, 0x28, 0x09, 0x52, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x12, 0x36, 0x0a, 0x0a, 0x63, 0x6f, 0x6e,
0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e,
0x61, 0x70, 0x69, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x43, 0x6f, 0x6e, 0x74,
0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x0a, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72,
0x73, 0x1a, 0x48, 0x0a, 0x09, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x1d,
0x0a, 0x0a, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01,
0x28, 0x09, 0x52, 0x09, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x64, 0x12, 0x1c, 0x0a,
0x09, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c,
0x52, 0x09, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x22, 0x27, 0x0a, 0x15, 0x49,
0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x71,
0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
0x52, 0x02, 0x69, 0x64, 0x22, 0x40, 0x0a, 0x16, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53,
0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x26,
0x0a, 0x07, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32,
0x0c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x07, 0x73,
0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x32, 0xc3, 0x03, 0x0a, 0x07, 0x4d, 0x61, 0x63, 0x68, 0x69,
0x6e, 0x65, 0x12, 0x4d, 0x0a, 0x12, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x50, 0x72, 0x65, 0x72, 0x65,
0x71, 0x75, 0x69, 0x73, 0x69, 0x74, 0x65, 0x73, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c,
0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79,
0x1a, 0x1f, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x50, 0x72, 0x65, 0x72,
0x65, 0x71, 0x75, 0x69, 0x73, 0x69, 0x74, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
0x65, 0x12, 0x40, 0x0a, 0x0b, 0x49, 0x6e, 0x69, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72,
0x12, 0x17, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x69, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74,
0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x61, 0x70, 0x69, 0x2e,
0x49, 0x6e, 0x69, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f,
0x6e, 0x73, 0x65, 0x12, 0x3e, 0x0a, 0x0b, 0x4a, 0x6f, 0x69, 0x6e, 0x43, 0x6c, 0x75, 0x73, 0x74,
0x65, 0x72, 0x12, 0x17, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4a, 0x6f, 0x69, 0x6e, 0x43, 0x6c, 0x75,
0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f,
0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d,
0x70, 0x74, 0x79, 0x1a, 0x12, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52,
0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x33, 0x0a, 0x07, 0x49, 0x6e, 0x73, 0x70, 0x65,
0x63, 0x74, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74,
0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x10, 0x2e, 0x61, 0x70, 0x69,
0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x49, 0x0a, 0x0e,
0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x1a,
0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72, 0x76,
0x69, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x61, 0x70, 0x69,
0x2e, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52,
0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x37, 0x5a, 0x35, 0x67, 0x69, 0x74, 0x68, 0x75,
0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x70, 0x73, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x6b, 0x69,
0x2f, 0x75, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61,
0x6c, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x62,
0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
0x70, 0x74, 0x79, 0x12, 0x33, 0x0a, 0x05, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x16, 0x2e, 0x67,
0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45,
0x6d, 0x70, 0x74, 0x79, 0x1a, 0x12, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x54, 0x6f, 0x6b, 0x65, 0x6e,
0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x33, 0x0a, 0x07, 0x49, 0x6e, 0x73, 0x70,
0x65, 0x63, 0x74, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f,
0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x10, 0x2e, 0x61, 0x70,
0x69, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x32, 0x0a,
0x05, 0x52, 0x65, 0x73, 0x65, 0x74, 0x12, 0x11, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x52, 0x65, 0x73,
0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67,
0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74,
0x79, 0x12, 0x49, 0x0a, 0x0e, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72, 0x76,
0x69, 0x63, 0x65, 0x12, 0x1a, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63,
0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a,
0x1b, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72,
0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x37, 0x5a, 0x35,
0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x70, 0x73, 0x76, 0x69, 0x64,
0x65, 0x72, 0x73, 0x6b, 0x69, 0x2f, 0x75, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2f, 0x69, 0x6e,
0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x2f, 0x61,
0x70, 0x69, 0x2f, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
@@ -744,48 +853,54 @@ func file_internal_machine_api_pb_machine_proto_rawDescGZIP() []byte {
return file_internal_machine_api_pb_machine_proto_rawDescData
}
var file_internal_machine_api_pb_machine_proto_msgTypes = make([]protoimpl.MessageInfo, 10)
var file_internal_machine_api_pb_machine_proto_msgTypes = make([]protoimpl.MessageInfo, 12)
var file_internal_machine_api_pb_machine_proto_goTypes = []any{
(*MachineInfo)(nil), // 0: api.MachineInfo
(*NetworkConfig)(nil), // 1: api.NetworkConfig
(*InitClusterRequest)(nil), // 2: api.InitClusterRequest
(*InitClusterResponse)(nil), // 3: api.InitClusterResponse
(*JoinClusterRequest)(nil), // 4: api.JoinClusterRequest
(*TokenResponse)(nil), // 5: api.TokenResponse
(*Service)(nil), // 6: api.Service
(*InspectServiceRequest)(nil), // 7: api.InspectServiceRequest
(*InspectServiceResponse)(nil), // 8: api.InspectServiceResponse
(*Service_Container)(nil), // 9: api.Service.Container
(*IP)(nil), // 10: api.IP
(*IPPrefix)(nil), // 11: api.IPPrefix
(*IPPort)(nil), // 12: api.IPPort
(*emptypb.Empty)(nil), // 13: google.protobuf.Empty
(*MachineInfo)(nil), // 0: api.MachineInfo
(*NetworkConfig)(nil), // 1: api.NetworkConfig
(*CheckPrerequisitesResponse)(nil), // 2: api.CheckPrerequisitesResponse
(*InitClusterRequest)(nil), // 3: api.InitClusterRequest
(*InitClusterResponse)(nil), // 4: api.InitClusterResponse
(*JoinClusterRequest)(nil), // 5: api.JoinClusterRequest
(*TokenResponse)(nil), // 6: api.TokenResponse
(*ResetRequest)(nil), // 7: api.ResetRequest
(*Service)(nil), // 8: api.Service
(*InspectServiceRequest)(nil), // 9: api.InspectServiceRequest
(*InspectServiceResponse)(nil), // 10: api.InspectServiceResponse
(*Service_Container)(nil), // 11: api.Service.Container
(*IP)(nil), // 12: api.IP
(*IPPrefix)(nil), // 13: api.IPPrefix
(*IPPort)(nil), // 14: api.IPPort
(*emptypb.Empty)(nil), // 15: google.protobuf.Empty
}
var file_internal_machine_api_pb_machine_proto_depIdxs = []int32{
1, // 0: api.MachineInfo.network:type_name -> api.NetworkConfig
10, // 1: api.MachineInfo.public_ip:type_name -> api.IP
11, // 2: api.NetworkConfig.subnet:type_name -> api.IPPrefix
10, // 3: api.NetworkConfig.management_ip:type_name -> api.IP
12, // 4: api.NetworkConfig.endpoints:type_name -> api.IPPort
11, // 5: api.InitClusterRequest.network:type_name -> api.IPPrefix
10, // 6: api.InitClusterRequest.public_ip:type_name -> api.IP
12, // 1: api.MachineInfo.public_ip:type_name -> api.IP
13, // 2: api.NetworkConfig.subnet:type_name -> api.IPPrefix
12, // 3: api.NetworkConfig.management_ip:type_name -> api.IP
14, // 4: api.NetworkConfig.endpoints:type_name -> api.IPPort
13, // 5: api.InitClusterRequest.network:type_name -> api.IPPrefix
12, // 6: api.InitClusterRequest.public_ip:type_name -> api.IP
0, // 7: api.InitClusterResponse.machine:type_name -> api.MachineInfo
0, // 8: api.JoinClusterRequest.machine:type_name -> api.MachineInfo
0, // 9: api.JoinClusterRequest.other_machines:type_name -> api.MachineInfo
9, // 10: api.Service.containers:type_name -> api.Service.Container
6, // 11: api.InspectServiceResponse.service:type_name -> api.Service
2, // 12: api.Machine.InitCluster:input_type -> api.InitClusterRequest
4, // 13: api.Machine.JoinCluster:input_type -> api.JoinClusterRequest
13, // 14: api.Machine.Token:input_type -> google.protobuf.Empty
13, // 15: api.Machine.Inspect:input_type -> google.protobuf.Empty
7, // 16: api.Machine.InspectService:input_type -> api.InspectServiceRequest
3, // 17: api.Machine.InitCluster:output_type -> api.InitClusterResponse
13, // 18: api.Machine.JoinCluster:output_type -> google.protobuf.Empty
5, // 19: api.Machine.Token:output_type -> api.TokenResponse
0, // 20: api.Machine.Inspect:output_type -> api.MachineInfo
8, // 21: api.Machine.InspectService:output_type -> api.InspectServiceResponse
17, // [17:22] is the sub-list for method output_type
12, // [12:17] is the sub-list for method input_type
11, // 10: api.Service.containers:type_name -> api.Service.Container
8, // 11: api.InspectServiceResponse.service:type_name -> api.Service
15, // 12: api.Machine.CheckPrerequisites:input_type -> google.protobuf.Empty
3, // 13: api.Machine.InitCluster:input_type -> api.InitClusterRequest
5, // 14: api.Machine.JoinCluster:input_type -> api.JoinClusterRequest
15, // 15: api.Machine.Token:input_type -> google.protobuf.Empty
15, // 16: api.Machine.Inspect:input_type -> google.protobuf.Empty
7, // 17: api.Machine.Reset:input_type -> api.ResetRequest
9, // 18: api.Machine.InspectService:input_type -> api.InspectServiceRequest
2, // 19: api.Machine.CheckPrerequisites:output_type -> api.CheckPrerequisitesResponse
4, // 20: api.Machine.InitCluster:output_type -> api.InitClusterResponse
15, // 21: api.Machine.JoinCluster:output_type -> google.protobuf.Empty
6, // 22: api.Machine.Token:output_type -> api.TokenResponse
0, // 23: api.Machine.Inspect:output_type -> api.MachineInfo
15, // 24: api.Machine.Reset:output_type -> google.protobuf.Empty
10, // 25: api.Machine.InspectService:output_type -> api.InspectServiceResponse
19, // [19:26] is the sub-list for method output_type
12, // [12:19] is the sub-list for method input_type
12, // [12:12] is the sub-list for extension type_name
12, // [12:12] is the sub-list for extension extendee
0, // [0:12] is the sub-list for field type_name
@@ -823,7 +938,7 @@ func file_internal_machine_api_pb_machine_proto_init() {
}
}
file_internal_machine_api_pb_machine_proto_msgTypes[2].Exporter = func(v any, i int) any {
switch v := v.(*InitClusterRequest); i {
switch v := v.(*CheckPrerequisitesResponse); i {
case 0:
return &v.state
case 1:
@@ -835,7 +950,7 @@ func file_internal_machine_api_pb_machine_proto_init() {
}
}
file_internal_machine_api_pb_machine_proto_msgTypes[3].Exporter = func(v any, i int) any {
switch v := v.(*InitClusterResponse); i {
switch v := v.(*InitClusterRequest); i {
case 0:
return &v.state
case 1:
@@ -847,7 +962,7 @@ func file_internal_machine_api_pb_machine_proto_init() {
}
}
file_internal_machine_api_pb_machine_proto_msgTypes[4].Exporter = func(v any, i int) any {
switch v := v.(*JoinClusterRequest); i {
switch v := v.(*InitClusterResponse); i {
case 0:
return &v.state
case 1:
@@ -859,7 +974,7 @@ func file_internal_machine_api_pb_machine_proto_init() {
}
}
file_internal_machine_api_pb_machine_proto_msgTypes[5].Exporter = func(v any, i int) any {
switch v := v.(*TokenResponse); i {
switch v := v.(*JoinClusterRequest); i {
case 0:
return &v.state
case 1:
@@ -871,7 +986,7 @@ func file_internal_machine_api_pb_machine_proto_init() {
}
}
file_internal_machine_api_pb_machine_proto_msgTypes[6].Exporter = func(v any, i int) any {
switch v := v.(*Service); i {
switch v := v.(*TokenResponse); i {
case 0:
return &v.state
case 1:
@@ -883,7 +998,7 @@ func file_internal_machine_api_pb_machine_proto_init() {
}
}
file_internal_machine_api_pb_machine_proto_msgTypes[7].Exporter = func(v any, i int) any {
switch v := v.(*InspectServiceRequest); i {
switch v := v.(*ResetRequest); i {
case 0:
return &v.state
case 1:
@@ -895,7 +1010,7 @@ func file_internal_machine_api_pb_machine_proto_init() {
}
}
file_internal_machine_api_pb_machine_proto_msgTypes[8].Exporter = func(v any, i int) any {
switch v := v.(*InspectServiceResponse); i {
switch v := v.(*Service); i {
case 0:
return &v.state
case 1:
@@ -907,6 +1022,30 @@ func file_internal_machine_api_pb_machine_proto_init() {
}
}
file_internal_machine_api_pb_machine_proto_msgTypes[9].Exporter = func(v any, i int) any {
switch v := v.(*InspectServiceRequest); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_internal_machine_api_pb_machine_proto_msgTypes[10].Exporter = func(v any, i int) any {
switch v := v.(*InspectServiceResponse); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_internal_machine_api_pb_machine_proto_msgTypes[11].Exporter = func(v any, i int) any {
switch v := v.(*Service_Container); i {
case 0:
return &v.state
@@ -919,7 +1058,7 @@ func file_internal_machine_api_pb_machine_proto_init() {
}
}
}
file_internal_machine_api_pb_machine_proto_msgTypes[2].OneofWrappers = []any{
file_internal_machine_api_pb_machine_proto_msgTypes[3].OneofWrappers = []any{
(*InitClusterRequest_PublicIp)(nil),
(*InitClusterRequest_PublicIpAuto)(nil),
}
@@ -929,7 +1068,7 @@ func file_internal_machine_api_pb_machine_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_internal_machine_api_pb_machine_proto_rawDesc,
NumEnums: 0,
NumMessages: 10,
NumMessages: 12,
NumExtensions: 0,
NumServices: 1,
},
+15
View File
@@ -8,10 +8,15 @@ import "google/protobuf/empty.proto";
import "internal/machine/api/pb/common.proto";
service Machine {
// CheckPrerequisites verifies if the machine meets all necessary system requirements to participate in the cluster.
rpc CheckPrerequisites(google.protobuf.Empty) returns (CheckPrerequisitesResponse);
rpc InitCluster(InitClusterRequest) returns (InitClusterResponse);
rpc JoinCluster(JoinClusterRequest) returns (google.protobuf.Empty);
rpc Token(google.protobuf.Empty) returns (TokenResponse);
rpc Inspect(google.protobuf.Empty) returns (MachineInfo);
// Reset restores the machine to a clean state, removing all cluster-related сonfiguration and data.
rpc Reset(ResetRequest) returns (google.protobuf.Empty);
rpc InspectService(InspectServiceRequest) returns (InspectServiceResponse);
}
@@ -29,6 +34,13 @@ message NetworkConfig {
bytes public_key = 4;
}
message CheckPrerequisitesResponse {
// Overall status of the checks.
bool satisfied = 1;
// Error message if not satisfied (empty if all checks pass).
string error = 2;
}
message InitClusterRequest {
string machineName = 1;
IPPrefix network = 2;
@@ -52,6 +64,9 @@ message TokenResponse {
string token = 1;
}
message ResetRequest {
}
message Service {
string id = 1;
string name = 2;
+85 -5
View File
@@ -20,21 +20,27 @@ import (
const _ = grpc.SupportPackageIsVersion9
const (
Machine_InitCluster_FullMethodName = "/api.Machine/InitCluster"
Machine_JoinCluster_FullMethodName = "/api.Machine/JoinCluster"
Machine_Token_FullMethodName = "/api.Machine/Token"
Machine_Inspect_FullMethodName = "/api.Machine/Inspect"
Machine_InspectService_FullMethodName = "/api.Machine/InspectService"
Machine_CheckPrerequisites_FullMethodName = "/api.Machine/CheckPrerequisites"
Machine_InitCluster_FullMethodName = "/api.Machine/InitCluster"
Machine_JoinCluster_FullMethodName = "/api.Machine/JoinCluster"
Machine_Token_FullMethodName = "/api.Machine/Token"
Machine_Inspect_FullMethodName = "/api.Machine/Inspect"
Machine_Reset_FullMethodName = "/api.Machine/Reset"
Machine_InspectService_FullMethodName = "/api.Machine/InspectService"
)
// MachineClient is the client API for Machine 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.
type MachineClient interface {
// CheckPrerequisites verifies if the machine meets all necessary system requirements to participate in the cluster.
CheckPrerequisites(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*CheckPrerequisitesResponse, error)
InitCluster(ctx context.Context, in *InitClusterRequest, opts ...grpc.CallOption) (*InitClusterResponse, error)
JoinCluster(ctx context.Context, in *JoinClusterRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
Token(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*TokenResponse, error)
Inspect(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*MachineInfo, error)
// Reset restores the machine to a clean state, removing all cluster-related сonfiguration and data.
Reset(ctx context.Context, in *ResetRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
InspectService(ctx context.Context, in *InspectServiceRequest, opts ...grpc.CallOption) (*InspectServiceResponse, error)
}
@@ -46,6 +52,16 @@ func NewMachineClient(cc grpc.ClientConnInterface) MachineClient {
return &machineClient{cc}
}
func (c *machineClient) CheckPrerequisites(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*CheckPrerequisitesResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(CheckPrerequisitesResponse)
err := c.cc.Invoke(ctx, Machine_CheckPrerequisites_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *machineClient) InitCluster(ctx context.Context, in *InitClusterRequest, opts ...grpc.CallOption) (*InitClusterResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(InitClusterResponse)
@@ -86,6 +102,16 @@ func (c *machineClient) Inspect(ctx context.Context, in *emptypb.Empty, opts ...
return out, nil
}
func (c *machineClient) Reset(ctx context.Context, in *ResetRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(emptypb.Empty)
err := c.cc.Invoke(ctx, Machine_Reset_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *machineClient) InspectService(ctx context.Context, in *InspectServiceRequest, opts ...grpc.CallOption) (*InspectServiceResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(InspectServiceResponse)
@@ -100,10 +126,14 @@ func (c *machineClient) InspectService(ctx context.Context, in *InspectServiceRe
// All implementations must embed UnimplementedMachineServer
// for forward compatibility.
type MachineServer interface {
// CheckPrerequisites verifies if the machine meets all necessary system requirements to participate in the cluster.
CheckPrerequisites(context.Context, *emptypb.Empty) (*CheckPrerequisitesResponse, error)
InitCluster(context.Context, *InitClusterRequest) (*InitClusterResponse, error)
JoinCluster(context.Context, *JoinClusterRequest) (*emptypb.Empty, error)
Token(context.Context, *emptypb.Empty) (*TokenResponse, error)
Inspect(context.Context, *emptypb.Empty) (*MachineInfo, error)
// Reset restores the machine to a clean state, removing all cluster-related сonfiguration and data.
Reset(context.Context, *ResetRequest) (*emptypb.Empty, error)
InspectService(context.Context, *InspectServiceRequest) (*InspectServiceResponse, error)
mustEmbedUnimplementedMachineServer()
}
@@ -115,6 +145,9 @@ type MachineServer interface {
// pointer dereference when methods are called.
type UnimplementedMachineServer struct{}
func (UnimplementedMachineServer) CheckPrerequisites(context.Context, *emptypb.Empty) (*CheckPrerequisitesResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method CheckPrerequisites not implemented")
}
func (UnimplementedMachineServer) InitCluster(context.Context, *InitClusterRequest) (*InitClusterResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method InitCluster not implemented")
}
@@ -127,6 +160,9 @@ func (UnimplementedMachineServer) Token(context.Context, *emptypb.Empty) (*Token
func (UnimplementedMachineServer) Inspect(context.Context, *emptypb.Empty) (*MachineInfo, error) {
return nil, status.Errorf(codes.Unimplemented, "method Inspect not implemented")
}
func (UnimplementedMachineServer) Reset(context.Context, *ResetRequest) (*emptypb.Empty, error) {
return nil, status.Errorf(codes.Unimplemented, "method Reset not implemented")
}
func (UnimplementedMachineServer) InspectService(context.Context, *InspectServiceRequest) (*InspectServiceResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method InspectService not implemented")
}
@@ -151,6 +187,24 @@ func RegisterMachineServer(s grpc.ServiceRegistrar, srv MachineServer) {
s.RegisterService(&Machine_ServiceDesc, srv)
}
func _Machine_CheckPrerequisites_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(emptypb.Empty)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(MachineServer).CheckPrerequisites(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: Machine_CheckPrerequisites_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(MachineServer).CheckPrerequisites(ctx, req.(*emptypb.Empty))
}
return interceptor(ctx, in, info, handler)
}
func _Machine_InitCluster_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(InitClusterRequest)
if err := dec(in); err != nil {
@@ -223,6 +277,24 @@ func _Machine_Inspect_Handler(srv interface{}, ctx context.Context, dec func(int
return interceptor(ctx, in, info, handler)
}
func _Machine_Reset_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ResetRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(MachineServer).Reset(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: Machine_Reset_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(MachineServer).Reset(ctx, req.(*ResetRequest))
}
return interceptor(ctx, in, info, handler)
}
func _Machine_InspectService_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(InspectServiceRequest)
if err := dec(in); err != nil {
@@ -248,6 +320,10 @@ var Machine_ServiceDesc = grpc.ServiceDesc{
ServiceName: "api.Machine",
HandlerType: (*MachineServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "CheckPrerequisites",
Handler: _Machine_CheckPrerequisites_Handler,
},
{
MethodName: "InitCluster",
Handler: _Machine_InitCluster_Handler,
@@ -264,6 +340,10 @@ var Machine_ServiceDesc = grpc.ServiceDesc{
MethodName: "Inspect",
Handler: _Machine_Inspect_Handler,
},
{
MethodName: "Reset",
Handler: _Machine_Reset_Handler,
},
{
MethodName: "InspectService",
Handler: _Machine_InspectService_Handler,
@@ -1,4 +1,4 @@
package caddyfile
package caddyconfig
import (
"encoding/json"
@@ -10,12 +10,12 @@ import (
"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/internal/machine/docker"
"github.com/psviderski/uncloud/pkg/api"
)
@@ -24,34 +24,38 @@ func GenerateConfig(containers []api.ServiceContainer, verifyResponse string) (*
httpHostUpstreams := make(map[string][]string)
httpsHostUpstreams := make(map[string][]string)
for _, ctr := range containers {
logger := slog.With("container", ctr.ID)
network, ok := ctr.NetworkSettings.Networks[docker.NetworkName]
if !ok {
// Container is not connected to the uncloud Docker network (could be host network).
continue
}
if network.IPAddress == "" {
logger.Error("Container has no IPv4 address.")
if !ctr.Healthy() {
continue
}
ip := ctr.UncloudNetworkIP()
if !ip.IsValid() {
// Container is not connected to the uncloud Docker network (could be host network).
continue
}
log := slog.With("container", ctr.ID)
ports, err := ctr.ServicePorts()
if err != nil {
logger.Error("Failed to parse service ports for container.", "err", err)
log.Error("Failed to parse service ports for container.", "err", err)
continue
}
for _, port := range ports {
if port.Mode != api.PortModeIngress {
continue
}
switch port.Protocol {
case api.ProtocolHTTP:
upstream := net.JoinHostPort(network.IPAddress, strconv.Itoa(int(port.ContainerPort)))
upstream := net.JoinHostPort(ip.String(), strconv.Itoa(int(port.ContainerPort)))
httpHostUpstreams[port.Hostname] = append(httpHostUpstreams[port.Hostname], upstream)
case api.ProtocolHTTPS:
upstream := net.JoinHostPort(network.IPAddress, strconv.Itoa(int(port.ContainerPort)))
upstream := net.JoinHostPort(ip.String(), strconv.Itoa(int(port.ContainerPort)))
httpsHostUpstreams[port.Hostname] = append(httpsHostUpstreams[port.Hostname], upstream)
default:
// TODO: implement L4 ingress routing for TCP and UDP.
logger.Error("Unsupported protocol for ingress port.", "port", port)
log.Error("Unsupported protocol for ingress port.", "port", port)
continue
}
}
@@ -61,6 +65,8 @@ func GenerateConfig(containers []api.ServiceContainer, verifyResponse string) (*
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.
@@ -69,6 +75,8 @@ func GenerateConfig(containers []api.ServiceContainer, verifyResponse string) (*
}
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),
}
@@ -109,6 +117,16 @@ func hostUpstreamsToRoutes(hostUpstreams map[string][]string, warnings *[]caddyc
}
}
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,
}
@@ -1,4 +1,4 @@
package caddyfile
package caddyconfig
import (
"strings"
@@ -14,6 +14,27 @@ import (
)
func TestGenerateConfig(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
@@ -23,25 +44,8 @@ func TestGenerateConfig(t *testing.T) {
{
name: "empty containers",
containers: []api.ServiceContainer{},
want: `{
"servers": {
"http": {
"listen": [":80"],
"routes": [{
"match": [{"path": ["/.uncloud-verify"]}],
"handle": [{
"body": "verification-response-body",
"handler": "static_response",
"status_code": 200
}]
}]
},
"https": {
"listen": [":443"]
}
}
}`,
wantErr: false,
want: configWithoutServices,
wantErr: false,
},
{
@@ -58,6 +62,14 @@ func TestGenerateConfig(t *testing.T) {
"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"}]
}]
},
@@ -69,10 +81,12 @@ func TestGenerateConfig(t *testing.T) {
"status_code": 200
}]
}
]
],
"logs": {}
},
"https": {
"listen": [":443"]
"listen": [":443"],
"logs": {}
}
}
}`,
@@ -93,6 +107,14 @@ func TestGenerateConfig(t *testing.T) {
"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"}
@@ -107,10 +129,12 @@ func TestGenerateConfig(t *testing.T) {
"status_code": 200
}]
}
]
],
"logs": {}
},
"https": {
"listen": [":443"]
"listen": [":443"],
"logs": {}
}
}
}`,
@@ -134,7 +158,8 @@ func TestGenerateConfig(t *testing.T) {
"status_code": 200
}]
}
]
],
"logs": {}
},
"https": {
"listen": [":443"],
@@ -143,10 +168,19 @@ func TestGenerateConfig(t *testing.T) {
"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": {}
}
}
}`,
@@ -178,6 +212,14 @@ func TestGenerateConfig(t *testing.T) {
"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"},
@@ -189,6 +231,14 @@ func TestGenerateConfig(t *testing.T) {
"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"},
@@ -204,7 +254,8 @@ func TestGenerateConfig(t *testing.T) {
"status_code": 200
}]
}
]
],
"logs": {}
},
"https": {
"listen": [":443"],
@@ -213,6 +264,14 @@ func TestGenerateConfig(t *testing.T) {
"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"},
@@ -220,7 +279,8 @@ func TestGenerateConfig(t *testing.T) {
]
}]
}
]
],
"logs": {}
}
}
}`,
@@ -231,24 +291,7 @@ func TestGenerateConfig(t *testing.T) {
containers: []api.ServiceContainer{
newContainerWithoutNetwork("ignored.example.com:8080/http"),
},
want: `{
"servers": {
"http": {
"listen": [":80"],
"routes": [{
"match": [{"path": ["/.uncloud-verify"]}],
"handle": [{
"body": "verification-response-body",
"handler": "static_response",
"status_code": 200
}]
}]
},
"https": {
"listen": [":443"]
}
}
}`,
want: configWithoutServices,
wantErr: false,
},
{
@@ -256,48 +299,76 @@ func TestGenerateConfig(t *testing.T) {
containers: []api.ServiceContainer{
newContainer("10.210.0.2", "invalid-port"),
},
want: `{
"servers": {
"http": {
"listen": [":80"],
"routes": [{
"match": [{"path": ["/.uncloud-verify"]}],
"handle": [{
"body": "verification-response-body",
"handler": "static_response",
"status_code": 200
}]
}]
},
"https": {
"listen": [":443"]
}
}
}`,
want: configWithoutServices,
wantErr: false,
},
{
name: "containers with unsupported protocols ignored",
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,
},
{
name: "restarting container ignored",
containers: []api.ServiceContainer{
newRestartingContainer("10.210.0.2", "app.example.com:8080/http"),
},
want: configWithoutServices,
wantErr: false,
},
{
name: "stopped container ignored",
containers: []api.ServiceContainer{
newStoppedContainer("10.210.0.2", "app.example.com:8080/http"),
},
want: configWithoutServices,
wantErr: false,
},
{
name: "mix of running, restarting, and stopped containers",
containers: []api.ServiceContainer{
newContainer("10.210.0.2", "app.example.com:8080/http"),
newRestartingContainer("10.210.0.3", "app.example.com:8080/http"),
newStoppedContainer("10.210.0.4", "app.example.com:8080/http"),
},
want: `{
"servers": {
"http": {
"listen": [":80"],
"routes": [{
"match": [{"path": ["/.uncloud-verify"]}],
"handle": [{
"body": "verification-response-body",
"handler": "static_response",
"status_code": 200
}]
}]
"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"]
"listen": [":443"],
"logs": {}
}
}
}`,
@@ -326,7 +397,11 @@ func TestGenerateConfig(t *testing.T) {
func newContainer(ip string, ports ...string) api.ServiceContainer {
portsLabel := strings.Join(ports, ",")
return api.ServiceContainer{Container: api.Container{ContainerJSON: types.ContainerJSON{
ContainerJSONBase: &types.ContainerJSONBase{},
ContainerJSONBase: &types.ContainerJSONBase{
State: &types.ContainerState{
Running: true,
},
},
NetworkSettings: &types.NetworkSettings{
Networks: map[string]*network.EndpointSettings{
docker.NetworkName: {
@@ -345,7 +420,11 @@ func newContainer(ip string, ports ...string) api.ServiceContainer {
func newContainerWithoutNetwork(ports ...string) api.ServiceContainer {
portsLabel := strings.Join(ports, ",")
return api.ServiceContainer{Container: api.Container{ContainerJSON: types.ContainerJSON{
ContainerJSONBase: &types.ContainerJSONBase{},
ContainerJSONBase: &types.ContainerJSONBase{
State: &types.ContainerState{
Running: true,
},
},
NetworkSettings: &types.NetworkSettings{
Networks: map[string]*network.EndpointSettings{
"other-network": {
@@ -360,3 +439,15 @@ func newContainerWithoutNetwork(ports ...string) api.ServiceContainer {
},
}}}
}
func newRestartingContainer(ip string, ports ...string) api.ServiceContainer {
ctr := newContainer(ip, ports...)
ctr.Container.State.Restarting = true
return ctr
}
func newStoppedContainer(ip string, ports ...string) api.ServiceContainer {
ctr := newContainer(ip, ports...)
ctr.Container.State.Running = false
return ctr
}
@@ -1,4 +1,4 @@
package caddyfile
package caddyconfig
import (
"context"
@@ -19,7 +19,7 @@ const (
)
// Controller monitors container changes in the cluster store and generates a configuration file for Caddy reverse
// proxy. The generated Caddyfile allows Caddy to route external traffic to service containers across the internal
// proxy. The generated configuration allows Caddy to route external traffic to service containers across the internal
// network.
type Controller struct {
store *store.Store
-5
View File
@@ -36,16 +36,11 @@ func NewDB(path string) (*sqlx.DB, error) {
schema := `
CREATE TABLE IF NOT EXISTS containers (
id TEXT NOT NULL PRIMARY KEY,
service_id TEXT NOT NULL,
service_name TEXT AS (json_extract(service_spec, '$.Name')),
service_spec TEXT NOT NULL CHECK (json_valid(service_spec)),
-- 'subsecond' modifier is used to store timestamps with millisecond precision.
created_at TIMESTAMP NOT NULL DEFAULT (datetime('subsecond')),
updated_at TIMESTAMP NOT NULL DEFAULT (datetime('subsecond'))
);
CREATE INDEX IF NOT EXISTS idx_containers_service_id ON containers (service_id);
CREATE INDEX IF NOT EXISTS idx_containers_service_name ON containers (service_name);
`
if _, err = db.Exec(schema); err != nil {
+126
View File
@@ -0,0 +1,126 @@
package dns
import (
"context"
"fmt"
"log/slog"
"net/netip"
"strings"
"sync"
"time"
"github.com/psviderski/uncloud/internal/machine/store"
"github.com/psviderski/uncloud/pkg/api"
)
// ClusterResolver implements Resolver by tracking containers in the cluster and resolving service names
// to their IP addresses.
type ClusterResolver struct {
store *store.Store
// serviceIPs maps service names to container IPs.
serviceIPs map[string][]netip.Addr
// mu protects the serviceIPs map.
mu sync.RWMutex
// lastUpdate tracks when records were last updated.
lastUpdate time.Time
log *slog.Logger
}
// NewClusterResolver creates a new cluster resolver using the cluster store.
func NewClusterResolver(store *store.Store) *ClusterResolver {
return &ClusterResolver{
store: store,
serviceIPs: make(map[string][]netip.Addr),
log: slog.With("component", "dns-resolver"),
}
}
// Run starts watching for container changes and updates DNS records accordingly.
func (r *ClusterResolver) Run(ctx context.Context) error {
containers, changes, err := r.store.SubscribeContainers(ctx)
if err != nil {
return fmt.Errorf("subscribe to container changes: %w", err)
}
r.log.Info("Subscribed to container changes in the cluster to keep DNS records updated.")
// TODO: implement machine membership check using Corrossion Admin client to filter available containers.
r.updateServiceIPs(containers)
for {
select {
case _, ok := <-changes:
if !ok {
return fmt.Errorf("containers subscription failed")
}
r.log.Debug("Cluster containers changed, updating DNS records.")
containers, err = r.store.ListContainers(ctx, store.ListOptions{})
if err != nil {
r.log.Error("Failed to list containers.", "err", err)
continue
}
// TODO: implement machine membership check using Corrossion Admin client to filter available containers.
r.updateServiceIPs(containers)
case <-ctx.Done():
return nil
}
}
}
// updateServiceIPs processes container records and updates the serviceIPs map.
func (r *ClusterResolver) updateServiceIPs(containers []store.ContainerRecord) {
newServiceIPs := make(map[string][]netip.Addr, len(r.serviceIPs))
containersCount := 0
for _, record := range containers {
if !record.Container.Healthy() {
continue
}
ip := record.Container.UncloudNetworkIP()
if !ip.IsValid() {
// Container is not connected to the uncloud Docker network (could be host network).
continue
}
ctr := api.ServiceContainer{Container: record.Container}
if ctr.ServiceID() == "" || ctr.ServiceName() == "" {
// Container is not part of a service, skip it.
continue
}
// TODO: remove normalisation after implementing service name validation:
//.https://github.com/psviderski/uncloud/issues/53
serviceName := strings.ToLower(ctr.ServiceName())
newServiceIPs[serviceName] = append(newServiceIPs[serviceName], ip)
// Also add the service ID as a valid lookup.
newServiceIPs[ctr.ServiceID()] = append(newServiceIPs[ctr.ServiceID()], ip)
containersCount++
}
// Update the serviceIPs map atomically.
r.mu.Lock()
r.serviceIPs = newServiceIPs
r.mu.Unlock()
r.log.Debug("DNS records updated.", "services", len(newServiceIPs)/2, "containers", containersCount)
}
// Resolve returns IP addresses of the service containers.
func (r *ClusterResolver) Resolve(serviceName string) []netip.Addr {
r.mu.RLock()
defer r.mu.RUnlock()
ips, ok := r.serviceIPs[serviceName]
if !ok || len(ips) == 0 {
return nil
}
// Return a copy of the IPs slice to prevent modification of the original.
ipsCopy := make([]netip.Addr, len(ips))
copy(ipsCopy, ips)
return ipsCopy
}
+347
View File
@@ -0,0 +1,347 @@
package dns
import (
"context"
"errors"
"fmt"
"log/slog"
"math/rand/v2"
"net"
"net/netip"
"strconv"
"strings"
"sync"
"time"
"github.com/miekg/dns"
)
const (
// InternalDomain is the cluster internal domain for service discovery. All DNS queries ending with this suffix
// will be resolved using the internal DNS server.
InternalDomain = "internal."
// Port is the standard DNS port.
Port = 53
// maxConcurrentForwards is the maximum number of concurrent forwarded queries to upstream DNS servers.
// 1024 is the default used by the Docker internal DNS server.
maxConcurrentForwards = 1024
// forwardingTimeout is the timeout for forwarding a DNS query to an upstream server.
forwardingTimeout = 3 * time.Second
)
// Resolver is an interface for resolving service names to IP addresses.
type Resolver interface {
// Resolve returns a list of IP addresses of the service containers.
// An empty list is returned if no service is found.
Resolve(serviceName string) []netip.Addr
}
// Server is an embedded internal DNS server for service discovery and forwarding external queries
// to upstream DNS servers.
type Server struct {
listenAddr netip.Addr
resolver Resolver
upstreamServers []netip.AddrPort
udpServer *dns.Server
tcpServer *dns.Server
inProgressReqs sync.WaitGroup
forwardSemaphore chan struct{}
log *slog.Logger
}
// NewServer creates a new DNS server with the given configuration.
// If upstreams is nil, nameservers from /etc/resolv.conf will be used. An empty upstreams list means to only resolve
// internal DNS queries and not forward any external queries.
func NewServer(listenAddr netip.Addr, resolver Resolver, upstreams []netip.AddrPort) (*Server, error) {
if !listenAddr.IsValid() {
return nil, fmt.Errorf("invalid listen address: %s", listenAddr)
}
if resolver == nil {
return nil, fmt.Errorf("resolver must be provided")
}
// Load upstreams from /etc/resolv.conf and set fallback servers only if upstreams is not provided.
if upstreams == nil {
if nameservers, err := parseNameserversFromResolvConf(); err != nil {
slog.Warn("Failed to parse nameservers from /etc/resolv.conf.", "err", err)
} else {
for _, ns := range nameservers {
if ns.Compare(listenAddr) == 0 {
// Skip this internal DNS server address if it has been configured in /etc/resolv.conf
// to not create a forwarding loop.
continue
}
upstreams = append(upstreams, netip.AddrPortFrom(ns, Port))
}
}
// Fallback to common public DNS servers if no nameservers were found in /etc/resolv.conf.
if upstreams == nil {
upstreams = []netip.AddrPort{
netip.AddrPortFrom(netip.MustParseAddr("1.1.1.1"), Port), // Cloudflare DNS
netip.AddrPortFrom(netip.MustParseAddr("8.8.8.8"), Port), // Google DNS
}
}
}
return &Server{
listenAddr: listenAddr,
resolver: resolver,
upstreamServers: upstreams,
forwardSemaphore: make(chan struct{}, maxConcurrentForwards),
log: slog.With("component", "dns-server"),
}, nil
}
// ListenAddr returns the address the DNS server is listening on.
func (s *Server) ListenAddr() netip.Addr {
return s.listenAddr
}
// Run starts the DNS server listening on both UDP and TCP ports. The server on TCP is not critical so it won't return
// an error if it fails to start. The server will run until the context is canceled or an error occurs.
func (s *Server) Run(ctx context.Context) error {
addr := net.JoinHostPort(s.listenAddr.String(), strconv.Itoa(Port))
s.udpServer = &dns.Server{
Addr: addr,
Net: "udp",
Handler: dns.HandlerFunc(s.handleRequest),
}
s.tcpServer = &dns.Server{
Addr: addr,
Net: "tcp",
Handler: dns.HandlerFunc(s.handleRequest),
}
errCh := make(chan error, 1) // Buffer size 1 is for UDP server error only.
go func() {
s.log.Info("Starting DNS server on UDP port.", "addr", addr, "upstreams", s.upstreamServers)
if err := s.udpServer.ListenAndServe(); err != nil {
errCh <- fmt.Errorf("listen and serve on %s/udp: %w", addr, err)
}
}()
go func() {
s.log.Info("Starting DNS server on TCP port.", "addr", addr, "upstreams", s.upstreamServers)
if err := s.tcpServer.ListenAndServe(); err != nil {
// TCP server is not critical, so log the error and continue.
slog.Warn("Failed to listen and serve DNS server on TCP port. "+
"TCP server is not critical and will be ignored.", "addr", addr, "err", err)
}
}()
select {
case err := <-errCh:
// Stop the servers if the UDP one fails.
s.stop()
return err
case <-ctx.Done():
s.log.Info("Stopping DNS server.")
return s.stop()
}
}
// stop gracefully shuts down the DNS server.
func (s *Server) stop() error {
var udpErr, tcpErr error
if s.udpServer != nil {
udpErr = s.udpServer.Shutdown()
}
if s.tcpServer != nil {
tcpErr = s.tcpServer.Shutdown()
}
// Wait for all in-progress requests to finish.
s.inProgressReqs.Wait()
if udpErr != nil {
return fmt.Errorf("shutdown DNS server (UDP): %w", udpErr)
}
if tcpErr != nil {
return fmt.Errorf("shutdown DNS server (TCP): %w", tcpErr)
}
return nil
}
// handleRequest processes a DNS query and returns an appropriate response.
func (s *Server) handleRequest(w dns.ResponseWriter, req *dns.Msg) {
s.inProgressReqs.Add(1)
defer s.inProgressReqs.Done()
if len(req.Question) == 0 {
resp := new(dns.Msg).SetRcode(req, dns.RcodeFormatError)
s.reply(w, req, resp)
return
}
// While the original DNS RFCs allow multiple questions, in practice it never works. So handle only the first one.
q := req.Question[0]
log := s.log.With("name", q.Name, "type", dns.TypeToString[q.Qtype])
log.Debug("Received DNS query.")
if !dns.IsSubDomain(InternalDomain, dns.CanonicalName(q.Name)) {
log.Debug("Forwarding non-internal DNS query to upstream DNS servers.")
// Use the same transport for the forwarded request as the original request.
resp, err := s.forwardRequest(req, w.LocalAddr().Network())
if err != nil {
log.Error("Failed to forward DNS query.", "err", err)
resp = new(dns.Msg).SetRcode(req, dns.RcodeServerFailure)
}
s.reply(w, req, resp)
return
}
// Handle the query for the internal domain.
resp := new(dns.Msg).SetReply(req)
resp.Authoritative = true
resp.RecursionAvailable = true
switch q.Qtype {
case dns.TypeA:
records := s.handleAQuery(q.Name)
if len(records) > 0 {
log.Debug("Found A records for internal DNS query.", "count", len(records))
resp.Answer = append(resp.Answer, records...)
} else {
log.Debug("No records found for internal DNS query.")
resp.SetRcode(req, dns.RcodeNameError)
}
// TODO: Handle other query types (SRV, TXT, etc.) as needed.
}
// Truncate the response if it exceeds the maximum size for the transport protocol.
maxSize := dns.MinMsgSize
if w.LocalAddr().Network() == "tcp" {
maxSize = dns.MaxMsgSize
} else {
// Retrieve the UDP buffer size from the EDNS0 record if present.
if opt := req.IsEdns0(); opt != nil {
if udpSize := int(opt.UDPSize()); udpSize > maxSize {
maxSize = udpSize
}
}
}
resp.Truncate(maxSize)
s.reply(w, req, resp)
}
func (s *Server) reply(w dns.ResponseWriter, req *dns.Msg, resp *dns.Msg) error {
if err := w.WriteMsg(resp); err != nil {
s.log.Error("Failed to write DNS response.", "err", err, "msg", resp)
// It may fail due to a malformed response message, e.g. exceeding the maximum size. In that case,
// attempt to send a server failure response instead so the client doesn't have to wait for a timeout.
if resp.Rcode != dns.RcodeServerFailure {
resp = new(dns.Msg).SetRcode(req, dns.RcodeServerFailure)
if err2 := w.WriteMsg(resp); err2 != nil {
s.log.Error("Failed to write DNS error response.", "err", err2, "msg", resp)
}
}
return err
}
return nil
}
// forwardRequest forwards a DNS query to system DNS servers
func (s *Server) forwardRequest(req *dns.Msg, proto string) (*dns.Msg, error) {
if len(s.upstreamServers) == 0 {
return nil, errors.New("no upstream DNS servers configured")
}
// Apply concurrency control for forwarded queries.
select {
case s.forwardSemaphore <- struct{}{}:
defer func() {
<-s.forwardSemaphore
}()
default:
return nil, fmt.Errorf("too many concurrent forwarded queries (max: %d)", maxConcurrentForwards)
}
// Create DNS client with timeout and same transport as original request
client := &dns.Client{
Net: proto,
Timeout: forwardingTimeout,
}
var lastErr error
for _, server := range s.upstreamServers {
resp, _, err := client.Exchange(req, server.String())
if err == nil {
return resp, nil
}
lastErr = err
s.log.Debug("Failed to forward DNS query to upstream server.", "server", server, "err", err)
}
return nil, lastErr
}
// handleAQuery processes an A query for the internal domain and returns A records for the requested name.
// The internal domain suffix is already stripped from the name. An empty list is returned if no records are found.
func (s *Server) handleAQuery(name string) []dns.RR {
serviceName := trimInternalDomain(name)
ips := s.resolver.Resolve(serviceName)
if len(ips) == 0 {
s.log.Debug("Failed to resolve service name.", "service", serviceName)
return nil
}
s.log.Debug("Resolved service name.", "service", serviceName, "ips", ips)
if len(ips) > 1 {
// TODO: sort by proximity to the requesting container/machine. For now, just shuffle the IPs.
rand.Shuffle(len(ips), func(i, j int) {
ips[i], ips[j] = ips[j], ips[i]
})
}
// Create A records for each IP.
records := make([]dns.RR, 0, len(ips))
for _, ip := range ips {
records = append(records, &dns.A{
Hdr: dns.RR_Header{
Name: name,
Rrtype: dns.TypeA,
Class: dns.ClassINET,
// TODO: should we increate the TTL to some reasonably small value like 5-30 seconds to allow
// at least some caching?
Ttl: 0,
},
A: net.ParseIP(ip.String()),
})
}
return records
}
// parseNameserversFromResolvConf parses the nameservers from /etc/resolv.conf.
func parseNameserversFromResolvConf() ([]netip.Addr, error) {
config, err := dns.ClientConfigFromFile("/etc/resolv.conf")
if err != nil {
return nil, err
}
var servers []netip.Addr
for _, server := range config.Servers {
if addr, err := netip.ParseAddr(server); err == nil {
servers = append(servers, addr)
}
}
return servers, nil
}
func trimInternalDomain(name string) string {
name = dns.CanonicalName(name)
if !dns.IsSubDomain(InternalDomain, name) {
return name
}
return strings.TrimSuffix(name, "."+InternalDomain)
}
+70
View File
@@ -11,6 +11,7 @@ import (
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/network"
"github.com/docker/docker/api/types/volume"
"github.com/docker/docker/errdefs"
"github.com/docker/docker/pkg/jsonmessage"
regtypes "github.com/google/go-containerregistry/pkg/v1/types"
@@ -341,6 +342,75 @@ func parseRemoteImageMessage(msg *pb.RemoteImage) (api.MachineRemoteImage, error
return mri, nil
}
// CreateVolume creates a new volume with the given options.
func (c *Client) CreateVolume(ctx context.Context, opts volume.CreateOptions) (volume.Volume, error) {
var vol volume.Volume
optsBytes, err := json.Marshal(opts)
if err != nil {
return vol, fmt.Errorf("marshal options: %w", err)
}
resp, err := c.grpcClient.CreateVolume(ctx, &pb.CreateVolumeRequest{Options: optsBytes})
if err != nil {
return vol, err
}
if err = json.Unmarshal(resp.Volume, &vol); err != nil {
return vol, fmt.Errorf("unmarshal volume: %w", err)
}
return vol, nil
}
// MachineVolumes represents a volume list response from a machine.
type MachineVolumes struct {
Metadata *pb.Metadata
Response volume.ListResponse
}
// ListVolumes returns a list of all volumes matching the filter.
func (c *Client) ListVolumes(ctx context.Context, opts volume.ListOptions) ([]MachineVolumes, error) {
optsBytes, err := json.Marshal(opts)
if err != nil {
return nil, fmt.Errorf("marshal options: %w", err)
}
resp, err := c.grpcClient.ListVolumes(ctx, &pb.ListVolumesRequest{Options: optsBytes})
if err != nil {
return nil, err
}
machineVolumes := make([]MachineVolumes, len(resp.Messages))
for i, msg := range resp.Messages {
machineVolumes[i].Metadata = msg.Metadata
if msg.Metadata != nil && msg.Metadata.Error != "" {
continue
}
if err = json.Unmarshal(msg.Response, &machineVolumes[i].Response); err != nil {
return nil, fmt.Errorf("unmarshal response: %w", err)
}
}
return machineVolumes, nil
}
// RemoveVolume removes a volume with the given ID.
func (c *Client) RemoveVolume(ctx context.Context, id string, force bool) error {
_, err := c.grpcClient.RemoveVolume(ctx, &pb.RemoveVolumeRequest{
Id: id,
Force: force,
})
if err != nil {
if status.Convert(err).Code() == codes.NotFound {
return errdefs.NotFound(err)
}
}
return err
}
// CreateServiceContainer creates a new container for the service with the given specifications.
func (c *Client) CreateServiceContainer(
ctx context.Context, serviceID string, spec api.ServiceSpec, containerName string,
+3 -3
View File
@@ -4,19 +4,19 @@ import (
"context"
"errors"
"fmt"
"log/slog"
"time"
dockercontainer "github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/events"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/client"
"github.com/psviderski/uncloud/internal/machine/store"
"github.com/psviderski/uncloud/pkg/api"
"log/slog"
"time"
)
const (
NetworkName = "uncloud"
UserChain = "DOCKER-USER"
// EventsDebounceInterval defines how long to wait before processing the next Docker event. Multiple events
// occurring within this window will be processed together as a single event to prevent system overload.
EventsDebounceInterval = 100 * time.Millisecond
+3 -3
View File
@@ -8,7 +8,7 @@ import (
"net/netip"
)
// EnsureUncloudNetwork is a stub for darwin.
func (m *Manager) EnsureUncloudNetwork(ctx context.Context, subnet netip.Prefix) error {
return fmt.Errorf("not supported on darwin")
// EnsureUncloudNetwork is a stub for Darwin.
func (m *Manager) EnsureUncloudNetwork(ctx context.Context, subnet netip.Prefix, dnsServer netip.Addr) error {
return fmt.Errorf("not supported on Darwin")
}
+48 -9
View File
@@ -3,24 +3,29 @@ package docker
import (
"context"
"fmt"
"log/slog"
"net/netip"
"strconv"
dnetwork "github.com/docker/docker/api/types/network"
"github.com/docker/docker/client"
"github.com/docker/docker/libnetwork/iptables"
"log/slog"
"net/netip"
"github.com/psviderski/uncloud/internal/machine/dns"
"github.com/psviderski/uncloud/internal/machine/firewall"
"github.com/psviderski/uncloud/internal/machine/network"
"github.com/psviderski/uncloud/pkg/api"
)
// EnsureUncloudNetwork creates the Docker bridge network NetworkName with the provided machine subnet
// if it doesn't exist. If the network exists but has a different subnet, it removes and recreates the network.
// It also configures iptables to allow container access from the WireGuard network.
func (m *Manager) EnsureUncloudNetwork(ctx context.Context, subnet netip.Prefix) error {
func (m *Manager) EnsureUncloudNetwork(ctx context.Context, subnet netip.Prefix, dnsServer netip.Addr) error {
// Ensure the Docker network 'uncloud' is created with the correct subnet.
needsCreation := false
nw, err := m.client.NetworkInspect(ctx, NetworkName, dnetwork.InspectOptions{})
if err != nil {
if !client.IsErrNotFound(err) {
return fmt.Errorf("inspect Docker network %q: %w", NetworkName, err)
return fmt.Errorf("inspect Docker network '%s': %w", NetworkName, err)
}
needsCreation = true
} else if nw.IPAM.Config[0].Subnet != subnet.String() {
@@ -31,7 +36,7 @@ func (m *Manager) EnsureUncloudNetwork(ctx context.Context, subnet netip.Prefix)
)
if err = m.client.NetworkRemove(ctx, NetworkName); err != nil {
// It can still fail if the network is in use by a container. Leave it to the user to resolve the issue.
return fmt.Errorf("remove Docker network %q: %w", NetworkName, err)
return fmt.Errorf("remove Docker network '%s': %w", NetworkName, err)
}
needsCreation = true
}
@@ -48,14 +53,22 @@ func (m *Manager) EnsureUncloudNetwork(ctx context.Context, subnet netip.Prefix)
},
},
},
Labels: map[string]string{
api.LabelManaged: "",
},
Options: map[string]string{
// Starting with Docker 28.2.0 (https://github.com/moby/moby/pull/49832), we have to explicitly
// allow direct routing from the WireGuard interface to the bridge network.
"com.docker.network.bridge.trusted_host_interfaces": network.WireGuardInterfaceName,
},
},
); err != nil {
return fmt.Errorf("create Docker network %q: %w", NetworkName, err)
return fmt.Errorf("create Docker network '%s': %w", NetworkName, err)
}
slog.Info("Docker network created.", "name", NetworkName, "subnet", subnet.String())
if nw, err = m.client.NetworkInspect(ctx, NetworkName, dnetwork.InspectOptions{}); err != nil {
return fmt.Errorf("inspect Docker network %q: %w", NetworkName, err)
return fmt.Errorf("inspect Docker network '%s': %w", NetworkName, err)
}
}
@@ -67,10 +80,36 @@ func (m *Manager) EnsureUncloudNetwork(ctx context.Context, subnet netip.Prefix)
// Bridge name doesn't seem to be documented but this is the source code where it is generated:
// https://github.com/moby/moby/blob/v27.2.1/libnetwork/drivers/bridge/bridge_linux.go#L664
bridgeName := "br-" + nw.ID[:12]
if err = configureIptables(bridgeName, dnsServer); err != nil {
return fmt.Errorf("configure iptables for Docker network '%s': %w", NetworkName, err)
}
return nil
}
// configureIptables configures iptables rules for the uncloud Docker network.
func configureIptables(bridgeName string, dnsServer netip.Addr) error {
ipt := iptables.GetIptable(iptables.IPv4)
rule := []string{"--in-interface", network.WireGuardInterfaceName, "--out-interface", bridgeName, "-j", "ACCEPT"}
if err = ipt.ProgramRule(iptables.Filter, UserChain, iptables.Insert, rule); err != nil {
// Allow traffic from other machines and their containers through the WG mesh to the Uncloud containers
// on the machine.
wgRule := []string{"--in-interface", network.WireGuardInterfaceName, "--out-interface", bridgeName, "-j", "ACCEPT"}
if err := ipt.ProgramRule(iptables.Filter, firewall.DockerUserChain, iptables.Insert, wgRule); err != nil {
return fmt.Errorf("insert iptables rule: %w", err)
}
// Allow DNS queries from Uncloud containers to the embedded DNS server.
for _, proto := range []string{"udp", "tcp"} {
dnsRule := []string{
"--in-interface", bridgeName,
"--dst", dnsServer.String(),
"--protocol", proto,
"--dport", strconv.Itoa(dns.Port),
"-j", "ACCEPT",
}
if err := ipt.ProgramRule(iptables.Filter, firewall.UncloudInputChain, iptables.Insert, dnsRule); err != nil {
return fmt.Errorf("insert iptables rule: %w", err)
}
}
return nil
+224 -6
View File
@@ -8,7 +8,9 @@ import (
"fmt"
"io"
"log/slog"
"net/netip"
"regexp"
"slices"
"strconv"
"strings"
@@ -17,7 +19,9 @@ import (
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/image"
"github.com/docker/docker/api/types/mount"
"github.com/docker/docker/api/types/network"
"github.com/docker/docker/api/types/volume"
"github.com/docker/docker/client"
"github.com/docker/go-connections/nat"
"github.com/google/go-containerregistry/pkg/authn"
@@ -27,6 +31,7 @@ import (
"github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/psviderski/uncloud/internal/machine/api/pb"
"github.com/psviderski/uncloud/internal/machine/dns"
"github.com/psviderski/uncloud/internal/secret"
"github.com/psviderski/uncloud/pkg/api"
"google.golang.org/grpc"
@@ -42,13 +47,17 @@ type Server struct {
pb.UnimplementedDockerServer
client *client.Client
db *sqlx.DB
// internalDNSIP is a function that returns the IP address of the internal DNS server. It may return an empty
// address if the address is unknown (e.g. when the machine is not initialised yet).
internalDNSIP func() netip.Addr
}
// NewServer creates a new Docker gRPC server with the provided Docker client.
func NewServer(cli *client.Client, db *sqlx.DB) *Server {
func NewServer(cli *client.Client, db *sqlx.DB, internalDNSIP func() netip.Addr) *Server {
return &Server{
client: cli,
db: db,
client: cli,
db: db,
internalDNSIP: internalDNSIP,
}
}
@@ -329,6 +338,86 @@ func (s *Server) InspectRemoteImage(
}, nil
}
// CreateVolume creates a new volume with the given options.
func (s *Server) CreateVolume(ctx context.Context, req *pb.CreateVolumeRequest) (*pb.CreateVolumeResponse, error) {
var opts volume.CreateOptions
if err := json.Unmarshal(req.Options, &opts); err != nil {
return nil, status.Errorf(codes.InvalidArgument, "unmarshal options: %v", err)
}
// Always add the managed label to the volume to indicate that it is managed by Uncloud.
if opts.Labels == nil {
opts.Labels = make(map[string]string)
}
opts.Labels[api.LabelManaged] = ""
vol, err := s.client.VolumeCreate(ctx, opts)
if err != nil {
return nil, status.Error(codes.Internal, err.Error())
}
volBytes, err := json.Marshal(vol)
if err != nil {
return nil, status.Errorf(codes.Internal, "marshal volume: %v", err)
}
return &pb.CreateVolumeResponse{Volume: volBytes}, nil
}
// ListVolumes returns a list of all volumes matching the filter.
func (s *Server) ListVolumes(ctx context.Context, req *pb.ListVolumesRequest) (*pb.ListVolumesResponse, error) {
var opts volume.ListOptions
if len(req.Options) > 0 {
if err := json.Unmarshal(req.Options, &opts); err != nil {
return nil, status.Errorf(codes.InvalidArgument, "unmarshal options: %v", err)
}
// Handle filters separately because they implement custom JSON unmarshalling.
var raw map[string]json.RawMessage
if err := json.Unmarshal(req.Options, &raw); err != nil {
return nil, status.Errorf(codes.InvalidArgument, "unmarshal options to raw map: %v", err)
}
if filtersBytes, ok := raw["Filters"]; ok {
args, err := filters.FromJSON(string(filtersBytes))
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "unmarshal filters: %v", err)
}
opts.Filters = args
}
}
resp, err := s.client.VolumeList(ctx, opts)
if err != nil {
return nil, status.Error(codes.Internal, err.Error())
}
respBytes, err := json.Marshal(resp)
if err != nil {
return nil, status.Errorf(codes.Internal, "marshal response: %v", err)
}
return &pb.ListVolumesResponse{
Messages: []*pb.MachineVolumes{
{
Response: respBytes,
},
},
}, nil
}
// RemoveVolume removes a volume with the given ID.
func (s *Server) RemoveVolume(ctx context.Context, req *pb.RemoveVolumeRequest) (*emptypb.Empty, error) {
if err := s.client.VolumeRemove(ctx, req.Id, req.Force); err != nil {
if client.IsErrNotFound(err) {
return nil, status.Error(codes.NotFound, err.Error())
}
return nil, status.Error(codes.Internal, err.Error())
}
return &emptypb.Empty{}, nil
}
// CreateServiceContainer creates a new container for the service with the given specifications.
func (s *Server) CreateServiceContainer(
ctx context.Context, req *pb.CreateServiceContainerRequest,
@@ -357,6 +446,7 @@ func (s *Server) CreateServiceContainer(
config := &container.Config{
Cmd: spec.Container.Command,
Env: spec.Container.Env.ToSlice(),
Entrypoint: spec.Container.Entrypoint,
Hostname: containerName,
Image: spec.Container.Image,
@@ -366,6 +456,7 @@ func (s *Server) CreateServiceContainer(
api.LabelServiceMode: spec.Mode,
api.LabelManaged: "",
},
User: spec.Container.User,
}
if spec.Mode == "" {
config.Labels[api.LabelServiceMode] = api.ServiceModeReplicated
@@ -385,6 +476,14 @@ func (s *Server) CreateServiceContainer(
config.Labels[api.LabelServicePorts] = strings.Join(encodedPorts, ",")
}
mounts, err := ToDockerMounts(spec.Volumes, spec.Container.VolumeMounts)
if err != nil {
return nil, err
}
if err = s.verifyDockerVolumesExist(ctx, mounts); err != nil {
return nil, err
}
portBindings := make(nat.PortMap)
for _, p := range spec.Ports {
if p.Mode != api.PortModeHost {
@@ -403,13 +502,38 @@ func (s *Server) CreateServiceContainer(
hostConfig := &container.HostConfig{
Binds: spec.Container.Volumes,
Init: spec.Container.Init,
Mounts: mounts,
PortBindings: portBindings,
Privileged: spec.Container.Privileged,
Resources: container.Resources{
NanoCPUs: spec.Container.Resources.CPU,
Memory: spec.Container.Resources.Memory,
MemoryReservation: spec.Container.Resources.MemoryReservation,
},
// Always restart service containers if they exit or a machine restarts.
// For one-off containers and batch jobs we plan to use a different service type/mode.
RestartPolicy: container.RestartPolicy{
Name: container.RestartPolicyAlways,
},
}
// Configure the container to use the internal DNS server if it's available.
dnsIP := s.internalDNSIP()
if dnsIP.IsValid() {
hostConfig.DNS = []string{dnsIP.String()}
// Optimize DNS resolution for service discovery by appending the search domain to names without a dot.
// For example, the first attempt for "my-service" will be "my-service.internal".
hostConfig.DNSOptions = []string{"ndots:1"}
hostConfig.DNSSearch = []string{dns.InternalDomain}
}
if spec.Container.LogDriver != nil {
hostConfig.LogConfig = container.LogConfig{
Type: spec.Container.LogDriver.Name,
Config: spec.Container.LogDriver.Options,
}
}
networkConfig := &network.NetworkingConfig{
EndpointsConfig: map[string]*network.EndpointSettings{
NetworkName: {},
@@ -440,9 +564,8 @@ func (s *Server) CreateServiceContainer(
return nil, status.Errorf(codes.Internal, "marshal service spec: %v", err)
}
if _, err = s.db.ExecContext(ctx,
`INSERT INTO containers (id, service_id, service_spec) VALUES ($1, $2, $3)`,
resp.ID, req.ServiceId, string(specBytes)); err != nil {
if _, err = s.db.ExecContext(ctx, `INSERT INTO containers (id, service_spec) VALUES ($1, $2)`,
resp.ID, string(specBytes)); err != nil {
removeContainer()
return nil, status.Errorf(codes.Internal, "store container in database: %v", err)
}
@@ -450,6 +573,101 @@ func (s *Server) CreateServiceContainer(
return &pb.CreateContainerResponse{Response: respBytes}, nil
}
func ToDockerMounts(volumes []api.VolumeSpec, mounts []api.VolumeMount) ([]mount.Mount, error) {
normalisedVolumes := make([]api.VolumeSpec, len(volumes))
for i, v := range volumes {
normalisedVolumes[i] = v.SetDefaults()
}
dockerMounts := make([]mount.Mount, 0, len(mounts))
for _, m := range mounts {
idx := slices.IndexFunc(normalisedVolumes, func(v api.VolumeSpec) bool {
return v.Name == m.VolumeName
})
if idx == -1 {
return nil, fmt.Errorf("volume mount references a volume that doesn't exist in the volumes spec: '%s'",
m.VolumeName)
}
vol := normalisedVolumes[idx]
if err := vol.Validate(); err != nil {
return nil, fmt.Errorf("invalid volume: %w", err)
}
dm := mount.Mount{
Type: mount.Type(vol.Type),
Target: m.ContainerPath,
ReadOnly: m.ReadOnly,
}
switch vol.Type {
case api.VolumeTypeBind:
dm.Source = vol.BindOptions.HostPath
dm.BindOptions = toDockerBindOptions(vol.BindOptions)
case api.VolumeTypeVolume:
dm.Source = vol.DockerVolumeName()
dm.VolumeOptions = &mount.VolumeOptions{
NoCopy: vol.VolumeOptions.NoCopy,
Labels: vol.VolumeOptions.Labels,
Subpath: vol.VolumeOptions.SubPath,
DriverConfig: vol.VolumeOptions.Driver,
}
case api.VolumeTypeTmpfs:
dm.TmpfsOptions = vol.TmpfsOptions
default:
return nil, fmt.Errorf("unsupported volume type: '%s'", vol.Type)
}
dockerMounts = append(dockerMounts, dm)
}
return dockerMounts, nil
}
func toDockerBindOptions(opts *api.BindOptions) *mount.BindOptions {
if opts == nil {
return nil
}
dockerOpts := &mount.BindOptions{
Propagation: opts.Propagation,
CreateMountpoint: opts.CreateHostPath,
}
switch opts.Recursive {
case "disabled":
dockerOpts.NonRecursive = true
case "writable":
dockerOpts.ReadOnlyNonRecursive = true
case "readonly":
dockerOpts.ReadOnlyForceRecursive = true
}
return dockerOpts
}
// verifyDockerVolumesExist checks if the Docker named volumes referenced in the mounts exist on the machine.
func (s *Server) verifyDockerVolumesExist(ctx context.Context, mounts []mount.Mount) error {
for _, m := range mounts {
if m.Type != mount.TypeVolume {
continue
}
// TODO: non-local volume drivers should likely be handled differently (needs proper investigation).
if _, err := s.client.VolumeInspect(ctx, m.Source); err != nil {
if client.IsErrNotFound(err) {
return status.Errorf(codes.NotFound, "volume '%s' not found", m.Source)
}
return status.Errorf(codes.Internal, "inspect volume '%s': %v", m.Source, err.Error())
}
// TODO: check if the volume driver and options are the same as in the mount and fail if not.
// Should we even ignore driver-specific options in the volume spec for externally managed volumes?
// Instead, just inspect the existing volume and construct the mount from it.
}
return nil
}
// InspectServiceContainer returns the container information and service specification that was used to create the
// container with the given ID.
func (s *Server) InspectServiceContainer(
@@ -0,0 +1,8 @@
package firewall
import "fmt"
// ConfigureIptablesChains is a stub for Darwin.
func ConfigureIptablesChains() error {
return fmt.Errorf("not supported on Darwin")
}
@@ -0,0 +1,74 @@
package firewall
import (
"fmt"
"strconv"
"strings"
"github.com/docker/docker/libnetwork/iptables"
"github.com/psviderski/uncloud/internal/machine/network"
)
const (
DockerUserChain = "DOCKER-USER"
UncloudInputChain = "UNCLOUD-INPUT"
)
// ConfigureIptablesChains sets up custom iptables chains and initial firewall rules for Uncloud networking.
func ConfigureIptablesChains() error {
// Ensure iptables UNCLOUD-INPUT chain with a RETURN rule exists. All existing rules are flushed.
ipt := iptables.GetIptable(iptables.IPv4)
if _, err := ipt.NewChain(UncloudInputChain, iptables.Filter); err != nil {
return fmt.Errorf("create iptables chain '%s': %w", UncloudInputChain, err)
}
if err := ipt.RawCombinedOutput("-t", string(iptables.Filter), "-F", UncloudInputChain); err != nil {
return fmt.Errorf("flush iptables chain '%s': %w", UncloudInputChain, err)
}
if err := ipt.AddReturnRule(UncloudInputChain); err != nil {
return fmt.Errorf("add the RETURN rule for iptables chain '%s': %w", UncloudInputChain, err)
}
// Ensure the main iptables INPUT chain has a jump rule to the UNCLOUD-INPUT chain before any DROP/REJECT rules.
jumpRule := []string{"-m", "comment", "--comment", "Uncloud-managed", "-j", UncloudInputChain}
if !ipt.Exists(iptables.Filter, "INPUT", jumpRule...) {
// Look for the first DROP/REJECT rule in the INPUT chain.
out, err := ipt.Raw("-t", string(iptables.Filter), "-L", "INPUT", "--line-numbers")
if err != nil {
return fmt.Errorf("get iptables rules for chain '%s': %w", UncloudInputChain, err)
}
firstRejectRuleNum := 0
for _, line := range strings.Split(string(out), "\n") {
fields := strings.Fields(line)
if len(fields) < 2 {
continue
}
if fields[1] == "DROP" || fields[1] == "REJECT" {
if ruleNum, err := strconv.Atoi(fields[0]); err == nil {
firstRejectRuleNum = ruleNum
break
}
}
}
var addJumpRule []string
if firstRejectRuleNum > 0 {
addJumpRule = append([]string{"-t", string(iptables.Filter), "-I", "INPUT", strconv.Itoa(firstRejectRuleNum)},
jumpRule...)
} else {
addJumpRule = append([]string{"-t", string(iptables.Filter), "-A", "INPUT"}, jumpRule...)
}
if err = ipt.RawCombinedOutput(addJumpRule...); err != nil {
return fmt.Errorf("add iptables rule '%s': %w", strings.Join(addJumpRule, " "), err)
}
}
// Allow WireGuard traffic to the machine.
acceptWireGuardRule := []string{"-p", "udp", "--dport", strconv.Itoa(network.WireGuardPort), "-j", "ACCEPT"}
err := ipt.ProgramRule(iptables.Filter, UncloudInputChain, iptables.Insert, acceptWireGuardRule)
if err != nil {
return fmt.Errorf("insert iptables rule '%s': %w", strings.Join(acceptWireGuardRule, " "), err)
}
return nil
}
+78 -6
View File
@@ -21,9 +21,10 @@ import (
"github.com/psviderski/uncloud/internal/fs"
"github.com/psviderski/uncloud/internal/machine/api/pb"
apiproxy "github.com/psviderski/uncloud/internal/machine/api/proxy"
"github.com/psviderski/uncloud/internal/machine/caddyfile"
"github.com/psviderski/uncloud/internal/machine/caddyconfig"
"github.com/psviderski/uncloud/internal/machine/cluster"
"github.com/psviderski/uncloud/internal/machine/corroservice"
"github.com/psviderski/uncloud/internal/machine/dns"
machinedocker "github.com/psviderski/uncloud/internal/machine/docker"
"github.com/psviderski/uncloud/internal/machine/network"
"github.com/psviderski/uncloud/internal/machine/store"
@@ -62,6 +63,8 @@ type Config struct {
// CaddyConfigPath specifies where the machine generates the Caddy reverse proxy configuration file for routing
// external traffic to service containers across the internal network. Default is DataDir/caddy/caddy.json.
CaddyConfigPath string
// DNSUpstreams specifies the upstream DNS servers for the embedded internal DNS server.
DNSUpstreams []netip.AddrPort
}
// SetDefaults returns a new Config with default values set where not provided.
@@ -206,7 +209,7 @@ func NewMachine(config *Config) (*Machine, error) {
}
c := cluster.NewCluster(corroStore, corroAdmin)
// Init a gRPC Docker server that proxies requests to the local Docker daemon.
// Init dependencies for a gRPC Docker server that proxies requests to the local Docker daemon.
dockerCli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
if err != nil {
return nil, fmt.Errorf("create Docker client: %w", err)
@@ -217,7 +220,6 @@ func NewMachine(config *Config) (*Machine, error) {
if err != nil {
return nil, fmt.Errorf("init machine database: %w", err)
}
dockerServer := machinedocker.NewServer(dockerCli, db)
// Init a local gRPC proxy server that proxies requests to the local or remote machine API servers.
proxyDirector := apiproxy.NewDirector(config.MachineSockPath, APIPort)
@@ -235,11 +237,16 @@ func NewMachine(config *Config) (*Machine, error) {
initialised: make(chan struct{}, 1),
store: corroStore,
cluster: c,
docker: dockerServer,
localProxyServer: localProxyServer,
proxyDirector: proxyDirector,
}
m.localMachineServer = newGRPCServer(m, c, dockerServer)
// Machine IP will only be available after the machine is initialised as a cluster member so wrap it in a function.
internalDNSIP := func() netip.Addr {
return m.IP()
}
m.docker = machinedocker.NewServer(dockerCli, db, internalDNSIP)
m.localMachineServer = newGRPCServer(m, c, m.docker)
if m.Initialised() {
m.initialised <- struct{}{}
@@ -270,6 +277,15 @@ func (m *Machine) Initialised() bool {
return m.state.ID != ""
}
// IP returns the machine IPv4 address in the cluster network which is the first address in the machine subnet.
func (m *Machine) IP() netip.Addr {
if !m.Initialised() {
return netip.Addr{}
}
return network.MachineIP(m.state.Network.Subnet)
}
func (m *Machine) Run(ctx context.Context) error {
// Docker dependency is essential for the machine to function. Block until it's ready.
if err := docker.WaitDaemonReady(ctx, m.config.DockerClient); err != nil {
@@ -368,11 +384,17 @@ func (m *Machine) Run(ctx context.Context) error {
// Create a new Caddyfile controller for managing the Caddy reverse proxy configuration.
// It will also serve the current machine ID at /.uncloud-verify to verify Caddy reachability.
caddyfileCtrl, err := caddyfile.NewController(m.store, m.config.CaddyConfigPath, m.state.ID)
caddyfileCtrl, err := caddyconfig.NewController(m.store, m.config.CaddyConfigPath, m.state.ID)
if err != nil {
return fmt.Errorf("create Caddyfile controller: %w", err)
}
dnsResolver := dns.NewClusterResolver(m.store)
dnsServer, err := dns.NewServer(m.IP(), dnsResolver, m.config.DNSUpstreams)
if err != nil {
return fmt.Errorf("create embedded DNS server: %w", err)
}
ctrl, err = newNetworkController(
m.state,
m.store,
@@ -380,6 +402,8 @@ func (m *Machine) Run(ctx context.Context) error {
m.config.CorrosionService,
m.config.DockerClient,
caddyfileCtrl,
dnsServer,
dnsResolver,
)
if err != nil {
return fmt.Errorf("initialise network controller: %w", err)
@@ -520,6 +544,38 @@ func (m *Machine) configureCorrosion() error {
return nil
}
// CheckPrerequisites verifies if the machine meets all necessary system requirements to participate in the cluster.
func (m *Machine) CheckPrerequisites(ctx context.Context, _ *emptypb.Empty) (*pb.CheckPrerequisitesResponse, error) {
// Check DNS port (UDP) availability.
if err := checkDNSPortAvailable(); err != nil {
return &pb.CheckPrerequisitesResponse{
Satisfied: false,
Error: err.Error(),
}, nil
}
return &pb.CheckPrerequisitesResponse{
Satisfied: true,
}, nil
}
// checkDNSPortAvailable verifies that DNS port 53/udp is available for Uncloud's embedded DNS service.
func checkDNSPortAvailable() error {
addr := &net.UDPAddr{
IP: net.IPv4(127, 0, 0, 210), // Use a unique loopback address to avoid conflicts.
Port: dns.Port,
}
conn, err := net.ListenUDP("udp", addr)
if err != nil {
return fmt.Errorf("DNS port %d/udp is already in use by another service: %w. Uncloud needs this port "+
"to run the embedded internal DNS service on WireGuard interface 'uncloud'. Please reconfigure "+
"any DNS servers (like dnsmasq, systemd-resolved, or named) that might be listening on all network "+
"interfaces (0.0.0.0) on the machine and try again", dns.Port, err)
}
conn.Close()
return nil
}
// InitCluster initialises a new cluster on the local machine with the provided network configuration.
func (m *Machine) InitCluster(ctx context.Context, req *pb.InitClusterRequest) (*pb.InitClusterResponse, error) {
if m.Initialised() {
@@ -720,6 +776,22 @@ func (m *Machine) Inspect(_ context.Context, _ *emptypb.Empty) (*pb.MachineInfo,
}, nil
}
// Reset restores the machine to a clean state, removing all cluster-related сonfiguration and data and scheduling
// a graceful shutdown. The uncloud daemon will restart the machine if managed by systemd.
func (m *Machine) Reset(ctx context.Context, _ *pb.ResetRequest) (*emptypb.Empty, error) {
slog.Info("Resetting machine to a clean state.")
// TODO: stop and remove all managed service containers.
// TODO: check if the request is coming from the unix or network socket. For the network socket, the reset should
// be called in a separate goroutine to avoid blocking the RPC response.
// TODO: stop the network controller
// TODO: implement and call Cleanup on the network controller to remove Docker network, WG interface, iptables
// rules, corrosion state, ?stop corrosion service.
// TODO: stop the machine and remove the machine.json state. The daemon should restart it to a clean state.
return &emptypb.Empty{}, status.Error(codes.Unimplemented, "reset machine is not implemented yet")
}
// InspectService returns detailed information about a service and its containers stored in the cluster store.
func (m *Machine) InspectService(
ctx context.Context, req *pb.InspectServiceRequest,
+89 -71
View File
@@ -4,22 +4,25 @@ import (
"context"
"errors"
"fmt"
"github.com/cenkalti/backoff/v4"
"github.com/docker/docker/client"
"golang.org/x/sync/errgroup"
"google.golang.org/grpc"
"log/slog"
"net"
"net/netip"
"slices"
"strconv"
"time"
"github.com/cenkalti/backoff/v4"
"github.com/docker/docker/client"
"github.com/psviderski/uncloud/internal/machine/api/pb"
"github.com/psviderski/uncloud/internal/machine/caddyfile"
"github.com/psviderski/uncloud/internal/machine/caddyconfig"
"github.com/psviderski/uncloud/internal/machine/corroservice"
"github.com/psviderski/uncloud/internal/machine/dns"
"github.com/psviderski/uncloud/internal/machine/docker"
"github.com/psviderski/uncloud/internal/machine/firewall"
"github.com/psviderski/uncloud/internal/machine/network"
"github.com/psviderski/uncloud/internal/machine/store"
"golang.org/x/sync/errgroup"
"google.golang.org/grpc"
)
const (
@@ -36,10 +39,11 @@ type networkController struct {
server *grpc.Server
corroService corroservice.Service
dockerCli *client.Client
caddyfileCtrl *caddyfile.Controller
caddyfileCtrl *caddyconfig.Controller
// TODO: DNS server/resolver listening on the machine IP, e.g. 10.210.0.1:53. It can't listen on 127.0.X.X
// like resolved does because it needs to be reachable from both the host and the containers.
// dnsServer is the embedded internal DNS server for the cluster listening on the machine IP.
dnsServer *dns.Server
dnsResolver *dns.ClusterResolver
}
func newNetworkController(
@@ -48,7 +52,9 @@ func newNetworkController(
server *grpc.Server,
corroService corroservice.Service,
dockerCli *client.Client,
caddyfileCtrl *caddyfile.Controller,
caddyfileCtrl *caddyconfig.Controller,
dnsServer *dns.Server,
dnsResolver *dns.ClusterResolver,
) (
*networkController, error,
) {
@@ -68,10 +74,16 @@ func newNetworkController(
corroService: corroService,
dockerCli: dockerCli,
caddyfileCtrl: caddyfileCtrl,
dnsServer: dnsServer,
dnsResolver: dnsResolver,
}, nil
}
func (nc *networkController) Run(ctx context.Context) error {
if err := firewall.ConfigureIptablesChains(); err != nil {
return fmt.Errorf("configure iptables chains: %w", err)
}
if err := nc.wgnet.Configure(*nc.state.Network); err != nil {
return fmt.Errorf("configure WireGuard network: %w", err)
}
@@ -110,86 +122,92 @@ func (nc *networkController) Run(ctx context.Context) error {
},
)
errGroup.Go(func() error {
slog.Info("Starting embedded DNS resolver.")
if err := nc.dnsResolver.Run(ctx); err != nil {
return fmt.Errorf("embedded DNS resolver failed: %w", err)
}
return nil
})
errGroup.Go(func() error {
slog.Info("Starting embedded DNS server.")
if err := nc.dnsServer.Run(ctx); err != nil {
return fmt.Errorf("embedded DNS server failed: %w", err)
}
return nil
})
// Setup Docker network and synchronise containers to the cluster store.
errGroup.Go(
func() error {
return nc.prepareAndWatchDocker(ctx)
},
)
errGroup.Go(func() error {
return nc.prepareAndWatchDocker(ctx)
})
// Handle machine changes in the cluster. Handling machine and endpoint changes should be done
// in separate goroutines to avoid a deadlock when reconfiguring the network.
errGroup.Go(
func() error {
if err := nc.handleMachineChanges(ctx); err != nil {
return fmt.Errorf("handle new machines: %w", err)
}
return nil
},
)
errGroup.Go(func() error {
if err := nc.handleMachineChanges(ctx); err != nil {
return fmt.Errorf("handle new machines: %w", err)
}
return nil
})
// Watch for endpoint changes and update the machine state accordingly.
errGroup.Go(
func() error {
for {
select {
case e, ok := <-nc.endpointChanges:
if !ok {
// The channel was closed, stop watching for changes.
nc.endpointChanges = nil
return nil
}
nc.state.mu.Lock()
for i := range nc.state.Network.Peers {
if nc.state.Network.Peers[i].PublicKey.Equal(e.PublicKey) {
nc.state.Network.Peers[i].Endpoint = &e.Endpoint
break
}
}
if err := nc.state.Save(); err != nil {
slog.Error("Failed to save machine state.", "err", err)
}
nc.state.mu.Unlock()
slog.Debug("Preserved endpoint change in the machine state.",
"public_key", e.PublicKey, "endpoint", e.Endpoint)
case <-ctx.Done():
errGroup.Go(func() error {
for {
select {
case e, ok := <-nc.endpointChanges:
if !ok {
// The channel was closed, stop watching for changes.
nc.endpointChanges = nil
return nil
}
}
},
)
errGroup.Go(
func() error {
if err := nc.wgnet.Run(ctx); err != nil {
return fmt.Errorf("WireGuard network failed: %w", err)
nc.state.mu.Lock()
for i := range nc.state.Network.Peers {
if nc.state.Network.Peers[i].PublicKey.Equal(e.PublicKey) {
nc.state.Network.Peers[i].Endpoint = &e.Endpoint
break
}
}
if err := nc.state.Save(); err != nil {
slog.Error("Failed to save machine state.", "err", err)
}
nc.state.mu.Unlock()
slog.Debug("Preserved endpoint change in the machine state.",
"public_key", e.PublicKey, "endpoint", e.Endpoint)
case <-ctx.Done():
return nil
}
return nil
},
)
}
})
errGroup.Go(func() error {
slog.Info("Starting Caddyfile controller.")
if err := nc.wgnet.Run(ctx); err != nil {
return fmt.Errorf("WireGuard network failed: %w", err)
}
return nil
})
errGroup.Go(func() error {
slog.Info("Starting Caddyconfig controller.")
if err := nc.caddyfileCtrl.Run(ctx); err != nil {
//goland:noinspection GoErrorStringFormat
return fmt.Errorf("Caddyfile controller failed: %w", err)
return fmt.Errorf("Caddyconfig controller failed: %w", err)
}
return nil
})
// Wait for the context to be done and stop the network API server.
errGroup.Go(
func() error {
<-ctx.Done()
slog.Info("Stopping network API server.")
// TODO: implement timeout for graceful shutdown.
nc.server.GracefulStop()
slog.Info("Network API server stopped.")
return nil
},
)
errGroup.Go(func() error {
<-ctx.Done()
slog.Info("Stopping network API server.")
// TODO: implement timeout for graceful shutdown.
nc.server.GracefulStop()
slog.Info("Network API server stopped.")
return nil
})
return errGroup.Wait()
}
@@ -202,7 +220,7 @@ func (nc *networkController) prepareAndWatchDocker(ctx context.Context) error {
return fmt.Errorf("wait for Docker daemon: %w", err)
}
if err := manager.EnsureUncloudNetwork(ctx, nc.state.Network.Subnet); err != nil {
if err := manager.EnsureUncloudNetwork(ctx, nc.state.Network.Subnet, nc.dnsServer.ListenAddr()); err != nil {
return fmt.Errorf("ensure Docker network: %w", err)
}
slog.Info("Docker network configured.")
+5 -4
View File
@@ -2,10 +2,11 @@ package network
import (
"fmt"
"github.com/psviderski/uncloud/internal/secret"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
"net"
"net/netip"
"github.com/psviderski/uncloud/internal/secret"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
)
type Config struct {
@@ -13,7 +14,7 @@ type Config struct {
// in the subnet. Other IP addresses are allocated to containers running on the machine.
Subnet netip.Prefix
// ManagementIP is the IPv6 address assigned to the machine within the WireGuard network. This address is used
// for cluster management traffic, such as gRPC communication with the machine API server and Serf gossip.
// for cluster management traffic, such as gRPC communication with the machine API server and Corrosion gossip.
ManagementIP netip.Addr
PrivateKey secret.Secret
PublicKey secret.Secret
@@ -23,7 +24,7 @@ type Config struct {
type PeerConfig struct {
Subnet *netip.Prefix `json:",omitempty"`
// ManagementIP is the IPv6 address assigned to the peer within the WireGuard network. This address is used
// for cluster management traffic, such as gRPC communication with the machine API server and Serf gossip.
// for cluster management traffic, such as gRPC communication with the machine API server and Corrosion gossip.
ManagementIP netip.Addr
Endpoint *netip.AddrPort `json:",omitempty"`
AllEndpoints []netip.AddrPort `json:",omitempty"`
+2 -1
View File
@@ -2,9 +2,10 @@ package network
import (
"fmt"
"github.com/psviderski/uncloud/internal/secret"
"net"
"net/netip"
"github.com/psviderski/uncloud/internal/secret"
)
// MachineIP returns the IP address of the machine which is the first address in the subnet.
+2
View File
@@ -7,6 +7,7 @@ import (
"strconv"
"time"
"github.com/psviderski/uncloud/internal/fs"
"golang.org/x/crypto/ssh"
"golang.org/x/crypto/ssh/agent"
)
@@ -63,6 +64,7 @@ func sshAgentAuth() (ssh.AuthMethod, func(), error) {
}
func privateKeyAuth(path string) (ssh.AuthMethod, error) {
path = fs.ExpandHomeDir(path)
key, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("read private key file %q: %w", path, err)
+51 -11
View File
@@ -4,17 +4,19 @@ import (
"context"
"errors"
"fmt"
"net/netip"
"time"
"github.com/cenkalti/backoff/v4"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/network"
dockerclient "github.com/docker/docker/client"
"google.golang.org/protobuf/types/known/emptypb"
"net/netip"
"time"
"github.com/docker/go-connections/nat"
"github.com/psviderski/uncloud/internal/machine"
"github.com/psviderski/uncloud/internal/machine/api/pb"
"github.com/psviderski/uncloud/internal/machine/cluster"
"google.golang.org/protobuf/types/known/emptypb"
)
const (
@@ -29,6 +31,31 @@ type Cluster struct {
type CreateClusterOptions struct {
Machines int
// Ports to forward from the cluster machines to the host.
PortMap nat.PortMap
}
func (c *Cluster) PopulateMachineIDs(ctx context.Context) error {
for i := range c.Machines {
if c.Machines[i].ID != "" {
continue
}
cli, err := c.Machines[i].Connect(ctx)
if err != nil {
return fmt.Errorf("connect to machine '%s': %w", c.Machines[i].Name, err)
}
//goland:noinspection GoDeferInLoop
defer cli.Close()
m, err := cli.Inspect(ctx, &emptypb.Empty{})
if err != nil {
return fmt.Errorf("inspect machine '%s': %w", c.Machines[i].Name, err)
}
c.Machines[i].ID = m.Id
}
return nil
}
func (p *Provisioner) CreateCluster(ctx context.Context, name string, opts CreateClusterOptions) (Cluster, error) {
@@ -48,7 +75,7 @@ func (p *Provisioner) CreateCluster(ctx context.Context, name string, opts Creat
ManagedLabel: "",
},
}
// Create a Docker network with the same as the cluster name.
// Create a Docker network with the same name as the cluster name.
if _, err = p.dockerCli.NetworkCreate(ctx, name, netOpts); err != nil {
return c, fmt.Errorf("create Docker network '%s': %w", name, err)
}
@@ -57,7 +84,8 @@ func (p *Provisioner) CreateCluster(ctx context.Context, name string, opts Creat
// Create machines (containers) in the created cluster network.
for i := 1; i < opts.Machines+1; i++ {
mopts := CreateMachineOptions{
Name: fmt.Sprintf("machine-%d", i),
Name: fmt.Sprintf("machine-%d", i),
PortMap: opts.PortMap,
}
m, err := p.CreateMachine(ctx, name, mopts)
if err != nil {
@@ -74,12 +102,15 @@ func (p *Provisioner) CreateCluster(ctx context.Context, name string, opts Creat
if err = p.initCluster(ctx, c.Machines); err != nil {
return c, err
}
if err = c.PopulateMachineIDs(ctx); err != nil {
return c, fmt.Errorf("populate machine IDs: %w", err)
}
if p.configUpdater != nil {
if err = p.configUpdater.AddCluster(c); err != nil {
return c, fmt.Errorf("add cluster to Uncloud config: %w", err)
}
fmt.Printf("Cluster '%s' added to Uncloud config.\n", c.Name)
fmt.Printf("Cluster '%s' added to Uncloud config as the current context.\n", name)
}
return c, nil
@@ -89,7 +120,7 @@ func (p *Provisioner) initCluster(ctx context.Context, machines []Machine) error
// Init a new cluster on the first machine.
initMachine := machines[0]
if err := p.WaitMachineReady(ctx, initMachine, 30*time.Second); err != nil {
if err := WaitMachineReady(ctx, initMachine, 30*time.Second); err != nil {
return fmt.Errorf("wait for machine %q to be ready: %w", initMachine.Name, err)
}
@@ -111,7 +142,7 @@ func (p *Provisioner) initCluster(ctx context.Context, machines []Machine) error
// Join the rest of the machines to the cluster.
for _, m := range machines[1:] {
if err = p.WaitMachineReady(ctx, m, 5*time.Second); err != nil {
if err = WaitMachineReady(ctx, m, 5*time.Second); err != nil {
return fmt.Errorf("wait for machine %q to be ready: %w", m.Name, err)
}
@@ -215,13 +246,17 @@ func (p *Provisioner) InspectCluster(ctx context.Context, name string) (Cluster,
c.Machines = append(c.Machines, m)
}
if err = c.PopulateMachineIDs(ctx); err != nil {
return c, fmt.Errorf("populate machine IDs: %w", err)
}
return c, nil
}
// WaitClusterReady waits for all machines in the cluster to be ready and UP.
func (p *Provisioner) WaitClusterReady(ctx context.Context, c Cluster, timeout time.Duration) error {
firstMachine := c.Machines[0]
if err := p.WaitMachineReady(ctx, firstMachine, timeout); err != nil {
if err := WaitMachineReady(ctx, firstMachine, timeout); err != nil {
return fmt.Errorf("wait for machine '%s' to be ready: %w", firstMachine.Name, err)
}
@@ -238,7 +273,7 @@ func (p *Provisioner) WaitClusterReady(ctx context.Context, c Cluster, timeout t
), ctx)
checkMachinesUp := func() error {
machines, err := cli.ListMachines(ctx)
machines, err := cli.ListMachines(ctx, nil)
if err != nil {
return fmt.Errorf("list machines: %w", err)
}
@@ -278,7 +313,12 @@ func (p *Provisioner) RemoveCluster(ctx context.Context, name string) error {
return fmt.Errorf("list Docker containers with cluster name '%s': %w", name, err)
}
for _, c := range containers {
if err = p.dockerCli.ContainerRemove(ctx, c.ID, container.RemoveOptions{Force: true}); err != nil {
removeOpts := container.RemoveOptions{
// Remove anonymous volumes attached to the container (typically /var/lib/docker).
RemoveVolumes: true,
Force: true,
}
if err = p.dockerCli.ContainerRemove(ctx, c.ID, removeOpts); err != nil {
return fmt.Errorf("remove Docker container '%s': %w", c.ID, err)
}
}
+13 -12
View File
@@ -2,6 +2,7 @@ package ucind
import (
"fmt"
"github.com/psviderski/uncloud/internal/cli/config"
)
@@ -19,22 +20,22 @@ func (u *ConfigUpdater) AddCluster(c Cluster) error {
return fmt.Errorf("read Uncloud config: %w", err)
}
if _, ok := cfg.Clusters[c.Name]; ok {
return fmt.Errorf("cluster '%s' already exists", c.Name)
if _, ok := cfg.Contexts[c.Name]; ok {
return fmt.Errorf("cluster context '%s' already exists", c.Name)
}
clusterCfg := &config.Cluster{
clusterCfg := &config.Context{
Name: c.Name,
Connections: make([]config.MachineConnection, len(c.Machines)),
}
for i, m := range c.Machines {
clusterCfg.Connections[i] = config.MachineConnection{
TCP: m.APIAddress,
TCP: &m.APIAddress,
}
}
cfg.Clusters[c.Name] = clusterCfg
cfg.CurrentCluster = c.Name
cfg.Contexts[c.Name] = clusterCfg
cfg.CurrentContext = c.Name
if err = cfg.Save(); err != nil {
return fmt.Errorf("save config: %w", err)
@@ -48,17 +49,17 @@ func (u *ConfigUpdater) RemoveCluster(name string) error {
return fmt.Errorf("read Uncloud config: %w", err)
}
if _, ok := cfg.Clusters[name]; !ok {
if _, ok := cfg.Contexts[name]; !ok {
return nil
}
delete(cfg.Clusters, name)
delete(cfg.Contexts, name)
if cfg.CurrentCluster == name {
cfg.CurrentCluster = ""
if cfg.CurrentContext == name {
cfg.CurrentContext = ""
}
if _, ok := cfg.Clusters["default"]; ok {
cfg.CurrentCluster = "default"
if _, ok := cfg.Contexts["default"]; ok {
cfg.CurrentContext = "default"
}
if err = cfg.Save(); err != nil {
+21 -10
View File
@@ -4,6 +4,11 @@ import (
"context"
"errors"
"fmt"
"io"
"net"
"net/netip"
"time"
"github.com/cenkalti/backoff/v4"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/image"
@@ -13,10 +18,6 @@ import (
"github.com/psviderski/uncloud/pkg/client"
"github.com/psviderski/uncloud/pkg/client/connector"
"google.golang.org/protobuf/types/known/emptypb"
"io"
"net"
"net/netip"
"time"
)
const (
@@ -29,6 +30,7 @@ const (
type Machine struct {
ClusterName string
ContainerName string
ID string
Name string
APIAddress netip.AddrPort
}
@@ -40,6 +42,8 @@ func (m *Machine) Connect(ctx context.Context) (*client.Client, error) {
type CreateMachineOptions struct {
Name string
Image string
// Ports to forward from the machine to the host.
PortMap nat.PortMap
}
func (p *Provisioner) CreateMachine(ctx context.Context, clusterName string, opts CreateMachineOptions) (Machine, error) {
@@ -86,6 +90,16 @@ func (p *Provisioner) CreateMachine(ctx context.Context, clusterName string, opt
Name: container.RestartPolicyAlways,
},
}
// Forward ports, if requested
if opts.PortMap != nil {
for port, bindings := range opts.PortMap {
if len(bindings) == 0 {
continue
}
config.ExposedPorts[port] = struct{}{}
hostConfig.PortBindings[port] = bindings
}
}
if _, err := p.createContainerWithImagePull(ctx, containerName, config, hostConfig); err != nil {
return m, err
@@ -158,10 +172,7 @@ func (p *Provisioner) waitPortPublished(ctx context.Context, containerID string,
}
binding, ok := c.NetworkSettings.Ports[port]
if !ok {
return nil, fmt.Errorf("port '%s' not published", port)
}
if len(binding) > 0 {
if ok && len(binding) > 0 {
return binding, nil
}
@@ -185,7 +196,7 @@ func randomMachineName() (string, error) {
}
// WaitMachineReady waits for the machine API to respond.
func (p *Provisioner) WaitMachineReady(ctx context.Context, m Machine, timeout time.Duration) error {
func WaitMachineReady(ctx context.Context, m Machine, timeout time.Duration) error {
cli, err := m.Connect(ctx)
if err != nil {
return fmt.Errorf("connect to machine over TCP '%s': %w", m.APIAddress, err)
@@ -194,7 +205,7 @@ func (p *Provisioner) WaitMachineReady(ctx context.Context, m Machine, timeout t
boff := backoff.WithContext(backoff.NewExponentialBackOff(
backoff.WithInitialInterval(100*time.Millisecond),
backoff.WithMaxInterval(1*time.Second),
backoff.WithMaxInterval(10*time.Second),
backoff.WithMaxElapsedTime(timeout),
), ctx)

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