Compare commits

..
137 Commits
Author SHA1 Message Date
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
147 changed files with 29718 additions and 1656 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 525 KiB

+36
View File
@@ -0,0 +1,36 @@
name: Go Tests
on:
push:
branches:
- "main"
- "test/**"
- "release/**"
pull_request:
branches:
- main
paths:
- ".github/**"
- "**.go"
- "go.*"
- "Makefile"
- "scripts/**"
- "test/**"
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
- name: Run tests
run: make test
timeout-minutes: 10
+4 -2
View File
@@ -27,8 +27,10 @@ go.work.sum
.env .env
.idea/ .idea/
data /data/
dist/ /dist/
/uc
/tmp
# Web # Web
node_modules/ node_modules/
+4
View File
@@ -24,6 +24,8 @@ builds:
goarch: goarch:
- amd64 - amd64
- arm64 - arm64
ldflags:
- -s -w -X github.com/psviderski/uncloud/internal/version.version={{ .Version }}
- id: uncloudd - id: uncloudd
main: ./cmd/uncloudd main: ./cmd/uncloudd
@@ -35,6 +37,8 @@ builds:
goarch: goarch:
- amd64 - amd64
- arm64 - arm64
ldflags:
- -s -w -X github.com/psviderski/uncloud/internal/version.version={{ .Version }}
archives: archives:
- id: uncloud - id: uncloud
+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 FROM alpine:${ALPINE_VERSION} AS corrosion-download
RUN wget -q -O /tmp/corrosion.tar.gz \ ARG TARGETARCH
https://github.com/psviderski/corrosion/releases/latest/download/corrosion-aarch64-unknown-linux-gnu.tar.gz \
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 \ && tar -xzf /tmp/corrosion.tar.gz -C /tmp \
&& install /tmp/corrosion /usr/local/bin/corrosion \ && install /tmp/corrosion /usr/local/bin/corrosion \
&& rm /tmp/corrosion.tar.gz /tmp/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 COPY --from=corrosion-download /usr/local/bin/corrosion /usr/local/bin/corrosion
CMD ["corrosion", "agent"] CMD ["corrosion", "agent"]
+49 -7
View File
@@ -1,5 +1,6 @@
CORROSION_IMAGE ?= ghcr.io/psviderski/corrosion:latest CORROSION_IMAGE ?= ghcr.io/psviderski/corrosion:latest
UCIND_IMAGE ?= ghcr.io/psviderski/ucind:latest UCIND_IMAGE ?= ghcr.io/psviderski/ucind:latest
DOCS_IMAGE ?= ghcr.io/psviderski/uncloud-docs:latest
update-dev: update-dev:
GOOS=linux GOARCH=amd64 go build -o uncloudd-linux-amd64 ./cmd/uncloudd && \ 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" ssh ubuntu@152.67.101.197 "sudo systemctl stop uncloud && sudo rm -rf /var/lib/uncloud"
demo-reset: 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 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 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" 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 .PHONY: proto
proto: proto:
protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative \ protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative \
@@ -45,14 +50,51 @@ proto:
corrosion-image: corrosion-image:
docker build -t "$(CORROSION_IMAGE)" --target corrosion . docker build -t "$(CORROSION_IMAGE)" --target corrosion .
.PHONY: corrosion-image-push .PHONY: corrosion-multiarch-image-push
corrosion-image-push: corrosion-image corrosion-multiarch-image-push:
docker push "$(CORROSION_IMAGE)" docker buildx build --push --platform linux/amd64,linux/arm64 -t "$(CORROSION_IMAGE)" --target corrosion .
.PHONY: ucind-image .PHONY: ucind-image
ucind-image: ucind-image:
docker build -t "$(UCIND_IMAGE)" --target ucind . docker build -t "$(UCIND_IMAGE)" --target ucind .
.PHONY: ucind-image-push .PHONY: ucind-multiarch-image-push
ucind-image-push: ucind-image ucind-multiarch-image-push:
docker push "$(UCIND_IMAGE)" 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
.PHONY: install-dev
install-dev:
go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.34.2
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.5.1
# TODO: install a pinned protoc version
+77 -37
View File
@@ -2,39 +2,41 @@
<img src="./website/images/logo.svg" height="100" alt="Uncloud logo"/> <img src="./website/images/logo.svg" height="100" alt="Uncloud logo"/>
<h1>Uncloud</h1> <h1>Uncloud</h1>
<p><strong>Docker simplicity. Multi-machine power.</strong></p> <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>
</p>
</div> </div>
Uncloud is a lightweight clustering and container orchestration tool that lets you deploy and manage web apps across 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 your Docker hosts and provides automatic service discovery, load balancing, ingress with HTTPS, and simple CLI commands
to manage your apps. to manage your apps.
Unlike traditional orchestrators, there's no central control plane and quorum to maintain. Each machine maintains a 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. some machines go offline.
Uncloud aims to be the solution for developers who want the flexibility of self-hosted infrastructure without the Uncloud is the solution for developers who want the flexibility of self-hosted infrastructure without the operational
operational complexity of Kubernetes. 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>
## ✨ Features ## ✨ Features
* **Deploy anywhere**: Combine cloud VMs, dedicated servers, and bare metal into a unified computing environment — * **Deploy anywhere**: Combine cloud VMs, dedicated servers, and bare metal into a unified computing environment —
regardless of location or provider. 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 * **Zero-config private network**: Automatic WireGuard mesh with peer discovery and NAT traversal. Containers get unique
IPs for direct cross-machine communication. 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 * **Imperative over declarative**: Favoring imperative operations over state reconciliation simplifies both the mental
model and troubleshooting. 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 * **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. [Uncloud DNS](https://github.com/psviderski/uncloud-dns) service.
* **Automatic HTTPS**: Built-in Caddy reverse proxy handles TLS certificate provisioning and renewal using Let's * **Automatic HTTPS**: Built-in Caddy reverse proxy handles TLS certificate provisioning and renewal using Let's
@@ -42,17 +44,22 @@ Deploy a highly available web app with automatic HTTPS across multiple regions a
* **Docker-like CLI**: Familiar commands for managing both infrastructure and applications. * **Docker-like CLI**: Familiar commands for managing both infrastructure and applications.
* **Remote management**: Control your entire infrastructure through SSH access to any single machine in the cluster. * **Remote management**: Control your entire infrastructure through SSH access to any single machine in the cluster.
Coming soon: ## 🎬 Quick demo
* Infrastructure as Code using Docker Compose format The screenshot below demonstrates how I use Uncloud to deploy the [Uncloud Documentation](https://docs.uncloud.run)
* Project isolation through environments/namespaces website to 2 remote machines (why not?) from the [`compose.yaml`](docs/compose.yaml) file on my local machine.
* Persistent volumes and secrets management
* Monitoring and log aggregation
* Database deployment and management
Here is a diagram of an Uncloud multi-provider cluster of 3 machines: 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.
![Diagram: multi-provider cluster of 3 machines](website/images/diagram.webp) ![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? ## 💫 Why Uncloud?
@@ -62,14 +69,15 @@ believe there's a sweet spot in between — a pragmatic solution for the majorit
scale. You should be able to: scale. You should be able to:
* **Own your infrastructure and data**: Whether driven by costs, compliance, or flexibility, run applications on any * **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. combination of cloud VMs and personal hardware while controlling your data and maintaining the cloud-like experience
* **Stay simple**: Don't worry about control planes, highly-available clusters, or complex YAML configurations for you love.
common use cases. * **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 * **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. 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. platform, whether you're running on a $5 VPS, a spare Mac mini, or a rack of bare metal servers.
## 🚀 Quick start ## 🚀 Quick start
@@ -83,7 +91,7 @@ platform, whether you're running on a $5 VPS, a spare Mac mini, or a rack of bar
curl -fsS https://get.uncloud.run/install.sh | sh curl -fsS https://get.uncloud.run/install.sh | sh
``` ```
2. Initialize your first machine: 2. Initialise your first machine:
```bash ```bash
uc machine init root@your-server-ip uc machine init root@your-server-ip
@@ -114,14 +122,20 @@ platform, whether you're running on a $5 VPS, a spare Mac mini, or a rack of bar
uncloud-uninstall 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 ## ⚙️ How it works
Check out the [design document](docs/design.md) to understand Uncloud's design philosophy and goals. Here, let's peek Check out the [design document](docs/design.md) to understand Uncloud's design philosophy and goals.
under the hood to see what happens when you run certain commands.
**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 ```bash
$ uc machine init --name oracle-vm ubuntu@152.67.101.197 $ uc machine init --name oracle-vm ubuntu@152.67.101.197
@@ -254,23 +268,49 @@ 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 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 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. machine is a full backup of your control plane.
</details>
## 🏗 Project status ## 🏗 Project status
Uncloud is currently in active development and is **not ready for production use**. Features may change significantly 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. 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) * 🐛 Found a bug? [Open an issue](https://github.com/psviderski/uncloud/issues)
* 💡 Have ideas? [Join the discussion](https://github.com/psviderski/uncloud/discussions) * 💡 Have ideas or need help? [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 ## 📫 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. features, and be the first to know when it's ready for production use.
* Watch this repository for releases. * Watch this repository for releases.
* Follow [@psviderski](https://github.com/psviderski) on GitHub.
## ❤️ Contributors ## ❤️ Contributors
+5 -4
View File
@@ -3,12 +3,13 @@ package main
import ( import (
"context" "context"
"fmt" "fmt"
"github.com/docker/docker/client"
"github.com/spf13/cobra"
"os" "os"
"strings" "strings"
"github.com/docker/docker/client"
"github.com/psviderski/uncloud/cmd/ucind/cluster" "github.com/psviderski/uncloud/cmd/ucind/cluster"
"github.com/psviderski/uncloud/internal/ucind" "github.com/psviderski/uncloud/internal/ucind"
"github.com/spf13/cobra"
) )
func main() { func main() {
@@ -44,9 +45,9 @@ func main() {
} }
// TODO: allow to override using UNCLOUD_CONFIG env var. // 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.") "path to the Uncloud configuration file.")
_ = cmd.MarkPersistentFlagFilename("uncloud-config", "toml") _ = cmd.MarkPersistentFlagFilename("uncloud-config", "yaml", "yml")
cmd.AddCommand( cmd.AddCommand(
cluster.NewRootCommand(), 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" "context"
"errors" "errors"
"fmt" "fmt"
"maps"
"slices"
"strings"
"github.com/docker/cli/cli/streams" "github.com/docker/cli/cli/streams"
"github.com/docker/compose/v2/pkg/progress" "github.com/docker/compose/v2/pkg/progress"
"github.com/psviderski/uncloud/internal/cli" "github.com/psviderski/uncloud/internal/cli"
"github.com/psviderski/uncloud/internal/machine/api/pb" "github.com/psviderski/uncloud/internal/machine/api/pb"
"github.com/psviderski/uncloud/pkg/api" "github.com/psviderski/uncloud/pkg/api"
"github.com/psviderski/uncloud/pkg/client" "github.com/psviderski/uncloud/pkg/client"
"github.com/psviderski/uncloud/pkg/client/deploy"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"maps"
"slices"
"strings"
) )
type deployOptions struct { type deployOptions struct {
image string image string
machine string machines []string
cluster string context string
} }
func NewDeployCommand() *cobra.Command { func NewDeployCommand() *cobra.Command {
@@ -39,18 +39,19 @@ func NewDeployCommand() *cobra.Command {
cmd.Flags().StringVar(&opts.image, "image", "", cmd.Flags().StringVar(&opts.image, "image", "",
"Caddy Docker image to deploy. (default caddy:LATEST_VERSION)") "Caddy Docker image to deploy. (default caddy:LATEST_VERSION)")
cmd.Flags().StringVarP(&opts.machine, "machine", "m", "", cmd.Flags().StringSliceVarP(&opts.machines, "machine", "m", nil,
"Machine names to deploy to (comma-separated). (default is all machines)") "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( cmd.Flags().StringVarP(
&opts.cluster, "cluster", "c", "", &opts.context, "context", "c", "",
"Name of the cluster to deploy to. (default is the current cluster)", "Name of the cluster context to deploy to. (default is the current context)",
) )
return cmd return cmd
} }
func runDeploy(ctx context.Context, uncli *cli.CLI, opts deployOptions) error { 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 { if err != nil {
return fmt.Errorf("connect to cluster: %w", err) 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()
fmt.Println("Preparing a deployment plan...") fmt.Println("Preparing a deployment plan...")
var filter deploy.MachineFilter placement := api.Placement{
if opts.machine != "" { Machines: cli.ExpandCommaSeparatedValues(opts.machines),
machines := strings.Split(opts.machine, ",")
for i, m := range machines {
machines[i] = strings.TrimSpace(m)
}
filter = machineFilter(machines)
} }
d, err := clusterClient.NewCaddyDeployment(opts.image, placement)
d, err := clusterClient.NewCaddyDeployment(opts.image, filter)
if err != nil { if err != nil {
return fmt.Errorf("create caddy deployment: %w", err) 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) plan, err := d.Plan(ctx)
if err != nil { 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) return fmt.Errorf("plan caddy deployment: %w", err)
} }
if len(plan.Operations) == 0 { if len(plan.Operations) == 0 {
if opts.machine != "" { fmt.Printf("%s service is up to date.\n", client.CaddyServiceName)
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)
}
} else { } else {
if svc.ID == "" { if svc.ID == "" {
if opts.machine != "" { if len(opts.machines) > 0 {
fmt.Println("This will run a Caddy container on selected machines.") fmt.Println("This will run a Caddy container on each selected machine.")
} else { } else {
fmt.Println("This will run a Caddy container on each machine.") 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. // 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 { 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) { if errors.Is(err, api.ErrNotFound) {
fmt.Println("Skipping DNS records update as no cluster domain is reserved (see 'uc dns').") fmt.Println("Skipping DNS records update as no cluster domain is reserved (see 'uc dns').")
return nil 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. // 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 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 var err error
records, err = clusterClient.CreateIngressRecords(ctx, client.CaddyServiceName) records, err = clusterClient.CreateIngressRecords(ctx, client.CaddyServiceName)
return err return err
@@ -190,18 +173,19 @@ func UpdateDomainRecords(ctx context.Context, clusterClient *client.Client, prog
if err != nil { if err != nil {
if errors.Is(err, client.ErrNoReachableMachines) { if errors.Is(err, client.ErrNoReachableMachines) {
fmt.Println() fmt.Println()
fmt.Println("DNS records could not be updated as there are no internet-reachable machines running " + fmt.Printf("DNS records for domain '%s' could not be updated as there are no internet-reachable "+
"caddy containers.") "machines running caddy containers.\n", domain)
fmt.Println() fmt.Println()
fmt.Println("Possible solutions:") fmt.Println("Possible solutions:")
fmt.Println("- Ensure your machines have public IP addresses") fmt.Println("- Ensure your machines have public IP addresses")
fmt.Println("- Use --public-ip flag when adding machines to override the automatically detected IPs") 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("- Check firewall settings on your machines")
fmt.Println("- Configure port forwarding if behind NAT") 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()
fmt.Println("Your services will not be accessible from the internet until at least one machine " + fmt.Println("Your services won't be accessible from the internet until at least one machine " +
"becomes reachable.") "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) 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 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" "context"
"errors" "errors"
"fmt" "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/docker/compose/v2/pkg/progress"
"github.com/psviderski/uncloud/internal/cli" "github.com/psviderski/uncloud/internal/cli"
"github.com/psviderski/uncloud/pkg/api" "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/compose"
"github.com/psviderski/uncloud/pkg/client/deploy" "github.com/psviderski/uncloud/pkg/client/deploy"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"strings"
) )
type deployOptions struct { type deployOptions struct {
files []string files []string
profiles []string
services []string services []string
noBuild bool
cluster string context string
} }
// NewDeployCommand creates a new command to deploy services from a Compose file. // NewDeployCommand creates a new command to deploy services from a Compose file.
func NewDeployCommand() *cobra.Command { func NewDeployCommand() *cobra.Command {
opts := deployOptions{} opts := deployOptions{}
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "deploy [FLAGS] [SERVICE...]", Use: "deploy [FLAGS] [SERVICE...]",
// TODO: remove WIP when the command is fully implemented. Short: "Deploy services from a Compose file.",
Short: "WIP: Deploy services from a Compose file.",
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
uncli := cmd.Context().Value("cli").(*cli.CLI) uncli := cmd.Context().Value("cli").(*cli.CLI)
@@ -40,29 +43,66 @@ func NewDeployCommand() *cobra.Command {
} }
cmd.Flags().StringSliceVarP(&opts.files, "file", "f", nil, cmd.Flags().StringSliceVarP(&opts.files, "file", "f", nil,
"One or more Compose files to deploy services from. (default compose-ports-long.yaml)") "One or more Compose files to deploy services from. (default compose.yaml)")
cmd.Flags().StringVarP(&opts.cluster, "cluster", "c", "", cmd.Flags().StringSliceVarP(&opts.profiles, "profile", "p", nil,
"Name of the cluster to deploy to (default is the current cluster)") "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 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. // runDeploy parses the Compose file(s) and deploys the services.
func runDeploy(ctx context.Context, uncli *cli.CLI, opts deployOptions) error { 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 { if err != nil {
return fmt.Errorf("load compose file(s): %w", err) return fmt.Errorf("load compose file(s): %w", err)
} }
if len(opts.services) > 0 { if len(opts.services) > 0 {
// TODO: handle dependencies properly. // Includes service dependencies by default. This is the default docker compose behavior.
project, err = project.WithSelectedServices(opts.services, types.IgnoreDependencies) project, err = project.WithSelectedServices(opts.services)
if err != nil { if err != nil {
return fmt.Errorf("select services: %w", err) 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 { if err != nil {
return fmt.Errorf("connect to cluster: %w", err) 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:") fmt.Println("Deployment plan:")
if err = printPlan(ctx, clusterClient, plan); err != nil {
for _, op := range plan.Operations { return fmt.Errorf("print deployment plan: %w", err)
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), " "))
} }
fmt.Println() fmt.Println()
@@ -128,6 +146,31 @@ func runDeploy(ctx context.Context, uncli *cli.CLI, opts deployOptions) error {
}, uncli.ProgressOut(), "Deploying services") }, 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 { func indent(text, prefix string) string {
lines := strings.Split(text, "\n") lines := strings.Split(text, "\n")
for i, line := range lines { for i, line := range lines {
+6 -5
View File
@@ -4,15 +4,16 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"github.com/psviderski/uncloud/internal/cli"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"google.golang.org/grpc/codes" "google.golang.org/grpc/codes"
"google.golang.org/grpc/status" "google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/emptypb" "google.golang.org/protobuf/types/known/emptypb"
"github.com/psviderski/uncloud/internal/cli"
) )
type releaseOptions struct { type releaseOptions struct {
cluster string context string
} }
func NewReleaseCommand() *cobra.Command { func NewReleaseCommand() *cobra.Command {
@@ -28,15 +29,15 @@ func NewReleaseCommand() *cobra.Command {
} }
cmd.Flags().StringVarP( cmd.Flags().StringVarP(
&opts.cluster, "cluster", "c", "", &opts.context, "context", "c", "",
"Name of the cluster. (default is the current cluster)", "Name of the cluster context. (default is the current context)",
) )
return cmd return cmd
} }
func release(ctx context.Context, uncli *cli.CLI, opts releaseOptions) error { 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 { if err != nil {
return fmt.Errorf("connect to cluster: %w", err) return fmt.Errorf("connect to cluster: %w", err)
} }
+5 -4
View File
@@ -4,6 +4,7 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"github.com/psviderski/uncloud/cmd/uncloud/caddy" "github.com/psviderski/uncloud/cmd/uncloud/caddy"
"github.com/psviderski/uncloud/internal/cli" "github.com/psviderski/uncloud/internal/cli"
"github.com/psviderski/uncloud/internal/machine/api/pb" "github.com/psviderski/uncloud/internal/machine/api/pb"
@@ -18,7 +19,7 @@ const DefaultUncloudDNSAPIEndpoint = "https://dns.uncloud.run/v1"
type reserveOptions struct { type reserveOptions struct {
endpoint string endpoint string
cluster string context string
} }
func NewReserveCommand() *cobra.Command { func NewReserveCommand() *cobra.Command {
@@ -36,15 +37,15 @@ func NewReserveCommand() *cobra.Command {
cmd.Flags().StringVar(&opts.endpoint, "endpoint", DefaultUncloudDNSAPIEndpoint, cmd.Flags().StringVar(&opts.endpoint, "endpoint", DefaultUncloudDNSAPIEndpoint,
"API endpoint for the Uncloud DNS service.") "API endpoint for the Uncloud DNS service.")
cmd.Flags().StringVarP( cmd.Flags().StringVarP(
&opts.cluster, "cluster", "c", "", &opts.context, "context", "c", "",
"Name of the cluster. (default is the current cluster)", "Name of the cluster context. (default is the current context)",
) )
return cmd return cmd
} }
func reserve(ctx context.Context, uncli *cli.CLI, opts reserveOptions) error { 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 { if err != nil {
return fmt.Errorf("connect to cluster: %w", err) return fmt.Errorf("connect to cluster: %w", err)
} }
+5 -4
View File
@@ -4,13 +4,14 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"github.com/psviderski/uncloud/internal/cli" "github.com/psviderski/uncloud/internal/cli"
"github.com/psviderski/uncloud/pkg/api" "github.com/psviderski/uncloud/pkg/api"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
type showOptions struct { type showOptions struct {
cluster string context string
} }
func NewShowCommand() *cobra.Command { func NewShowCommand() *cobra.Command {
@@ -26,15 +27,15 @@ func NewShowCommand() *cobra.Command {
} }
cmd.Flags().StringVarP( cmd.Flags().StringVarP(
&opts.cluster, "cluster", "c", "", &opts.context, "context", "c", "",
"Name of the cluster. (default is the current cluster)", "Name of the cluster context. (default is the current context)",
) )
return cmd return cmd
} }
func show(ctx context.Context, uncli *cli.CLI, opts showOptions) error { 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 { if err != nil {
return fmt.Errorf("connect to cluster: %w", err) return fmt.Errorf("connect to cluster: %w", err)
} }
+30 -21
View File
@@ -4,20 +4,19 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"net/netip"
"time"
"github.com/cenkalti/backoff/v4" "github.com/cenkalti/backoff/v4"
"github.com/docker/compose/v2/pkg/progress" "github.com/docker/compose/v2/pkg/progress"
"github.com/psviderski/uncloud/cmd/uncloud/caddy" "github.com/psviderski/uncloud/cmd/uncloud/caddy"
"github.com/psviderski/uncloud/internal/cli" "github.com/psviderski/uncloud/internal/cli"
"github.com/psviderski/uncloud/internal/cli/config" "github.com/psviderski/uncloud/internal/cli/config"
"github.com/psviderski/uncloud/internal/machine/api/pb"
"github.com/psviderski/uncloud/pkg/api" "github.com/psviderski/uncloud/pkg/api"
"github.com/psviderski/uncloud/pkg/client" "github.com/psviderski/uncloud/pkg/client"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"google.golang.org/grpc/codes" "google.golang.org/grpc/codes"
"google.golang.org/grpc/status" "google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/emptypb"
"net/netip"
"time"
) )
type addOptions struct { type addOptions struct {
@@ -25,7 +24,8 @@ type addOptions struct {
noCaddy bool noCaddy bool
publicIP string publicIP string
sshKey string sshKey string
cluster string context string
version string
} }
func NewAddCommand() *cobra.Command { 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.", "blank '' or 'none' to disable ingress on this machine, or specify an IP address.",
) )
cmd.Flags().StringVarP( cmd.Flags().StringVarP(
&opts.sshKey, "ssh-key", "i", "", &opts.sshKey, "ssh-key", "i", "~/.ssh/id_ed25519",
"path to SSH private key for SSH remote login. (default ~/.ssh/id_*)", "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( cmd.Flags().StringVarP(
&opts.cluster, "cluster", "c", "", &opts.context, "context", "c", "",
"Name of the cluster to add the machine to. (default is the current cluster)", "Name of the cluster context to add the machine to. (default is the current context)",
) )
return cmd return cmd
} }
@@ -87,10 +92,17 @@ func add(ctx context.Context, uncli *cli.CLI, remoteMachine cli.RemoteMachine, o
publicIP = &ip 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 { if err != nil {
return err return err
} }
defer clusterClient.Close()
defer machineClient.Close() defer machineClient.Close()
if opts.noCaddy { 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) 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, // 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. // 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 := "" caddyImage := ""
caddySvc, err := machineClient.InspectService(ctx, client.CaddyServiceName) caddySvc, err := clusterClient.InspectService(ctx, client.CaddyServiceName)
if err != nil { if err != nil {
if !errors.Is(err, api.ErrNotFound) { if !errors.Is(err, api.ErrNotFound) {
return fmt.Errorf("inspect caddy service: %w", err) 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 { if err != nil {
return fmt.Errorf("create caddy deployment: %w", err) return fmt.Errorf("create caddy deployment: %w", err)
} }
@@ -162,7 +171,7 @@ func waitClusterInitialised(ctx context.Context, client *client.Client) error {
), ctx) ), ctx)
check := func() error { check := func() error {
_, err := client.ListMachines(ctx) _, err := client.ListMachines(ctx, nil)
if err == nil { if err == nil {
return nil return nil
} }
+25 -11
View File
@@ -3,6 +3,8 @@ package machine
import ( import (
"context" "context"
"fmt" "fmt"
"net/netip"
"github.com/docker/compose/v2/pkg/progress" "github.com/docker/compose/v2/pkg/progress"
"github.com/psviderski/uncloud/cmd/uncloud/caddy" "github.com/psviderski/uncloud/cmd/uncloud/caddy"
"github.com/psviderski/uncloud/cmd/uncloud/dns" "github.com/psviderski/uncloud/cmd/uncloud/dns"
@@ -10,8 +12,8 @@ import (
"github.com/psviderski/uncloud/internal/cli/config" "github.com/psviderski/uncloud/internal/cli/config"
"github.com/psviderski/uncloud/internal/machine/api/pb" "github.com/psviderski/uncloud/internal/machine/api/pb"
"github.com/psviderski/uncloud/internal/machine/cluster" "github.com/psviderski/uncloud/internal/machine/cluster"
"github.com/psviderski/uncloud/pkg/api"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"net/netip"
) )
type initOptions struct { type initOptions struct {
@@ -22,15 +24,17 @@ type initOptions struct {
noDNS bool noDNS bool
publicIP string publicIP string
sshKey string sshKey string
cluster string version string
context string
} }
func NewInitCommand() *cobra.Command { func NewInitCommand() *cobra.Command {
opts := initOptions{} opts := initOptions{}
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "init [USER@HOST:PORT]", Use: "init [USER@HOST:PORT]",
Short: "Initialise a new cluster that consists of the local or remote machine.", Short: "Initialise a new cluster with a remote machine as the first member.",
// TODO: include usage examples of initialising a local and remote machine. // TODO: include usage examples of initialising a remote machine.
// TODO: support initialising a cluster on the local machine.
Args: cobra.MaximumNArgs(1), Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
uncli := cmd.Context().Value("cli").(*cli.CLI) 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.", "blank '' or 'none' to disable ingress on this machine, or specify an IP address.",
) )
cmd.Flags().StringVarP( cmd.Flags().StringVarP(
&opts.sshKey, "ssh-key", "i", "", &opts.sshKey, "ssh-key", "i", "~/.ssh/id_ed25519",
"Path to SSH private key for SSH remote login. (default ~/.ssh/id_*)", "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( cmd.Flags().StringVarP(
&opts.cluster, "cluster", "c", "", &opts.context, "context", "c", "default",
"Name of the cluster in the local config if initialising a remote machine.", "Name of the created context for the initialised cluster in the Uncloud config.",
) )
return cmd return cmd
@@ -106,8 +114,14 @@ func initCluster(ctx context.Context, uncli *cli.CLI, remoteMachine *cli.RemoteM
} }
publicIP = &ip publicIP = &ip
} }
client, err := uncli.InitCluster(ctx, cli.InitClusterOptions{
client, err := uncli.InitCluster(ctx, remoteMachine, opts.cluster, opts.name, netPrefix, publicIP) Context: opts.context,
MachineName: opts.name,
Network: netPrefix,
PublicIP: publicIP,
RemoteMachine: remoteMachine,
Version: opts.version,
})
if err != nil { if err != nil {
return err return err
} }
@@ -132,7 +146,7 @@ func initCluster(ctx context.Context, uncli *cli.CLI, remoteMachine *cli.RemoteM
} }
if !opts.noCaddy { if !opts.noCaddy {
d, err := client.NewCaddyDeployment("", nil) d, err := client.NewCaddyDeployment("", api.Placement{})
if err != nil { if err != nil {
return fmt.Errorf("create caddy deployment: %w", err) return fmt.Errorf("create caddy deployment: %w", err)
} }
@@ -3,29 +3,30 @@ package machine
import ( import (
"context" "context"
"fmt" "fmt"
"github.com/spf13/cobra"
"net/netip" "net/netip"
"os" "os"
"strings" "strings"
"text/tabwriter" "text/tabwriter"
"github.com/psviderski/uncloud/internal/cli" "github.com/psviderski/uncloud/internal/cli"
"github.com/psviderski/uncloud/internal/machine/network" "github.com/psviderski/uncloud/internal/machine/network"
"github.com/spf13/cobra"
) )
func NewListCommand() *cobra.Command { func NewListCommand() *cobra.Command {
var cluster string var clusterContext string
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "ls", Use: "ls",
Aliases: []string{"list"}, Aliases: []string{"list"},
Short: "List machines in a cluster.", Short: "List machines in a cluster.",
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
uncli := cmd.Context().Value("cli").(*cli.CLI) uncli := cmd.Context().Value("cli").(*cli.CLI)
return list(cmd.Context(), uncli, cluster) return list(cmd.Context(), uncli, clusterContext)
}, },
} }
cmd.Flags().StringVarP( cmd.Flags().StringVarP(
&cluster, "cluster", "c", "", &clusterContext, "context", "c", "",
"Name of the cluster. (default is the current cluster)", "Name of the cluster context. (default is the current context)",
) )
return cmd return cmd
} }
@@ -37,7 +38,7 @@ func list(ctx context.Context, uncli *cli.CLI, clusterName string) error {
} }
defer client.Close() defer client.Close()
machines, err := client.ListMachines(ctx) machines, err := client.ListMachines(ctx, nil)
if err != nil { if err != nil {
return fmt.Errorf("list machines: %w", err) return fmt.Errorf("list machines: %w", err)
} }
+3 -2
View File
@@ -6,8 +6,9 @@ import (
func NewRootCommand() *cobra.Command { func NewRootCommand() *cobra.Command {
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "machine", Use: "machine",
Short: "Manage machines in an Uncloud cluster.", Aliases: []string{"m"},
Short: "Manage machines in an Uncloud cluster.",
} }
cmd.AddCommand( cmd.AddCommand(
NewAddCommand(), NewAddCommand(),
+12 -4
View File
@@ -7,12 +7,15 @@ import (
"strings" "strings"
"github.com/psviderski/uncloud/cmd/uncloud/caddy" "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/dns"
"github.com/psviderski/uncloud/cmd/uncloud/machine" "github.com/psviderski/uncloud/cmd/uncloud/machine"
"github.com/psviderski/uncloud/cmd/uncloud/service" "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"
"github.com/psviderski/uncloud/internal/cli/config" "github.com/psviderski/uncloud/internal/cli/config"
"github.com/psviderski/uncloud/internal/fs" "github.com/psviderski/uncloud/internal/fs"
"github.com/psviderski/uncloud/internal/version"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@@ -26,6 +29,7 @@ func main() {
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "uncloud", Use: "uncloud",
Short: "A CLI tool for managing Uncloud resources such as clusters, machines, and services.", Short: "A CLI tool for managing Uncloud resources such as clusters, machines, and services.",
Version: version.String(),
SilenceUsage: true, SilenceUsage: true,
SilenceErrors: true, SilenceErrors: true,
PersistentPreRunE: func(cmd *cobra.Command, args []string) error { PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
@@ -37,7 +41,7 @@ func main() {
return fmt.Errorf("parse TCP address: %w", err) return fmt.Errorf("parse TCP address: %w", err)
} }
conn = &config.MachineConnection{ conn = &config.MachineConnection{
TCP: addrPort, TCP: &addrPort,
} }
} else { } else {
dest := opts.connect dest := opts.connect
@@ -53,7 +57,7 @@ func main() {
configPath := fs.ExpandHomeDir(opts.configPath) configPath := fs.ExpandHomeDir(opts.configPath)
uncli, err := cli.New(configPath, conn) uncli, err := cli.New(configPath, conn)
if err != nil { 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)) cmd.SetContext(context.WithValue(cmd.Context(), "cli", uncli))
return nil return nil
@@ -64,13 +68,16 @@ func main() {
"Connect to a remote cluster machine without using the Uncloud configuration file.\n"+ "Connect to a remote cluster machine without using the Uncloud configuration file.\n"+
"Format: [ssh://]user@host[:port] or tcp://host:port") "Format: [ssh://]user@host[:port] or tcp://host:port")
// TODO: allow to override using UNCLOUD_CONFIG env var. // 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.") "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( cmd.AddCommand(
NewDeployCommand(), NewDeployCommand(),
NewBuildCommand(),
caddy.NewRootCommand(), caddy.NewRootCommand(),
cmdcontext.NewRootCommand(),
dns.NewRootCommand(), dns.NewRootCommand(),
machine.NewRootCommand(), machine.NewRootCommand(),
service.NewRootCommand(), service.NewRootCommand(),
@@ -79,6 +86,7 @@ func main() {
service.NewRmCommand(), service.NewRmCommand(),
service.NewRunCommand(), service.NewRunCommand(),
service.NewScaleCommand(), service.NewScaleCommand(),
volume.NewRootCommand(),
) )
cobra.CheckErr(cmd.Execute()) cobra.CheckErr(cmd.Execute())
} }
+8 -7
View File
@@ -3,14 +3,15 @@ package service
import ( import (
"context" "context"
"fmt" "fmt"
"github.com/docker/docker/pkg/stringid"
"github.com/docker/go-units"
"os" "os"
"text/tabwriter" "text/tabwriter"
"time" "time"
"github.com/spf13/cobra" "github.com/docker/docker/pkg/stringid"
"github.com/docker/go-units"
"github.com/psviderski/uncloud/internal/cli" "github.com/psviderski/uncloud/internal/cli"
"github.com/spf13/cobra"
) )
type inspectOptions struct { type inspectOptions struct {
@@ -21,7 +22,7 @@ type inspectOptions struct {
func NewInspectCommand() *cobra.Command { func NewInspectCommand() *cobra.Command {
opts := inspectOptions{} opts := inspectOptions{}
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "inspect", Use: "inspect SERVICE",
Short: "Display detailed information on a service.", Short: "Display detailed information on a service.",
Args: cobra.ExactArgs(1), Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
@@ -31,8 +32,8 @@ func NewInspectCommand() *cobra.Command {
}, },
} }
cmd.Flags().StringVarP( cmd.Flags().StringVarP(
&opts.cluster, "cluster", "c", "", &opts.cluster, "context", "c", "",
"Name of the cluster. (default is the current cluster)", "Name of the cluster context. (default is the current context)",
) )
return cmd return cmd
} }
@@ -49,7 +50,7 @@ func inspect(ctx context.Context, uncli *cli.CLI, opts inspectOptions) error {
return fmt.Errorf("inspect service: %w", err) return fmt.Errorf("inspect service: %w", err)
} }
machines, err := client.ListMachines(ctx) machines, err := client.ListMachines(ctx, nil)
if err != nil { if err != nil {
return fmt.Errorf("list machines: %w", err) return fmt.Errorf("list machines: %w", err)
} }
@@ -3,14 +3,16 @@ package service
import ( import (
"context" "context"
"fmt" "fmt"
"github.com/psviderski/uncloud/internal/cli"
"github.com/spf13/cobra"
"os" "os"
"strings" "strings"
"text/tabwriter" "text/tabwriter"
"github.com/psviderski/uncloud/internal/cli"
"github.com/spf13/cobra"
) )
func NewListCommand() *cobra.Command { func NewListCommand() *cobra.Command {
// TODO(lhf): rename to context
var cluster string var cluster string
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "ls", Use: "ls",
@@ -22,8 +24,8 @@ func NewListCommand() *cobra.Command {
}, },
} }
cmd.Flags().StringVarP( cmd.Flags().StringVarP(
&cluster, "cluster", "c", "", &cluster, "context", "c", "",
"Name of the cluster. (default is the current cluster)", "Name of the cluster context. (default is the current context)",
) )
return cmd return cmd
} }
+4 -5
View File
@@ -3,11 +3,10 @@ package service
import ( import (
"context" "context"
"fmt" "fmt"
"github.com/docker/cli/cli/streams"
"github.com/docker/compose/v2/pkg/progress" "github.com/docker/compose/v2/pkg/progress"
"github.com/psviderski/uncloud/internal/cli" "github.com/psviderski/uncloud/internal/cli"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"os"
) )
type rmOptions struct { type rmOptions struct {
@@ -29,8 +28,8 @@ func NewRmCommand() *cobra.Command {
}, },
} }
cmd.Flags().StringVarP( cmd.Flags().StringVarP(
&opts.cluster, "cluster", "c", "", &opts.cluster, "context", "c", "",
"Name of the cluster. (default is the current cluster)", "Name of the cluster context. (default is the current context)",
) )
return cmd 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 fmt.Errorf("remove service '%s': %w", s, err)
} }
return nil return nil
}, streams.NewOut(os.Stdout), "Removing service "+s) }, uncli.ProgressOut(), "Removing service "+s)
} }
return nil return nil
+3 -2
View File
@@ -6,8 +6,9 @@ import (
func NewRootCommand() *cobra.Command { func NewRootCommand() *cobra.Command {
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "service", Use: "service",
Short: "Manage services in an Uncloud cluster.", Aliases: []string{"svc"},
Short: "Manage services in an Uncloud cluster.",
} }
cmd.AddCommand( cmd.AddCommand(
NewInspectCommand(), NewInspectCommand(),
+260 -82
View File
@@ -3,26 +3,35 @@ package service
import ( import (
"context" "context"
"fmt" "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/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/api"
"github.com/psviderski/uncloud/pkg/client/deploy" "github.com/psviderski/uncloud/pkg/client/deploy"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"slices"
"strings"
) )
type runOptions struct { type runOptions struct {
command []string command []string
cpu dockeropts.NanoCPUs
entrypoint string entrypoint string
entrypointChanged bool entrypointChanged bool
env []string
image string image string
machines []string machines []string
memory dockeropts.MemBytes
mode string mode string
name string name string
privileged bool
publish []string publish []string
pull string pull string
replicas uint replicas uint
user string
volumes []string volumes []string
cluster 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", "", cmd.Flags().StringVar(&opts.entrypoint, "entrypoint", "",
"Overwrite the default ENTRYPOINT of the image. Pass an empty string \"\" to reset it.") "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, cmd.Flags().StringVar(&opts.mode, "mode", api.ServiceModeReplicated,
fmt.Sprintf("Replication mode of the service: either '%s' (a specified number of containers across "+ fmt.Sprintf("Replication mode of the service: either '%s' (a specified number of containers across "+
"the machines) or '%s' (one container on every machine).", "the machines) or '%s' (one container on every machine).",
api.ServiceModeReplicated, api.ServiceModeGlobal)) api.ServiceModeReplicated, api.ServiceModeGlobal))
cmd.Flags().StringSliceVarP(&opts.machines, "machine", "m", nil, 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)") "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", "", cmd.Flags().StringVarP(&opts.name, "name", "n", "",
"Assign a name to the service. A random name is generated if not specified.") "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, cmd.Flags().StringSliceVarP(&opts.publish, "publish", "p", nil,
"Publish a service port to make it accessible outside the cluster. Can be specified multiple times.\n"+ "Publish a service port to make it accessible outside the cluster. Can be specified multiple times.\n"+
"Format: [hostname:][load_balancer_port:]container_port[/protocol] or [host_ip:]:host_port:container_port[/protocol]@host\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"+ "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"+ "and a cluster domain is reserved, service-name.cluster-domain will be used as the hostname.\n"+
"Examples:\n"+ "Examples:\n"+
" -p 8080/https Publish port 8080 as HTTPS via load balancer with default service-name.cluster-domain hostname\n"+ " -p 8080/https Publish port 8080 as HTTPS via reverse proxy with default service-name.cluster-domain hostname\n"+
" -p app.example.com:8080/https Publish port 8080 as HTTPS via load balancer with custom hostname\n"+ " -p app.example.com:8080/https Publish port 8080 as HTTPS via reverse proxy with custom hostname\n"+
" -p 9000:8080 Publish port 8080 as TCP port 9000 via load balancer\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") " -p 53:5353/udp@host Bind UDP port 5353 to host port 53")
cmd.Flags().StringVar(&opts.pull, "pull", api.PullPolicyMissing, cmd.Flags().StringVar(&opts.pull, "pull", api.PullPolicyMissing,
fmt.Sprintf("Pull image from the registry before running service containers ('%s', '%s', '%s').", fmt.Sprintf("Pull image from the registry before running service containers ('%s', '%s', '%s').",
api.PullPolicyAlways, api.PullPolicyMissing, api.PullPolicyNever)) api.PullPolicyAlways, api.PullPolicyMissing, api.PullPolicyNever))
cmd.Flags().UintVar(&opts.replicas, "replicas", 1, cmd.Flags().UintVar(&opts.replicas, "replicas", 1,
"Number of containers to run for the service. Only valid for a replicated service.") "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, cmd.Flags().StringSliceVarP(&opts.volumes, "volume", "v", nil,
"Bind mount a host file or directory into a service container using the format "+ "Mount a data volume or host path into service containers. Service containers will be scheduled on the machine(s) where\n"+
"/host/path:/container/path[:ro]. Can be specified multiple times.") "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( cmd.Flags().StringVarP(
&opts.cluster, "cluster", "c", "", &opts.cluster, "context", "c", "",
"Name of the cluster to run the service in. (default is the current cluster)", "Name of the cluster context to run the service in. (default is the current context)",
) )
return cmd return cmd
} }
func run(ctx context.Context, uncli *cli.CLI, opts runOptions) error { func run(ctx context.Context, uncli *cli.CLI, opts runOptions) error {
switch opts.mode { spec, err := prepareServiceSpec(opts)
case api.ServiceModeReplicated, api.ServiceModeGlobal: if err != nil {
default: return err
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)
} }
clusterClient, err := uncli.ConnectCluster(ctx, opts.cluster) 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() 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 { if err != nil {
return fmt.Errorf("run service: %w", err) return err
} }
svc, err := clusterClient.InspectService(ctx, resp.ID) svc, err := clusterClient.InspectService(ctx, resp.ID)
@@ -183,3 +157,207 @@ func run(ctx context.Context, uncli *cli.CLI, opts runOptions) error {
return nil 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( cmd.Flags().StringVarP(
&opts.cluster, "cluster", "c", "", &opts.cluster, "context", "c", "",
"Name of the cluster. (default is the current cluster)", "Name of the cluster context. (default is the current context)",
) )
return cmd return cmd
@@ -143,7 +143,7 @@ func confirm() (bool, error) {
Negative("No"). Negative("No").
Value(&confirmed), Value(&confirmed),
), ),
) ).WithAccessible(true)
if err := form.Run(); err != nil { if err := form.Run(); err != nil {
return false, err 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 -1
View File
@@ -2,14 +2,16 @@ package main
import ( import (
"context" "context"
"github.com/spf13/cobra"
"log/slog" "log/slog"
"os" "os"
"os/signal" "os/signal"
"syscall" "syscall"
"github.com/psviderski/uncloud/internal/daemon" "github.com/psviderski/uncloud/internal/daemon"
"github.com/psviderski/uncloud/internal/log" "github.com/psviderski/uncloud/internal/log"
"github.com/psviderski/uncloud/internal/machine" "github.com/psviderski/uncloud/internal/machine"
"github.com/psviderski/uncloud/internal/version"
"github.com/spf13/cobra"
) )
func main() { func main() {
@@ -22,6 +24,7 @@ func main() {
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "uncloudd", Use: "uncloudd",
Short: "Uncloud machine daemon.", Short: "Uncloud machine daemon.",
Version: version.String(),
SilenceUsage: true, SilenceUsage: true,
SilenceErrors: true, SilenceErrors: true,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
+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,104 @@
# 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
```
## 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 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 ### 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 ### Initialisation
+13 -10
View File
@@ -11,6 +11,7 @@ require (
github.com/caddyserver/caddy/v2 v2.8.4 github.com/caddyserver/caddy/v2 v2.8.4
github.com/cenkalti/backoff/v4 v4.3.0 github.com/cenkalti/backoff/v4 v4.3.0
github.com/charmbracelet/huh v0.6.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/compose-spec/compose-go/v2 v2.4.5
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf
github.com/deckarep/golang-set/v2 v2.8.0 github.com/deckarep/golang-set/v2 v2.8.0
@@ -21,6 +22,8 @@ require (
github.com/docker/docker v27.4.0-rc.2+incompatible github.com/docker/docker v27.4.0-rc.2+incompatible
github.com/docker/go-connections v0.5.0 github.com/docker/go-connections v0.5.0
github.com/docker/go-units 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/google/go-containerregistry v0.20.2
github.com/hashicorp/memberlist v0.5.1 github.com/hashicorp/memberlist v0.5.1
github.com/hashicorp/serf v0.10.1 github.com/hashicorp/serf v0.10.1
@@ -33,6 +36,7 @@ require (
github.com/ipfs/go-log/v2 v2.5.1 github.com/ipfs/go-log/v2 v2.5.1
github.com/jmoiron/sqlx v1.4.0 github.com/jmoiron/sqlx v1.4.0
github.com/lmittmann/tint v1.0.5 github.com/lmittmann/tint v1.0.5
github.com/miekg/dns v1.1.65
github.com/opencontainers/go-digest v1.0.0 github.com/opencontainers/go-digest v1.0.0
github.com/opencontainers/image-spec v1.1.0 github.com/opencontainers/image-spec v1.1.0
github.com/siderolabs/discovery-api v0.1.4 github.com/siderolabs/discovery-api v0.1.4
@@ -43,9 +47,9 @@ require (
github.com/vishvananda/netlink v1.3.0 github.com/vishvananda/netlink v1.3.0
go.uber.org/zap v1.27.0 go.uber.org/zap v1.27.0
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba go4.org/netipx v0.0.0-20231129151722-fdeea329fbba
golang.org/x/crypto v0.32.0 golang.org/x/crypto v0.33.0
golang.org/x/net v0.34.0 golang.org/x/net v0.35.0
golang.org/x/sync v0.10.0 golang.org/x/sync v0.11.0
golang.org/x/sys v0.31.0 golang.org/x/sys v0.31.0
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6 golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6
@@ -82,7 +86,6 @@ require (
github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/charmbracelet/bubbles v0.20.0 // indirect github.com/charmbracelet/bubbles v0.20.0 // indirect
github.com/charmbracelet/bubbletea v1.1.1 // 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/ansi v0.3.2 // indirect
github.com/charmbracelet/x/exp/strings v0.0.0-20240919170804-a4978c8e603a // indirect github.com/charmbracelet/x/exp/strings v0.0.0-20240919170804-a4978c8e603a // indirect
github.com/charmbracelet/x/term v0.2.0 // indirect github.com/charmbracelet/x/term v0.2.0 // indirect
@@ -135,7 +138,6 @@ require (
github.com/google/btree v1.1.2 // indirect github.com/google/btree v1.1.2 // indirect
github.com/google/cel-go v0.20.1 // indirect github.com/google/cel-go v0.20.1 // indirect
github.com/google/flatbuffers v24.3.25+incompatible // 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/pprof v0.0.0-20250317173921-a4b03ec1a45e // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/google/uuid v1.6.0 // indirect github.com/google/uuid v1.6.0 // indirect
@@ -196,7 +198,6 @@ require (
github.com/mdlayher/socket v0.5.1 // indirect github.com/mdlayher/socket v0.5.1 // indirect
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
github.com/mholt/acmez/v2 v2.0.3 // indirect github.com/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/miekg/pkcs11 v1.1.1 // indirect
github.com/minio/sha256-simd v1.0.1 // indirect github.com/minio/sha256-simd v1.0.1 // indirect
github.com/mitchellh/cli v1.1.5 // indirect github.com/mitchellh/cli v1.1.5 // indirect
@@ -212,6 +213,8 @@ require (
github.com/moby/patternmatcher v0.6.0 // indirect github.com/moby/patternmatcher v0.6.0 // indirect
github.com/moby/sys/sequential 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/sys/signal v0.7.1 // indirect
github.com/moby/sys/user v0.3.0 // indirect
github.com/moby/sys/userns v0.1.0 // indirect
github.com/moby/term v0.5.0 // indirect github.com/moby/term v0.5.0 // indirect
github.com/morikuni/aec v1.0.0 // indirect github.com/morikuni/aec v1.0.0 // indirect
github.com/mr-tron/base58 v1.2.0 // indirect github.com/mr-tron/base58 v1.2.0 // indirect
@@ -302,11 +305,11 @@ require (
go.uber.org/zap/exp v0.2.0 // indirect go.uber.org/zap/exp v0.2.0 // indirect
golang.org/x/crypto/x509roots/fallback v0.0.0-20240507223354-67b13616a595 // 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/exp v0.0.0-20241215155358-4a5509556b9e // indirect
golang.org/x/mod v0.22.0 // indirect golang.org/x/mod v0.23.0 // indirect
golang.org/x/term v0.28.0 // indirect golang.org/x/term v0.29.0 // indirect
golang.org/x/text v0.21.0 // indirect golang.org/x/text v0.22.0 // indirect
golang.org/x/time v0.8.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 golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
google.golang.org/genproto v0.0.0-20240401170217-c3f982113cda // indirect google.golang.org/genproto v0.0.0-20240401170217-c3f982113cda // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 // indirect
+20 -18
View File
@@ -343,6 +343,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 h1:dhn8MZ1gZ0mzeodTG3jt5Vj/o87xZKuNAprG2mQfMfc=
github.com/go-viper/mapstructure/v2 v2.0.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= 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/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 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E= github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E=
@@ -397,8 +399,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.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.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.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 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 h1:B1wPJ1SN/S7pB+ZAimcciVD+r+yV/l/DSArMxlbwseo=
github.com/google/go-containerregistry v0.20.2/go.mod h1:z38EKdKh4h7IP2gSfUUqEvalZBqs6AoLeWfUy34nQC8= github.com/google/go-containerregistry v0.20.2/go.mod h1:z38EKdKh4h7IP2gSfUUqEvalZBqs6AoLeWfUy34nQC8=
github.com/google/go-tpm v0.9.0 h1:sQF6YqWMi+SCXpsmS3fd21oPy/vSddwZry4JnmltHVk= github.com/google/go-tpm v0.9.0 h1:sQF6YqWMi+SCXpsmS3fd21oPy/vSddwZry4JnmltHVk=
@@ -719,8 +721,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/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.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.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI=
github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ= github.com/miekg/dns v1.1.65 h1:0+tIPHzUW0GCge7IiK3guGP57VAw7hoPDfApjkMD1Fc=
github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ= 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.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 h1:Ugu9pdy6vAYku5DEpVWVFPYnzV+bxB+iRdbuFSu7TvU=
github.com/miekg/pkcs11 v1.1.1/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= github.com/miekg/pkcs11 v1.1.1/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs=
@@ -1194,8 +1196,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.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.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.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= 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 h1:TgSqweA595vD0Zt86JzLv3Pb/syKg8gd5KMGGbJPYFw=
golang.org/x/crypto/x509roots/fallback v0.0.0-20240507223354-67b13616a595/go.mod h1:kNa9WdvYnzFwC79zRpLRMJbdEFlhyM5RPFBBZp/wWH8= 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= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@@ -1212,8 +1214,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.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 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.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM=
golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= 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-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -1235,8 +1237,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.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.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.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= 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.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 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE=
golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
@@ -1250,8 +1252,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-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/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.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 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-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -1308,8 +1310,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.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.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= 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.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek= 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.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.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
@@ -1320,8 +1322,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.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.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.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= 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 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg=
golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 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= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -1343,8 +1345,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.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 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.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY=
golang.org/x/tools v0.29.0/go.mod h1:KMQVMRsVxU6nHCFXrBPhDB8XncLNLM0lIy/F14RP588= 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-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-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+197
View File
@@ -0,0 +1,197 @@
package cli
import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"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/registry"
)
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()
// Print the build output
decoder := json.NewDecoder(buildResponse.Body)
for {
var message map[string]interface{}
if err := decoder.Decode(&message); err == io.EOF {
break
} else if err != nil {
return "", fmt.Errorf("failed to decode build output for service %s: %w", service.Name, err)
}
if stream, ok := message["stream"]; ok {
fmt.Print(stream)
}
}
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)
// Handle output and errors
decoder := json.NewDecoder(pushResponse)
for {
var message map[string]interface{}
if err := decoder.Decode(&message); err == io.EOF {
break
} else if err != nil {
return fmt.Errorf("failed to decode push output for image %s: %w", imageName, err)
}
if stream, ok := message["stream"]; ok {
fmt.Print(stream)
} else if errorMessage, ok := message["error"]; ok {
return fmt.Errorf("error pushing image %s: %s", imageName, errorMessage)
} else if status, ok := message["status"]; ok {
fmt.Printf(" %s\n", status)
}
}
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" "context"
"errors" "errors"
"fmt" "fmt"
"net/netip"
"os"
"github.com/charmbracelet/huh"
"github.com/docker/cli/cli/streams" "github.com/docker/cli/cli/streams"
"github.com/psviderski/uncloud/internal/cli/config" "github.com/psviderski/uncloud/internal/cli/config"
"github.com/psviderski/uncloud/internal/fs" "github.com/psviderski/uncloud/internal/fs"
@@ -13,17 +17,15 @@ import (
"github.com/psviderski/uncloud/pkg/api" "github.com/psviderski/uncloud/pkg/api"
"github.com/psviderski/uncloud/pkg/client" "github.com/psviderski/uncloud/pkg/client"
"github.com/psviderski/uncloud/pkg/client/connector" "github.com/psviderski/uncloud/pkg/client/connector"
"net/netip" "google.golang.org/grpc/codes"
"os" "google.golang.org/grpc/status"
"github.com/charmbracelet/huh"
"google.golang.org/protobuf/types/known/emptypb" "google.golang.org/protobuf/types/known/emptypb"
) )
const defaultClusterName = "default" const defaultContextName = "default"
type CLI struct { type CLI struct {
config *config.Config Config *config.Config
conn *config.MachineConnection conn *config.MachineConnection
} }
@@ -41,65 +43,72 @@ func New(configPath string, conn *config.MachineConnection) (*CLI, error) {
} }
return &CLI{ return &CLI{
config: cfg, Config: cfg,
}, nil }, nil
} }
func (cli *CLI) CreateCluster(name string) error { func (cli *CLI) CreateContext(name string) error {
if _, ok := cli.config.Clusters[name]; ok { if _, ok := cli.Config.Contexts[name]; ok {
return fmt.Errorf("cluster %q already exists", name) return fmt.Errorf("context '%s' already exists", name)
} }
cli.config.Clusters[name] = &config.Cluster{ cli.Config.Contexts[name] = &config.Context{
Name: name, Name: name,
} }
return cli.config.Save() return cli.Config.Save()
} }
func (cli *CLI) SetCurrentCluster(name string) error { func (cli *CLI) SetCurrentContext(name string) error {
if _, ok := cli.config.Clusters[name]; !ok { if _, ok := cli.Config.Contexts[name]; !ok {
return api.ErrNotFound return api.ErrNotFound
} }
cli.config.CurrentCluster = name cli.Config.CurrentContext = name
return cli.config.Save() 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. // 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 { if cli.conn != nil {
return connectCluster(ctx, *cli.conn) return connectCluster(ctx, *cli.conn)
} }
if len(cli.config.Clusters) == 0 { if len(cli.Config.Contexts) == 0 {
return nil, errors.New( return nil, fmt.Errorf(
"no clusters found in the Uncloud config. " + "no cluster contexts found in the Uncloud config (%s). "+
"Please initialise a cluster with `uncloud machine init` first", "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 the cluster is not specified, use the current cluster if set.
if cli.config.CurrentCluster == "" { if cli.Config.CurrentContext == "" {
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 {
return nil, fmt.Errorf( return nil, fmt.Errorf(
"current cluster %q not found in the config. "+ "the current cluster context is not set in the Uncloud config (%s). "+
"Please specify a cluster with the --cluster flag or update current_cluster in the config", "Please specify the context with the '--context' flag or set 'current_context' in the config",
cli.config.CurrentCluster, 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 { 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 { 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. // 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) c, err := connectCluster(ctx, conn)
if err != nil { 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 return c, nil
@@ -129,46 +138,42 @@ func connectCluster(ctx context.Context, conn config.MachineConnection) (*client
KeyPath: keyPath, KeyPath: keyPath,
} }
return client.New(ctx, connector.NewSSHConnector(sshConfig)) return client.New(ctx, connector.NewSSHConnector(sshConfig))
} else if conn.TCP.IsValid() { } else if conn.TCP != nil && conn.TCP.IsValid() {
return client.New(ctx, connector.NewTCPConnector(conn.TCP)) return client.New(ctx, connector.NewTCPConnector(*conn.TCP))
} }
return nil, errors.New("connection configuration is invalid") 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. // 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. // The client should be closed after use by the caller.
func (cli *CLI) InitCluster( func (cli *CLI) InitCluster(ctx context.Context, opts InitClusterOptions) (*client.Client, error) {
ctx context.Context, if opts.RemoteMachine != nil {
remoteMachine *RemoteMachine, return cli.initRemoteMachine(ctx, opts)
clusterName,
machineName string,
netPrefix netip.Prefix,
publicIP *netip.Addr,
) (*client.Client, error) {
if remoteMachine != nil {
return cli.initRemoteMachine(ctx, *remoteMachine, clusterName, machineName, netPrefix, publicIP)
} }
// TODO: implement local machine initialisation // TODO: implement local machine initialisation
return nil, fmt.Errorf("local machine initialisation is not implemented yet") return nil, fmt.Errorf("local machine initialisation is not implemented yet")
} }
func (cli *CLI) initRemoteMachine( func (cli *CLI) initRemoteMachine(ctx context.Context, opts InitClusterOptions) (*client.Client, error) {
ctx context.Context, contextName := opts.Context
remoteMachine RemoteMachine, if contextName == "" {
clusterName, contextName = defaultContextName
machineName string,
netPrefix netip.Prefix,
publicIP *netip.Addr,
) (*client.Client, error) {
if clusterName == "" {
clusterName = defaultClusterName
} }
if _, ok := cli.config.Clusters[clusterName]; ok { if _, ok := cli.Config.Contexts[contextName]; ok {
return nil, fmt.Errorf("cluster %q already exists", clusterName) 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 { if err != nil {
return nil, err return nil, err
} }
@@ -190,13 +195,24 @@ func (cli *CLI) initRemoteMachine(
} }
} }
req := &pb.InitClusterRequest{ // Check machine meets all necessary system requirements before proceeding.
MachineName: machineName, checkResp, err := machineClient.CheckPrerequisites(ctx, &emptypb.Empty{})
Network: pb.NewIPPrefix(netPrefix), // 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 := &pb.InitClusterRequest{
req.PublicIpConfig = &pb.InitClusterRequest_PublicIp{PublicIp: pb.NewIP(*publicIP)} 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 { } else {
// Invalid or in other words zero IP means to automatically detect the public IP. // Invalid or in other words zero IP means to automatically detect the public IP.
req.PublicIpConfig = &pb.InitClusterRequest_PublicIpAuto{PublicIpAuto: true} req.PublicIpConfig = &pb.InitClusterRequest_PublicIpAuto{PublicIpAuto: true}
@@ -207,44 +223,58 @@ func (cli *CLI) initRemoteMachine(
if err != nil { if err != nil {
return nil, fmt.Errorf("init cluster: %w", err) return nil, fmt.Errorf("init cluster: %w", err)
} }
fmt.Printf("Cluster %q initialised with machine %q\n", clusterName, resp.Machine.Name) 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.CreateCluster(clusterName); err != nil { if err = cli.CreateContext(contextName); err != nil {
return nil, fmt.Errorf("save cluster to config: %w", err) 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 err = cli.SetCurrentContext(contextName); err != nil {
if len(cli.config.Clusters) == 1 { return nil, fmt.Errorf("set current cluster context: %w", err)
if err = cli.SetCurrentCluster(clusterName); err != nil {
return nil, fmt.Errorf("set current cluster: %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{ connCfg := config.MachineConnection{
SSH: config.NewSSHDestination(remoteMachine.User, remoteMachine.Host, remoteMachine.Port), SSH: config.NewSSHDestination(opts.RemoteMachine.User, opts.RemoteMachine.Host, opts.RemoteMachine.Port),
SSHKeyFile: remoteMachine.KeyPath, SSHKeyFile: opts.RemoteMachine.KeyPath,
} }
cli.config.Clusters[clusterName].Connections = append(cli.config.Clusters[clusterName].Connections, connCfg) cli.Config.Contexts[contextName].Connections = append(cli.Config.Contexts[contextName].Connections, connCfg)
if err = cli.config.Save(); err != nil { if err = cli.Config.Save(); err != nil {
return nil, fmt.Errorf("save config: %w", err) return nil, fmt.Errorf("save config: %w", err)
} }
return machineClient, nil return machineClient, nil
} }
// AddMachine provisions a remote machine and adds it to the cluster. It returns a client to interact with the machine type AddMachineOptions struct {
// which should be closed after use by the caller. Context string
func (cli *CLI) AddMachine( MachineName string
ctx context.Context, remoteMachine RemoteMachine, clusterName, machineName string, publicIP *netip.Addr, PublicIP *netip.Addr
) (*client.Client, error) { RemoteMachine RemoteMachine
c, err := cli.ConnectCluster(ctx, clusterName) Version string
if err != nil { }
return nil, fmt.Errorf("connect to cluster: %w", err)
}
defer c.Close()
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 { 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() { defer func() {
if err != nil { 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. // 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{}) minfo, err := machineClient.Inspect(ctx, &emptypb.Empty{})
if err != nil { if err != nil {
return nil, fmt.Errorf("inspect machine: %w", err) return nil, nil, fmt.Errorf("inspect machine: %w", err)
} }
if minfo.Id != "" { if minfo.Id != "" {
if err = cli.promptResetMachine(); err != nil { 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{}) tokenResp, err := machineClient.Token(ctx, &emptypb.Empty{})
if err != nil { 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) token, err := machine.ParseToken(tokenResp.Token)
if err != nil { 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. // 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) endpoints[i] = pb.NewIPPort(addrPort)
} }
addReq := &pb.AddMachineRequest{ addReq := &pb.AddMachineRequest{
Name: machineName, Name: opts.MachineName,
Network: &pb.NetworkConfig{ Network: &pb.NetworkConfig{
Endpoints: endpoints, Endpoints: endpoints,
PublicKey: token.PublicKey, PublicKey: token.PublicKey,
}, },
} }
if publicIP != nil { if opts.PublicIP != nil {
if publicIP.IsValid() { if opts.PublicIP.IsValid() {
addReq.PublicIp = pb.NewIP(*publicIP) addReq.PublicIp = pb.NewIP(*opts.PublicIP)
} else if token.PublicIP.IsValid() { } else if token.PublicIP.IsValid() {
// Invalid or in other words zero IP means to use an automatically detected public IP from the token. // Invalid or in other words zero IP means to use an automatically detected public IP from the token.
addReq.PublicIp = pb.NewIP(token.PublicIP) addReq.PublicIp = pb.NewIP(token.PublicIP)
@@ -295,13 +336,13 @@ func (cli *CLI) AddMachine(
addResp, err := c.AddMachine(ctx, addReq) addResp, err := c.AddMachine(ctx, addReq)
if err != nil { 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. // 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 { 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) otherMachines := make([]*pb.MachineInfo, 0, len(machines)-1)
for _, m := range machines { for _, m := range machines {
@@ -316,30 +357,34 @@ func (cli *CLI) AddMachine(
OtherMachines: otherMachines, OtherMachines: otherMachines,
} }
if _, err = machineClient.JoinCluster(ctx, joinReq); err != nil { 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{ connCfg := config.MachineConnection{
SSH: config.NewSSHDestination(remoteMachine.User, remoteMachine.Host, remoteMachine.Port), SSH: config.NewSSHDestination(opts.RemoteMachine.User, opts.RemoteMachine.Host, opts.RemoteMachine.Port),
SSHKeyFile: remoteMachine.KeyPath, SSHKeyFile: opts.RemoteMachine.KeyPath,
} }
if clusterName == "" { if contextName == "" {
clusterName = cli.config.CurrentCluster contextName = cli.Config.CurrentContext
} }
cli.config.Clusters[clusterName].Connections = append(cli.config.Clusters[clusterName].Connections, connCfg) cli.Config.Contexts[contextName].Connections = append(cli.Config.Contexts[contextName].Connections, connCfg)
if err = cli.config.Save(); err != nil { if err = cli.Config.Save(); err != nil {
return nil, fmt.Errorf("save config: %w", err) 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 // 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. // 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. // 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) sshClient, err := sshexec.Connect(remoteMachine.User, remoteMachine.Host, remoteMachine.Port, remoteMachine.KeyPath)
if err != nil { if err != nil {
@@ -350,7 +395,7 @@ func (cli *CLI) provisionRemoteMachine(ctx context.Context, remoteMachine Remote
} }
exec := sshexec.NewRemote(sshClient) exec := sshexec.NewRemote(sshClient)
// Install and run the Uncloud daemon and dependencies on the remote machine. // 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) return nil, fmt.Errorf("provision machine: %w", err)
} }
@@ -387,7 +432,7 @@ func (cli *CLI) promptResetMachine() error {
Negative("No"). Negative("No").
Value(&confirm), Value(&confirm),
), ),
) ).WithAccessible(true)
if err := form.Run(); err != nil { if err := form.Run(); err != nil {
return fmt.Errorf("prompt user to confirm: %w", err) return fmt.Errorf("prompt user to confirm: %w", err)
} }
@@ -396,7 +441,8 @@ func (cli *CLI) promptResetMachine() error {
return fmt.Errorf("remote machine is already initialised as a cluster member") return fmt.Errorf("remote machine is already initialised as a cluster member")
} }
// TODO: implement resetting the remote machine. // 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. // 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 ( import (
"fmt" "fmt"
"github.com/BurntSushi/toml"
"os" "os"
"path/filepath" "path/filepath"
"github.com/goccy/go-yaml"
) )
type Config struct { type Config struct {
Clusters map[string]*Cluster `toml:"clusters"` CurrentContext string `yaml:"current_context"`
CurrentCluster string `toml:"current_cluster"` Contexts map[string]*Context `yaml:"contexts"`
// path is the file path config is read from. // path is the file path config is read from.
path string path string
@@ -18,10 +19,10 @@ type Config struct {
func NewFromFile(path string) (*Config, error) { func NewFromFile(path string) (*Config, error) {
_, err := os.Stat(path) _, err := os.Stat(path)
if err != nil && !os.IsNotExist(err) { 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{ c := &Config{
Clusters: map[string]*Cluster{}, Contexts: map[string]*Context{},
path: path, path: path,
} }
if os.IsNotExist(err) { if os.IsNotExist(err) {
@@ -34,30 +35,37 @@ func NewFromFile(path string) (*Config, error) {
return c, nil return c, nil
} }
func (c *Config) Path() string {
return c.path
}
func (c *Config) Read() error { func (c *Config) Read() error {
_, err := toml.DecodeFile(c.path, c) data, err := os.ReadFile(c.path)
if err != nil { 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 return nil
} }
func (c *Config) Save() error { func (c *Config) Save() error {
dir, _ := filepath.Split(c.path) dir, _ := filepath.Split(c.path)
if err := os.MkdirAll(dir, 0700); err != nil { 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) f, err := os.OpenFile(c.path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil { 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 := yaml.NewEncoder(f, yaml.Indent(2), yaml.IndentSequence(true))
encoder.Indent = ""
if err = encoder.Encode(c); err != nil { if err = encoder.Encode(c); err != nil {
_ = f.Close() _ = 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() return f.Close()
} }
+8 -5
View File
@@ -5,6 +5,7 @@ import (
"net/netip" "net/netip"
"strconv" "strconv"
"strings" "strings"
"github.com/psviderski/uncloud/internal/secret" "github.com/psviderski/uncloud/internal/secret"
) )
@@ -14,11 +15,13 @@ const (
) )
type MachineConnection struct { type MachineConnection struct {
SSH SSHDestination `toml:"ssh,omitempty"` SSH SSHDestination `yaml:"ssh,omitempty"`
SSHKeyFile string `toml:"ssh_key_file,omitempty"` SSHKeyFile string `yaml:"ssh_key_file,omitempty"`
TCP netip.AddrPort `toml:"tcp,omitempty"` // TCP is the address and port of the machine's API server.
Host string `toml:"host,omitempty"` // The pointer is used to omit the field when not set. Otherwise, yaml marshalling includes an empty object.
PublicKey secret.Secret `toml:"public_key,omitempty"` 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". // 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" "context"
"fmt" "fmt"
"os" "os"
"strings"
"github.com/psviderski/uncloud/internal/sshexec" "github.com/psviderski/uncloud/internal/sshexec"
) )
@@ -17,36 +19,38 @@ type RemoteMachine struct {
KeyPath string KeyPath string
} }
func installCmd(user string) string { func installCmd(user string, version string) string {
sudoPrefix := "sudo" sudoPrefix := ""
var env []string
// Add the SSH user (non-root) to the uncloud group to allow access to the Uncloud daemon unix socket. // 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 if user != "root" {
sudoPrefix = "sudo"
curlBashCmd := fmt.Sprintf( env = append(env, "UNCLOUD_GROUP_ADD_USER="+sshexec.Quote(user))
"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 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 return curlBashCmd
} }
// provisionMachine provisions the remote machine by downloading the Uncloud install script from GitHub and running it. // 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") user, err := exec.Run(ctx, "whoami")
if err != nil { if err != nil {
return fmt.Errorf("run whoami: %w", err) return fmt.Errorf("run whoami: %w", err)
} }
installCmd := installCmd(user) cmd := installCmd(user, version)
fmt.Println("Downloading Uncloud install script:", installScriptURL) 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 { if err = exec.Stream(ctx, cmd, os.Stdout, os.Stderr); err != nil {
return fmt.Errorf("download and run install script: %w", err) return fmt.Errorf("download and run install script: %w", err)
} }
+19 -2
View File
@@ -8,12 +8,29 @@ import (
func TestInstallCmd(t *testing.T) { func TestInstallCmd(t *testing.T) {
t.Run("root", func(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, "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) { t.Run("nonroot", func(t *testing.T) {
cmd := installCmd("nonroot") cmd := installCmd("nonroot", "")
assert.Contains(t, cmd, "sudo") 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"). Negative("No").
Value(&confirmed), Value(&confirmed),
), ),
) ).WithAccessible(true)
if err := form.Run(); err != nil { if err := form.Run(); err != nil {
return false, err 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 { 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 { 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 { 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 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 { type CreateServiceContainerRequest struct {
state protoimpl.MessageState state protoimpl.MessageState
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
@@ -996,7 +1299,7 @@ type CreateServiceContainerRequest struct {
func (x *CreateServiceContainerRequest) Reset() { func (x *CreateServiceContainerRequest) Reset() {
*x = CreateServiceContainerRequest{} *x = CreateServiceContainerRequest{}
if protoimpl.UnsafeEnabled { 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 := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -1009,7 +1312,7 @@ func (x *CreateServiceContainerRequest) String() string {
func (*CreateServiceContainerRequest) ProtoMessage() {} func (*CreateServiceContainerRequest) ProtoMessage() {}
func (x *CreateServiceContainerRequest) ProtoReflect() protoreflect.Message { 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 { if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -1022,7 +1325,7 @@ func (x *CreateServiceContainerRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use CreateServiceContainerRequest.ProtoReflect.Descriptor instead. // Deprecated: Use CreateServiceContainerRequest.ProtoReflect.Descriptor instead.
func (*CreateServiceContainerRequest) Descriptor() ([]byte, []int) { 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 { func (x *CreateServiceContainerRequest) GetServiceId() string {
@@ -1060,7 +1363,7 @@ type ServiceContainer struct {
func (x *ServiceContainer) Reset() { func (x *ServiceContainer) Reset() {
*x = ServiceContainer{} *x = ServiceContainer{}
if protoimpl.UnsafeEnabled { 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 := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -1073,7 +1376,7 @@ func (x *ServiceContainer) String() string {
func (*ServiceContainer) ProtoMessage() {} func (*ServiceContainer) ProtoMessage() {}
func (x *ServiceContainer) ProtoReflect() protoreflect.Message { 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 { if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -1086,7 +1389,7 @@ func (x *ServiceContainer) ProtoReflect() protoreflect.Message {
// Deprecated: Use ServiceContainer.ProtoReflect.Descriptor instead. // Deprecated: Use ServiceContainer.ProtoReflect.Descriptor instead.
func (*ServiceContainer) Descriptor() ([]byte, []int) { 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 { func (x *ServiceContainer) GetContainer() []byte {
@@ -1116,7 +1419,7 @@ type ListServiceContainersRequest struct {
func (x *ListServiceContainersRequest) Reset() { func (x *ListServiceContainersRequest) Reset() {
*x = ListServiceContainersRequest{} *x = ListServiceContainersRequest{}
if protoimpl.UnsafeEnabled { 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 := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -1129,7 +1432,7 @@ func (x *ListServiceContainersRequest) String() string {
func (*ListServiceContainersRequest) ProtoMessage() {} func (*ListServiceContainersRequest) ProtoMessage() {}
func (x *ListServiceContainersRequest) ProtoReflect() protoreflect.Message { 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 { if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -1142,7 +1445,7 @@ func (x *ListServiceContainersRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use ListServiceContainersRequest.ProtoReflect.Descriptor instead. // Deprecated: Use ListServiceContainersRequest.ProtoReflect.Descriptor instead.
func (*ListServiceContainersRequest) Descriptor() ([]byte, []int) { 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 { func (x *ListServiceContainersRequest) GetServiceId() string {
@@ -1172,7 +1475,7 @@ type ListServiceContainersResponse struct {
func (x *ListServiceContainersResponse) Reset() { func (x *ListServiceContainersResponse) Reset() {
*x = ListServiceContainersResponse{} *x = ListServiceContainersResponse{}
if protoimpl.UnsafeEnabled { 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 := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -1185,7 +1488,7 @@ func (x *ListServiceContainersResponse) String() string {
func (*ListServiceContainersResponse) ProtoMessage() {} func (*ListServiceContainersResponse) ProtoMessage() {}
func (x *ListServiceContainersResponse) ProtoReflect() protoreflect.Message { 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 { if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -1198,7 +1501,7 @@ func (x *ListServiceContainersResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use ListServiceContainersResponse.ProtoReflect.Descriptor instead. // Deprecated: Use ListServiceContainersResponse.ProtoReflect.Descriptor instead.
func (*ListServiceContainersResponse) Descriptor() ([]byte, []int) { 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 { func (x *ListServiceContainersResponse) GetMessages() []*MachineServiceContainers {
@@ -1220,7 +1523,7 @@ type MachineServiceContainers struct {
func (x *MachineServiceContainers) Reset() { func (x *MachineServiceContainers) Reset() {
*x = MachineServiceContainers{} *x = MachineServiceContainers{}
if protoimpl.UnsafeEnabled { 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 := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -1233,7 +1536,7 @@ func (x *MachineServiceContainers) String() string {
func (*MachineServiceContainers) ProtoMessage() {} func (*MachineServiceContainers) ProtoMessage() {}
func (x *MachineServiceContainers) ProtoReflect() protoreflect.Message { 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 { if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -1246,7 +1549,7 @@ func (x *MachineServiceContainers) ProtoReflect() protoreflect.Message {
// Deprecated: Use MachineServiceContainers.ProtoReflect.Descriptor instead. // Deprecated: Use MachineServiceContainers.ProtoReflect.Descriptor instead.
func (*MachineServiceContainers) Descriptor() ([]byte, []int) { 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 { 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, 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, 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, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08,
0x6d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x22, 0x88, 0x01, 0x0a, 0x1d, 0x43, 0x72, 0x65, 0x6d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x22, 0x2f, 0x0a, 0x13, 0x43, 0x72, 0x65, 0x61,
0x61, 0x74, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x74, 0x65, 0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12,
0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x65, 0x18, 0x0a, 0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c,
0x72, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x52, 0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x2e, 0x0a, 0x14, 0x43, 0x72, 0x65,
0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x49, 0x64, 0x12, 0x21, 0x0a, 0x0c, 0x73, 0x65, 0x72, 0x61, 0x74, 0x65, 0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x70, 0x65, 0x63, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x76, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28,
0x0b, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x53, 0x70, 0x65, 0x63, 0x12, 0x25, 0x0a, 0x0e, 0x0c, 0x52, 0x06, 0x76, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x22, 0x2e, 0x0a, 0x12, 0x4c, 0x69, 0x73,
0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x74, 0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12,
0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x4e, 0x18, 0x0a, 0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c,
0x61, 0x6d, 0x65, 0x22, 0x53, 0x0a, 0x10, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x52, 0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x46, 0x0a, 0x13, 0x4c, 0x69, 0x73,
0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x1c, 0x0a, 0x09, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x74, 0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
0x69, 0x6e, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x63, 0x6f, 0x6e, 0x74, 0x12, 0x2f, 0x0a, 0x08, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03,
0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x21, 0x0a, 0x0c, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65,
0x5f, 0x73, 0x70, 0x65, 0x63, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0b, 0x73, 0x65, 0x72, 0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x73, 0x52, 0x08, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65,
0x76, 0x69, 0x63, 0x65, 0x53, 0x70, 0x65, 0x63, 0x22, 0x57, 0x0a, 0x1c, 0x4c, 0x69, 0x73, 0x74, 0x73, 0x22, 0x57, 0x0a, 0x0e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x56, 0x6f, 0x6c, 0x75,
0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x6d, 0x65, 0x73, 0x12, 0x29, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18,
0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x65, 0x72, 0x76, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d, 0x65, 0x74, 0x61,
0x69, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x65, 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x1a,
0x72, 0x76, 0x69, 0x63, 0x65, 0x49, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c,
0x6e, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x08, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x3b, 0x0a, 0x13, 0x52, 0x65,
0x73, 0x22, 0x5a, 0x0a, 0x1d, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69,
0x73, 0x65, 0x12, 0x39, 0x0a, 0x08, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x18, 0x01, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x66, 0x6f, 0x72, 0x63, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08,
0x20, 0x03, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x52, 0x05, 0x66, 0x6f, 0x72, 0x63, 0x65, 0x22, 0x88, 0x01, 0x0a, 0x1d, 0x43, 0x72, 0x65, 0x61,
0x6e, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x74, 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, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x65, 0x72,
0x18, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73,
0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x12, 0x29, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x49, 0x64, 0x12, 0x21, 0x0a, 0x0c, 0x73, 0x65, 0x72, 0x76,
0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x61, 0x70, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x70, 0x65, 0x63, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0b,
0x69, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x53, 0x70, 0x65, 0x63, 0x12, 0x25, 0x0a, 0x0e, 0x63,
0x64, 0x61, 0x74, 0x61, 0x12, 0x35, 0x0a, 0x0a, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20,
0x72, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x53, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x4e, 0x61,
0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x6d, 0x65, 0x22, 0x53, 0x0a, 0x10, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e,
0x0a, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x32, 0xf3, 0x07, 0x0a, 0x06, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x1c, 0x0a, 0x09, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69,
0x44, 0x6f, 0x63, 0x6b, 0x65, 0x72, 0x12, 0x4c, 0x0a, 0x0f, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x6e, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x63, 0x6f, 0x6e, 0x74, 0x61,
0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x1b, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x21, 0x0a, 0x0c, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x5f,
0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x73, 0x70, 0x65, 0x63, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0b, 0x73, 0x65, 0x72, 0x76,
0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x72, 0x65, 0x69, 0x63, 0x65, 0x53, 0x70, 0x65, 0x63, 0x22, 0x57, 0x0a, 0x1c, 0x4c, 0x69, 0x73, 0x74, 0x53,
0x61, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73,
0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4f, 0x0a, 0x10, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x43, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x65, 0x72, 0x76, 0x69,
0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x1c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x65, 0x72,
0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x76, 0x69, 0x63, 0x65, 0x49, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e,
0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x73, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73,
0x70, 0x65, 0x63, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x73, 0x22, 0x5a, 0x0a, 0x1d, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43,
0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x44, 0x0a, 0x0e, 0x53, 0x74, 0x61, 0x72, 0x74, 0x43, 0x6f, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x1a, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x53, 0x74, 0x65, 0x12, 0x39, 0x0a, 0x08, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x18, 0x01, 0x20,
0x61, 0x72, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x03, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e,
0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65,
0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x42, 0x0a, 0x0d, 0x53, 0x72, 0x73, 0x52, 0x08, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x22, 0x7c, 0x0a, 0x18,
0x74, 0x6f, 0x70, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x19, 0x2e, 0x61, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f,
0x70, 0x69, 0x2e, 0x53, 0x74, 0x6f, 0x70, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x12, 0x29, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61,
0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x61, 0x70, 0x69,
0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64,
0x49, 0x0a, 0x0e, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x61, 0x12, 0x35, 0x0a, 0x0a, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72,
0x73, 0x12, 0x1a, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x53, 0x65,
0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x0a,
0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x32, 0xbc, 0x09, 0x0a, 0x06, 0x44,
0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x46, 0x0a, 0x0f, 0x52, 0x65, 0x6f, 0x63, 0x6b, 0x65, 0x72, 0x12, 0x4c, 0x0a, 0x0f, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x43,
0x6d, 0x6f, 0x76, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x1b, 0x2e, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x1b, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43,
0x61, 0x70, 0x69, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65,
0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x72, 0x65, 0x61,
0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f,
0x74, 0x79, 0x12, 0x36, 0x0a, 0x09, 0x50, 0x75, 0x6c, 0x6c, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x12, 0x6e, 0x73, 0x65, 0x12, 0x4f, 0x0a, 0x10, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x43, 0x6f,
0x15, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x50, 0x75, 0x6c, 0x6c, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x52, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x1c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e,
0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x10, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4a, 0x53, 0x4f, 0x73, 0x70, 0x65, 0x63, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65,
0x4e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x30, 0x01, 0x12, 0x43, 0x0a, 0x0c, 0x49, 0x6e, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x73, 0x70,
0x73, 0x70, 0x65, 0x63, 0x74, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x12, 0x18, 0x2e, 0x61, 0x70, 0x69, 0x65, 0x63, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70,
0x2e, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x44, 0x0a, 0x0e, 0x53, 0x74, 0x61, 0x72, 0x74, 0x43, 0x6f, 0x6e,
0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x1a, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x53, 0x74, 0x61,
0x63, 0x74, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x72, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65,
0x55, 0x0a, 0x12, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74,
0x49, 0x6d, 0x61, 0x67, 0x65, 0x12, 0x1e, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x73, 0x70, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x42, 0x0a, 0x0d, 0x53, 0x74,
0x65, 0x63, 0x74, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x52, 0x65, 0x6f, 0x70, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x19, 0x2e, 0x61, 0x70,
0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x73, 0x70, 0x69, 0x2e, 0x53, 0x74, 0x6f, 0x70, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52,
0x65, 0x63, 0x74, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x52, 0x65, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e,
0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x5a, 0x0a, 0x16, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x49,
0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x0a, 0x0e, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73,
0x12, 0x22, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x53, 0x65, 0x72, 0x12, 0x1a, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61,
0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x61,
0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72,
0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x46, 0x0a, 0x0f, 0x52, 0x65, 0x6d,
0x73, 0x65, 0x12, 0x4e, 0x0a, 0x17, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72, 0x6f, 0x76, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x1b, 0x2e, 0x61,
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, 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, 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, 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, 0x79, 0x12, 0x36, 0x0a, 0x09, 0x50, 0x75, 0x6c, 0x6c, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x12, 0x15,
0x70, 0x73, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x6b, 0x69, 0x2f, 0x75, 0x6e, 0x63, 0x6c, 0x6f, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x50, 0x75, 0x6c, 0x6c, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x52, 0x65,
0x75, 0x64, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x10, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4a, 0x53, 0x4f, 0x4e,
0x69, 0x6e, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x30, 0x01, 0x12, 0x43, 0x0a, 0x0c, 0x49, 0x6e, 0x73,
0x6f, 0x33, 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 ( var (
@@ -1468,7 +1806,7 @@ func file_internal_machine_api_pb_docker_proto_rawDescGZIP() []byte {
return file_internal_machine_api_pb_docker_proto_rawDescData 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{ var file_internal_machine_api_pb_docker_proto_goTypes = []any{
(*CreateContainerRequest)(nil), // 0: api.CreateContainerRequest (*CreateContainerRequest)(nil), // 0: api.CreateContainerRequest
(*CreateContainerResponse)(nil), // 1: api.CreateContainerResponse (*CreateContainerResponse)(nil), // 1: api.CreateContainerResponse
@@ -1488,55 +1826,69 @@ var file_internal_machine_api_pb_docker_proto_goTypes = []any{
(*InspectRemoteImageRequest)(nil), // 15: api.InspectRemoteImageRequest (*InspectRemoteImageRequest)(nil), // 15: api.InspectRemoteImageRequest
(*InspectRemoteImageResponse)(nil), // 16: api.InspectRemoteImageResponse (*InspectRemoteImageResponse)(nil), // 16: api.InspectRemoteImageResponse
(*RemoteImage)(nil), // 17: api.RemoteImage (*RemoteImage)(nil), // 17: api.RemoteImage
(*CreateServiceContainerRequest)(nil), // 18: api.CreateServiceContainerRequest (*CreateVolumeRequest)(nil), // 18: api.CreateVolumeRequest
(*ServiceContainer)(nil), // 19: api.ServiceContainer (*CreateVolumeResponse)(nil), // 19: api.CreateVolumeResponse
(*ListServiceContainersRequest)(nil), // 20: api.ListServiceContainersRequest (*ListVolumesRequest)(nil), // 20: api.ListVolumesRequest
(*ListServiceContainersResponse)(nil), // 21: api.ListServiceContainersResponse (*ListVolumesResponse)(nil), // 21: api.ListVolumesResponse
(*MachineServiceContainers)(nil), // 22: api.MachineServiceContainers (*MachineVolumes)(nil), // 22: api.MachineVolumes
(*Metadata)(nil), // 23: api.Metadata (*RemoveVolumeRequest)(nil), // 23: api.RemoveVolumeRequest
(*emptypb.Empty)(nil), // 24: google.protobuf.Empty (*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{ var file_internal_machine_api_pb_docker_proto_depIdxs = []int32{
8, // 0: api.ListContainersResponse.messages:type_name -> api.MachineContainers 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 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 17, // 4: api.InspectRemoteImageResponse.messages:type_name -> api.RemoteImage
23, // 5: api.RemoteImage.metadata:type_name -> api.Metadata 29, // 5: api.RemoteImage.metadata:type_name -> api.Metadata
22, // 6: api.ListServiceContainersResponse.messages:type_name -> api.MachineServiceContainers 22, // 6: api.ListVolumesResponse.messages:type_name -> api.MachineVolumes
23, // 7: api.MachineServiceContainers.metadata:type_name -> api.Metadata 29, // 7: api.MachineVolumes.metadata:type_name -> api.Metadata
19, // 8: api.MachineServiceContainers.containers:type_name -> api.ServiceContainer 28, // 8: api.ListServiceContainersResponse.messages:type_name -> api.MachineServiceContainers
0, // 9: api.Docker.CreateContainer:input_type -> api.CreateContainerRequest 29, // 9: api.MachineServiceContainers.metadata:type_name -> api.Metadata
2, // 10: api.Docker.InspectContainer:input_type -> api.InspectContainerRequest 25, // 10: api.MachineServiceContainers.containers:type_name -> api.ServiceContainer
4, // 11: api.Docker.StartContainer:input_type -> api.StartContainerRequest 0, // 11: api.Docker.CreateContainer:input_type -> api.CreateContainerRequest
5, // 12: api.Docker.StopContainer:input_type -> api.StopContainerRequest 2, // 12: api.Docker.InspectContainer:input_type -> api.InspectContainerRequest
6, // 13: api.Docker.ListContainers:input_type -> api.ListContainersRequest 4, // 13: api.Docker.StartContainer:input_type -> api.StartContainerRequest
9, // 14: api.Docker.RemoveContainer:input_type -> api.RemoveContainerRequest 5, // 14: api.Docker.StopContainer:input_type -> api.StopContainerRequest
10, // 15: api.Docker.PullImage:input_type -> api.PullImageRequest 6, // 15: api.Docker.ListContainers:input_type -> api.ListContainersRequest
12, // 16: api.Docker.InspectImage:input_type -> api.InspectImageRequest 9, // 16: api.Docker.RemoveContainer:input_type -> api.RemoveContainerRequest
15, // 17: api.Docker.InspectRemoteImage:input_type -> api.InspectRemoteImageRequest 10, // 17: api.Docker.PullImage:input_type -> api.PullImageRequest
18, // 18: api.Docker.CreateServiceContainer:input_type -> api.CreateServiceContainerRequest 12, // 18: api.Docker.InspectImage:input_type -> api.InspectImageRequest
2, // 19: api.Docker.InspectServiceContainer:input_type -> api.InspectContainerRequest 15, // 19: api.Docker.InspectRemoteImage:input_type -> api.InspectRemoteImageRequest
20, // 20: api.Docker.ListServiceContainers:input_type -> api.ListServiceContainersRequest 18, // 20: api.Docker.CreateVolume:input_type -> api.CreateVolumeRequest
9, // 21: api.Docker.RemoveServiceContainer:input_type -> api.RemoveContainerRequest 20, // 21: api.Docker.ListVolumes:input_type -> api.ListVolumesRequest
1, // 22: api.Docker.CreateContainer:output_type -> api.CreateContainerResponse 23, // 22: api.Docker.RemoveVolume:input_type -> api.RemoveVolumeRequest
3, // 23: api.Docker.InspectContainer:output_type -> api.InspectContainerResponse 24, // 23: api.Docker.CreateServiceContainer:input_type -> api.CreateServiceContainerRequest
24, // 24: api.Docker.StartContainer:output_type -> google.protobuf.Empty 2, // 24: api.Docker.InspectServiceContainer:input_type -> api.InspectContainerRequest
24, // 25: api.Docker.StopContainer:output_type -> google.protobuf.Empty 26, // 25: api.Docker.ListServiceContainers:input_type -> api.ListServiceContainersRequest
7, // 26: api.Docker.ListContainers:output_type -> api.ListContainersResponse 9, // 26: api.Docker.RemoveServiceContainer:input_type -> api.RemoveContainerRequest
24, // 27: api.Docker.RemoveContainer:output_type -> google.protobuf.Empty 1, // 27: api.Docker.CreateContainer:output_type -> api.CreateContainerResponse
11, // 28: api.Docker.PullImage:output_type -> api.JSONMessage 3, // 28: api.Docker.InspectContainer:output_type -> api.InspectContainerResponse
13, // 29: api.Docker.InspectImage:output_type -> api.InspectImageResponse 30, // 29: api.Docker.StartContainer:output_type -> google.protobuf.Empty
16, // 30: api.Docker.InspectRemoteImage:output_type -> api.InspectRemoteImageResponse 30, // 30: api.Docker.StopContainer:output_type -> google.protobuf.Empty
1, // 31: api.Docker.CreateServiceContainer:output_type -> api.CreateContainerResponse 7, // 31: api.Docker.ListContainers:output_type -> api.ListContainersResponse
19, // 32: api.Docker.InspectServiceContainer:output_type -> api.ServiceContainer 30, // 32: api.Docker.RemoveContainer:output_type -> google.protobuf.Empty
21, // 33: api.Docker.ListServiceContainers:output_type -> api.ListServiceContainersResponse 11, // 33: api.Docker.PullImage:output_type -> api.JSONMessage
24, // 34: api.Docker.RemoveServiceContainer:output_type -> google.protobuf.Empty 13, // 34: api.Docker.InspectImage:output_type -> api.InspectImageResponse
22, // [22:35] is the sub-list for method output_type 16, // 35: api.Docker.InspectRemoteImage:output_type -> api.InspectRemoteImageResponse
9, // [9:22] is the sub-list for method input_type 19, // 36: api.Docker.CreateVolume:output_type -> api.CreateVolumeResponse
9, // [9:9] is the sub-list for extension type_name 21, // 37: api.Docker.ListVolumes:output_type -> api.ListVolumesResponse
9, // [9:9] is the sub-list for extension extendee 30, // 38: api.Docker.RemoveVolume:output_type -> google.protobuf.Empty
0, // [0:9] is the sub-list for field type_name 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() } 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 { 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: case 0:
return &v.state return &v.state
case 1: 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 { 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: case 0:
return &v.state return &v.state
case 1: 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 { 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: case 0:
return &v.state return &v.state
case 1: 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 { 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: case 0:
return &v.state return &v.state
case 1: 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 { 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 { switch v := v.(*MachineServiceContainers); i {
case 0: case 0:
return &v.state return &v.state
@@ -1829,7 +2253,7 @@ func file_internal_machine_api_pb_docker_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(), GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_internal_machine_api_pb_docker_proto_rawDesc, RawDescriptor: file_internal_machine_api_pb_docker_proto_rawDesc,
NumEnums: 0, NumEnums: 0,
NumMessages: 23, NumMessages: 29,
NumExtensions: 0, NumExtensions: 0,
NumServices: 1, NumServices: 1,
}, },
+36
View File
@@ -14,12 +14,17 @@ service Docker {
rpc StopContainer(StopContainerRequest) returns (google.protobuf.Empty); rpc StopContainer(StopContainerRequest) returns (google.protobuf.Empty);
rpc ListContainers(ListContainersRequest) returns (ListContainersResponse); rpc ListContainers(ListContainersRequest) returns (ListContainersResponse);
rpc RemoveContainer(RemoveContainerRequest) returns (google.protobuf.Empty); rpc RemoveContainer(RemoveContainerRequest) returns (google.protobuf.Empty);
rpc PullImage(PullImageRequest) returns (stream JSONMessage); rpc PullImage(PullImageRequest) returns (stream JSONMessage);
rpc InspectImage(InspectImageRequest) returns (InspectImageResponse); rpc InspectImage(InspectImageRequest) returns (InspectImageResponse);
// InspectRemoteImage returns the image metadata for an image in a remote registry using the machine's // InspectRemoteImage returns the image metadata for an image in a remote registry using the machine's
// Docker auth credentials if necessary. // Docker auth credentials if necessary.
rpc InspectRemoteImage(InspectRemoteImageRequest) returns (InspectRemoteImageResponse); 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 CreateServiceContainer(CreateServiceContainerRequest) returns (CreateContainerResponse);
rpc InspectServiceContainer(InspectContainerRequest) returns (ServiceContainer); rpc InspectServiceContainer(InspectContainerRequest) returns (ServiceContainer);
rpc ListServiceContainers(ListServiceContainersRequest) returns (ListServiceContainersResponse); rpc ListServiceContainers(ListServiceContainersRequest) returns (ListServiceContainersResponse);
@@ -129,6 +134,37 @@ message RemoteImage {
bytes manifest = 3; 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 { message CreateServiceContainerRequest {
string service_id = 1; string service_id = 1;
// JSON serialised api.ServiceSpec. // JSON serialised api.ServiceSpec.
+114
View File
@@ -29,6 +29,9 @@ const (
Docker_PullImage_FullMethodName = "/api.Docker/PullImage" Docker_PullImage_FullMethodName = "/api.Docker/PullImage"
Docker_InspectImage_FullMethodName = "/api.Docker/InspectImage" Docker_InspectImage_FullMethodName = "/api.Docker/InspectImage"
Docker_InspectRemoteImage_FullMethodName = "/api.Docker/InspectRemoteImage" 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_CreateServiceContainer_FullMethodName = "/api.Docker/CreateServiceContainer"
Docker_InspectServiceContainer_FullMethodName = "/api.Docker/InspectServiceContainer" Docker_InspectServiceContainer_FullMethodName = "/api.Docker/InspectServiceContainer"
Docker_ListServiceContainers_FullMethodName = "/api.Docker/ListServiceContainers" 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 // InspectRemoteImage returns the image metadata for an image in a remote registry using the machine's
// Docker auth credentials if necessary. // Docker auth credentials if necessary.
InspectRemoteImage(ctx context.Context, in *InspectRemoteImageRequest, opts ...grpc.CallOption) (*InspectRemoteImageResponse, error) 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) CreateServiceContainer(ctx context.Context, in *CreateServiceContainerRequest, opts ...grpc.CallOption) (*CreateContainerResponse, error)
InspectServiceContainer(ctx context.Context, in *InspectContainerRequest, opts ...grpc.CallOption) (*ServiceContainer, error) InspectServiceContainer(ctx context.Context, in *InspectContainerRequest, opts ...grpc.CallOption) (*ServiceContainer, error)
ListServiceContainers(ctx context.Context, in *ListServiceContainersRequest, opts ...grpc.CallOption) (*ListServiceContainersResponse, 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 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) { func (c *dockerClient) CreateServiceContainer(ctx context.Context, in *CreateServiceContainerRequest, opts ...grpc.CallOption) (*CreateContainerResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(CreateContainerResponse) 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 // InspectRemoteImage returns the image metadata for an image in a remote registry using the machine's
// Docker auth credentials if necessary. // Docker auth credentials if necessary.
InspectRemoteImage(context.Context, *InspectRemoteImageRequest) (*InspectRemoteImageResponse, error) 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) CreateServiceContainer(context.Context, *CreateServiceContainerRequest) (*CreateContainerResponse, error)
InspectServiceContainer(context.Context, *InspectContainerRequest) (*ServiceContainer, error) InspectServiceContainer(context.Context, *InspectContainerRequest) (*ServiceContainer, error)
ListServiceContainers(context.Context, *ListServiceContainersRequest) (*ListServiceContainersResponse, 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) { func (UnimplementedDockerServer) InspectRemoteImage(context.Context, *InspectRemoteImageRequest) (*InspectRemoteImageResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method InspectRemoteImage not implemented") 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) { func (UnimplementedDockerServer) CreateServiceContainer(context.Context, *CreateServiceContainerRequest) (*CreateContainerResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method CreateServiceContainer not implemented") 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) 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) { func _Docker_CreateServiceContainer_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(CreateServiceContainerRequest) in := new(CreateServiceContainerRequest)
if err := dec(in); err != nil { if err := dec(in); err != nil {
@@ -558,6 +660,18 @@ var Docker_ServiceDesc = grpc.ServiceDesc{
MethodName: "InspectRemoteImage", MethodName: "InspectRemoteImage",
Handler: _Docker_InspectRemoteImage_Handler, Handler: _Docker_InspectRemoteImage_Handler,
}, },
{
MethodName: "CreateVolume",
Handler: _Docker_CreateVolume_Handler,
},
{
MethodName: "ListVolumes",
Handler: _Docker_ListVolumes_Handler,
},
{
MethodName: "RemoveVolume",
Handler: _Docker_RemoveVolume_Handler,
},
{ {
MethodName: "CreateServiceContainer", MethodName: "CreateServiceContainer",
Handler: _Docker_CreateServiceContainer_Handler, Handler: _Docker_CreateServiceContainer_Handler,
+249 -167
View File
@@ -21,6 +21,63 @@ const (
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
) )
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[0]
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[0]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use CheckPrerequisitesResponse.ProtoReflect.Descriptor instead.
func (*CheckPrerequisitesResponse) Descriptor() ([]byte, []int) {
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{0}
}
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 MachineInfo struct { type MachineInfo struct {
state protoimpl.MessageState state protoimpl.MessageState
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
@@ -35,7 +92,7 @@ type MachineInfo struct {
func (x *MachineInfo) Reset() { func (x *MachineInfo) Reset() {
*x = MachineInfo{} *x = MachineInfo{}
if protoimpl.UnsafeEnabled { if protoimpl.UnsafeEnabled {
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[0] mi := &file_internal_machine_api_pb_machine_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -48,7 +105,7 @@ func (x *MachineInfo) String() string {
func (*MachineInfo) ProtoMessage() {} func (*MachineInfo) ProtoMessage() {}
func (x *MachineInfo) ProtoReflect() protoreflect.Message { func (x *MachineInfo) ProtoReflect() protoreflect.Message {
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[0] mi := &file_internal_machine_api_pb_machine_proto_msgTypes[1]
if protoimpl.UnsafeEnabled && x != nil { if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -61,7 +118,7 @@ func (x *MachineInfo) ProtoReflect() protoreflect.Message {
// Deprecated: Use MachineInfo.ProtoReflect.Descriptor instead. // Deprecated: Use MachineInfo.ProtoReflect.Descriptor instead.
func (*MachineInfo) Descriptor() ([]byte, []int) { func (*MachineInfo) Descriptor() ([]byte, []int) {
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{0} return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{1}
} }
func (x *MachineInfo) GetId() string { func (x *MachineInfo) GetId() string {
@@ -106,7 +163,7 @@ type NetworkConfig struct {
func (x *NetworkConfig) Reset() { func (x *NetworkConfig) Reset() {
*x = NetworkConfig{} *x = NetworkConfig{}
if protoimpl.UnsafeEnabled { if protoimpl.UnsafeEnabled {
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[1] mi := &file_internal_machine_api_pb_machine_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -119,7 +176,7 @@ func (x *NetworkConfig) String() string {
func (*NetworkConfig) ProtoMessage() {} func (*NetworkConfig) ProtoMessage() {}
func (x *NetworkConfig) ProtoReflect() protoreflect.Message { func (x *NetworkConfig) ProtoReflect() protoreflect.Message {
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[1] mi := &file_internal_machine_api_pb_machine_proto_msgTypes[2]
if protoimpl.UnsafeEnabled && x != nil { if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -132,7 +189,7 @@ func (x *NetworkConfig) ProtoReflect() protoreflect.Message {
// Deprecated: Use NetworkConfig.ProtoReflect.Descriptor instead. // Deprecated: Use NetworkConfig.ProtoReflect.Descriptor instead.
func (*NetworkConfig) Descriptor() ([]byte, []int) { func (*NetworkConfig) Descriptor() ([]byte, []int) {
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{1} return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{2}
} }
func (x *NetworkConfig) GetSubnet() *IPPrefix { func (x *NetworkConfig) GetSubnet() *IPPrefix {
@@ -180,7 +237,7 @@ type InitClusterRequest struct {
func (x *InitClusterRequest) Reset() { func (x *InitClusterRequest) Reset() {
*x = InitClusterRequest{} *x = InitClusterRequest{}
if protoimpl.UnsafeEnabled { 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 := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -193,7 +250,7 @@ func (x *InitClusterRequest) String() string {
func (*InitClusterRequest) ProtoMessage() {} func (*InitClusterRequest) ProtoMessage() {}
func (x *InitClusterRequest) ProtoReflect() protoreflect.Message { 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 { if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -206,7 +263,7 @@ func (x *InitClusterRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use InitClusterRequest.ProtoReflect.Descriptor instead. // Deprecated: Use InitClusterRequest.ProtoReflect.Descriptor instead.
func (*InitClusterRequest) Descriptor() ([]byte, []int) { 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 { func (x *InitClusterRequest) GetMachineName() string {
@@ -271,7 +328,7 @@ type InitClusterResponse struct {
func (x *InitClusterResponse) Reset() { func (x *InitClusterResponse) Reset() {
*x = InitClusterResponse{} *x = InitClusterResponse{}
if protoimpl.UnsafeEnabled { 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 := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -284,7 +341,7 @@ func (x *InitClusterResponse) String() string {
func (*InitClusterResponse) ProtoMessage() {} func (*InitClusterResponse) ProtoMessage() {}
func (x *InitClusterResponse) ProtoReflect() protoreflect.Message { 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 { if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -297,7 +354,7 @@ func (x *InitClusterResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use InitClusterResponse.ProtoReflect.Descriptor instead. // Deprecated: Use InitClusterResponse.ProtoReflect.Descriptor instead.
func (*InitClusterResponse) Descriptor() ([]byte, []int) { 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 { func (x *InitClusterResponse) GetMachine() *MachineInfo {
@@ -319,7 +376,7 @@ type JoinClusterRequest struct {
func (x *JoinClusterRequest) Reset() { func (x *JoinClusterRequest) Reset() {
*x = JoinClusterRequest{} *x = JoinClusterRequest{}
if protoimpl.UnsafeEnabled { 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 := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -332,7 +389,7 @@ func (x *JoinClusterRequest) String() string {
func (*JoinClusterRequest) ProtoMessage() {} func (*JoinClusterRequest) ProtoMessage() {}
func (x *JoinClusterRequest) ProtoReflect() protoreflect.Message { 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 { if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -345,7 +402,7 @@ func (x *JoinClusterRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use JoinClusterRequest.ProtoReflect.Descriptor instead. // Deprecated: Use JoinClusterRequest.ProtoReflect.Descriptor instead.
func (*JoinClusterRequest) Descriptor() ([]byte, []int) { 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 { func (x *JoinClusterRequest) GetMachine() *MachineInfo {
@@ -373,7 +430,7 @@ type TokenResponse struct {
func (x *TokenResponse) Reset() { func (x *TokenResponse) Reset() {
*x = TokenResponse{} *x = TokenResponse{}
if protoimpl.UnsafeEnabled { 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 := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -386,7 +443,7 @@ func (x *TokenResponse) String() string {
func (*TokenResponse) ProtoMessage() {} func (*TokenResponse) ProtoMessage() {}
func (x *TokenResponse) ProtoReflect() protoreflect.Message { 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 { if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -399,7 +456,7 @@ func (x *TokenResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use TokenResponse.ProtoReflect.Descriptor instead. // Deprecated: Use TokenResponse.ProtoReflect.Descriptor instead.
func (*TokenResponse) Descriptor() ([]byte, []int) { 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 { func (x *TokenResponse) GetToken() string {
@@ -423,7 +480,7 @@ type Service struct {
func (x *Service) Reset() { func (x *Service) Reset() {
*x = Service{} *x = Service{}
if protoimpl.UnsafeEnabled { if protoimpl.UnsafeEnabled {
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[6] mi := &file_internal_machine_api_pb_machine_proto_msgTypes[7]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -436,7 +493,7 @@ func (x *Service) String() string {
func (*Service) ProtoMessage() {} func (*Service) ProtoMessage() {}
func (x *Service) ProtoReflect() protoreflect.Message { 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[7]
if protoimpl.UnsafeEnabled && x != nil { if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -449,7 +506,7 @@ func (x *Service) ProtoReflect() protoreflect.Message {
// Deprecated: Use Service.ProtoReflect.Descriptor instead. // Deprecated: Use Service.ProtoReflect.Descriptor instead.
func (*Service) Descriptor() ([]byte, []int) { 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{7}
} }
func (x *Service) GetId() string { func (x *Service) GetId() string {
@@ -491,7 +548,7 @@ type InspectServiceRequest struct {
func (x *InspectServiceRequest) Reset() { func (x *InspectServiceRequest) Reset() {
*x = InspectServiceRequest{} *x = InspectServiceRequest{}
if protoimpl.UnsafeEnabled { if protoimpl.UnsafeEnabled {
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[7] mi := &file_internal_machine_api_pb_machine_proto_msgTypes[8]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -504,7 +561,7 @@ func (x *InspectServiceRequest) String() string {
func (*InspectServiceRequest) ProtoMessage() {} func (*InspectServiceRequest) ProtoMessage() {}
func (x *InspectServiceRequest) ProtoReflect() protoreflect.Message { 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[8]
if protoimpl.UnsafeEnabled && x != nil { if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -517,7 +574,7 @@ func (x *InspectServiceRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use InspectServiceRequest.ProtoReflect.Descriptor instead. // Deprecated: Use InspectServiceRequest.ProtoReflect.Descriptor instead.
func (*InspectServiceRequest) Descriptor() ([]byte, []int) { 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{8}
} }
func (x *InspectServiceRequest) GetId() string { func (x *InspectServiceRequest) GetId() string {
@@ -538,7 +595,7 @@ type InspectServiceResponse struct {
func (x *InspectServiceResponse) Reset() { func (x *InspectServiceResponse) Reset() {
*x = InspectServiceResponse{} *x = InspectServiceResponse{}
if protoimpl.UnsafeEnabled { if protoimpl.UnsafeEnabled {
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[8] mi := &file_internal_machine_api_pb_machine_proto_msgTypes[9]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -551,7 +608,7 @@ func (x *InspectServiceResponse) String() string {
func (*InspectServiceResponse) ProtoMessage() {} func (*InspectServiceResponse) ProtoMessage() {}
func (x *InspectServiceResponse) ProtoReflect() protoreflect.Message { 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[9]
if protoimpl.UnsafeEnabled && x != nil { if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -564,7 +621,7 @@ func (x *InspectServiceResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use InspectServiceResponse.ProtoReflect.Descriptor instead. // Deprecated: Use InspectServiceResponse.ProtoReflect.Descriptor instead.
func (*InspectServiceResponse) Descriptor() ([]byte, []int) { 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{9}
} }
func (x *InspectServiceResponse) GetService() *Service { func (x *InspectServiceResponse) GetService() *Service {
@@ -587,7 +644,7 @@ type Service_Container struct {
func (x *Service_Container) Reset() { func (x *Service_Container) Reset() {
*x = Service_Container{} *x = Service_Container{}
if protoimpl.UnsafeEnabled { if protoimpl.UnsafeEnabled {
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[9] mi := &file_internal_machine_api_pb_machine_proto_msgTypes[10]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -600,7 +657,7 @@ func (x *Service_Container) String() string {
func (*Service_Container) ProtoMessage() {} func (*Service_Container) ProtoMessage() {}
func (x *Service_Container) ProtoReflect() protoreflect.Message { 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[10]
if protoimpl.UnsafeEnabled && x != nil { if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -613,7 +670,7 @@ func (x *Service_Container) ProtoReflect() protoreflect.Message {
// Deprecated: Use Service_Container.ProtoReflect.Descriptor instead. // Deprecated: Use Service_Container.ProtoReflect.Descriptor instead.
func (*Service_Container) Descriptor() ([]byte, []int) { 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{7, 0}
} }
func (x *Service_Container) GetMachineId() string { func (x *Service_Container) GetMachineId() string {
@@ -640,96 +697,106 @@ var file_internal_machine_api_pb_machine_proto_rawDesc = []byte{
0x70, 0x74, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x24, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x70, 0x74, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x24, 0x69, 0x6e, 0x74, 0x65, 0x72,
0x6e, 0x61, 0x6c, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x6e, 0x61, 0x6c, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f,
0x70, 0x62, 0x2f, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x70, 0x62, 0x2f, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22,
0x85, 0x01, 0x0a, 0x0b, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x50, 0x0a, 0x1a, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x50, 0x72, 0x65, 0x72, 0x65, 0x71, 0x75, 0x69,
0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x73, 0x69, 0x74, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1c, 0x0a,
0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x09, 0x73, 0x61, 0x74, 0x69, 0x73, 0x66, 0x69, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08,
0x61, 0x6d, 0x65, 0x12, 0x2c, 0x0a, 0x07, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x18, 0x03, 0x52, 0x09, 0x73, 0x61, 0x74, 0x69, 0x73, 0x66, 0x69, 0x65, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x65,
0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f,
0x72, 0x6b, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x07, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x72, 0x22, 0x85, 0x01, 0x0a, 0x0b, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66,
0x6b, 0x12, 0x24, 0x0a, 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x69, 0x70, 0x18, 0x04, 0x6f, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69,
0x20, 0x01, 0x28, 0x0b, 0x32, 0x07, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x50, 0x52, 0x08, 0x70, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52,
0x75, 0x62, 0x6c, 0x69, 0x63, 0x49, 0x70, 0x22, 0xae, 0x01, 0x0a, 0x0d, 0x4e, 0x65, 0x74, 0x77, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x2c, 0x0a, 0x07, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b,
0x6f, 0x72, 0x6b, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x25, 0x0a, 0x06, 0x73, 0x75, 0x62, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4e, 0x65, 0x74,
0x6e, 0x65, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x77, 0x6f, 0x72, 0x6b, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x07, 0x6e, 0x65, 0x74, 0x77,
0x49, 0x50, 0x50, 0x72, 0x65, 0x66, 0x69, 0x78, 0x52, 0x06, 0x73, 0x75, 0x62, 0x6e, 0x65, 0x74, 0x6f, 0x72, 0x6b, 0x12, 0x24, 0x0a, 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x69, 0x70,
0x12, 0x2c, 0x0a, 0x0d, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x07, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x50, 0x52,
0x70, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x07, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x50, 0x08, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x49, 0x70, 0x22, 0xae, 0x01, 0x0a, 0x0d, 0x4e, 0x65,
0x52, 0x0c, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x49, 0x70, 0x12, 0x29, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x25, 0x0a, 0x06, 0x73,
0x0a, 0x09, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x75, 0x62, 0x6e, 0x65, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x61, 0x70,
0x0b, 0x32, 0x0b, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x50, 0x50, 0x6f, 0x72, 0x74, 0x52, 0x09, 0x69, 0x2e, 0x49, 0x50, 0x50, 0x72, 0x65, 0x66, 0x69, 0x78, 0x52, 0x06, 0x73, 0x75, 0x62, 0x6e,
0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x70, 0x75, 0x62, 0x65, 0x74, 0x12, 0x2c, 0x0a, 0x0d, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74,
0x6c, 0x69, 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x70, 0x5f, 0x69, 0x70, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x07, 0x2e, 0x61, 0x70, 0x69, 0x2e,
0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x22, 0xc3, 0x01, 0x0a, 0x12, 0x49, 0x6e, 0x69, 0x49, 0x50, 0x52, 0x0c, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x49, 0x70,
0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x29, 0x0a, 0x09, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x18, 0x03, 0x20,
0x20, 0x0a, 0x0b, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x03, 0x28, 0x0b, 0x32, 0x0b, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x50, 0x50, 0x6f, 0x72, 0x74,
0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x4e, 0x61, 0x6d, 0x52, 0x09, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x70,
0x65, 0x12, 0x27, 0x0a, 0x07, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x18, 0x02, 0x20, 0x01, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52,
0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x50, 0x50, 0x72, 0x65, 0x66, 0x69, 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x22, 0xc3, 0x01, 0x0a, 0x12, 0x49,
0x78, 0x52, 0x07, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x12, 0x26, 0x0a, 0x09, 0x70, 0x75, 0x6e, 0x69, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
0x62, 0x6c, 0x69, 0x63, 0x5f, 0x69, 0x70, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x07, 0x2e, 0x74, 0x12, 0x20, 0x0a, 0x0b, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x4e, 0x61, 0x6d, 0x65,
0x61, 0x70, 0x69, 0x2e, 0x49, 0x50, 0x48, 0x00, 0x52, 0x08, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x4e,
0x49, 0x70, 0x12, 0x26, 0x0a, 0x0e, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x69, 0x70, 0x5f, 0x61, 0x6d, 0x65, 0x12, 0x27, 0x0a, 0x07, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x18, 0x02,
0x61, 0x75, 0x74, 0x6f, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x48, 0x00, 0x52, 0x0c, 0x70, 0x75, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x50, 0x50, 0x72, 0x65,
0x62, 0x6c, 0x69, 0x63, 0x49, 0x70, 0x41, 0x75, 0x74, 0x6f, 0x42, 0x12, 0x0a, 0x10, 0x70, 0x75, 0x66, 0x69, 0x78, 0x52, 0x07, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x12, 0x26, 0x0a, 0x09,
0x62, 0x6c, 0x69, 0x63, 0x5f, 0x69, 0x70, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0x41, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x69, 0x70, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32,
0x0a, 0x13, 0x49, 0x6e, 0x69, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x73, 0x07, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x50, 0x48, 0x00, 0x52, 0x08, 0x70, 0x75, 0x62, 0x6c,
0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2a, 0x0a, 0x07, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x69, 0x63, 0x49, 0x70, 0x12, 0x26, 0x0a, 0x0e, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x69,
0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d, 0x61, 0x63, 0x70, 0x5f, 0x61, 0x75, 0x74, 0x6f, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x48, 0x00, 0x52, 0x0c,
0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x07, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x49, 0x70, 0x41, 0x75, 0x74, 0x6f, 0x42, 0x12, 0x0a, 0x10,
0x65, 0x22, 0x79, 0x0a, 0x12, 0x4a, 0x6f, 0x69, 0x6e, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x69, 0x70, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67,
0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2a, 0x0a, 0x07, 0x6d, 0x61, 0x63, 0x68, 0x69, 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, 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, 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, 0x69, 0x6e, 0x65, 0x22, 0x79, 0x0a, 0x12, 0x4a, 0x6f, 0x69, 0x6e, 0x43, 0x6c, 0x75, 0x73, 0x74,
0x68, 0x69, 0x6e, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x61, 0x70, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2a, 0x0a, 0x07, 0x6d, 0x61, 0x63,
0x69, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x0d, 0x6f, 0x68, 0x69, 0x6e, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x61, 0x70, 0x69,
0x74, 0x68, 0x65, 0x72, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x22, 0x25, 0x0a, 0x0d, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x07, 0x6d, 0x61,
0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x14, 0x0a, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x12, 0x37, 0x0a, 0x0e, 0x6f, 0x74, 0x68, 0x65, 0x72, 0x5f, 0x6d,
0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x6f, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x10, 0x2e,
0x6b, 0x65, 0x6e, 0x22, 0xc3, 0x01, 0x0a, 0x07, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x61, 0x70, 0x69, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52,
0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x0d, 0x6f, 0x74, 0x68, 0x65, 0x72, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x22, 0x25,
0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x0a, 0x0d, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12,
0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05,
0x09, 0x52, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x12, 0x36, 0x0a, 0x0a, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0xc3, 0x01, 0x0a, 0x07, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63,
0x69, 0x6e, 0x65, 0x72, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x61, 0x70, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69,
0x69, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52,
0x6e, 0x65, 0x72, 0x52, 0x0a, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x1a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x18, 0x03, 0x20,
0x48, 0x0a, 0x09, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x1d, 0x0a, 0x0a, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x12, 0x36, 0x0a, 0x0a, 0x63, 0x6f, 0x6e,
0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e,
0x52, 0x09, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x64, 0x12, 0x1c, 0x0a, 0x09, 0x63, 0x61, 0x70, 0x69, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x43, 0x6f, 0x6e, 0x74,
0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x0a, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72,
0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x22, 0x27, 0x0a, 0x15, 0x49, 0x6e, 0x73, 0x73, 0x1a, 0x48, 0x0a, 0x09, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x1d,
0x70, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x0a, 0x0a, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01,
0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x28, 0x09, 0x52, 0x09, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x64, 0x12, 0x1c, 0x0a,
0x69, 0x64, 0x22, 0x40, 0x0a, 0x16, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72, 0x09, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c,
0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x26, 0x0a, 0x07, 0x52, 0x09, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x22, 0x27, 0x0a, 0x15, 0x49,
0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0c, 0x2e, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x71,
0x61, 0x70, 0x69, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x07, 0x73, 0x65, 0x72, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
0x76, 0x69, 0x63, 0x65, 0x32, 0xc0, 0x02, 0x0a, 0x07, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x02, 0x69, 0x64, 0x22, 0x40, 0x0a, 0x16, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53,
0x12, 0x40, 0x0a, 0x0b, 0x49, 0x6e, 0x69, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x12, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x26,
0x17, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x69, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x0a, 0x07, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32,
0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x0c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x07, 0x73,
0x6e, 0x69, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x32, 0x8f, 0x03, 0x0a, 0x07, 0x4d, 0x61, 0x63, 0x68, 0x69,
0x73, 0x65, 0x12, 0x3e, 0x0a, 0x0b, 0x4a, 0x6f, 0x69, 0x6e, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x6e, 0x65, 0x12, 0x4d, 0x0a, 0x12, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x50, 0x72, 0x65, 0x72, 0x65,
0x72, 0x12, 0x17, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4a, 0x6f, 0x69, 0x6e, 0x43, 0x6c, 0x75, 0x73, 0x71, 0x75, 0x69, 0x73, 0x69, 0x74, 0x65, 0x73, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c,
0x74, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79,
0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x1a, 0x1f, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x50, 0x72, 0x65, 0x72,
0x74, 0x79, 0x12, 0x33, 0x0a, 0x05, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x16, 0x2e, 0x67, 0x6f, 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, 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, 0x70, 0x74, 0x79, 0x12, 0x33, 0x0a, 0x05, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x16, 0x2e, 0x67,
0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x33, 0x0a, 0x07, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45,
0x63, 0x74, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x12, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x54, 0x6f, 0x6b, 0x65, 0x6e,
0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x10, 0x2e, 0x61, 0x70, 0x69, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x33, 0x0a, 0x07, 0x49, 0x6e, 0x73, 0x70,
0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x49, 0x0a, 0x0e, 0x65, 0x63, 0x74, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f,
0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x1a, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x10, 0x2e, 0x61, 0x70,
0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x49, 0x0a,
0x69, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x61, 0x70, 0x69, 0x0e, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12,
0x2e, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x1a, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72,
0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x37, 0x5a, 0x35, 0x67, 0x69, 0x74, 0x68, 0x75, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x61, 0x70,
0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x70, 0x73, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x6b, 0x69, 0x69, 0x2e, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65,
0x2f, 0x75, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x37, 0x5a, 0x35, 0x67, 0x69, 0x74, 0x68,
0x6c, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x62, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x70, 0x73, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x6b,
0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, 0x69, 0x2f, 0x75, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e,
0x61, 0x6c, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70,
0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
} }
var ( var (
@@ -744,48 +811,51 @@ func file_internal_machine_api_pb_machine_proto_rawDescGZIP() []byte {
return file_internal_machine_api_pb_machine_proto_rawDescData 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, 11)
var file_internal_machine_api_pb_machine_proto_goTypes = []any{ var file_internal_machine_api_pb_machine_proto_goTypes = []any{
(*MachineInfo)(nil), // 0: api.MachineInfo (*CheckPrerequisitesResponse)(nil), // 0: api.CheckPrerequisitesResponse
(*NetworkConfig)(nil), // 1: api.NetworkConfig (*MachineInfo)(nil), // 1: api.MachineInfo
(*InitClusterRequest)(nil), // 2: api.InitClusterRequest (*NetworkConfig)(nil), // 2: api.NetworkConfig
(*InitClusterResponse)(nil), // 3: api.InitClusterResponse (*InitClusterRequest)(nil), // 3: api.InitClusterRequest
(*JoinClusterRequest)(nil), // 4: api.JoinClusterRequest (*InitClusterResponse)(nil), // 4: api.InitClusterResponse
(*TokenResponse)(nil), // 5: api.TokenResponse (*JoinClusterRequest)(nil), // 5: api.JoinClusterRequest
(*Service)(nil), // 6: api.Service (*TokenResponse)(nil), // 6: api.TokenResponse
(*InspectServiceRequest)(nil), // 7: api.InspectServiceRequest (*Service)(nil), // 7: api.Service
(*InspectServiceResponse)(nil), // 8: api.InspectServiceResponse (*InspectServiceRequest)(nil), // 8: api.InspectServiceRequest
(*Service_Container)(nil), // 9: api.Service.Container (*InspectServiceResponse)(nil), // 9: api.InspectServiceResponse
(*IP)(nil), // 10: api.IP (*Service_Container)(nil), // 10: api.Service.Container
(*IPPrefix)(nil), // 11: api.IPPrefix (*IP)(nil), // 11: api.IP
(*IPPort)(nil), // 12: api.IPPort (*IPPrefix)(nil), // 12: api.IPPrefix
(*emptypb.Empty)(nil), // 13: google.protobuf.Empty (*IPPort)(nil), // 13: api.IPPort
(*emptypb.Empty)(nil), // 14: google.protobuf.Empty
} }
var file_internal_machine_api_pb_machine_proto_depIdxs = []int32{ var file_internal_machine_api_pb_machine_proto_depIdxs = []int32{
1, // 0: api.MachineInfo.network:type_name -> api.NetworkConfig 2, // 0: api.MachineInfo.network:type_name -> api.NetworkConfig
10, // 1: api.MachineInfo.public_ip:type_name -> api.IP 11, // 1: api.MachineInfo.public_ip:type_name -> api.IP
11, // 2: api.NetworkConfig.subnet:type_name -> api.IPPrefix 12, // 2: api.NetworkConfig.subnet:type_name -> api.IPPrefix
10, // 3: api.NetworkConfig.management_ip:type_name -> api.IP 11, // 3: api.NetworkConfig.management_ip:type_name -> api.IP
12, // 4: api.NetworkConfig.endpoints:type_name -> api.IPPort 13, // 4: api.NetworkConfig.endpoints:type_name -> api.IPPort
11, // 5: api.InitClusterRequest.network:type_name -> api.IPPrefix 12, // 5: api.InitClusterRequest.network:type_name -> api.IPPrefix
10, // 6: api.InitClusterRequest.public_ip:type_name -> api.IP 11, // 6: api.InitClusterRequest.public_ip:type_name -> api.IP
0, // 7: api.InitClusterResponse.machine:type_name -> api.MachineInfo 1, // 7: api.InitClusterResponse.machine:type_name -> api.MachineInfo
0, // 8: api.JoinClusterRequest.machine:type_name -> api.MachineInfo 1, // 8: api.JoinClusterRequest.machine:type_name -> api.MachineInfo
0, // 9: api.JoinClusterRequest.other_machines:type_name -> api.MachineInfo 1, // 9: api.JoinClusterRequest.other_machines:type_name -> api.MachineInfo
9, // 10: api.Service.containers:type_name -> api.Service.Container 10, // 10: api.Service.containers:type_name -> api.Service.Container
6, // 11: api.InspectServiceResponse.service:type_name -> api.Service 7, // 11: api.InspectServiceResponse.service:type_name -> api.Service
2, // 12: api.Machine.InitCluster:input_type -> api.InitClusterRequest 14, // 12: api.Machine.CheckPrerequisites:input_type -> google.protobuf.Empty
4, // 13: api.Machine.JoinCluster:input_type -> api.JoinClusterRequest 3, // 13: api.Machine.InitCluster:input_type -> api.InitClusterRequest
13, // 14: api.Machine.Token:input_type -> google.protobuf.Empty 5, // 14: api.Machine.JoinCluster:input_type -> api.JoinClusterRequest
13, // 15: api.Machine.Inspect:input_type -> google.protobuf.Empty 14, // 15: api.Machine.Token:input_type -> google.protobuf.Empty
7, // 16: api.Machine.InspectService:input_type -> api.InspectServiceRequest 14, // 16: api.Machine.Inspect:input_type -> google.protobuf.Empty
3, // 17: api.Machine.InitCluster:output_type -> api.InitClusterResponse 8, // 17: api.Machine.InspectService:input_type -> api.InspectServiceRequest
13, // 18: api.Machine.JoinCluster:output_type -> google.protobuf.Empty 0, // 18: api.Machine.CheckPrerequisites:output_type -> api.CheckPrerequisitesResponse
5, // 19: api.Machine.Token:output_type -> api.TokenResponse 4, // 19: api.Machine.InitCluster:output_type -> api.InitClusterResponse
0, // 20: api.Machine.Inspect:output_type -> api.MachineInfo 14, // 20: api.Machine.JoinCluster:output_type -> google.protobuf.Empty
8, // 21: api.Machine.InspectService:output_type -> api.InspectServiceResponse 6, // 21: api.Machine.Token:output_type -> api.TokenResponse
17, // [17:22] is the sub-list for method output_type 1, // 22: api.Machine.Inspect:output_type -> api.MachineInfo
12, // [12:17] is the sub-list for method input_type 9, // 23: api.Machine.InspectService:output_type -> api.InspectServiceResponse
18, // [18:24] is the sub-list for method output_type
12, // [12:18] is the sub-list for method input_type
12, // [12:12] is the sub-list for extension type_name 12, // [12:12] is the sub-list for extension type_name
12, // [12:12] is the sub-list for extension extendee 12, // [12:12] is the sub-list for extension extendee
0, // [0:12] is the sub-list for field type_name 0, // [0:12] is the sub-list for field type_name
@@ -799,7 +869,7 @@ func file_internal_machine_api_pb_machine_proto_init() {
file_internal_machine_api_pb_common_proto_init() file_internal_machine_api_pb_common_proto_init()
if !protoimpl.UnsafeEnabled { if !protoimpl.UnsafeEnabled {
file_internal_machine_api_pb_machine_proto_msgTypes[0].Exporter = func(v any, i int) any { file_internal_machine_api_pb_machine_proto_msgTypes[0].Exporter = func(v any, i int) any {
switch v := v.(*MachineInfo); i { switch v := v.(*CheckPrerequisitesResponse); i {
case 0: case 0:
return &v.state return &v.state
case 1: case 1:
@@ -811,7 +881,7 @@ func file_internal_machine_api_pb_machine_proto_init() {
} }
} }
file_internal_machine_api_pb_machine_proto_msgTypes[1].Exporter = func(v any, i int) any { file_internal_machine_api_pb_machine_proto_msgTypes[1].Exporter = func(v any, i int) any {
switch v := v.(*NetworkConfig); i { switch v := v.(*MachineInfo); i {
case 0: case 0:
return &v.state return &v.state
case 1: case 1:
@@ -823,7 +893,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 { file_internal_machine_api_pb_machine_proto_msgTypes[2].Exporter = func(v any, i int) any {
switch v := v.(*InitClusterRequest); i { switch v := v.(*NetworkConfig); i {
case 0: case 0:
return &v.state return &v.state
case 1: case 1:
@@ -835,7 +905,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 { 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: case 0:
return &v.state return &v.state
case 1: case 1:
@@ -847,7 +917,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 { 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: case 0:
return &v.state return &v.state
case 1: case 1:
@@ -859,7 +929,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 { 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: case 0:
return &v.state return &v.state
case 1: case 1:
@@ -871,7 +941,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 { 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: case 0:
return &v.state return &v.state
case 1: case 1:
@@ -883,7 +953,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 { file_internal_machine_api_pb_machine_proto_msgTypes[7].Exporter = func(v any, i int) any {
switch v := v.(*InspectServiceRequest); i { switch v := v.(*Service); i {
case 0: case 0:
return &v.state return &v.state
case 1: case 1:
@@ -895,7 +965,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 { file_internal_machine_api_pb_machine_proto_msgTypes[8].Exporter = func(v any, i int) any {
switch v := v.(*InspectServiceResponse); i { switch v := v.(*InspectServiceRequest); i {
case 0: case 0:
return &v.state return &v.state
case 1: case 1:
@@ -907,6 +977,18 @@ 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 { file_internal_machine_api_pb_machine_proto_msgTypes[9].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[10].Exporter = func(v any, i int) any {
switch v := v.(*Service_Container); i { switch v := v.(*Service_Container); i {
case 0: case 0:
return &v.state return &v.state
@@ -919,7 +1001,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_PublicIp)(nil),
(*InitClusterRequest_PublicIpAuto)(nil), (*InitClusterRequest_PublicIpAuto)(nil),
} }
@@ -929,7 +1011,7 @@ func file_internal_machine_api_pb_machine_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(), GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_internal_machine_api_pb_machine_proto_rawDesc, RawDescriptor: file_internal_machine_api_pb_machine_proto_rawDesc,
NumEnums: 0, NumEnums: 0,
NumMessages: 10, NumMessages: 11,
NumExtensions: 0, NumExtensions: 0,
NumServices: 1, NumServices: 1,
}, },
+9
View File
@@ -8,6 +8,8 @@ import "google/protobuf/empty.proto";
import "internal/machine/api/pb/common.proto"; import "internal/machine/api/pb/common.proto";
service Machine { 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 InitCluster(InitClusterRequest) returns (InitClusterResponse);
rpc JoinCluster(JoinClusterRequest) returns (google.protobuf.Empty); rpc JoinCluster(JoinClusterRequest) returns (google.protobuf.Empty);
rpc Token(google.protobuf.Empty) returns (TokenResponse); rpc Token(google.protobuf.Empty) returns (TokenResponse);
@@ -29,6 +31,13 @@ message NetworkConfig {
bytes public_key = 4; 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 { message InitClusterRequest {
string machineName = 1; string machineName = 1;
IPPrefix network = 2; IPPrefix network = 2;
+43 -5
View File
@@ -20,17 +20,19 @@ import (
const _ = grpc.SupportPackageIsVersion9 const _ = grpc.SupportPackageIsVersion9
const ( const (
Machine_InitCluster_FullMethodName = "/api.Machine/InitCluster" Machine_CheckPrerequisites_FullMethodName = "/api.Machine/CheckPrerequisites"
Machine_JoinCluster_FullMethodName = "/api.Machine/JoinCluster" Machine_InitCluster_FullMethodName = "/api.Machine/InitCluster"
Machine_Token_FullMethodName = "/api.Machine/Token" Machine_JoinCluster_FullMethodName = "/api.Machine/JoinCluster"
Machine_Inspect_FullMethodName = "/api.Machine/Inspect" Machine_Token_FullMethodName = "/api.Machine/Token"
Machine_InspectService_FullMethodName = "/api.Machine/InspectService" Machine_Inspect_FullMethodName = "/api.Machine/Inspect"
Machine_InspectService_FullMethodName = "/api.Machine/InspectService"
) )
// MachineClient is the client API for Machine service. // 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. // 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 { type MachineClient interface {
CheckPrerequisites(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*CheckPrerequisitesResponse, error)
InitCluster(ctx context.Context, in *InitClusterRequest, opts ...grpc.CallOption) (*InitClusterResponse, error) InitCluster(ctx context.Context, in *InitClusterRequest, opts ...grpc.CallOption) (*InitClusterResponse, error)
JoinCluster(ctx context.Context, in *JoinClusterRequest, opts ...grpc.CallOption) (*emptypb.Empty, 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) Token(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*TokenResponse, error)
@@ -46,6 +48,16 @@ func NewMachineClient(cc grpc.ClientConnInterface) MachineClient {
return &machineClient{cc} 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) { func (c *machineClient) InitCluster(ctx context.Context, in *InitClusterRequest, opts ...grpc.CallOption) (*InitClusterResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(InitClusterResponse) out := new(InitClusterResponse)
@@ -100,6 +112,7 @@ func (c *machineClient) InspectService(ctx context.Context, in *InspectServiceRe
// All implementations must embed UnimplementedMachineServer // All implementations must embed UnimplementedMachineServer
// for forward compatibility. // for forward compatibility.
type MachineServer interface { type MachineServer interface {
CheckPrerequisites(context.Context, *emptypb.Empty) (*CheckPrerequisitesResponse, error)
InitCluster(context.Context, *InitClusterRequest) (*InitClusterResponse, error) InitCluster(context.Context, *InitClusterRequest) (*InitClusterResponse, error)
JoinCluster(context.Context, *JoinClusterRequest) (*emptypb.Empty, error) JoinCluster(context.Context, *JoinClusterRequest) (*emptypb.Empty, error)
Token(context.Context, *emptypb.Empty) (*TokenResponse, error) Token(context.Context, *emptypb.Empty) (*TokenResponse, error)
@@ -115,6 +128,9 @@ type MachineServer interface {
// pointer dereference when methods are called. // pointer dereference when methods are called.
type UnimplementedMachineServer struct{} 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) { func (UnimplementedMachineServer) InitCluster(context.Context, *InitClusterRequest) (*InitClusterResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method InitCluster not implemented") return nil, status.Errorf(codes.Unimplemented, "method InitCluster not implemented")
} }
@@ -151,6 +167,24 @@ func RegisterMachineServer(s grpc.ServiceRegistrar, srv MachineServer) {
s.RegisterService(&Machine_ServiceDesc, srv) 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) { func _Machine_InitCluster_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(InitClusterRequest) in := new(InitClusterRequest)
if err := dec(in); err != nil { if err := dec(in); err != nil {
@@ -248,6 +282,10 @@ var Machine_ServiceDesc = grpc.ServiceDesc{
ServiceName: "api.Machine", ServiceName: "api.Machine",
HandlerType: (*MachineServer)(nil), HandlerType: (*MachineServer)(nil),
Methods: []grpc.MethodDesc{ Methods: []grpc.MethodDesc{
{
MethodName: "CheckPrerequisites",
Handler: _Machine_CheckPrerequisites_Handler,
},
{ {
MethodName: "InitCluster", MethodName: "InitCluster",
Handler: _Machine_InitCluster_Handler, Handler: _Machine_InitCluster_Handler,
@@ -1,4 +1,4 @@
package caddyfile package caddyconfig
import ( import (
"encoding/json" "encoding/json"
@@ -10,12 +10,12 @@ import (
"net/http" "net/http"
"slices" "slices"
"strconv" "strconv"
"time"
"github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig" "github.com/caddyserver/caddy/v2/caddyconfig"
"github.com/caddyserver/caddy/v2/modules/caddyhttp" "github.com/caddyserver/caddy/v2/modules/caddyhttp"
"github.com/caddyserver/caddy/v2/modules/caddyhttp/reverseproxy" "github.com/caddyserver/caddy/v2/modules/caddyhttp/reverseproxy"
"github.com/psviderski/uncloud/internal/machine/docker"
"github.com/psviderski/uncloud/pkg/api" "github.com/psviderski/uncloud/pkg/api"
) )
@@ -24,34 +24,38 @@ func GenerateConfig(containers []api.ServiceContainer, verifyResponse string) (*
httpHostUpstreams := make(map[string][]string) httpHostUpstreams := make(map[string][]string)
httpsHostUpstreams := make(map[string][]string) httpsHostUpstreams := make(map[string][]string)
for _, ctr := range containers { for _, ctr := range containers {
logger := slog.With("container", ctr.ID) if !ctr.Healthy() {
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.")
continue 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() ports, err := ctr.ServicePorts()
if err != nil { 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 continue
} }
for _, port := range ports { for _, port := range ports {
if port.Mode != api.PortModeIngress {
continue
}
switch port.Protocol { switch port.Protocol {
case api.ProtocolHTTP: 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) httpHostUpstreams[port.Hostname] = append(httpHostUpstreams[port.Hostname], upstream)
case api.ProtocolHTTPS: 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) httpsHostUpstreams[port.Hostname] = append(httpsHostUpstreams[port.Hostname], upstream)
default: default:
// TODO: implement L4 ingress routing for TCP and UDP. // 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 continue
} }
} }
@@ -61,6 +65,8 @@ func GenerateConfig(containers []api.ServiceContainer, verifyResponse string) (*
servers := make(map[string]*caddyhttp.Server) servers := make(map[string]*caddyhttp.Server)
servers["http"] = &caddyhttp.Server{ servers["http"] = &caddyhttp.Server{
Listen: []string{fmt.Sprintf(":%d", caddyhttp.DefaultHTTPPort)}, Listen: []string{fmt.Sprintf(":%d", caddyhttp.DefaultHTTPPort)},
// All http requests to this server are logged to the default logger.
Logs: &caddyhttp.ServerLogConfig{},
Routes: append( Routes: append(
hostUpstreamsToRoutes(httpHostUpstreams, &warnings), hostUpstreamsToRoutes(httpHostUpstreams, &warnings),
// Add a route to respond with a static verification response at the /.uncloud-verify path. // 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{ servers["https"] = &caddyhttp.Server{
Listen: []string{fmt.Sprintf(":%d", caddyhttp.DefaultHTTPSPort)}, 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), Routes: hostUpstreamsToRoutes(httpsHostUpstreams, &warnings),
} }
@@ -109,6 +117,16 @@ func hostUpstreamsToRoutes(hostUpstreams map[string][]string, warnings *[]caddyc
} }
} }
handler := &reverseproxy.Handler{ 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, Upstreams: upstreamPool,
} }
@@ -1,4 +1,4 @@
package caddyfile package caddyconfig
import ( import (
"strings" "strings"
@@ -14,6 +14,27 @@ import (
) )
func TestGenerateConfig(t *testing.T) { 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 { tests := []struct {
name string name string
containers []api.ServiceContainer containers []api.ServiceContainer
@@ -23,25 +44,8 @@ func TestGenerateConfig(t *testing.T) {
{ {
name: "empty containers", name: "empty containers",
containers: []api.ServiceContainer{}, containers: []api.ServiceContainer{},
want: `{ want: configWithoutServices,
"servers": { wantErr: false,
"http": {
"listen": [":80"],
"routes": [{
"match": [{"path": ["/.uncloud-verify"]}],
"handle": [{
"body": "verification-response-body",
"handler": "static_response",
"status_code": 200
}]
}]
},
"https": {
"listen": [":443"]
}
}
}`,
wantErr: false,
}, },
{ {
@@ -58,6 +62,14 @@ func TestGenerateConfig(t *testing.T) {
"match": [{"host": ["app.example.com"]}], "match": [{"host": ["app.example.com"]}],
"handle": [{ "handle": [{
"handler": "reverse_proxy", "handler": "reverse_proxy",
"health_checks": {
"passive": {
"fail_duration": 30000000000
}
},
"load_balancing": {
"retries": 3
},
"upstreams": [{"dial": "10.210.0.2:8080"}] "upstreams": [{"dial": "10.210.0.2:8080"}]
}] }]
}, },
@@ -69,10 +81,12 @@ func TestGenerateConfig(t *testing.T) {
"status_code": 200 "status_code": 200
}] }]
} }
] ],
"logs": {}
}, },
"https": { "https": {
"listen": [":443"] "listen": [":443"],
"logs": {}
} }
} }
}`, }`,
@@ -93,6 +107,14 @@ func TestGenerateConfig(t *testing.T) {
"match": [{"host": ["app.example.com"]}], "match": [{"host": ["app.example.com"]}],
"handle": [{ "handle": [{
"handler": "reverse_proxy", "handler": "reverse_proxy",
"health_checks": {
"passive": {
"fail_duration": 30000000000
}
},
"load_balancing": {
"retries": 3
},
"upstreams": [ "upstreams": [
{"dial": "10.210.0.2:8080"}, {"dial": "10.210.0.2:8080"},
{"dial": "10.210.0.3:8080"} {"dial": "10.210.0.3:8080"}
@@ -107,10 +129,12 @@ func TestGenerateConfig(t *testing.T) {
"status_code": 200 "status_code": 200
}] }]
} }
] ],
"logs": {}
}, },
"https": { "https": {
"listen": [":443"] "listen": [":443"],
"logs": {}
} }
} }
}`, }`,
@@ -134,7 +158,8 @@ func TestGenerateConfig(t *testing.T) {
"status_code": 200 "status_code": 200
}] }]
} }
] ],
"logs": {}
}, },
"https": { "https": {
"listen": [":443"], "listen": [":443"],
@@ -143,10 +168,19 @@ func TestGenerateConfig(t *testing.T) {
"match": [{"host": ["secure.example.com"]}], "match": [{"host": ["secure.example.com"]}],
"handle": [{ "handle": [{
"handler": "reverse_proxy", "handler": "reverse_proxy",
"health_checks": {
"passive": {
"fail_duration": 30000000000
}
},
"load_balancing": {
"retries": 3
},
"upstreams": [{"dial": "10.210.0.2:8000"}] "upstreams": [{"dial": "10.210.0.2:8000"}]
}] }]
} }
] ],
"logs": {}
} }
} }
}`, }`,
@@ -178,6 +212,14 @@ func TestGenerateConfig(t *testing.T) {
"match": [{"host": ["app.example.com"]}], "match": [{"host": ["app.example.com"]}],
"handle": [{ "handle": [{
"handler": "reverse_proxy", "handler": "reverse_proxy",
"health_checks": {
"passive": {
"fail_duration": 30000000000
}
},
"load_balancing": {
"retries": 3
},
"upstreams": [ "upstreams": [
{"dial": "10.210.0.2:8080"}, {"dial": "10.210.0.2:8080"},
{"dial": "10.210.0.3:8080"}, {"dial": "10.210.0.3:8080"},
@@ -189,6 +231,14 @@ func TestGenerateConfig(t *testing.T) {
"match": [{"host": ["web.example.com"]}], "match": [{"host": ["web.example.com"]}],
"handle": [{ "handle": [{
"handler": "reverse_proxy", "handler": "reverse_proxy",
"health_checks": {
"passive": {
"fail_duration": 30000000000
}
},
"load_balancing": {
"retries": 3
},
"upstreams": [ "upstreams": [
{"dial": "10.210.0.2:8000"}, {"dial": "10.210.0.2:8000"},
{"dial": "10.210.0.4:8000"}, {"dial": "10.210.0.4:8000"},
@@ -204,7 +254,8 @@ func TestGenerateConfig(t *testing.T) {
"status_code": 200 "status_code": 200
}] }]
} }
] ],
"logs": {}
}, },
"https": { "https": {
"listen": [":443"], "listen": [":443"],
@@ -213,6 +264,14 @@ func TestGenerateConfig(t *testing.T) {
"match": [{"host": ["secure.example.com"]}], "match": [{"host": ["secure.example.com"]}],
"handle": [{ "handle": [{
"handler": "reverse_proxy", "handler": "reverse_proxy",
"health_checks": {
"passive": {
"fail_duration": 30000000000
}
},
"load_balancing": {
"retries": 3
},
"upstreams": [ "upstreams": [
{"dial": "10.210.0.3:8888"}, {"dial": "10.210.0.3:8888"},
{"dial": "10.210.0.4: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{ containers: []api.ServiceContainer{
newContainerWithoutNetwork("ignored.example.com:8080/http"), newContainerWithoutNetwork("ignored.example.com:8080/http"),
}, },
want: `{ want: configWithoutServices,
"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, wantErr: false,
}, },
{ {
@@ -256,48 +299,76 @@ func TestGenerateConfig(t *testing.T) {
containers: []api.ServiceContainer{ containers: []api.ServiceContainer{
newContainer("10.210.0.2", "invalid-port"), newContainer("10.210.0.2", "invalid-port"),
}, },
want: `{ want: configWithoutServices,
"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, wantErr: false,
}, },
{ {
name: "containers with unsupported protocols ignored", name: "containers with unsupported protocols and host mode ignored",
containers: []api.ServiceContainer{ containers: []api.ServiceContainer{
newContainer("10.210.0.2", "5000/tcp"), newContainer("10.210.0.2", "5000/tcp"),
newContainer("10.210.0.3", "5000/udp"), newContainer("10.210.0.3", "5000/udp"),
newContainer("10.210.0.4", "80:8080/tcp@host"), 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: `{ want: `{
"servers": { "servers": {
"http": { "http": {
"listen": [":80"], "listen": [":80"],
"routes": [{ "routes": [
"match": [{"path": ["/.uncloud-verify"]}], {
"handle": [{ "match": [{"host": ["app.example.com"]}],
"body": "verification-response-body", "handle": [{
"handler": "static_response", "handler": "reverse_proxy",
"status_code": 200 "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": { "https": {
"listen": [":443"] "listen": [":443"],
"logs": {}
} }
} }
}`, }`,
@@ -326,7 +397,11 @@ func TestGenerateConfig(t *testing.T) {
func newContainer(ip string, ports ...string) api.ServiceContainer { func newContainer(ip string, ports ...string) api.ServiceContainer {
portsLabel := strings.Join(ports, ",") portsLabel := strings.Join(ports, ",")
return api.ServiceContainer{Container: api.Container{ContainerJSON: types.ContainerJSON{ return api.ServiceContainer{Container: api.Container{ContainerJSON: types.ContainerJSON{
ContainerJSONBase: &types.ContainerJSONBase{}, ContainerJSONBase: &types.ContainerJSONBase{
State: &types.ContainerState{
Running: true,
},
},
NetworkSettings: &types.NetworkSettings{ NetworkSettings: &types.NetworkSettings{
Networks: map[string]*network.EndpointSettings{ Networks: map[string]*network.EndpointSettings{
docker.NetworkName: { docker.NetworkName: {
@@ -345,7 +420,11 @@ func newContainer(ip string, ports ...string) api.ServiceContainer {
func newContainerWithoutNetwork(ports ...string) api.ServiceContainer { func newContainerWithoutNetwork(ports ...string) api.ServiceContainer {
portsLabel := strings.Join(ports, ",") portsLabel := strings.Join(ports, ",")
return api.ServiceContainer{Container: api.Container{ContainerJSON: types.ContainerJSON{ return api.ServiceContainer{Container: api.Container{ContainerJSON: types.ContainerJSON{
ContainerJSONBase: &types.ContainerJSONBase{}, ContainerJSONBase: &types.ContainerJSONBase{
State: &types.ContainerState{
Running: true,
},
},
NetworkSettings: &types.NetworkSettings{ NetworkSettings: &types.NetworkSettings{
Networks: map[string]*network.EndpointSettings{ Networks: map[string]*network.EndpointSettings{
"other-network": { "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 ( import (
"context" "context"
@@ -19,7 +19,7 @@ const (
) )
// Controller monitors container changes in the cluster store and generates a configuration file for Caddy reverse // 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. // network.
type Controller struct { type Controller struct {
store *store.Store store *store.Store
-5
View File
@@ -36,16 +36,11 @@ func NewDB(path string) (*sqlx.DB, error) {
schema := ` schema := `
CREATE TABLE IF NOT EXISTS containers ( CREATE TABLE IF NOT EXISTS containers (
id TEXT NOT NULL PRIMARY KEY, 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)), service_spec TEXT NOT NULL CHECK (json_valid(service_spec)),
-- 'subsecond' modifier is used to store timestamps with millisecond precision. -- 'subsecond' modifier is used to store timestamps with millisecond precision.
created_at TIMESTAMP NOT NULL DEFAULT (datetime('subsecond')), created_at TIMESTAMP NOT NULL DEFAULT (datetime('subsecond')),
updated_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 { 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"
"github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/network" "github.com/docker/docker/api/types/network"
"github.com/docker/docker/api/types/volume"
"github.com/docker/docker/errdefs" "github.com/docker/docker/errdefs"
"github.com/docker/docker/pkg/jsonmessage" "github.com/docker/docker/pkg/jsonmessage"
regtypes "github.com/google/go-containerregistry/pkg/v1/types" regtypes "github.com/google/go-containerregistry/pkg/v1/types"
@@ -341,6 +342,75 @@ func parseRemoteImageMessage(msg *pb.RemoteImage) (api.MachineRemoteImage, error
return mri, nil 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. // CreateServiceContainer creates a new container for the service with the given specifications.
func (c *Client) CreateServiceContainer( func (c *Client) CreateServiceContainer(
ctx context.Context, serviceID string, spec api.ServiceSpec, containerName string, ctx context.Context, serviceID string, spec api.ServiceSpec, containerName string,
+3 -3
View File
@@ -4,19 +4,19 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"log/slog"
"time"
dockercontainer "github.com/docker/docker/api/types/container" dockercontainer "github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/events" "github.com/docker/docker/api/types/events"
"github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/filters"
"github.com/docker/docker/client" "github.com/docker/docker/client"
"github.com/psviderski/uncloud/internal/machine/store" "github.com/psviderski/uncloud/internal/machine/store"
"github.com/psviderski/uncloud/pkg/api" "github.com/psviderski/uncloud/pkg/api"
"log/slog"
"time"
) )
const ( const (
NetworkName = "uncloud" NetworkName = "uncloud"
UserChain = "DOCKER-USER"
// EventsDebounceInterval defines how long to wait before processing the next Docker event. Multiple events // 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. // occurring within this window will be processed together as a single event to prevent system overload.
EventsDebounceInterval = 100 * time.Millisecond EventsDebounceInterval = 100 * time.Millisecond
+3 -3
View File
@@ -8,7 +8,7 @@ import (
"net/netip" "net/netip"
) )
// EnsureUncloudNetwork is a stub for darwin. // EnsureUncloudNetwork is a stub for Darwin.
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 {
return fmt.Errorf("not supported on darwin") return fmt.Errorf("not supported on Darwin")
} }
+39 -9
View File
@@ -3,24 +3,28 @@ package docker
import ( import (
"context" "context"
"fmt" "fmt"
"log/slog"
"net/netip"
"strconv"
dnetwork "github.com/docker/docker/api/types/network" dnetwork "github.com/docker/docker/api/types/network"
"github.com/docker/docker/client" "github.com/docker/docker/client"
"github.com/docker/docker/libnetwork/iptables" "github.com/docker/docker/libnetwork/iptables"
"log/slog" "github.com/psviderski/uncloud/internal/machine/dns"
"net/netip" "github.com/psviderski/uncloud/internal/machine/firewall"
"github.com/psviderski/uncloud/internal/machine/network" "github.com/psviderski/uncloud/internal/machine/network"
) )
// EnsureUncloudNetwork creates the Docker bridge network NetworkName with the provided machine subnet // 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. // 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. // 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. // Ensure the Docker network 'uncloud' is created with the correct subnet.
needsCreation := false needsCreation := false
nw, err := m.client.NetworkInspect(ctx, NetworkName, dnetwork.InspectOptions{}) nw, err := m.client.NetworkInspect(ctx, NetworkName, dnetwork.InspectOptions{})
if err != nil { if err != nil {
if !client.IsErrNotFound(err) { 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 needsCreation = true
} else if nw.IPAM.Config[0].Subnet != subnet.String() { } else if nw.IPAM.Config[0].Subnet != subnet.String() {
@@ -31,7 +35,7 @@ func (m *Manager) EnsureUncloudNetwork(ctx context.Context, subnet netip.Prefix)
) )
if err = m.client.NetworkRemove(ctx, NetworkName); err != nil { 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. // 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 needsCreation = true
} }
@@ -50,12 +54,12 @@ func (m *Manager) EnsureUncloudNetwork(ctx context.Context, subnet netip.Prefix)
}, },
}, },
); err != nil { ); 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()) slog.Info("Docker network created.", "name", NetworkName, "subnet", subnet.String())
if nw, err = m.client.NetworkInspect(ctx, NetworkName, dnetwork.InspectOptions{}); err != nil { 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 +71,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: // 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 // https://github.com/moby/moby/blob/v27.2.1/libnetwork/drivers/bridge/bridge_linux.go#L664
bridgeName := "br-" + nw.ID[:12] 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) ipt := iptables.GetIptable(iptables.IPv4)
rule := []string{"--in-interface", network.WireGuardInterfaceName, "--out-interface", bridgeName, "-j", "ACCEPT"} // Allow traffic from other machines and their containers through the WG mesh to the Uncloud containers
if err = ipt.ProgramRule(iptables.Filter, UserChain, iptables.Insert, rule); err != nil { // 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) 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 return nil
+218 -6
View File
@@ -8,7 +8,9 @@ import (
"fmt" "fmt"
"io" "io"
"log/slog" "log/slog"
"net/netip"
"regexp" "regexp"
"slices"
"strconv" "strconv"
"strings" "strings"
@@ -17,7 +19,9 @@ import (
"github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/image" "github.com/docker/docker/api/types/image"
"github.com/docker/docker/api/types/mount"
"github.com/docker/docker/api/types/network" "github.com/docker/docker/api/types/network"
"github.com/docker/docker/api/types/volume"
"github.com/docker/docker/client" "github.com/docker/docker/client"
"github.com/docker/go-connections/nat" "github.com/docker/go-connections/nat"
"github.com/google/go-containerregistry/pkg/authn" "github.com/google/go-containerregistry/pkg/authn"
@@ -27,6 +31,7 @@ import (
"github.com/opencontainers/go-digest" "github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1" ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/psviderski/uncloud/internal/machine/api/pb" "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/internal/secret"
"github.com/psviderski/uncloud/pkg/api" "github.com/psviderski/uncloud/pkg/api"
"google.golang.org/grpc" "google.golang.org/grpc"
@@ -42,13 +47,17 @@ type Server struct {
pb.UnimplementedDockerServer pb.UnimplementedDockerServer
client *client.Client client *client.Client
db *sqlx.DB 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. // 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{ return &Server{
client: cli, client: cli,
db: db, db: db,
internalDNSIP: internalDNSIP,
} }
} }
@@ -329,6 +338,80 @@ func (s *Server) InspectRemoteImage(
}, nil }, 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)
}
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. // CreateServiceContainer creates a new container for the service with the given specifications.
func (s *Server) CreateServiceContainer( func (s *Server) CreateServiceContainer(
ctx context.Context, req *pb.CreateServiceContainerRequest, ctx context.Context, req *pb.CreateServiceContainerRequest,
@@ -357,6 +440,7 @@ func (s *Server) CreateServiceContainer(
config := &container.Config{ config := &container.Config{
Cmd: spec.Container.Command, Cmd: spec.Container.Command,
Env: spec.Container.Env.ToSlice(),
Entrypoint: spec.Container.Entrypoint, Entrypoint: spec.Container.Entrypoint,
Hostname: containerName, Hostname: containerName,
Image: spec.Container.Image, Image: spec.Container.Image,
@@ -366,6 +450,7 @@ func (s *Server) CreateServiceContainer(
api.LabelServiceMode: spec.Mode, api.LabelServiceMode: spec.Mode,
api.LabelManaged: "", api.LabelManaged: "",
}, },
User: spec.Container.User,
} }
if spec.Mode == "" { if spec.Mode == "" {
config.Labels[api.LabelServiceMode] = api.ServiceModeReplicated config.Labels[api.LabelServiceMode] = api.ServiceModeReplicated
@@ -385,6 +470,14 @@ func (s *Server) CreateServiceContainer(
config.Labels[api.LabelServicePorts] = strings.Join(encodedPorts, ",") 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) portBindings := make(nat.PortMap)
for _, p := range spec.Ports { for _, p := range spec.Ports {
if p.Mode != api.PortModeHost { if p.Mode != api.PortModeHost {
@@ -403,13 +496,38 @@ func (s *Server) CreateServiceContainer(
hostConfig := &container.HostConfig{ hostConfig := &container.HostConfig{
Binds: spec.Container.Volumes, Binds: spec.Container.Volumes,
Init: spec.Container.Init, Init: spec.Container.Init,
Mounts: mounts,
PortBindings: portBindings, 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. // 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. // For one-off containers and batch jobs we plan to use a different service type/mode.
RestartPolicy: container.RestartPolicy{ RestartPolicy: container.RestartPolicy{
Name: container.RestartPolicyAlways, 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{ networkConfig := &network.NetworkingConfig{
EndpointsConfig: map[string]*network.EndpointSettings{ EndpointsConfig: map[string]*network.EndpointSettings{
NetworkName: {}, NetworkName: {},
@@ -440,9 +558,8 @@ func (s *Server) CreateServiceContainer(
return nil, status.Errorf(codes.Internal, "marshal service spec: %v", err) return nil, status.Errorf(codes.Internal, "marshal service spec: %v", err)
} }
if _, err = s.db.ExecContext(ctx, if _, err = s.db.ExecContext(ctx, `INSERT INTO containers (id, service_spec) VALUES ($1, $2)`,
`INSERT INTO containers (id, service_id, service_spec) VALUES ($1, $2, $3)`, resp.ID, string(specBytes)); err != nil {
resp.ID, req.ServiceId, string(specBytes)); err != nil {
removeContainer() removeContainer()
return nil, status.Errorf(codes.Internal, "store container in database: %v", err) return nil, status.Errorf(codes.Internal, "store container in database: %v", err)
} }
@@ -450,6 +567,101 @@ func (s *Server) CreateServiceContainer(
return &pb.CreateContainerResponse{Response: respBytes}, nil 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 // InspectServiceContainer returns the container information and service specification that was used to create the
// container with the given ID. // container with the given ID.
func (s *Server) InspectServiceContainer( 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
}
+62 -6
View File
@@ -21,9 +21,10 @@ import (
"github.com/psviderski/uncloud/internal/fs" "github.com/psviderski/uncloud/internal/fs"
"github.com/psviderski/uncloud/internal/machine/api/pb" "github.com/psviderski/uncloud/internal/machine/api/pb"
apiproxy "github.com/psviderski/uncloud/internal/machine/api/proxy" 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/cluster"
"github.com/psviderski/uncloud/internal/machine/corroservice" "github.com/psviderski/uncloud/internal/machine/corroservice"
"github.com/psviderski/uncloud/internal/machine/dns"
machinedocker "github.com/psviderski/uncloud/internal/machine/docker" machinedocker "github.com/psviderski/uncloud/internal/machine/docker"
"github.com/psviderski/uncloud/internal/machine/network" "github.com/psviderski/uncloud/internal/machine/network"
"github.com/psviderski/uncloud/internal/machine/store" "github.com/psviderski/uncloud/internal/machine/store"
@@ -62,6 +63,8 @@ type Config struct {
// CaddyConfigPath specifies where the machine generates the Caddy reverse proxy configuration file for routing // 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. // external traffic to service containers across the internal network. Default is DataDir/caddy/caddy.json.
CaddyConfigPath string 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. // 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) 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()) dockerCli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
if err != nil { if err != nil {
return nil, fmt.Errorf("create Docker client: %w", err) return nil, fmt.Errorf("create Docker client: %w", err)
@@ -217,7 +220,6 @@ func NewMachine(config *Config) (*Machine, error) {
if err != nil { if err != nil {
return nil, fmt.Errorf("init machine database: %w", err) 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. // Init a local gRPC proxy server that proxies requests to the local or remote machine API servers.
proxyDirector := apiproxy.NewDirector(config.MachineSockPath, APIPort) proxyDirector := apiproxy.NewDirector(config.MachineSockPath, APIPort)
@@ -235,11 +237,16 @@ func NewMachine(config *Config) (*Machine, error) {
initialised: make(chan struct{}, 1), initialised: make(chan struct{}, 1),
store: corroStore, store: corroStore,
cluster: c, cluster: c,
docker: dockerServer,
localProxyServer: localProxyServer, localProxyServer: localProxyServer,
proxyDirector: proxyDirector, 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() { if m.Initialised() {
m.initialised <- struct{}{} m.initialised <- struct{}{}
@@ -270,6 +277,15 @@ func (m *Machine) Initialised() bool {
return m.state.ID != "" 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 { func (m *Machine) Run(ctx context.Context) error {
// Docker dependency is essential for the machine to function. Block until it's ready. // Docker dependency is essential for the machine to function. Block until it's ready.
if err := docker.WaitDaemonReady(ctx, m.config.DockerClient); err != nil { 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. // 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. // 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 { if err != nil {
return fmt.Errorf("create Caddyfile controller: %w", err) 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( ctrl, err = newNetworkController(
m.state, m.state,
m.store, m.store,
@@ -380,6 +402,8 @@ func (m *Machine) Run(ctx context.Context) error {
m.config.CorrosionService, m.config.CorrosionService,
m.config.DockerClient, m.config.DockerClient,
caddyfileCtrl, caddyfileCtrl,
dnsServer,
dnsResolver,
) )
if err != nil { if err != nil {
return fmt.Errorf("initialise network controller: %w", err) return fmt.Errorf("initialise network controller: %w", err)
@@ -520,6 +544,38 @@ func (m *Machine) configureCorrosion() error {
return nil 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. // 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) { func (m *Machine) InitCluster(ctx context.Context, req *pb.InitClusterRequest) (*pb.InitClusterResponse, error) {
if m.Initialised() { if m.Initialised() {
+87 -69
View File
@@ -4,22 +4,25 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"github.com/cenkalti/backoff/v4"
"github.com/docker/docker/client"
"golang.org/x/sync/errgroup"
"google.golang.org/grpc"
"log/slog" "log/slog"
"net" "net"
"net/netip" "net/netip"
"slices" "slices"
"strconv" "strconv"
"time" "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/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/corroservice"
"github.com/psviderski/uncloud/internal/machine/dns"
"github.com/psviderski/uncloud/internal/machine/docker" "github.com/psviderski/uncloud/internal/machine/docker"
"github.com/psviderski/uncloud/internal/machine/firewall"
"github.com/psviderski/uncloud/internal/machine/network" "github.com/psviderski/uncloud/internal/machine/network"
"github.com/psviderski/uncloud/internal/machine/store" "github.com/psviderski/uncloud/internal/machine/store"
"golang.org/x/sync/errgroup"
"google.golang.org/grpc"
) )
const ( const (
@@ -36,10 +39,11 @@ type networkController struct {
server *grpc.Server server *grpc.Server
corroService corroservice.Service corroService corroservice.Service
dockerCli *client.Client 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 // dnsServer is the embedded internal DNS server for the cluster listening on the machine IP.
// like resolved does because it needs to be reachable from both the host and the containers. dnsServer *dns.Server
dnsResolver *dns.ClusterResolver
} }
func newNetworkController( func newNetworkController(
@@ -48,7 +52,9 @@ func newNetworkController(
server *grpc.Server, server *grpc.Server,
corroService corroservice.Service, corroService corroservice.Service,
dockerCli *client.Client, dockerCli *client.Client,
caddyfileCtrl *caddyfile.Controller, caddyfileCtrl *caddyconfig.Controller,
dnsServer *dns.Server,
dnsResolver *dns.ClusterResolver,
) ( ) (
*networkController, error, *networkController, error,
) { ) {
@@ -68,10 +74,16 @@ func newNetworkController(
corroService: corroService, corroService: corroService,
dockerCli: dockerCli, dockerCli: dockerCli,
caddyfileCtrl: caddyfileCtrl, caddyfileCtrl: caddyfileCtrl,
dnsServer: dnsServer,
dnsResolver: dnsResolver,
}, nil }, nil
} }
func (nc *networkController) Run(ctx context.Context) error { 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 { if err := nc.wgnet.Configure(*nc.state.Network); err != nil {
return fmt.Errorf("configure WireGuard network: %w", err) return fmt.Errorf("configure WireGuard network: %w", err)
} }
@@ -110,65 +122,73 @@ 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. // Setup Docker network and synchronise containers to the cluster store.
errGroup.Go( errGroup.Go(func() error {
func() error { return nc.prepareAndWatchDocker(ctx)
return nc.prepareAndWatchDocker(ctx) })
},
)
// Handle machine changes in the cluster. Handling machine and endpoint changes should be done // 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. // in separate goroutines to avoid a deadlock when reconfiguring the network.
errGroup.Go( errGroup.Go(func() error {
func() error { if err := nc.handleMachineChanges(ctx); err != nil {
if err := nc.handleMachineChanges(ctx); err != nil { return fmt.Errorf("handle new machines: %w", err)
return fmt.Errorf("handle new machines: %w", err) }
} return nil
return nil })
},
)
// Watch for endpoint changes and update the machine state accordingly. // Watch for endpoint changes and update the machine state accordingly.
errGroup.Go( errGroup.Go(func() error {
func() error { for {
for { select {
select { case e, ok := <-nc.endpointChanges:
case e, ok := <-nc.endpointChanges: if !ok {
if !ok { // The channel was closed, stop watching for changes.
// The channel was closed, stop watching for changes. nc.endpointChanges = nil
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():
return nil return nil
} }
}
},
)
errGroup.Go( nc.state.mu.Lock()
func() error { for i := range nc.state.Network.Peers {
if err := nc.wgnet.Run(ctx); err != nil { if nc.state.Network.Peers[i].PublicKey.Equal(e.PublicKey) {
return fmt.Errorf("WireGuard network failed: %w", err) 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 {
if err := nc.wgnet.Run(ctx); err != nil {
return fmt.Errorf("WireGuard network failed: %w", err)
}
return nil
})
errGroup.Go(func() error { errGroup.Go(func() error {
slog.Info("Starting Caddyfile controller.") slog.Info("Starting Caddyfile controller.")
@@ -180,16 +200,14 @@ func (nc *networkController) Run(ctx context.Context) error {
}) })
// Wait for the context to be done and stop the network API server. // Wait for the context to be done and stop the network API server.
errGroup.Go( errGroup.Go(func() error {
func() error { <-ctx.Done()
<-ctx.Done() slog.Info("Stopping network API server.")
slog.Info("Stopping network API server.") // TODO: implement timeout for graceful shutdown.
// TODO: implement timeout for graceful shutdown. nc.server.GracefulStop()
nc.server.GracefulStop() slog.Info("Network API server stopped.")
slog.Info("Network API server stopped.") return nil
return nil })
},
)
return errGroup.Wait() return errGroup.Wait()
} }
@@ -202,7 +220,7 @@ func (nc *networkController) prepareAndWatchDocker(ctx context.Context) error {
return fmt.Errorf("wait for Docker daemon: %w", err) 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) return fmt.Errorf("ensure Docker network: %w", err)
} }
slog.Info("Docker network configured.") slog.Info("Docker network configured.")
+5 -4
View File
@@ -2,10 +2,11 @@ package network
import ( import (
"fmt" "fmt"
"github.com/psviderski/uncloud/internal/secret"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
"net" "net"
"net/netip" "net/netip"
"github.com/psviderski/uncloud/internal/secret"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
) )
type Config struct { type Config struct {
@@ -13,7 +14,7 @@ type Config struct {
// in the subnet. Other IP addresses are allocated to containers running on the machine. // in the subnet. Other IP addresses are allocated to containers running on the machine.
Subnet netip.Prefix Subnet netip.Prefix
// ManagementIP is the IPv6 address assigned to the machine within the WireGuard network. This address is used // 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 ManagementIP netip.Addr
PrivateKey secret.Secret PrivateKey secret.Secret
PublicKey secret.Secret PublicKey secret.Secret
@@ -23,7 +24,7 @@ type Config struct {
type PeerConfig struct { type PeerConfig struct {
Subnet *netip.Prefix `json:",omitempty"` Subnet *netip.Prefix `json:",omitempty"`
// ManagementIP is the IPv6 address assigned to the peer within the WireGuard network. This address is used // 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 ManagementIP netip.Addr
Endpoint *netip.AddrPort `json:",omitempty"` Endpoint *netip.AddrPort `json:",omitempty"`
AllEndpoints []netip.AddrPort `json:",omitempty"` AllEndpoints []netip.AddrPort `json:",omitempty"`
+2 -1
View File
@@ -2,9 +2,10 @@ package network
import ( import (
"fmt" "fmt"
"github.com/psviderski/uncloud/internal/secret"
"net" "net"
"net/netip" "net/netip"
"github.com/psviderski/uncloud/internal/secret"
) )
// MachineIP returns the IP address of the machine which is the first address in the subnet. // 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" "strconv"
"time" "time"
"github.com/psviderski/uncloud/internal/fs"
"golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh"
"golang.org/x/crypto/ssh/agent" "golang.org/x/crypto/ssh/agent"
) )
@@ -63,6 +64,7 @@ func sshAgentAuth() (ssh.AuthMethod, func(), error) {
} }
func privateKeyAuth(path string) (ssh.AuthMethod, error) { func privateKeyAuth(path string) (ssh.AuthMethod, error) {
path = fs.ExpandHomeDir(path)
key, err := os.ReadFile(path) key, err := os.ReadFile(path)
if err != nil { if err != nil {
return nil, fmt.Errorf("read private key file %q: %w", path, err) return nil, fmt.Errorf("read private key file %q: %w", path, err)
+51 -11
View File
@@ -4,17 +4,19 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"net/netip"
"time"
"github.com/cenkalti/backoff/v4" "github.com/cenkalti/backoff/v4"
"github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/network" "github.com/docker/docker/api/types/network"
dockerclient "github.com/docker/docker/client" dockerclient "github.com/docker/docker/client"
"google.golang.org/protobuf/types/known/emptypb" "github.com/docker/go-connections/nat"
"net/netip"
"time"
"github.com/psviderski/uncloud/internal/machine" "github.com/psviderski/uncloud/internal/machine"
"github.com/psviderski/uncloud/internal/machine/api/pb" "github.com/psviderski/uncloud/internal/machine/api/pb"
"github.com/psviderski/uncloud/internal/machine/cluster" "github.com/psviderski/uncloud/internal/machine/cluster"
"google.golang.org/protobuf/types/known/emptypb"
) )
const ( const (
@@ -29,6 +31,31 @@ type Cluster struct {
type CreateClusterOptions struct { type CreateClusterOptions struct {
Machines int 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) { 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: "", 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 { if _, err = p.dockerCli.NetworkCreate(ctx, name, netOpts); err != nil {
return c, fmt.Errorf("create Docker network '%s': %w", name, err) 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. // Create machines (containers) in the created cluster network.
for i := 1; i < opts.Machines+1; i++ { for i := 1; i < opts.Machines+1; i++ {
mopts := CreateMachineOptions{ mopts := CreateMachineOptions{
Name: fmt.Sprintf("machine-%d", i), Name: fmt.Sprintf("machine-%d", i),
PortMap: opts.PortMap,
} }
m, err := p.CreateMachine(ctx, name, mopts) m, err := p.CreateMachine(ctx, name, mopts)
if err != nil { 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 { if err = p.initCluster(ctx, c.Machines); err != nil {
return c, err return c, err
} }
if err = c.PopulateMachineIDs(ctx); err != nil {
return c, fmt.Errorf("populate machine IDs: %w", err)
}
if p.configUpdater != nil { if p.configUpdater != nil {
if err = p.configUpdater.AddCluster(c); err != nil { if err = p.configUpdater.AddCluster(c); err != nil {
return c, fmt.Errorf("add cluster to Uncloud config: %w", err) 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 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. // Init a new cluster on the first machine.
initMachine := machines[0] 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) 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. // Join the rest of the machines to the cluster.
for _, m := range machines[1:] { 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) 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) 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 return c, nil
} }
// WaitClusterReady waits for all machines in the cluster to be ready and UP. // 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 { func (p *Provisioner) WaitClusterReady(ctx context.Context, c Cluster, timeout time.Duration) error {
firstMachine := c.Machines[0] 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) 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) ), ctx)
checkMachinesUp := func() error { checkMachinesUp := func() error {
machines, err := cli.ListMachines(ctx) machines, err := cli.ListMachines(ctx, nil)
if err != nil { if err != nil {
return fmt.Errorf("list machines: %w", err) 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) return fmt.Errorf("list Docker containers with cluster name '%s': %w", name, err)
} }
for _, c := range containers { 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) return fmt.Errorf("remove Docker container '%s': %w", c.ID, err)
} }
} }
+13 -12
View File
@@ -2,6 +2,7 @@ package ucind
import ( import (
"fmt" "fmt"
"github.com/psviderski/uncloud/internal/cli/config" "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) return fmt.Errorf("read Uncloud config: %w", err)
} }
if _, ok := cfg.Clusters[c.Name]; ok { if _, ok := cfg.Contexts[c.Name]; ok {
return fmt.Errorf("cluster '%s' already exists", c.Name) return fmt.Errorf("cluster context '%s' already exists", c.Name)
} }
clusterCfg := &config.Cluster{ clusterCfg := &config.Context{
Name: c.Name, Name: c.Name,
Connections: make([]config.MachineConnection, len(c.Machines)), Connections: make([]config.MachineConnection, len(c.Machines)),
} }
for i, m := range c.Machines { for i, m := range c.Machines {
clusterCfg.Connections[i] = config.MachineConnection{ clusterCfg.Connections[i] = config.MachineConnection{
TCP: m.APIAddress, TCP: &m.APIAddress,
} }
} }
cfg.Clusters[c.Name] = clusterCfg cfg.Contexts[c.Name] = clusterCfg
cfg.CurrentCluster = c.Name cfg.CurrentContext = c.Name
if err = cfg.Save(); err != nil { if err = cfg.Save(); err != nil {
return fmt.Errorf("save config: %w", err) 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) return fmt.Errorf("read Uncloud config: %w", err)
} }
if _, ok := cfg.Clusters[name]; !ok { if _, ok := cfg.Contexts[name]; !ok {
return nil return nil
} }
delete(cfg.Clusters, name) delete(cfg.Contexts, name)
if cfg.CurrentCluster == name { if cfg.CurrentContext == name {
cfg.CurrentCluster = "" cfg.CurrentContext = ""
} }
if _, ok := cfg.Clusters["default"]; ok { if _, ok := cfg.Contexts["default"]; ok {
cfg.CurrentCluster = "default" cfg.CurrentContext = "default"
} }
if err = cfg.Save(); err != nil { if err = cfg.Save(); err != nil {
+21 -10
View File
@@ -4,6 +4,11 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"io"
"net"
"net/netip"
"time"
"github.com/cenkalti/backoff/v4" "github.com/cenkalti/backoff/v4"
"github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/image" "github.com/docker/docker/api/types/image"
@@ -13,10 +18,6 @@ import (
"github.com/psviderski/uncloud/pkg/client" "github.com/psviderski/uncloud/pkg/client"
"github.com/psviderski/uncloud/pkg/client/connector" "github.com/psviderski/uncloud/pkg/client/connector"
"google.golang.org/protobuf/types/known/emptypb" "google.golang.org/protobuf/types/known/emptypb"
"io"
"net"
"net/netip"
"time"
) )
const ( const (
@@ -29,6 +30,7 @@ const (
type Machine struct { type Machine struct {
ClusterName string ClusterName string
ContainerName string ContainerName string
ID string
Name string Name string
APIAddress netip.AddrPort APIAddress netip.AddrPort
} }
@@ -40,6 +42,8 @@ func (m *Machine) Connect(ctx context.Context) (*client.Client, error) {
type CreateMachineOptions struct { type CreateMachineOptions struct {
Name string Name string
Image 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) { 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, 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 { if _, err := p.createContainerWithImagePull(ctx, containerName, config, hostConfig); err != nil {
return m, err return m, err
@@ -158,10 +172,7 @@ func (p *Provisioner) waitPortPublished(ctx context.Context, containerID string,
} }
binding, ok := c.NetworkSettings.Ports[port] binding, ok := c.NetworkSettings.Ports[port]
if !ok { if ok && len(binding) > 0 {
return nil, fmt.Errorf("port '%s' not published", port)
}
if len(binding) > 0 {
return binding, nil return binding, nil
} }
@@ -185,7 +196,7 @@ func randomMachineName() (string, error) {
} }
// WaitMachineReady waits for the machine API to respond. // 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) cli, err := m.Connect(ctx)
if err != nil { if err != nil {
return fmt.Errorf("connect to machine over TCP '%s': %w", m.APIAddress, err) 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( boff := backoff.WithContext(backoff.NewExponentialBackOff(
backoff.WithInitialInterval(100*time.Millisecond), backoff.WithInitialInterval(100*time.Millisecond),
backoff.WithMaxInterval(1*time.Second), backoff.WithMaxInterval(10*time.Second),
backoff.WithMaxElapsedTime(timeout), backoff.WithMaxElapsedTime(timeout),
), ctx) ), ctx)
+7
View File
@@ -0,0 +1,7 @@
package version
var version string
func String() string {
return version
}
+20 -6
View File
@@ -6,6 +6,7 @@ import (
"slices" "slices"
"github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/volume"
"github.com/psviderski/uncloud/internal/machine/api/pb" "github.com/psviderski/uncloud/internal/machine/api/pb"
"google.golang.org/grpc/metadata" "google.golang.org/grpc/metadata"
) )
@@ -16,6 +17,7 @@ type Client interface {
ImageClient ImageClient
MachineClient MachineClient
ServiceClient ServiceClient
VolumeClient
} }
type ContainerClient interface { type ContainerClient interface {
@@ -39,29 +41,41 @@ type ImageClient interface {
type MachineClient interface { type MachineClient interface {
InspectMachine(ctx context.Context, id string) (*pb.MachineMember, error) InspectMachine(ctx context.Context, id string) (*pb.MachineMember, error)
ListMachines(ctx context.Context) ([]*pb.MachineMember, error) ListMachines(ctx context.Context, filter *MachineFilter) (MachineMembersList, error)
} }
type ServiceClient interface { type ServiceClient interface {
RunService(ctx context.Context, spec ServiceSpec) (RunServiceResponse, error)
InspectService(ctx context.Context, id string) (Service, error) InspectService(ctx context.Context, id string) (Service, error)
RemoveService(ctx context.Context, id string) error
}
type VolumeClient interface {
CreateVolume(ctx context.Context, machineNameOrID string, opts volume.CreateOptions) (MachineVolume, error)
ListVolumes(ctx context.Context, filter *VolumeFilter) ([]MachineVolume, error)
RemoveVolume(ctx context.Context, machineNameOrID, volumeName string, force bool) error
} }
// ProxyMachinesContext returns a new context that proxies gRPC requests to the specified machines. // ProxyMachinesContext returns a new context that proxies gRPC requests to the specified machines.
// If namesOrIDs is nil, all machines are included. // If namesOrIDs is nil, all machines are included.
func ProxyMachinesContext(ctx context.Context, cli MachineClient, namesOrIDs []string) (context.Context, error) { func ProxyMachinesContext(
machines, err := cli.ListMachines(ctx) ctx context.Context, cli MachineClient, namesOrIDs []string,
) (context.Context, MachineMembersList, error) {
machines, err := cli.ListMachines(ctx, nil)
if err != nil { if err != nil {
return nil, fmt.Errorf("list machines: %w", err) return nil, nil, fmt.Errorf("list machines: %w", err)
} }
var proxiedMachines MachineMembersList
md := metadata.New(nil) md := metadata.New(nil)
for _, m := range machines { for _, m := range machines {
if namesOrIDs == nil || if len(namesOrIDs) == 0 ||
slices.Contains(namesOrIDs, m.Machine.Name) || slices.Contains(namesOrIDs, m.Machine.Id) { slices.Contains(namesOrIDs, m.Machine.Name) || slices.Contains(namesOrIDs, m.Machine.Id) {
proxiedMachines = append(proxiedMachines, m)
machineIP, _ := m.Machine.Network.ManagementIp.ToAddr() machineIP, _ := m.Machine.Network.ManagementIp.ToAddr()
md.Append("machines", machineIP.String()) md.Append("machines", machineIP.String())
} }
} }
return metadata.NewOutgoingContext(ctx, md), nil return metadata.NewOutgoingContext(ctx, md), proxiedMachines, nil
} }
+20 -5
View File
@@ -3,6 +3,7 @@ package api
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"net/netip"
"strings" "strings"
"time" "time"
@@ -11,6 +12,9 @@ import (
) )
const ( const (
// DockerNetworkName is the name of the Docker network used by uncloud. Keep the value in sync with NetworkName
// in internal/machine/docker/manager.go.
DockerNetworkName = "uncloud"
LabelManaged = "uncloud.managed" LabelManaged = "uncloud.managed"
LabelServiceID = "uncloud.service.id" LabelServiceID = "uncloud.service.id"
LabelServiceName = "uncloud.service.name" LabelServiceName = "uncloud.service.name"
@@ -90,6 +94,22 @@ func (c *Container) HumanState() (string, error) {
c.State.ExitCode, units.HumanDuration(time.Now().UTC().Sub(finishedAt))), nil c.State.ExitCode, units.HumanDuration(time.Now().UTC().Sub(finishedAt))), nil
} }
// UncloudNetworkIP returns the IP address of the container in the uncloud Docker network.
func (c *Container) UncloudNetworkIP() netip.Addr {
network, ok := c.NetworkSettings.Networks[DockerNetworkName]
if !ok {
// Container is not connected to the uncloud Docker network (could be host network).
return netip.Addr{}
}
ip, err := netip.ParseAddr(network.IPAddress)
if err != nil {
return netip.Addr{}
}
return ip
}
func (c *Container) UnmarshalJSON(data []byte) error { func (c *Container) UnmarshalJSON(data []byte) error {
// A temporary type that's identical to Container but doesn't have the UnmarshalJSON method. // A temporary type that's identical to Container but doesn't have the UnmarshalJSON method.
type ContainerAlias Container type ContainerAlias Container
@@ -110,11 +130,6 @@ type ServiceContainer struct {
ServiceSpec ServiceSpec ServiceSpec ServiceSpec
} }
type MachineContainer struct {
MachineID string
Container Container
}
// ServiceID returns the ID of the service this container belongs to. // ServiceID returns the ID of the service this container belongs to.
func (c *ServiceContainer) ServiceID() string { func (c *ServiceContainer) ServiceID() string {
return c.Config.Labels[LabelServiceID] return c.Config.Labels[LabelServiceID]
+38
View File
@@ -0,0 +1,38 @@
package api
import "github.com/psviderski/uncloud/internal/machine/api/pb"
// MachineFilter defines criteria to filter machines in ListMachines.
type MachineFilter struct {
// Available filters machines that are not DOWN.
Available bool
// NamesOrIDs filters machines by their names or IDs.
NamesOrIDs []string
}
type MachineMembersList []*pb.MachineMember
func (m MachineMembersList) FindByManagementIP(ip string) *pb.MachineMember {
for _, machine := range m {
addr, err := machine.Machine.Network.ManagementIp.ToAddr()
if err != nil {
continue
}
if addr.String() == ip {
return machine
}
}
return nil
}
func (m MachineMembersList) FindByNameOrID(nameOrID string) *pb.MachineMember {
for _, machine := range m {
if machine.Machine.Id == nameOrID || machine.Machine.Name == nameOrID {
return machine
}
}
return nil
}
+8
View File
@@ -0,0 +1,8 @@
package api
// Placement defines the placement constraints for service containers.
type Placement struct {
// Machines is a list of machine names or IDs where service containers are allowed to be deployed.
// If empty, containers can be deployed to any available machine in the cluster.
Machines []string `json:",omitempty"`
}

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