Compare commits

...
253 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
Pavel Sviderski 0e80f2e5b2 chore: replace immutable hash with container spec comparison 2025-03-31 15:58:02 +10:00
Pavel Sviderski 64f6a4f3d3 chore: use original service spec instead of deriving from container, fixes diff for scale 2025-03-30 12:54:36 +10:00
Pavel Sviderski 0975cbab66 chore: handle RemoveContainer server-side that deletes container from machine db 2025-03-29 20:06:49 +10:00
Pavel Sviderski 615022c9ac feat(scale): set file permissions for machine.db database to 600 (may container secrets as env vars) 2025-03-29 15:19:37 +10:00
Pavel Sviderski ecbd4378e4 feat(scale): store container spec in local machine.db sqlite 2025-03-29 15:14:43 +10:00
Pavel Sviderski 1becd033e6 chore: rename CaddyConfigPath field 2025-03-29 11:39:45 +10:00
Pavel Sviderski 1cba1eef0a refactor: handle CreateContainer server-side 2025-03-28 19:01:19 +10:00
Pavel Sviderski 3893005866 fix(compose): remove ServiceSpec validation for 0 replicas, it's handled when resolving defaults 2025-03-27 20:40:11 +10:00
Pavel Sviderski 1911e03c82 feat(compose): handle image pull policy 2025-03-27 15:15:57 +10:00
Pavel Sviderski 641b11a92d feat: implement --pull policy for run command: always, missing, never 2025-03-27 15:06:32 +10:00
Pavel Sviderski 7c7530f977 chore: add comments and TODOs about the disabled image digest resolver 2025-03-27 14:12:20 +10:00
Pavel Sviderski 6e7fe405ef feat(compose): partially implement image resolver (pinner) but disable for now 2025-03-26 20:42:41 +10:00
Pavel Sviderski 03c5c054e1 feat(compose): client methods for inspecting images 2025-03-26 20:38:41 +10:00
Pavel Sviderski c42968c812 feat(compose): add InspectRemoteImage api endpoint to inspect images in registry 2025-03-26 20:37:53 +10:00
Pavel Sviderski ffef41081b feat(compose): add ContainerSpec.PullPolicy field to define when to pull image from registry 2025-03-25 11:03:19 +10:00
Pavel Sviderski 58b51b974c refactor: postpone spec resolver initialisation when creating Deployment 2025-03-24 21:56:30 +10:00
Pavel Sviderski 2d923380a3 chore: compare container ports to spec in assertContainerMatchesSpec 2025-03-24 20:24:40 +10:00
Pavel Sviderski 3910101a88 refactor: embed ServiceSpecResolver into Deployment to handle spec only there 2025-03-24 20:10:28 +10:00
Pavel Sviderski 7a9d0cf9db refactor: move compose and deploy packages under client 2025-03-24 14:41:50 +10:00
Pavel Sviderski 042dd594e0 refactor: decouple deploy and compose packages from client dependency 2025-03-24 14:38:16 +10:00
Pavel Sviderski 1471a94162 refactor(compose): move compose deployment from client to compose pkg 2025-03-24 11:11:36 +10:00
Pavel Sviderski b1abd07dde refactor: move api, client, compose packages to pkg 2025-03-22 18:43:00 +10:00
Pavel Sviderski 3757813b20 refactor: rename main module to github.com/psviderski/uncloud 2025-03-22 18:25:08 +10:00
Pavel Sviderski f1a150821c fix(compose): client.ImageInspect to handle broadcasted responses to multiple machines 2025-03-22 08:44:17 +10:00
Pavel Sviderski 874af1ba10 feat(compose): implement InspectImage rpc with broadcast support 2025-03-21 19:43:58 +10:00
Pavel Sviderski 2ad098c612 feat(deploy): introduce proto for InspectImage and InspectRemoteImage methods 2025-03-20 21:36:41 +10:00
Pavel Sviderski 4172cb826f refactor: compose deployment plan to not use PrepareDeploymentSpec 2025-03-19 16:54:00 +10:00
Pavel Sviderski 041d51b74f refactor: compose deployment with pluggable spec resolver, basic test 2025-03-19 16:28:17 +10:00
Pavel Sviderski fb01604d21 fix: recreate containers only if spec changed by comparing spec hash 2025-03-19 13:11:04 +10:00
Pavel SviderskiandGitHub bd8bcbd2f9 Merge pull request #40 from cedws/userguide 2025-03-19 09:24:50 +10:00
Connor Edwards 07501813c8 docs: s/uncloud/uc 2025-03-18 21:04:43 +09:00
Pavel Sviderski 1bbcfa8519 feat(entrypoint): allow to override service ENTRYPOINT for run command 2025-03-18 16:44:15 +10:00
Pavel Sviderski e8715fce38 fix: list containers if some of them concurrently removed, fixes #33 2025-03-18 15:39:15 +10:00
Pavel Sviderski a0a66f34c4 feat(connect): add --connect flag to connect to remote cluster machine without using config 2025-03-18 14:01:38 +10:00
Pavel Sviderski b799b7518e feat(compose): add TODO to address deployment idempotency 2025-03-17 20:32:14 +10:00
Connor Edwards 2571498ed2 docs: add basic user guide 2025-03-16 14:51:42 +09:00
Pavel Sviderski 5bcbfc332d feat(compose): deploy compose services without waiting dependencies 2025-03-15 20:45:02 +10:00
Pavel Sviderski 34574e41fc feat(compose): plan compose deployment 2025-03-15 20:43:30 +10:00
Pavel Sviderski c7355afa3d refactor: move Confirm logic to common place from caddy deployment 2025-03-15 20:42:44 +10:00
Pavel Sviderski a907fdb1af chore: update todo 2025-03-15 20:41:57 +10:00
Pavel Sviderski c9ba4adccb chore: add todo for ServiceSpec that it may return a spec not equal to original 2025-03-15 20:41:02 +10:00
Pavel Sviderski a0551d2d64 fix: print service endpoints for run service only when non-empty 2025-03-15 19:35:21 +10:00
Pavel Sviderski 76129ea056 refactor: include service name in deployment plan 2025-03-15 19:34:37 +10:00
Pavel Sviderski 6ffd35f596 feat(compose): parse Compose project and transform ports extension 2025-03-15 14:18:06 +10:00
Pavel Sviderski 0ef776e7f1 chore: add link to uncloud-dns repo in README 2025-03-14 22:01:19 +10:00
Pavel Sviderski 0b22b2136a chore: add deployments and managed DNS features to README 2025-03-14 21:59:40 +10:00
Pavel Sviderski e1530bb6f3 feat(deploy): load compose project 2025-03-14 21:41:06 +10:00
Pavel Sviderski 7fc07ce4bd feat(deploy): init a new deploy command to deploy services from Compose file 2025-03-12 20:37:01 +10:00
Pavel Sviderski eb42082955 fix(dns): update DNS records when reserving a new cluster domain, fixes #36 2025-03-11 21:12:00 +10:00
Pavel Sviderski 8719e37856 fix(dns): retry HTTP requests to verify Caddy reachability for up to 5 seconds 2025-03-10 21:26:02 +10:00
Pavel Sviderski e1cc39b0d5 lint: variable collides with imported package name 2025-03-09 19:26:06 +10:00
Pavel Sviderski efe646f54c chore: validate unsupported tcp/udp protocols for ingress port 2025-03-08 19:07:51 +10:00
Pavel Sviderski e130d803e4 fix(website): demo min height to not shrink before video is loaded 2025-03-06 17:30:16 +10:00
Pavel Sviderski 56e7e8baa2 chore(readme): cleanup coming soon features 2025-03-06 17:13:29 +10:00
Pavel Sviderski b963ef39d1 chore(makefile): demo-reset target to reset machines before recording a VHS demo 2025-03-06 16:55:16 +10:00
Pavel Sviderski 1df0fa290f chore(readme): add uc machine ls command 2025-03-06 16:54:35 +10:00
Pavel Sviderski c3bb0c9f49 chore(readme): add Quick demo section with video, update how it works outputs 2025-03-06 16:49:58 +10:00
Pavel Sviderski cf65cecd9e chore: replace terminal example with a demo video on landing 2025-03-06 15:49:06 +10:00
Pavel Sviderski fff17dbb60 add VHS tape script for the terminal demo 2025-03-05 22:17:54 +10:00
Pavel Sviderski 586a4fa129 feat(scale): basic service scale command 2025-03-05 20:24:54 +10:00
Pavel Sviderski 2a82f75cad fix: deduplicate discovered machine IPs when adding a new machine 2025-03-05 09:14:11 +10:00
Pavel Sviderski 805afd0d28 fix(dns): update cluster domain DNS records when adding a new machine to cluster 2025-03-04 20:49:41 +10:00
Pavel Sviderski 11098ea7d1 fix: url for uninstall.sh script in install script 2025-03-04 12:54:48 +10:00
Pavel Sviderski 90e357a1dc feat: install uncloud-uninstall script when provisioning machine 2025-03-04 12:50:29 +10:00
Pavel Sviderski 91d58bc701 chore: add uninstall.sh script to remove uncloud and managed containers and network from machine 2025-03-04 12:14:49 +10:00
Pavel Sviderski 69d3a511d6 feat(dns): reserve cluster domain when initialising cluster by default + update dns records 2025-03-03 19:12:21 +10:00
Pavel Sviderski c3123820b2 fix: install script tar flag 2025-03-03 18:51:41 +10:00
Pavel Sviderski 2e49d94fad fix: deduplicate discovered machine IPs 2025-03-03 18:42:00 +10:00
Pavel Sviderski bb51e01c5c fix: install script unpack uncloudd binary to tmp dir 2025-03-03 18:38:51 +10:00
Pavel Sviderski cdb2884e4e fix: check for dockerd command to determine if docker engine is installed 2025-03-03 18:05:52 +10:00
Pavel Sviderski 137abde808 feat: set container hostname equal to container name 2025-03-03 16:58:42 +10:00
Pavel Sviderski 342969b7f4 fix(caddy): remove debug prints 2025-03-03 15:05:57 +10:00
Pavel Sviderski d9e350c4dd fix(caddy): config generation for multiple https containers 2025-03-03 15:04:04 +10:00
Pavel Sviderski 3f4f263c05 test: increase test cluster wait timeout 5->30 seconds 2025-03-03 12:21:22 +10:00
Pavel Sviderski 0760ba3b18 test: replicated deployment with machine filter 2025-03-03 11:26:08 +10:00
Pavel Sviderski 0896bd27fb test: basic replicated deployment, prioritise host with the most up-to-date containers 2025-03-02 10:23:49 +10:00
Pavel Sviderski a16b2d1c74 feat: service ls command include ID column if there are multiple services with same name 2025-03-01 22:31:58 +10:00
Pavel Sviderski 52117954a8 fix: specs equals ignores replicas 2025-03-01 22:31:18 +10:00
Pavel Sviderski 435a794fa5 fix: e2e tests 2025-02-28 18:00:02 +10:00
Pavel Sviderski 9642863d7b feat(replicas): support --replicas and --machines for run command 2025-02-28 17:53:58 +10:00
Pavel Sviderski 6bb9bdeb5a feat(replicas): implement rolling strategy for replicated service with basic spread strategy 2025-02-28 17:51:38 +10:00
Pavel Sviderski 30f1e135f1 feat: print ENDPOINTS column when listing services 2025-02-27 20:37:02 +10:00
Pavel Sviderski 58ecc17ac1 feat: print service endpoints on service creation 2025-02-27 20:37:02 +10:00
Anton Ovchinnikov 4c239ed79a fix(smm): Update site description 2025-02-27 00:08:03 +01:00
Anton Ovchinnikov 968dd50705 fix(smm): Use PNG logo, add Twitter tags 2025-02-26 23:55:26 +01:00
Pavel Sviderski d7f437107d fix(dns): error message for ingress port without hostname and no cluster domain 2025-02-26 21:13:18 +10:00
Pavel Sviderski 72548fbd1b feat(dns): allow to specify http(s) ports without hostname to default to cluster domain 2025-02-26 21:10:55 +10:00
Pavel Sviderski a5f6ec4a68 feat(dns): expose service as service-name.cluster-domain if cluster domain reserved 2025-02-26 21:10:21 +10:00
Pavel Sviderski 4cc68e3133 include cloud vm in dev set in Makefile 2025-02-26 15:52:33 +10:00
Pavel Sviderski 2bacebe900 refactor(dns): remove leading dot from domain when interecting with Uncloud DNS 2025-02-26 15:45:28 +10:00
Pavel Sviderski 09bea1d12d feat(dns): update ingress records in Uncloud DNS when deploying Caddy 2025-02-26 14:23:00 +10:00
Pavel Sviderski 5e9b883ad5 fix: increase wait time for machine to 5 min, discard unknown when decoding json to proto 2025-02-26 12:19:04 +10:00
Pavel Sviderski 1046396edc feat(dns): configure and store public IP when adding new machine 2025-02-26 11:16:13 +10:00
Pavel SviderskiandGitHub 35804458b0 Merge pull request #28 from psviderski/tonyo/27-feat-social-tags
feat(smm): Add OpenGraph tags
2025-02-26 10:10:49 +10:00
Anton Ovchinnikov 6bb64ac5dc feat(smm): Add OpenGraph tags 2025-02-26 00:35:33 +01:00
Pavel Sviderski e5f68947f9 feat(dns): missed CLI changes 2025-02-25 21:22:32 +10:00
Pavel Sviderski 289d964b2a feat(dns): configure and store public IP when initialising machine 2025-02-25 21:21:37 +10:00
Pavel Sviderski d0fd9db258 feat(dns): serve machine IP at /.uncloud-verify to verify Caddy reachability 2025-02-25 17:36:44 +10:00
Pavel Sviderski 0ea997e826 feat(dns): update caddy deploy command to create ingress records (stubs for now) 2025-02-24 20:33:54 +10:00
Pavel Sviderski 2d84059d7f feat(dns): client for creating ingress records 2025-02-24 20:33:08 +10:00
Pavel Sviderski ecec42ea60 feat(dns): cluster API for creating DNS records for cluster domain 2025-02-24 20:32:30 +10:00
Pavel Sviderski af4629b679 feat(dns): add CLI commands to reserve, show, delete cluster domain 2025-02-24 17:37:43 +10:00
Pavel Sviderski f6cf5cc250 feat(dns): cluster API for reserving/releasing cluster domain in Uncloud DNS 2025-02-24 17:35:19 +10:00
Pavel Sviderski f37bfd01fe feat: init cluster API for updating domain records in Uncloud DNS 2025-02-22 21:20:16 +10:00
Pavel Sviderski d9897e5d1e feat: set restart policy for all services to always 2025-02-19 16:53:55 +10:00
Pavel Sviderski bcb67a4073 refactor: e2e tests for container config validation 2025-02-19 15:19:00 +10:00
Pavel Sviderski bcb5a2a173 feat: deploy caddy service when adding new machine to cluster 2025-02-18 21:35:36 +10:00
Pavel Sviderski c3b88077ac feat: deploy caddy service when initialising new cluster on remote machine 2025-02-18 18:38:38 +10:00
Pavel Sviderski 98e1bdc9b5 fix: fail global deployment if no matchine machines found, improve caddy deploy output in particular 2025-02-17 20:31:37 +10:00
Pavel Sviderski e13408d79f feat: allow to filter machines for global service deployments 2025-02-17 20:26:40 +10:00
Pavel Sviderski 8610322682 fix: panic when pulling non-existent image 2025-02-17 16:12:37 +10:00
Pavel Sviderski 0be42f1f59 feat: print deployment plan for caddy deployment 2025-02-17 14:29:56 +10:00
Pavel Sviderski dcbfcbd0fb fix: show user-friendly image name when inspecting service 2025-02-16 21:12:56 +10:00
Pavel Sviderski d0e46186ed feat: look up latest caddy version tag when deploying caddy service 2025-02-16 21:10:06 +10:00
Pavel Sviderski 625a8baed3 feat: implement 'caddy deploy' command to deploy global service using caddy:latest image 2025-02-15 10:51:02 +10:00
Pavel Sviderski 98db5ed440 feat: caddy deploy command and stub implementation 2025-02-15 08:52:14 +10:00
193 changed files with 37357 additions and 3464 deletions
+1
View File
@@ -4,6 +4,7 @@
# Allow files and directories.
!cmd/
!internal/
!pkg/
!scripts/docker/
!go.*
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
+7 -2
View File
@@ -27,8 +27,13 @@ go.work.sum
.env
.idea/
data
dist/
/data/
/dist/
/uc
/tmp
# Web
node_modules/
# VS Code
.vscode
+4
View File
@@ -24,6 +24,8 @@ builds:
goarch:
- amd64
- arm64
ldflags:
- -s -w -X github.com/psviderski/uncloud/internal/version.version={{ .Version }}
- id: uncloudd
main: ./cmd/uncloudd
@@ -35,6 +37,8 @@ builds:
goarch:
- amd64
- arm64
ldflags:
- -s -w -X github.com/psviderski/uncloud/internal/version.version={{ .Version }}
archives:
- id: uncloud
+12 -3
View File
@@ -17,13 +17,22 @@ RUN GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -o uncloudd cmd/uncloudd/main
FROM alpine:${ALPINE_VERSION} AS corrosion-download
RUN wget -q -O /tmp/corrosion.tar.gz \
https://github.com/psviderski/corrosion/releases/latest/download/corrosion-aarch64-unknown-linux-gnu.tar.gz \
ARG TARGETARCH
RUN CORROSION_ARCH=$(case "${TARGETARCH}" in \
"amd64") echo "x86_64" ;; \
"arm64") echo "aarch64" ;; \
*) echo "Architecture '${TARGETARCH}' not supported" >&2 && exit 1 ;; \
esac) \
&& wget -q -O /tmp/corrosion.tar.gz \
"https://github.com/psviderski/corrosion/releases/latest/download/corrosion-${CORROSION_ARCH}-unknown-linux-gnu.tar.gz" \
&& tar -xzf /tmp/corrosion.tar.gz -C /tmp \
&& install /tmp/corrosion /usr/local/bin/corrosion \
&& rm /tmp/corrosion.tar.gz /tmp/corrosion
FROM chainguard/wolfi-base:latest AS corrosion
# Beware that more modern images like chainguard/wolfi-base build glibc with flags that require newer CPU features,
# e.g. x86-64-v2. So such image may fail with "Fatal glibc error: CPU does not support x86-64-v2" on older CPUs.
FROM gcr.io/distroless/cc-debian12:latest AS corrosion
COPY --from=corrosion-download /usr/local/bin/corrosion /usr/local/bin/corrosion
CMD ["corrosion", "agent"]
+62 -6
View File
@@ -1,5 +1,6 @@
CORROSION_IMAGE ?= ghcr.io/psviderski/corrosion:latest
UCIND_IMAGE ?= ghcr.io/psviderski/ucind:latest
DOCS_IMAGE ?= ghcr.io/psviderski/uncloud-docs:latest
update-dev:
GOOS=linux GOARCH=amd64 go build -o uncloudd-linux-amd64 ./cmd/uncloudd && \
@@ -8,6 +9,10 @@ update-dev:
scp uncloudd-linux-amd64 spy@192.168.40.176:~/ && \
ssh spy@192.168.40.176 sudo install ./uncloudd-linux-amd64 /usr/local/bin/uncloudd && \
rm uncloudd-linux-amd64
# GOOS=linux GOARCH=arm64 go build -o uncloudd-linux-arm64 ./cmd/uncloudd && \
# scp uncloudd-linux-arm64 ubuntu@152.67.101.197:~/ && \
# ssh ubuntu@152.67.101.197 sudo install ./uncloudd-linux-arm64 /usr/local/bin/uncloudd && \
# rm uncloudd-linux-arm64
update-restart-dev:
GOOS=linux GOARCH=amd64 go build -o uncloudd-linux-amd64 ./cmd/uncloudd && \
@@ -16,12 +21,26 @@ update-restart-dev:
scp uncloudd-linux-amd64 spy@192.168.40.176:~/ && \
ssh spy@192.168.40.176 "sudo install ./uncloudd-linux-amd64 /usr/local/bin/uncloudd && sudo systemctl restart uncloud" && \
rm uncloudd-linux-amd64
# GOOS=linux GOARCH=arm64 go build -o uncloudd-linux-arm64 ./cmd/uncloudd && \
# scp uncloudd-linux-arm64 ubuntu@152.67.101.197:~/ && \
# ssh ubuntu@152.67.101.197 "sudo install ./uncloudd-linux-arm64 /usr/local/bin/uncloudd && sudo systemctl restart uncloud" && \
# rm uncloudd-linux-arm64
reset-dev:
ssh spy@192.168.40.243 "sudo systemctl stop uncloud && sudo rm -rf /var/lib/uncloud"
ssh spy@192.168.40.176 "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:
rm -fv ~/.config/uncloud/config.yaml
ssh ubuntu@152.67.101.197 "AUTO_CONFIRM=true sudo -E uncloud-uninstall && docker rmi caddy:2.9.1"
ssh root@5.223.45.199 "AUTO_CONFIRM=true sudo -E uncloud-uninstall"
ssh spy@192.168.40.243 "AUTO_CONFIRM=true sudo -E uncloud-uninstall"
.PHONY: ucind-cluster
ucind-cluster:
go run ./cmd/ucind cluster rm && go run ./cmd/ucind cluster create -m 3
.PHONY: proto
proto:
protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative \
@@ -31,14 +50,51 @@ proto:
corrosion-image:
docker build -t "$(CORROSION_IMAGE)" --target corrosion .
.PHONY: corrosion-image-push
corrosion-image-push: corrosion-image
docker push "$(CORROSION_IMAGE)"
.PHONY: corrosion-multiarch-image-push
corrosion-multiarch-image-push:
docker buildx build --push --platform linux/amd64,linux/arm64 -t "$(CORROSION_IMAGE)" --target corrosion .
.PHONY: ucind-image
ucind-image:
docker build -t "$(UCIND_IMAGE)" --target ucind .
.PHONY: ucind-image-push
ucind-image-push: ucind-image
docker push "$(UCIND_IMAGE)"
.PHONY: ucind-multiarch-image-push
ucind-multiarch-image-push:
docker buildx build --push --platform linux/amd64,linux/arm64 -t "$(UCIND_IMAGE)" --target ucind .
.PHONY: test
test:
ifeq ($(TEST_NAME),)
go test -count=1 -v ./...
else
go test -count=1 -v -run ^$(TEST_NAME)$$ ./...
endif
.PHONY: test-clean
test-clean:
@CONTAINERS=$$(docker ps --filter "name=ucind-test" -q); \
if [ -n "$$CONTAINERS" ]; then \
echo "Killing containers..."; \
docker kill $$CONTAINERS; \
fi; \
CONTAINERS_STOPPED=$$(docker ps -a --filter "name=ucind-test" -q); \
if [ -n "$$CONTAINERS_STOPPED" ]; then \
echo "Removing stopped containers..."; \
docker rm $$CONTAINERS_STOPPED; \
else \
echo "Nothing to clean."; \
fi
.PHONY: vet
vet:
go vet ./...
.PHONY: docs-image-push
docs-image:
docker buildx build --push --platform linux/amd64,linux/arm64 -t "$(DOCS_IMAGE)" ./docs
.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
+187 -51
View File
@@ -2,47 +2,64 @@
<img src="./website/images/logo.svg" height="100" alt="Uncloud logo"/>
<h1>Uncloud</h1>
<p><strong>Docker simplicity. Multi-machine power.</strong></p>
<p>
<a href="https://docs.uncloud.run"><img src="https://img.shields.io/badge/Docs-blue.svg?style=for-the-badge&logo=gitbook&logoColor=white" alt="Documentation"></a>
<a href="https://discord.gg/eR35KQJhPu"><img src="https://img.shields.io/badge/discord-5865F2.svg?style=for-the-badge&logo=discord&logoColor=white" alt="Join Discord"></a>
<a href="https://x.com/psviderski"><img src="https://img.shields.io/badge/follow-black?style=for-the-badge&logo=X&logoColor=while" alt="Follow on X"></a>
</p>
</div>
Uncloud is a lightweight clustering and container orchestration tool that lets you deploy and manage web apps across
cloud VMs and bare metal with minimized cluster management overhead. It creates a secure WireGuard mesh network between
cloud VMs and bare metal with minimised cluster management overhead. It creates a secure WireGuard mesh network between
your Docker hosts and provides automatic service discovery, load balancing, ingress with HTTPS, and simple CLI commands
to manage your apps.
Unlike traditional orchestrators, there's no central control plane and quorum to maintain. Each machine maintains a
synchronized copy of the cluster state through peer-to-peer communication, keeping cluster operations functional even if
synchronised copy of the cluster state through peer-to-peer communication, keeping cluster operations functional even if
some machines go offline.
Uncloud aims to be the solution for developers who want the flexibility of self-hosted infrastructure without the
operational complexity of Kubernetes.
Uncloud is the solution for developers who want the flexibility of self-hosted infrastructure without the operational
complexity of Kubernetes.
## ✨ Features
* **Deploy anywhere**: Combine cloud VMs, dedicated servers, and bare metal into a unified computing environment —
regardless of location or provider.
* **Docker Compose**: Familiar [Docker Compose](https://compose-spec.io/) format for defining services and volumes. No
need to learn a new bespoke DSL.
* **Zero-downtime deployments**: Rolling updates without service interruption. Automatic rollback on failure is coming
soon.
* **Service discovery**: Built-in DNS server resolves service names to container IPs.
* **Persistent storage**: Run stateful services with Docker volumes managed across machines.
* **Zero-config private network**: Automatic WireGuard mesh with peer discovery and NAT traversal. Containers get unique
IPs for direct cross-machine communication.
* **No control plane**: Fully decentralized design eliminates single points of failure and reduces operational overhead.
* **No control plane**: Fully decentralised design eliminates single points of failure and reduces operational overhead.
* **Imperative over declarative**: Favoring imperative operations over state reconciliation simplifies both the mental
model and troubleshooting.
* **Service discovery**: Built-in DNS server resolves service names to container IPs.
* **Managed DNS**: Automatic DNS records `*.<id>.cluster.uncloud.run` for services with public access via managed
[Uncloud DNS](https://github.com/psviderski/uncloud-dns) service.
* **Automatic HTTPS**: Built-in Caddy reverse proxy handles TLS certificate provisioning and renewal using Let's
Encrypt.
* **Docker-like CLI**: Familiar commands for managing both infrastructure and applications.
* **Remote management**: Control your entire infrastructure through SSH access to any single machine in the cluster.
Coming soon:
## 🎬 Quick demo
* Project isolation through environments/namespaces
* Infrastructure as Code using Docker Compose format
* Persistent volumes and secrets management
* Monitoring and log aggregation
* Database deployment and management
* Curated application catalog
The screenshot below demonstrates how I use Uncloud to deploy the [Uncloud Documentation](https://docs.uncloud.run)
website to 2 remote machines (why not?) from the [`compose.yaml`](docs/compose.yaml) file on my local machine.
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?
@@ -52,60 +69,111 @@ believe there's a sweet spot in between — a pragmatic solution for the majorit
scale. You should be able to:
* **Own your infrastructure and data**: Whether driven by costs, compliance, or flexibility, run applications on any
combination of cloud VMs and personal hardware while maintaining the cloud-like experience you love.
* **Stay simple**: Don't worry about control planes, highly-available clusters, or complex YAML configurations for
common use cases.
combination of cloud VMs and personal hardware while controlling your data and maintaining the cloud-like experience
you love.
* **Stay simple as you grow**: Start with a single machine and add more whenever you need without changing your
workflow. No worrying about highly-available control planes or complex YAML configurations.
* **Build with proven primitives**: Get production-grade networking, deployment primitives, service discovery, load
balancing, and ingress with HTTPS out of the box without becoming a distributed systems expert.
* **Support sustainable computing** 🌿: Minimize system overhead to maximize resources available for your applications.
* **Support sustainable computing** 🌿: Minimise system overhead to maximise resources available for your applications.
Uncloud's goal is to make deployment and management of containerized applications feel as seamless as using a cloud
Uncloud's goal is to make deployment and management of containerised applications feel as seamless as using a cloud
platform, whether you're running on a $5 VPS, a spare Mac mini, or a rack of bare metal servers.
## 🚀 Quick start
1. Install Uncloud CLI:
```bash
brew install psviderski/tap/uncloud
```bash
brew install psviderski/tap/uncloud
# or using curl (macOS/Linux)
curl -fsS https://get.uncloud.run/install.sh | sh
```
# or using curl (macOS/Linux)
curl -fsS https://get.uncloud.run/install.sh | sh
```
2. Initialize your first machine:
2. Initialise your first machine:
```bash
uc machine init root@your-server-ip
```
```bash
uc machine init root@your-server-ip
```
3. Create a DNS A record in your DNS provider (Cloudflare, Namecheap, etc.) that points `app.example.com` to your
3. Deploy your app from a Docker image and publish its container port 8000 as HTTPS using `app.example.com` domain:
```bash
uc run -p app.example.com:8000/https image/my-app
```
4. Create a DNS A record in your DNS provider (Cloudflare, Namecheap, etc.) that points `app.example.com` to your
server's IP address. Allow a few minutes for DNS propagation.
4. Deploy your app from a Docker image:
```bash
uc run -p app.example.com:8000/https my-app-image
```
That's it! Your app is now running and accessible at https://app.example.com ✨
That's it! Your app is now running and accessible at https://app.example.com ✨
5. Clean up when you're done:
```bash
uc ls
# Copy the service ID from the output and remove it:
uc rm my-app-id
```
```bash
uc ls
# Copy the service name from the output and run the rm command:
uc rm my-app-name
```
If you want to fully uninstall Uncloud on a machine, run:
```bash
uncloud-uninstall
```
View the [Documentation](https://docs.uncloud.run) for more information.
## ⚙️ How it works
Check out the [design document](docs/design.md) to understand Uncloud's design philosophy and goals. Here, let's peek
under the hood to see what happens when you run certain commands.
Check out the [design document](docs/design.md) to understand Uncloud's design philosophy and goals.
**When you initialize a new cluster on a machine:**
Here is a diagram of an Uncloud multi-provider cluster of 3 machines:
![Diagram: multi-provider cluster of 3 machines](website/images/diagram.webp)
<details>
<summary>Peek under the hood to see what happens when you run certain commands.</summary>
**When you initialise a new cluster on a machine:**
```bash
uc machine init root@your-server-ip
$ uc machine init --name oracle-vm ubuntu@152.67.101.197
Downloading Uncloud install script: https://raw.githubusercontent.com/psviderski/uncloud/refs/heads/main/scripts/install.sh
⏳ Running Uncloud install script...
✓ Docker is already installed.
⏳ Installing Docker...
...
✓ Docker installed successfully.
✓ Linux user and group 'uncloud' created.
✓ Linux user 'ubuntu' added to group 'uncloud'.
⏳ Installing Uncloud binaries...
⏳ Downloading uncloudd binary: https://github.com/psviderski/uncloud/releases/latest/download/uncloudd_linux_arm64.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-aarch64-unknown-linux-gnu.tar.gz
✓ uncloud-corrosion binary installed: /usr/local/bin/uncloud-corrosion
✓ Systemd unit file created: /etc/systemd/system/uncloud-corrosion.service
⏳ Starting Uncloud machine daemon (uncloud.service)...
✓ Uncloud machine daemon started.
✓ Uncloud installed on the machine successfully! 🎉
Cluster "default" initialised with machine "oracle-vm"
Waiting for the machine to be ready...
Reserved cluster domain: xuw3xd.cluster.uncloud.run
[+] Deploying service caddy 1/1
✔ Container caddy-c47x on oracle-vm Started 0.9s
Updating cluster domain records in Uncloud DNS to point to machines running caddy service...
[+] Verifying internet access to caddy service 1/1
✔ Machine oracle-vm (152.67.101.197) Reachable 0.1s
DNS records updated to use only the internet-reachable machines running caddy service:
*.xuw3xd.cluster.uncloud.run A → 152.67.101.197
```
1. The CLI SSHs into the machine and installs Docker, the `uncloudd` machine daemon and
@@ -121,7 +189,42 @@ uc machine init root@your-server-ip
**When you add another machine:**
```bash
uc machine add ubuntu@second-server-ip
$ uc machine add --name hetzner-server root@5.223.45.199
Downloading Uncloud install script: https://raw.githubusercontent.com/psviderski/uncloud/refs/heads/main/scripts/install.sh
⏳ Running Uncloud install script...
✓ Docker is already installed.
✓ 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! 🎉
Machine "hetzner-server" added to cluster
Waiting for the machine to be ready...
[+] Deploying service caddy 1/1
✔ Container caddy-d36c on hetzner-server Started 1.0s
Updating cluster domain records in Uncloud DNS to point to machines running caddy service...
[+] Verifying internet access to caddy service 2/2
✔ Machine hetzner-server (5.223.45.199) Reachable 0.2s
✔ Machine oracle-vm (152.67.101.197) Reachable 0.1s
DNS records updated to use only the internet-reachable machines running caddy service:
*.xuw3xd.cluster.uncloud.run A → 152.67.101.197, 5.223.45.199
$ uc machine ls
NAME STATE ADDRESS PUBLIC IP WIREGUARD ENDPOINTS
oracle-vm Up 10.210.0.1/24 152.67.101.197 10.0.0.95:51820, 152.67.101.197:51820
hetzner-server Up 10.210.1.1/24 5.223.45.199 5.223.45.199:51820, [2a01:4ff:2f0:128b::1]:51820
```
1. The second machine gets provisioned just like the first. A non-root SSH user will need `sudo` access.
@@ -140,7 +243,14 @@ and automatically establish a WireGuard tunnel with it.
**When you run a service:**
```bash
uc run -p app.example.com:8000/https my-app-image
$ uc run -p app.example.com:8000/https image/my-app
[+] Running service my-app-1b3b (replicated mode) 1/1
✔ Container my-app-1b3b-tcex on oracle-vm Started
my-app-1b3b endpoints:
• https://app.example.com → :8000
• https://my-app-1b3b.xuw3xd.cluster.uncloud.run → :8000
```
1. CLI picks a machine to run your container.
@@ -158,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
using the CLI or directly over SSH. They all have the complete cluster state and can control everything. It's like each
machine is a full backup of your control plane.
</details>
## 🏗 Project status
Uncloud is currently in active development and is **not ready for production use**. Features may change significantly
and there may be breaking changes between releases.
I'd love your input! Here's how you can contribute:
We'd love your input! Here's how you can contribute:
* 🐛 Found a bug? [Open an issue](https://github.com/psviderski/uncloud/issues)
* 💡 Have ideas? [Join the discussion](https://github.com/psviderski/uncloud/discussions)
* 💡 Have 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
* [Subscribe](https://uncloud.run/#subscribe) to my newsletter to follow the progress, get early insights into new
* Join our [Discord server](https://discord.gg/eR35KQJhPu) for real-time discussions, support, and updates.
* Follow [@psviderski](https://x.com/psviderski) on X/Twitter.
* Subscribe to [my newsletter](https://uncloud.run/#subscribe) to follow the progress, get early insights into new
features, and be the first to know when it's ready for production use.
* Watch this repository for releases.
* Follow [@psviderski](https://github.com/psviderski) on GitHub.
## ❤️ Contributors
+1 -1
View File
@@ -3,7 +3,7 @@ package cluster
import (
"fmt"
"github.com/spf13/cobra"
"uncloud/internal/ucind"
"github.com/psviderski/uncloud/internal/ucind"
)
func NewCreateCommand() *cobra.Command {
+1 -1
View File
@@ -3,7 +3,7 @@ package cluster
import (
"fmt"
"github.com/spf13/cobra"
"uncloud/internal/ucind"
"github.com/psviderski/uncloud/internal/ucind"
)
func NewRemoveCommand() *cobra.Command {
+7 -6
View File
@@ -3,12 +3,13 @@ package main
import (
"context"
"fmt"
"github.com/docker/docker/client"
"github.com/spf13/cobra"
"os"
"strings"
"uncloud/cmd/ucind/cluster"
"uncloud/internal/ucind"
"github.com/docker/docker/client"
"github.com/psviderski/uncloud/cmd/ucind/cluster"
"github.com/psviderski/uncloud/internal/ucind"
"github.com/spf13/cobra"
)
func main() {
@@ -44,9 +45,9 @@ func main() {
}
// TODO: allow to override using UNCLOUD_CONFIG env var.
cmd.PersistentFlags().StringVar(&configPath, "uncloud-config", "~/.config/uncloud/config.toml",
cmd.PersistentFlags().StringVar(&configPath, "uncloud-config", "~/.config/uncloud/config.yaml",
"path to the Uncloud configuration file.")
_ = cmd.MarkPersistentFlagFilename("uncloud-config", "toml")
_ = cmd.MarkPersistentFlagFilename("uncloud-config", "yaml", "yml")
cmd.AddCommand(
cluster.NewRootCommand(),
+77
View File
@@ -0,0 +1,77 @@
package main
import (
"context"
"fmt"
composecli "github.com/compose-spec/compose-go/v2/cli"
"github.com/psviderski/uncloud/internal/cli"
"github.com/psviderski/uncloud/pkg/client/compose"
"github.com/spf13/cobra"
)
// NewBuildCommand creates a new command to build services from a Compose file.
func NewBuildCommand() *cobra.Command {
opts := cli.BuildOptions{}
cmd := &cobra.Command{
Use: "build [FLAGS] [SERVICE...]",
Short: "Build services from a Compose file.",
RunE: func(cmd *cobra.Command, args []string) error {
uncli := cmd.Context().Value("cli").(*cli.CLI)
if len(args) > 0 {
opts.Services = args
}
return runBuild(cmd.Context(), uncli, opts)
},
}
cmd.Flags().StringSliceVarP(&opts.Files, "file", "f", nil,
"One or more Compose files to build (default compose.yaml)")
cmd.Flags().StringSliceVarP(&opts.Profiles, "profile", "p", nil,
"One or more Compose profiles to enable.")
cmd.Flags().BoolVarP(&opts.Push, "push", "P", false,
"Push built images to the registry after building. (default false)")
cmd.Flags().BoolVarP(&opts.NoCache, "no-cache", "n", false,
"Do not use cache when building images. (default false)")
return cmd
}
// TODO: deduplicate with a similar functino for deploy options
// projectOpts returns the project options for the Compose file(s).
func projectOptsFromBuildOpts(opts cli.BuildOptions) []composecli.ProjectOptionsFn {
projectOpts := []composecli.ProjectOptionsFn{}
if len(opts.Profiles) > 0 {
projectOpts = append(projectOpts, composecli.WithDefaultProfiles(opts.Profiles...))
}
return projectOpts
}
// runBuild parses the Compose file(s), builds the services, and pushes them if requested.
func runBuild(ctx context.Context, uncli *cli.CLI, opts cli.BuildOptions) error {
projectOpts := projectOptsFromBuildOpts(opts)
project, err := compose.LoadProject(ctx, opts.Files, projectOpts...)
if err != nil {
return fmt.Errorf("load compose file(s): %w", err)
}
if len(opts.Services) > 0 {
project, err = project.WithSelectedServices(opts.Services)
if err != nil {
return fmt.Errorf("select services: %w", err)
}
}
servicesToBuild := cli.GetServicesThatNeedBuild(project)
if len(servicesToBuild) == 0 {
fmt.Println("No services to build.")
return nil
}
return cli.BuildServices(ctx, servicesToBuild, opts)
}
+200
View File
@@ -0,0 +1,200 @@
package caddy
import (
"context"
"errors"
"fmt"
"maps"
"slices"
"strings"
"github.com/docker/cli/cli/streams"
"github.com/docker/compose/v2/pkg/progress"
"github.com/psviderski/uncloud/internal/cli"
"github.com/psviderski/uncloud/internal/machine/api/pb"
"github.com/psviderski/uncloud/pkg/api"
"github.com/psviderski/uncloud/pkg/client"
"github.com/spf13/cobra"
)
type deployOptions struct {
image string
machines []string
context string
}
func NewDeployCommand() *cobra.Command {
opts := deployOptions{}
cmd := &cobra.Command{
Use: "deploy",
Short: "Deploy or upgrade Caddy reverse proxy across all machines in the cluster.",
Long: "Deploy or upgrade Caddy reverse proxy across all machines in the cluster.\n" +
"A rolling update is performed when updating existing containers to minimise disruption.",
RunE: func(cmd *cobra.Command, args []string) error {
uncli := cmd.Context().Value("cli").(*cli.CLI)
return runDeploy(cmd.Context(), uncli, opts)
},
}
cmd.Flags().StringVar(&opts.image, "image", "",
"Caddy Docker image to deploy. (default caddy:LATEST_VERSION)")
cmd.Flags().StringSliceVarP(&opts.machines, "machine", "m", nil,
"Machine names to deploy to. Can be specified multiple times or as a comma-separated "+
"list of machine names. (default is all machines)")
cmd.Flags().StringVarP(
&opts.context, "context", "c", "",
"Name of the cluster context to deploy to. (default is the current context)",
)
return cmd
}
func runDeploy(ctx context.Context, uncli *cli.CLI, opts deployOptions) error {
clusterClient, err := uncli.ConnectCluster(ctx, opts.context)
if err != nil {
return fmt.Errorf("connect to cluster: %w", err)
}
defer clusterClient.Close()
svc, err := clusterClient.InspectService(ctx, client.CaddyServiceName)
if err != nil {
if !errors.Is(err, api.ErrNotFound) {
return fmt.Errorf("inspect caddy service: %w", err)
}
fmt.Printf("Service: %s (not running)\n", client.CaddyServiceName)
} else {
fmt.Printf("Service: %s (%s mode)\n", svc.Name, svc.Mode)
// Collect unique images of all containers in the running caddy service.
images := make(map[string]struct{}, len(svc.Containers))
for _, c := range svc.Containers {
images[c.Container.Config.Image] = struct{}{}
}
currentImages := slices.Collect(maps.Keys(images))
if len(currentImages) > 1 {
commaSeparatedImages := strings.Join(currentImages, ", ")
fmt.Printf("Current images (multiple versions detected): %s\n", commaSeparatedImages)
} else {
fmt.Printf("Current image: %s\n", currentImages[0])
}
}
if opts.image != "" {
fmt.Printf("Target image: %s\n", opts.image)
}
fmt.Println()
fmt.Println("Preparing a deployment plan...")
placement := api.Placement{
Machines: cli.ExpandCommaSeparatedValues(opts.machines),
}
d, err := clusterClient.NewCaddyDeployment(opts.image, placement)
if err != nil {
return fmt.Errorf("create caddy deployment: %w", err)
}
if opts.image == "" {
fmt.Printf("Target image: %s (latest stable)\n", d.Spec.Container.Image)
}
plan, err := d.Plan(ctx)
if err != nil {
return fmt.Errorf("plan caddy deployment: %w", err)
}
if len(plan.Operations) == 0 {
fmt.Printf("%s service is up to date.\n", client.CaddyServiceName)
} else {
if svc.ID == "" {
if len(opts.machines) > 0 {
fmt.Println("This will run a Caddy container on each selected machine.")
} else {
fmt.Println("This will run a Caddy container on each machine.")
}
}
// Initialise a machine and container name resolver to properly format the 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()
fmt.Println("Deployment plan:")
fmt.Println(plan.Format(resolver))
fmt.Println()
confirmed, err := cli.Confirm()
if err != nil {
return fmt.Errorf("confirm deployment: %w", err)
}
if !confirmed {
fmt.Println("Cancelled. No changes were made.")
return nil
}
err = progress.RunWithTitle(ctx, func(ctx context.Context) error {
if _, err = d.Run(ctx); err != nil {
return fmt.Errorf("deploy caddy: %w", err)
}
return nil
}, uncli.ProgressOut(), fmt.Sprintf("Deploying service %s (%s mode)", d.Spec.Name, d.Spec.Mode))
if err != nil {
return err
}
}
fmt.Println()
return UpdateDomainRecords(ctx, clusterClient, uncli.ProgressOut())
}
func UpdateDomainRecords(ctx context.Context, clusterClient *client.Client, progressOut *streams.Out) error {
domain, err := clusterClient.GetDomain(ctx)
if err != nil {
if errors.Is(err, api.ErrNotFound) {
fmt.Println("Skipping DNS records update as no cluster domain is reserved (see 'uc dns').")
return nil
}
return fmt.Errorf("get cluster domain: %w", err)
}
fmt.Println("Updating cluster domain records in Uncloud DNS to point to machines running caddy service...")
// TODO: split the method into two: one to get the records and one to update them to ask for update confirmation.
var records []*pb.DNSRecord
err = progress.RunWithTitle(ctx, func(ctx context.Context) error {
var err error
records, err = clusterClient.CreateIngressRecords(ctx, client.CaddyServiceName)
return err
}, progressOut, "Verifying internet access to caddy service")
if err != nil {
if errors.Is(err, client.ErrNoReachableMachines) {
fmt.Println()
fmt.Printf("DNS records for domain '%s' could not be updated as there are no internet-reachable "+
"machines running caddy containers.\n", domain)
fmt.Println()
fmt.Println("Possible solutions:")
fmt.Println("- Ensure your machines have public IP addresses")
fmt.Println("- Use --public-ip flag when adding machines to override the automatically detected IPs")
fmt.Println("- Check firewall settings on your machines")
fmt.Println("- Configure port forwarding if behind NAT")
fmt.Println("- Retry Caddy deployment with 'uc caddy deploy' after resolving connectivity issues")
fmt.Println()
fmt.Println("Your services won't be accessible from the internet until at least one machine " +
"becomes reachable. If you aren't planning to expose any services publicly, you can release " +
"the domain by running 'uc dns release'.")
}
return fmt.Errorf("failed to update DNS records pointing to caddy service: %w", err)
}
fmt.Println()
fmt.Println("DNS records updated to use only the internet-reachable machines running caddy service:")
for _, r := range records {
fmt.Printf(" %s %s → %s\n", r.Name, r.Type, strings.Join(r.Values, ", "))
}
return nil
}
+16
View File
@@ -0,0 +1,16 @@
package caddy
import (
"github.com/spf13/cobra"
)
func NewRootCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "caddy",
Short: "Manage Caddy reverse proxy service.",
}
cmd.AddCommand(
NewDeployCommand(),
)
return cmd
}
+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
}
+180
View File
@@ -0,0 +1,180 @@
package main
import (
"context"
"errors"
"fmt"
"strings"
composecli "github.com/compose-spec/compose-go/v2/cli"
"github.com/docker/compose/v2/pkg/progress"
"github.com/psviderski/uncloud/internal/cli"
"github.com/psviderski/uncloud/pkg/api"
"github.com/psviderski/uncloud/pkg/client"
"github.com/psviderski/uncloud/pkg/client/compose"
"github.com/psviderski/uncloud/pkg/client/deploy"
"github.com/spf13/cobra"
)
type deployOptions struct {
files []string
profiles []string
services []string
noBuild bool
context string
}
// NewDeployCommand creates a new command to deploy services from a Compose file.
func NewDeployCommand() *cobra.Command {
opts := deployOptions{}
cmd := &cobra.Command{
Use: "deploy [FLAGS] [SERVICE...]",
Short: "Deploy 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 runDeploy(cmd.Context(), uncli, opts)
},
}
cmd.Flags().StringSliceVarP(&opts.files, "file", "f", nil,
"One or more Compose files to deploy services from. (default compose.yaml)")
cmd.Flags().StringSliceVarP(&opts.profiles, "profile", "p", nil,
"One or more Compose profiles to enable.")
cmd.Flags().StringVarP(&opts.context, "context", "c", "",
"Name of the cluster context to deploy to (default is the current context)")
cmd.Flags().BoolVarP(&opts.noBuild, "no-build", "n", false,
"Do not build images before deploying services. (default false)")
// TODO: Consider adding a filter flag to specify which machines to deploy to but keep the rest running.
// Could be useful to test a new version on a subset of machines before rolling out to all.
return cmd
}
// projectOpts returns the project options for the Compose file(s).
func projectOpts(opts deployOptions) []composecli.ProjectOptionsFn {
projectOpts := []composecli.ProjectOptionsFn{}
if len(opts.profiles) > 0 {
projectOpts = append(projectOpts, composecli.WithDefaultProfiles(opts.profiles...))
}
return projectOpts
}
// runDeploy parses the Compose file(s) and deploys the services.
func runDeploy(ctx context.Context, uncli *cli.CLI, opts deployOptions) error {
projectOpts := projectOpts(opts)
project, err := compose.LoadProject(ctx, opts.files, projectOpts...)
if err != nil {
return fmt.Errorf("load compose file(s): %w", err)
}
if len(opts.services) > 0 {
// Includes service dependencies by default. This is the default docker compose behavior.
project, err = project.WithSelectedServices(opts.services)
if err != nil {
return fmt.Errorf("select services: %w", err)
}
}
servicesToBuild := cli.GetServicesThatNeedBuild(project)
if len(servicesToBuild) > 0 {
if opts.noBuild {
fmt.Println("Not building services as requested.")
} else {
buildOpts := cli.BuildOptions{
Push: true,
NoCache: false,
}
if err := cli.BuildServices(ctx, servicesToBuild, buildOpts); err != nil {
return fmt.Errorf("build services: %w", err)
}
}
}
clusterClient, err := uncli.ConnectCluster(ctx, opts.context)
if err != nil {
return fmt.Errorf("connect to cluster: %w", err)
}
defer clusterClient.Close()
composeDeploy, err := compose.NewDeployment(ctx, clusterClient, project)
if err != nil {
return fmt.Errorf("create compose deployment: %w", err)
}
plan, err := composeDeploy.Plan(ctx)
if err != nil {
return fmt.Errorf("plan deployment: %w", err)
}
if len(plan.Operations) == 0 {
fmt.Println("Services are up to date.")
return nil
}
fmt.Println("Deployment plan:")
if err = printPlan(ctx, clusterClient, plan); err != nil {
return fmt.Errorf("print deployment plan: %w", err)
}
fmt.Println()
confirmed, err := cli.Confirm()
if err != nil {
return fmt.Errorf("confirm deployment: %w", err)
}
if !confirmed {
fmt.Println("Cancelled. No changes were made.")
return nil
}
return progress.RunWithTitle(ctx, func(ctx context.Context) error {
if err := plan.Execute(ctx, clusterClient); err != nil {
return fmt.Errorf("deploy services: %w", err)
}
return nil
}, uncli.ProgressOut(), "Deploying services")
}
func printPlan(ctx context.Context, cli *client.Client, plan deploy.SequenceOperation) error {
for _, op := range plan.Operations {
svcPlan, ok := op.(*deploy.Plan)
if !ok {
fmt.Println("- " + op.Format(nil))
continue
}
svc, err := cli.InspectService(ctx, svcPlan.ServiceID)
if err != nil && !errors.Is(err, api.ErrNotFound) {
return fmt.Errorf("inspect service: %w", err)
}
// Initialise a machine and container name resolver to properly format the service plan output.
resolver, err := cli.ServiceOperationNameResolver(ctx, svc)
if err != nil {
return fmt.Errorf("create machine and container name resolver for service operations: %w", err)
}
fmt.Printf("- Deploy service [name=%s]\n", svcPlan.ServiceName)
fmt.Println(indent(svcPlan.Format(resolver), " "))
}
return nil
}
func indent(text, prefix string) string {
lines := strings.Split(text, "\n")
for i, line := range lines {
lines[i] = prefix + line
}
return strings.Join(lines, "\n")
}
+55
View File
@@ -0,0 +1,55 @@
package dns
import (
"context"
"errors"
"fmt"
"github.com/psviderski/uncloud/internal/cli"
"github.com/spf13/cobra"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/emptypb"
)
type releaseOptions struct {
context string
}
func NewReleaseCommand() *cobra.Command {
opts := releaseOptions{}
cmd := &cobra.Command{
Use: "release",
Short: "Release the reserved cluster domain.",
RunE: func(cmd *cobra.Command, args []string) error {
uncli := cmd.Context().Value("cli").(*cli.CLI)
return release(cmd.Context(), uncli, opts)
},
}
cmd.Flags().StringVarP(
&opts.context, "context", "c", "",
"Name of the cluster context. (default is the current context)",
)
return cmd
}
func release(ctx context.Context, uncli *cli.CLI, opts releaseOptions) error {
client, err := uncli.ConnectCluster(ctx, opts.context)
if err != nil {
return fmt.Errorf("connect to cluster: %w", err)
}
defer client.Close()
domain, err := client.ReleaseDomain(ctx, &emptypb.Empty{})
if err != nil {
if status.Convert(err).Code() == codes.NotFound {
return errors.New("no domain reserved")
}
}
fmt.Printf("Released cluster domain: %s\n", domain.Name)
return nil
}
+75
View File
@@ -0,0 +1,75 @@
package dns
import (
"context"
"errors"
"fmt"
"github.com/psviderski/uncloud/cmd/uncloud/caddy"
"github.com/psviderski/uncloud/internal/cli"
"github.com/psviderski/uncloud/internal/machine/api/pb"
"github.com/psviderski/uncloud/pkg/api"
"github.com/psviderski/uncloud/pkg/client"
"github.com/spf13/cobra"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
const DefaultUncloudDNSAPIEndpoint = "https://dns.uncloud.run/v1"
type reserveOptions struct {
endpoint string
context string
}
func NewReserveCommand() *cobra.Command {
opts := reserveOptions{}
cmd := &cobra.Command{
Use: "reserve",
Short: "Reserve a cluster domain in Uncloud DNS.",
RunE: func(cmd *cobra.Command, args []string) error {
uncli := cmd.Context().Value("cli").(*cli.CLI)
return reserve(cmd.Context(), uncli, opts)
},
}
cmd.Flags().StringVar(&opts.endpoint, "endpoint", DefaultUncloudDNSAPIEndpoint,
"API endpoint for the Uncloud DNS service.")
cmd.Flags().StringVarP(
&opts.context, "context", "c", "",
"Name of the cluster context. (default is the current context)",
)
return cmd
}
func reserve(ctx context.Context, uncli *cli.CLI, opts reserveOptions) error {
clusterClient, err := uncli.ConnectCluster(ctx, opts.context)
if err != nil {
return fmt.Errorf("connect to cluster: %w", err)
}
defer clusterClient.Close()
domain, err := clusterClient.ReserveDomain(ctx, &pb.ReserveDomainRequest{Endpoint: opts.endpoint})
if err != nil {
if status.Convert(err).Code() == codes.AlreadyExists {
return errors.New("domain already reserved")
}
return err
}
fmt.Printf("Reserved cluster domain: %s\n", domain.Name)
// Update cluster domain records in Uncloud DNS to point to machines running caddy service if it has been deployed.
if _, err = clusterClient.InspectService(ctx, client.CaddyServiceName); err != nil {
if errors.Is(err, api.ErrNotFound) {
fmt.Println("Deploy the Caddy reverse proxy service ('uc caddy deploy') to enable internet access " +
"to your services via the reserved or your custom domain.")
return nil
}
return fmt.Errorf("inspect caddy service: %w", err)
}
return caddy.UpdateDomainRecords(ctx, clusterClient, uncli.ProgressOut())
}
+22
View File
@@ -0,0 +1,22 @@
package dns
import (
"github.com/spf13/cobra"
)
func NewRootCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "dns",
Short: "Manage cluster domain in Uncloud DNS.",
Long: "Manage cluster domain in Uncloud DNS.\n" +
"DNS commands allow you to reserve or release a unique '<id>.cluster.uncloud.run' domain for your " +
"cluster. When reserved, Caddy service deployments will automatically update DNS records to route " +
"traffic to the services in the cluster.",
}
cmd.AddCommand(
NewReleaseCommand(),
NewReserveCommand(),
NewShowCommand(),
)
return cmd
}
+54
View File
@@ -0,0 +1,54 @@
package dns
import (
"context"
"errors"
"fmt"
"github.com/psviderski/uncloud/internal/cli"
"github.com/psviderski/uncloud/pkg/api"
"github.com/spf13/cobra"
)
type showOptions struct {
context string
}
func NewShowCommand() *cobra.Command {
opts := showOptions{}
cmd := &cobra.Command{
Use: "show",
Short: "Print the cluster domain name.",
RunE: func(cmd *cobra.Command, args []string) error {
uncli := cmd.Context().Value("cli").(*cli.CLI)
return show(cmd.Context(), uncli, opts)
},
}
cmd.Flags().StringVarP(
&opts.context, "context", "c", "",
"Name of the cluster context. (default is the current context)",
)
return cmd
}
func show(ctx context.Context, uncli *cli.CLI, opts showOptions) error {
clusterClient, err := uncli.ConnectCluster(ctx, opts.context)
if err != nil {
return fmt.Errorf("connect to cluster: %w", err)
}
defer clusterClient.Close()
domain, err := clusterClient.GetDomain(ctx)
if err != nil {
if errors.Is(err, api.ErrNotFound) {
return errors.New("no domain reserved")
}
return err
}
fmt.Println(domain)
return nil
}
+149 -11
View File
@@ -1,16 +1,31 @@
package machine
import (
"context"
"errors"
"fmt"
"net/netip"
"time"
"github.com/cenkalti/backoff/v4"
"github.com/docker/compose/v2/pkg/progress"
"github.com/psviderski/uncloud/cmd/uncloud/caddy"
"github.com/psviderski/uncloud/internal/cli"
"github.com/psviderski/uncloud/internal/cli/config"
"github.com/psviderski/uncloud/pkg/api"
"github.com/psviderski/uncloud/pkg/client"
"github.com/spf13/cobra"
"uncloud/internal/cli"
"uncloud/internal/cli/config"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
type addOptions struct {
name string
sshKey string
cluster string
name string
noCaddy bool
publicIP string
sshKey string
context string
version string
}
func NewAddCommand() *cobra.Command {
@@ -33,17 +48,140 @@ func NewAddCommand() *cobra.Command {
KeyPath: opts.sshKey,
}
return uncli.AddMachine(cmd.Context(), remoteMachine, opts.cluster, opts.name)
return add(cmd.Context(), uncli, remoteMachine, opts)
},
}
cmd.Flags().StringVarP(&opts.name, "name", "n", "", "Assign a name to the machine.")
cmd.Flags().StringVarP(
&opts.sshKey, "ssh-key", "i", "",
"path to SSH private key for SSH remote login. (default ~/.ssh/id_*)",
cmd.Flags().BoolVar(
&opts.noCaddy, "no-caddy", false,
"Don't deploy Caddy reverse proxy service to the machine.",
)
cmd.Flags().StringVar(
&opts.publicIP, "public-ip", "auto",
"Public IP address of the machine for ingress configuration. Use 'auto' for automatic detection, "+
"blank '' or 'none' to disable ingress on this machine, or specify an IP address.",
)
cmd.Flags().StringVarP(
&opts.cluster, "cluster", "c", "",
"Name of the cluster to add the machine to. (default is the current cluster)",
&opts.sshKey, "ssh-key", "i", "~/.ssh/id_ed25519",
"Path to SSH private key for remote login (if not already added to SSH agent).",
)
cmd.Flags().StringVar(
&opts.version, "version", "latest",
"Version of the Uncloud daemon to install on the machine.",
)
cmd.Flags().StringVarP(
&opts.context, "context", "c", "",
"Name of the cluster context to add the machine to. (default is the current context)",
)
return cmd
}
func add(ctx context.Context, uncli *cli.CLI, remoteMachine cli.RemoteMachine, opts addOptions) error {
var publicIP *netip.Addr
switch opts.publicIP {
case "auto":
publicIP = &netip.Addr{}
case "", "none":
publicIP = nil
default:
ip, err := netip.ParseAddr(opts.publicIP)
if err != nil {
return fmt.Errorf("parse public IP: %w", err)
}
publicIP = &ip
}
clusterClient, machineClient, err := uncli.AddMachine(ctx, cli.AddMachineOptions{
Context: opts.context,
MachineName: opts.name,
PublicIP: publicIP,
RemoteMachine: remoteMachine,
Version: opts.version,
})
if err != nil {
return err
}
defer clusterClient.Close()
defer machineClient.Close()
if opts.noCaddy {
return nil
}
// Wait for the cluster to be initialised to be able to deploy the Caddy service.
fmt.Println("Waiting for the machine to be ready...")
fmt.Println()
if err = waitClusterInitialised(ctx, machineClient); err != nil {
return fmt.Errorf("wait for cluster to be initialised on machine: %w", err)
}
// Deploy a Caddy service container to the added machine. If caddy service is already deployed on other machines,
// use the deployed image version. Otherwise, use the latest version.
// NOTE: We use the cluster client to inspect and scale the Caddy service because the newly added machine may have
// issues accessing the Machine API of existing machines in the cluster.
// See the issue for more details: https://github.com/psviderski/uncloud/issues/65.
caddyImage := ""
caddySvc, err := clusterClient.InspectService(ctx, client.CaddyServiceName)
if err != nil {
if !errors.Is(err, api.ErrNotFound) {
return fmt.Errorf("inspect caddy service: %w", err)
}
} else {
caddyImage = caddySvc.Containers[0].Container.Config.Image
// Find the latest created container and use its image.
var latestCreated time.Time
for _, c := range caddySvc.Containers[1:] {
created, err := time.Parse(time.RFC3339Nano, c.Container.Created)
if err != nil {
continue
}
if created.After(latestCreated) {
latestCreated = created
caddyImage = c.Container.Config.Image
}
}
}
// TODO: scale the existing Caddy service to the new machine instead of running a new deployment
// that may cause a small downtime.
d, err := clusterClient.NewCaddyDeployment(caddyImage, api.Placement{})
if err != nil {
return fmt.Errorf("create caddy deployment: %w", err)
}
err = progress.RunWithTitle(ctx, func(ctx context.Context) error {
if _, err = d.Run(ctx); err != nil {
return fmt.Errorf("deploy caddy: %w", err)
}
return nil
}, uncli.ProgressOut(), fmt.Sprintf("Deploying service %s", d.Spec.Name))
if err != nil {
return err
}
fmt.Println()
return caddy.UpdateDomainRecords(ctx, machineClient, uncli.ProgressOut())
}
func waitClusterInitialised(ctx context.Context, client *client.Client) error {
boff := backoff.WithContext(backoff.NewExponentialBackOff(
backoff.WithMaxInterval(1*time.Second),
backoff.WithMaxElapsedTime(5*time.Minute),
), ctx)
check := func() error {
_, err := client.ListMachines(ctx, nil)
if err == nil {
return nil
}
statusErr := status.Convert(err)
if statusErr.Code() == codes.FailedPrecondition {
return err
}
return backoff.Permanent(err)
}
return backoff.Retry(check, boff)
}
+125 -21
View File
@@ -1,27 +1,40 @@
package machine
import (
"context"
"fmt"
"github.com/spf13/cobra"
"net/netip"
"uncloud/internal/cli"
"uncloud/internal/cli/config"
"uncloud/internal/machine/cluster"
"github.com/docker/compose/v2/pkg/progress"
"github.com/psviderski/uncloud/cmd/uncloud/caddy"
"github.com/psviderski/uncloud/cmd/uncloud/dns"
"github.com/psviderski/uncloud/internal/cli"
"github.com/psviderski/uncloud/internal/cli/config"
"github.com/psviderski/uncloud/internal/machine/api/pb"
"github.com/psviderski/uncloud/internal/machine/cluster"
"github.com/psviderski/uncloud/pkg/api"
"github.com/spf13/cobra"
)
type initOptions struct {
name string
network string
sshKey string
cluster string
dnsEndpoint string
name string
network string
noCaddy bool
noDNS bool
publicIP string
sshKey string
version string
context string
}
func NewInitCommand() *cobra.Command {
opts := initOptions{}
cmd := &cobra.Command{
Use: "init [USER@HOST:PORT]",
Short: "Initialise a new cluster that consists of the local or remote machine.",
// TODO: include usage examples of initialising a local and remote machine.
Short: "Initialise a new cluster with a remote machine as the first member.",
// TODO: include usage examples of initialising a remote machine.
// TODO: support initialising a cluster on the local machine.
Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
uncli := cmd.Context().Value("cli").(*cli.CLI)
@@ -39,27 +52,118 @@ func NewInitCommand() *cobra.Command {
KeyPath: opts.sshKey,
}
}
netPrefix, err := netip.ParsePrefix(opts.network)
if err != nil {
return fmt.Errorf("parse network CIDR: %w", err)
}
return uncli.InitCluster(cmd.Context(), remoteMachine, opts.cluster, opts.name, netPrefix)
return initCluster(cmd.Context(), uncli, remoteMachine, opts)
},
}
cmd.Flags().StringVarP(&opts.name, "name", "n", "", "Assign a name to the machine.")
cmd.Flags().StringVar(&opts.dnsEndpoint, "dns-endpoint", dns.DefaultUncloudDNSAPIEndpoint,
"API endpoint for the Uncloud DNS service.")
cmd.Flags().StringVarP(
&opts.name, "name", "n", "",
"Assign a name to the machine.",
)
cmd.Flags().StringVar(
&opts.network, "network", cluster.DefaultNetwork.String(),
"IPv4 network CIDR to use for machines and services.",
)
cmd.Flags().StringVarP(
&opts.sshKey, "ssh-key", "i", "",
"path to SSH private key for SSH remote login. (default ~/.ssh/id_*)",
cmd.Flags().BoolVar(
&opts.noCaddy, "no-caddy", false,
"Don't deploy Caddy reverse proxy service to the machine.",
)
cmd.Flags().BoolVar(
&opts.noDNS, "no-dns", false,
"Don't reserve a cluster domain in Uncloud DNS.",
)
cmd.Flags().StringVar(
&opts.publicIP, "public-ip", "auto",
"Public IP address of the machine for ingress configuration. Use 'auto' for automatic detection, "+
"blank '' or 'none' to disable ingress on this machine, or specify an IP address.",
)
cmd.Flags().StringVarP(
&opts.cluster, "cluster", "c", "",
"Name of the cluster in the local config if initialising a remote machine.",
&opts.sshKey, "ssh-key", "i", "~/.ssh/id_ed25519",
"Path to SSH private key for remote login (if not already added to SSH agent).",
)
cmd.Flags().StringVar(
&opts.version, "version", "latest",
"Version of the Uncloud daemon to install on the machine.",
)
cmd.Flags().StringVarP(
&opts.context, "context", "c", "default",
"Name of the created context for the initialised cluster in the Uncloud config.",
)
return cmd
}
func initCluster(ctx context.Context, uncli *cli.CLI, remoteMachine *cli.RemoteMachine, opts initOptions) error {
netPrefix, err := netip.ParsePrefix(opts.network)
if err != nil {
return fmt.Errorf("parse network CIDR: %w", err)
}
var publicIP *netip.Addr
switch opts.publicIP {
case "auto":
publicIP = &netip.Addr{}
case "", "none":
publicIP = nil
default:
ip, err := netip.ParseAddr(opts.publicIP)
if err != nil {
return fmt.Errorf("parse public IP: %w", err)
}
publicIP = &ip
}
client, err := uncli.InitCluster(ctx, cli.InitClusterOptions{
Context: opts.context,
MachineName: opts.name,
Network: netPrefix,
PublicIP: publicIP,
RemoteMachine: remoteMachine,
Version: opts.version,
})
if err != nil {
return err
}
defer client.Close()
if opts.noCaddy && opts.noDNS {
return nil
}
// Deploy the Caddy service to the initialised machine.
// The creation of a deployment plan talks to cluster API. Since the API needs a few moments to become available
// after cluster initialisation, we keep the user informed during this wait.
fmt.Println("Waiting for the machine to be ready...")
fmt.Println()
if !opts.noDNS {
domain, err := client.ReserveDomain(ctx, &pb.ReserveDomainRequest{Endpoint: opts.dnsEndpoint})
if err != nil {
return fmt.Errorf("reserve cluster domain in Uncloud DNS: %w", err)
}
fmt.Printf("Reserved cluster domain: %s\n", domain.Name)
}
if !opts.noCaddy {
d, err := client.NewCaddyDeployment("", api.Placement{})
if err != nil {
return fmt.Errorf("create caddy deployment: %w", err)
}
err = progress.RunWithTitle(ctx, func(ctx context.Context) error {
if _, err = d.Run(ctx); err != nil {
return fmt.Errorf("deploy caddy: %w", err)
}
return nil
}, uncli.ProgressOut(), fmt.Sprintf("Deploying service %s", d.Spec.Name))
if err != nil {
return err
}
fmt.Println()
return caddy.UpdateDomainRecords(ctx, client, uncli.ProgressOut())
}
return nil
}
@@ -3,30 +3,30 @@ package machine
import (
"context"
"fmt"
"github.com/spf13/cobra"
"net/netip"
"os"
"strings"
"text/tabwriter"
"uncloud/internal/cli"
"uncloud/internal/machine/network"
"uncloud/internal/secret"
"github.com/psviderski/uncloud/internal/cli"
"github.com/psviderski/uncloud/internal/machine/network"
"github.com/spf13/cobra"
)
func NewListCommand() *cobra.Command {
var cluster string
var clusterContext string
cmd := &cobra.Command{
Use: "ls",
Aliases: []string{"list"},
Short: "List machines in a cluster.",
RunE: func(cmd *cobra.Command, args []string) error {
uncli := cmd.Context().Value("cli").(*cli.CLI)
return list(cmd.Context(), uncli, cluster)
return list(cmd.Context(), uncli, clusterContext)
},
}
cmd.Flags().StringVarP(
&cluster, "cluster", "c", "",
"Name of the cluster. (default is the current cluster)",
&clusterContext, "context", "c", "",
"Name of the cluster context. (default is the current context)",
)
return cmd
}
@@ -38,7 +38,7 @@ func list(ctx context.Context, uncli *cli.CLI, clusterName string) error {
}
defer client.Close()
machines, err := client.ListMachines(ctx)
machines, err := client.ListMachines(ctx, nil)
if err != nil {
return fmt.Errorf("list machines: %w", err)
}
@@ -46,7 +46,7 @@ func list(ctx context.Context, uncli *cli.CLI, clusterName string) error {
// Print the list of machines in a table format.
tw := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', 0)
// Print header.
if _, err = fmt.Fprintln(tw, "NAME\tSTATE\tADDRESS\tPUBLIC KEY\tENDPOINTS"); err != nil {
if _, err = fmt.Fprintln(tw, "NAME\tSTATE\tADDRESS\tPUBLIC IP\tWIREGUARD ENDPOINTS"); err != nil {
return fmt.Errorf("write header: %w", err)
}
// Print rows.
@@ -54,14 +54,21 @@ func list(ctx context.Context, uncli *cli.CLI, clusterName string) error {
m := member.Machine
subnet, _ := m.Network.Subnet.ToPrefix()
subnet = netip.PrefixFrom(network.MachineIP(subnet), subnet.Bits())
publicIP := "-"
if m.PublicIp != nil {
ip, _ := m.PublicIp.ToAddr()
publicIP = ip.String()
}
endpoints := make([]string, len(m.Network.Endpoints))
for i, ep := range m.Network.Endpoints {
addrPort, _ := ep.ToAddrPort()
endpoints[i] = addrPort.String()
}
publicKey := secret.Secret(m.Network.PublicKey)
if _, err = fmt.Fprintf(
tw, "%s\t%s\t%s\t%s\t%s\n", m.Name, capitalise(member.State.String()), subnet, publicKey, strings.Join(endpoints, ", "),
tw, "%s\t%s\t%s\t%s\t%s\n", m.Name, capitalise(member.State.String()), subnet, publicIP, strings.Join(endpoints, ", "),
); err != nil {
return fmt.Errorf("write row: %w", err)
}
+3 -2
View File
@@ -6,8 +6,9 @@ import (
func NewRootCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "machine",
Short: "Manage machines in an Uncloud cluster.",
Use: "machine",
Aliases: []string{"m"},
Short: "Manage machines in an Uncloud cluster.",
}
cmd.AddCommand(
NewAddCommand(),
+2 -2
View File
@@ -3,8 +3,8 @@ package machine
import (
"fmt"
"github.com/spf13/cobra"
"uncloud/internal/daemon"
"uncloud/internal/machine"
"github.com/psviderski/uncloud/internal/daemon"
"github.com/psviderski/uncloud/internal/machine"
)
type tokenOptions struct {
+56 -16
View File
@@ -3,50 +3,90 @@ package main
import (
"context"
"fmt"
"github.com/spf13/cobra"
"os"
"net/netip"
"strings"
"uncloud/cmd/uncloud/machine"
"uncloud/cmd/uncloud/service"
"uncloud/internal/cli"
"github.com/psviderski/uncloud/cmd/uncloud/caddy"
cmdcontext "github.com/psviderski/uncloud/cmd/uncloud/context"
"github.com/psviderski/uncloud/cmd/uncloud/dns"
"github.com/psviderski/uncloud/cmd/uncloud/machine"
"github.com/psviderski/uncloud/cmd/uncloud/service"
"github.com/psviderski/uncloud/cmd/uncloud/volume"
"github.com/psviderski/uncloud/internal/cli"
"github.com/psviderski/uncloud/internal/cli/config"
"github.com/psviderski/uncloud/internal/fs"
"github.com/psviderski/uncloud/internal/version"
"github.com/spf13/cobra"
)
type globalOptions struct {
configPath string
connect string
}
func main() {
var configPath string
opts := globalOptions{}
cmd := &cobra.Command{
Use: "uncloud",
Short: "A CLI tool for managing Uncloud resources such as clusters, machines, and services.",
Version: version.String(),
SilenceUsage: true,
SilenceErrors: true,
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
if strings.HasPrefix(configPath, "~/") {
home, err := os.UserHomeDir()
if err != nil {
return fmt.Errorf("get user home directory to resolve %q: %w", configPath, err)
var conn *config.MachineConnection
if opts.connect != "" {
if strings.HasPrefix(opts.connect, "tcp://") {
addrPort, err := netip.ParseAddrPort(opts.connect[len("tcp://"):])
if err != nil {
return fmt.Errorf("parse TCP address: %w", err)
}
conn = &config.MachineConnection{
TCP: &addrPort,
}
} else {
dest := opts.connect
if strings.HasPrefix(dest, "ssh://") {
dest = dest[len("ssh://"):]
}
conn = &config.MachineConnection{
SSH: config.SSHDestination(dest),
}
}
configPath = strings.Replace(configPath, "~", home, 1)
}
uncli, err := cli.New(configPath)
configPath := fs.ExpandHomeDir(opts.configPath)
uncli, err := cli.New(configPath, conn)
if err != nil {
return fmt.Errorf("initialize CLI: %w", err)
return fmt.Errorf("initialise CLI: %w", err)
}
cmd.SetContext(context.WithValue(cmd.Context(), "cli", uncli))
return nil
},
}
cmd.PersistentFlags().StringVar(&opts.connect, "connect", "",
"Connect to a remote cluster machine without using the Uncloud configuration file.\n"+
"Format: [ssh://]user@host[:port] or tcp://host:port")
// TODO: allow to override using UNCLOUD_CONFIG env var.
cmd.PersistentFlags().StringVar(&configPath, "uncloud-config", "~/.config/uncloud/config.toml",
"path to the Uncloud configuration file.")
_ = cmd.MarkPersistentFlagFilename("uncloud-config", "toml")
cmd.PersistentFlags().StringVar(&opts.configPath, "uncloud-config", "~/.config/uncloud/config.yaml",
"Path to the Uncloud configuration file.")
_ = cmd.MarkPersistentFlagFilename("uncloud-config", "yaml", "yml")
// TODO: make --context a global flag and pass it as a value of the command context.
cmd.AddCommand(
NewDeployCommand(),
NewBuildCommand(),
caddy.NewRootCommand(),
cmdcontext.NewRootCommand(),
dns.NewRootCommand(),
machine.NewRootCommand(),
service.NewRootCommand(),
service.NewInspectCommand(),
service.NewListCommand(),
service.NewRmCommand(),
service.NewRunCommand(),
service.NewScaleCommand(),
volume.NewRootCommand(),
)
cobra.CheckErr(cmd.Execute())
}
+9 -8
View File
@@ -3,14 +3,15 @@ package service
import (
"context"
"fmt"
"github.com/docker/docker/pkg/stringid"
"github.com/docker/go-units"
"os"
"text/tabwriter"
"time"
"github.com/docker/docker/pkg/stringid"
"github.com/docker/go-units"
"github.com/psviderski/uncloud/internal/cli"
"github.com/spf13/cobra"
"uncloud/internal/cli"
)
type inspectOptions struct {
@@ -21,7 +22,7 @@ type inspectOptions struct {
func NewInspectCommand() *cobra.Command {
opts := inspectOptions{}
cmd := &cobra.Command{
Use: "inspect",
Use: "inspect SERVICE",
Short: "Display detailed information on a service.",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
@@ -31,8 +32,8 @@ func NewInspectCommand() *cobra.Command {
},
}
cmd.Flags().StringVarP(
&opts.cluster, "cluster", "c", "",
"Name of the cluster. (default is the current cluster)",
&opts.cluster, "context", "c", "",
"Name of the cluster context. (default is the current context)",
)
return cmd
}
@@ -49,7 +50,7 @@ func inspect(ctx context.Context, uncli *cli.CLI, opts inspectOptions) error {
return fmt.Errorf("inspect service: %w", err)
}
machines, err := client.ListMachines(ctx)
machines, err := client.ListMachines(ctx, nil)
if err != nil {
return fmt.Errorf("list machines: %w", err)
}
@@ -89,7 +90,7 @@ func inspect(ctx context.Context, uncli *cli.CLI, opts inspectOptions) error {
tw,
"%s\t%s\t%s\t%s\t%s\n",
stringid.TruncateID(ctr.Container.ID),
ctr.Container.Image,
ctr.Container.Config.Image,
created,
state,
machine,
-53
View File
@@ -1,53 +0,0 @@
package service
import (
"context"
"fmt"
"github.com/spf13/cobra"
"os"
"text/tabwriter"
"uncloud/internal/cli"
)
func NewListCommand() *cobra.Command {
var cluster string
cmd := &cobra.Command{
Use: "ls",
Aliases: []string{"list"},
Short: "List services.",
RunE: func(cmd *cobra.Command, args []string) error {
uncli := cmd.Context().Value("cli").(*cli.CLI)
return list(cmd.Context(), uncli, cluster)
},
}
cmd.Flags().StringVarP(
&cluster, "cluster", "c", "",
"Name of the cluster. (default is the current cluster)",
)
return cmd
}
func list(ctx context.Context, uncli *cli.CLI, clusterName string) error {
client, err := uncli.ConnectCluster(ctx, clusterName)
if err != nil {
return fmt.Errorf("connect to cluster: %w", err)
}
defer client.Close()
services, err := client.ListServices(ctx)
if err != nil {
return fmt.Errorf("list services: %w", err)
}
// Print the list of services in a table format.
tw := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', 0)
if _, err = fmt.Fprintln(tw, "SERVICE ID\tNAME\tMODE\tREPLICAS"); err != nil {
return fmt.Errorf("write header: %w", err)
}
for _, s := range services {
if _, err = fmt.Fprintf(tw, "%s\t%s\t%s\t%d\n", s.ID, s.Name, s.Mode, len(s.Containers)); err != nil {
return fmt.Errorf("write row: %w", err)
}
}
return tw.Flush()
}
+81
View File
@@ -0,0 +1,81 @@
package service
import (
"context"
"fmt"
"os"
"strings"
"text/tabwriter"
"github.com/psviderski/uncloud/internal/cli"
"github.com/spf13/cobra"
)
func NewListCommand() *cobra.Command {
// TODO(lhf): rename to context
var cluster string
cmd := &cobra.Command{
Use: "ls",
Aliases: []string{"list"},
Short: "List services.",
RunE: func(cmd *cobra.Command, args []string) error {
uncli := cmd.Context().Value("cli").(*cli.CLI)
return list(cmd.Context(), uncli, cluster)
},
}
cmd.Flags().StringVarP(
&cluster, "context", "c", "",
"Name of the cluster context. (default is the current context)",
)
return cmd
}
func list(ctx context.Context, uncli *cli.CLI, clusterName string) error {
client, err := uncli.ConnectCluster(ctx, clusterName)
if err != nil {
return fmt.Errorf("connect to cluster: %w", err)
}
defer client.Close()
services, err := client.ListServices(ctx)
if err != nil {
return fmt.Errorf("list services: %w", err)
}
serviceNames := make(map[string]struct{}, len(services))
haveDuplicateNames := false
for _, svc := range services {
if _, exists := serviceNames[svc.Name]; exists {
haveDuplicateNames = true
break
}
serviceNames[svc.Name] = struct{}{}
}
// Print the list of services in a table format.
tw := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', 0)
// Include the ID column if there are duplicate service names to differentiate them.
if haveDuplicateNames {
if _, err = fmt.Fprintf(tw, "ID\t"); err != nil {
return fmt.Errorf("write header: %w", err)
}
}
if _, err = fmt.Fprintln(tw, "NAME\tMODE\tREPLICAS\tENDPOINTS"); err != nil {
return fmt.Errorf("write header: %w", err)
}
for _, s := range services {
endpointsSlice := s.Endpoints()
endpoints := strings.Join(endpointsSlice, ", ")
if haveDuplicateNames {
if _, err = fmt.Fprintf(tw, "%s\t", s.ID); err != nil {
return fmt.Errorf("write row: %w", err)
}
}
if _, err = fmt.Fprintf(tw, "%s\t%s\t%d\t%s\n", s.Name, s.Mode, len(s.Containers), endpoints); err != nil {
return fmt.Errorf("write row: %w", err)
}
}
return tw.Flush()
}
+5 -6
View File
@@ -3,11 +3,10 @@ package service
import (
"context"
"fmt"
"github.com/docker/cli/cli/streams"
"github.com/docker/compose/v2/pkg/progress"
"github.com/psviderski/uncloud/internal/cli"
"github.com/spf13/cobra"
"os"
"uncloud/internal/cli"
)
type rmOptions struct {
@@ -29,8 +28,8 @@ func NewRmCommand() *cobra.Command {
},
}
cmd.Flags().StringVarP(
&opts.cluster, "cluster", "c", "",
"Name of the cluster. (default is the current cluster)",
&opts.cluster, "context", "c", "",
"Name of the cluster context. (default is the current context)",
)
return cmd
}
@@ -48,7 +47,7 @@ func rm(ctx context.Context, uncli *cli.CLI, opts rmOptions) error {
return fmt.Errorf("remove service '%s': %w", s, err)
}
return nil
}, streams.NewOut(os.Stdout), "Removing service "+s)
}, uncli.ProgressOut(), "Removing service "+s)
}
return nil
+4 -2
View File
@@ -6,14 +6,16 @@ import (
func NewRootCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "service",
Short: "Manage services in an Uncloud cluster.",
Use: "service",
Aliases: []string{"svc"},
Short: "Manage services in an Uncloud cluster.",
}
cmd.AddCommand(
NewInspectCommand(),
NewListCommand(),
NewRmCommand(),
NewRunCommand(),
NewScaleCommand(),
)
return cmd
}
+297 -49
View File
@@ -3,19 +3,36 @@ package service
import (
"context"
"fmt"
"os"
"strings"
dockeropts "github.com/docker/cli/opts"
"github.com/docker/compose/v2/pkg/progress"
"github.com/docker/docker/daemon/names"
"github.com/psviderski/uncloud/internal/cli"
"github.com/psviderski/uncloud/internal/secret"
"github.com/psviderski/uncloud/pkg/api"
"github.com/psviderski/uncloud/pkg/client/deploy"
"github.com/spf13/cobra"
"uncloud/internal/api"
"uncloud/internal/cli"
)
type runOptions struct {
command []string
image string
machine string
mode string
name string
publish []string
volumes []string
command []string
cpu dockeropts.NanoCPUs
entrypoint string
entrypointChanged bool
env []string
image string
machines []string
memory dockeropts.MemBytes
mode string
name string
privileged bool
publish []string
pull string
replicas uint
user string
volumes []string
cluster string
}
@@ -30,6 +47,7 @@ func NewRunCommand() *cobra.Command {
RunE: func(cmd *cobra.Command, args []string) error {
uncli := cmd.Context().Value("cli").(*cli.CLI)
opts.entrypointChanged = cmd.Flag("entrypoint").Changed
opts.image = args[0]
if len(args) > 1 {
opts.command = args[1:]
@@ -39,77 +57,307 @@ func NewRunCommand() *cobra.Command {
},
}
// TODO: implement placement constraints and translate --machine to a constraint.
//cmd.Flags().StringVarP(
// &opts.machine, "machine", "m", "",
// "Name or ID of the machine to run the service on. (default is first available)",
//)
cmd.Flags().VarP(&opts.cpu, "cpu", "",
"Maximum number of CPU cores a service container can use. Fractional values are allowed: "+
"0.5 for half a core or 2.25 for two and a quarter cores.")
cmd.Flags().StringVar(&opts.entrypoint, "entrypoint", "",
"Overwrite the default ENTRYPOINT of the image. Pass an empty string \"\" to reset it.")
cmd.Flags().StringSliceVarP(&opts.env, "env", "e", nil,
"Set an environment variable for service containers. Can be specified multiple times.\n"+
"Format: VAR=value or just VAR to use the value from the local environment.")
cmd.Flags().StringVar(&opts.mode, "mode", api.ServiceModeReplicated,
fmt.Sprintf("Replication mode of the service: either %q (a specified number of containers across "+
"the machines) or %q (one container on every machine).",
fmt.Sprintf("Replication mode of the service: either '%s' (a specified number of containers across "+
"the machines) or '%s' (one container on every machine).",
api.ServiceModeReplicated, api.ServiceModeGlobal))
cmd.Flags().StringSliceVarP(&opts.machines, "machine", "m", nil,
"Placement constraint by machine names, limiting which machines the service can run on. Can be specified "+
"multiple times or as a comma-separated list of machine names. (default is any suitable machine)")
cmd.Flags().VarP(&opts.memory, "memory", "",
"Maximum amount of memory a service container can use. Value is a positive integer with optional unit suffix "+
"(b, k, m, g). Default unit is bytes if no suffix specified.\n"+
"Examples: 1073741824, 1024m, 1g (all equal 1 gibibyte)")
cmd.Flags().StringVarP(&opts.name, "name", "n", "",
"Assign a name to the service. A random name is generated if not specified.")
cmd.Flags().BoolVar(&opts.privileged, "privileged", false,
"Give extended privileges to service containers. This is a security risk and should be used with caution.")
cmd.Flags().StringSliceVarP(&opts.publish, "publish", "p", nil,
"Publish a service port to make it accessible outside the cluster. Can be specified multiple times.\n"+
"Format: [hostname:][load_balancer_port:]container_port[/protocol] or [host_ip:]:host_port:container_port[/protocol]@host\n"+
"Supported protocols: tcp, udp, http, https (default is tcp). If a hostname for http(s) port is not specified, a random hostname is generated.\n"+
"Supported protocols: tcp, udp, http, https (default is tcp). If a hostname for http(s) port is not specified\n"+
"and a cluster domain is reserved, service-name.cluster-domain will be used as the hostname.\n"+
"Examples:\n"+
" -p app.example.com:8080/https Publish port 8080 as HTTPS via load balancer with custom hostname\n"+
" -p 9000:8080 Publish port 8080 as TCP port 9000 via load balancer\n"+
" -p 8080/https Publish port 8080 as HTTPS via reverse proxy with default service-name.cluster-domain hostname\n"+
" -p app.example.com:8080/https Publish port 8080 as HTTPS via reverse proxy with custom hostname\n"+
// TODO: add support for publishing L4 tcp/udp ports.
//" -p 9000:8080 Publish port 8080 as TCP port 9000 via reverse proxy\n"+
" -p 53:5353/udp@host Bind UDP port 5353 to host port 53")
cmd.Flags().StringVar(&opts.pull, "pull", api.PullPolicyMissing,
fmt.Sprintf("Pull image from the registry before running service containers ('%s', '%s', '%s').",
api.PullPolicyAlways, api.PullPolicyMissing, api.PullPolicyNever))
cmd.Flags().UintVar(&opts.replicas, "replicas", 1,
"Number of containers to run for the service. Only valid for a replicated service.")
cmd.Flags().StringVarP(&opts.user, "user", "u", "",
"User name or UID and optionally group name or GID used for running the command inside service containers.\n"+
"Format: USER[:GROUP] or UID[:GID]. If not specified, the user is set to the default user of the image.")
cmd.Flags().StringSliceVarP(&opts.volumes, "volume", "v", nil,
"Bind mount a host file or directory into a service container using the format "+
"/host/path:/container/path[:ro]. Can be specified multiple times.")
"Mount a data volume or host path into service containers. Service containers will be scheduled on the machine(s) where\n"+
"the volume is located. Can be specified multiple times.\n"+
"Format: volume_name:/container/path[:ro|volume-nocopy] or /host/path:/container/path[:ro]\n"+
"Examples:\n"+
" -v postgres-data:/var/lib/postgresql/data Mount volume 'postgres-data' to /var/lib/postgresql/data in container\n"+
" -v /data/uploads:/app/uploads Bind mount /data/uploads host directory to /app/uploads in container\n"+
" -v /host/path:/container/path:ro Bind mount a host directory or file as read-only")
cmd.Flags().StringVarP(
&opts.cluster, "cluster", "c", "",
"Name of the cluster to run the service in. (default is the current cluster)",
&opts.cluster, "context", "c", "",
"Name of the cluster context to run the service in. (default is the current context)",
)
return cmd
}
func run(ctx context.Context, uncli *cli.CLI, opts runOptions) error {
spec, err := prepareServiceSpec(opts)
if err != nil {
return err
}
clusterClient, err := uncli.ConnectCluster(ctx, opts.cluster)
if err != nil {
return fmt.Errorf("connect to cluster: %w", err)
}
defer clusterClient.Close()
var resp api.RunServiceResponse
err = progress.RunWithTitle(ctx, func(ctx context.Context) error {
resp, err = clusterClient.RunService(ctx, spec)
if err != nil {
return fmt.Errorf("run service: %w", err)
}
return nil
}, uncli.ProgressOut(), fmt.Sprintf("Running service %s (%s mode)", spec.Name, spec.Mode))
if err != nil {
return err
}
svc, err := clusterClient.InspectService(ctx, resp.ID)
if err != nil {
return fmt.Errorf("inspect service: %w", err)
}
endpoints := svc.Endpoints()
if len(endpoints) > 0 {
fmt.Println()
fmt.Printf("%s endpoints:\n", svc.Name)
for _, endpoint := range endpoints {
fmt.Printf(" • %s\n", endpoint)
}
}
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:
case api.ServiceModeReplicated, api.ServiceModeGlobal:
default:
return fmt.Errorf("invalid replication mode: %q", opts.mode)
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 fmt.Errorf("invalid service port '%s': %w", publishPort, err)
return spec, 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,
Volumes: opts.volumes,
},
Mode: opts.mode,
Name: opts.name,
Ports: ports,
}
if err := spec.Validate(); err != nil {
return fmt.Errorf("invalid service configuration: %w", err)
}
client, err := uncli.ConnectCluster(ctx, opts.cluster)
volumes, mounts, err := parseVolumeFlags(opts.volumes)
if err != nil {
return fmt.Errorf("connect to cluster: %w", err)
}
defer client.Close()
if _, err = client.RunService(ctx, spec); err != nil {
return fmt.Errorf("run service: %w", err)
return spec, err
}
return nil
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
}
+152
View File
@@ -0,0 +1,152 @@
package service
import (
"context"
"fmt"
"strconv"
"github.com/charmbracelet/huh"
"github.com/docker/compose/v2/pkg/progress"
"github.com/psviderski/uncloud/internal/cli"
"github.com/psviderski/uncloud/pkg/api"
"github.com/spf13/cobra"
)
type scaleOptions struct {
service string
replicas uint
cluster string
}
func NewScaleCommand() *cobra.Command {
opts := scaleOptions{}
cmd := &cobra.Command{
Use: "scale SERVICE REPLICAS",
Short: "Scale a replicated service by changing the number of replicas.",
Long: "Scale a replicated service by changing the number of replicas. Scaling down requires confirmation.",
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
uncli := cmd.Context().Value("cli").(*cli.CLI)
opts.service = args[0]
replicas, err := strconv.ParseUint(args[1], 10, 0)
if err != nil {
return fmt.Errorf("invalid number of replicas: %w", err)
}
opts.replicas = uint(replicas)
return scale(cmd.Context(), uncli, opts)
},
}
cmd.Flags().StringVarP(
&opts.cluster, "context", "c", "",
"Name of the cluster context. (default is the current context)",
)
return cmd
}
func scale(ctx context.Context, uncli *cli.CLI, opts scaleOptions) error {
if opts.replicas == 0 {
return fmt.Errorf(
"scaling to zero replicas is not supported. This would effectively remove the service without preserving "+
"its configuration, making it impossible to scale back up. Uncloud derives the service configuration "+
"from existing containers. Use 'uc rm %s' instead if you want to remove the service",
opts.service,
)
}
clusterClient, err := uncli.ConnectCluster(ctx, opts.cluster)
if err != nil {
return fmt.Errorf("connect to cluster: %w", err)
}
defer clusterClient.Close()
svc, err := clusterClient.InspectService(ctx, opts.service)
if err != nil {
return fmt.Errorf("inspect service '%s': %w", opts.service, err)
}
if svc.Mode != api.ServiceModeReplicated {
return fmt.Errorf("scaling is only supported for services in %s mode, service '%s' is in %s mode",
api.ServiceModeReplicated, svc.Name, svc.Mode)
}
currentReplicas := uint(len(svc.Containers))
if currentReplicas == opts.replicas {
fmt.Printf("Service '%s' already has %d replicas. No changes required.\n", svc.Name, currentReplicas)
return nil
}
// TODO: Check if all containers have the same spec. If not, prompt user to choose which one to scale.
// This can happen if a service deployment failed midway and some containers were not updated.
spec := svc.Containers[0].Container.ServiceSpec
spec.Replicas = opts.replicas
deployment := clusterClient.NewDeployment(spec, nil)
plan, err := deployment.Plan(ctx)
if err != nil {
return fmt.Errorf("plan deployment: %w", err)
}
if len(plan.Operations) == 0 {
fmt.Printf("Service '%s' is already scaled to %d replicas.\n", svc.Name, opts.replicas)
return nil
}
if opts.replicas < currentReplicas {
// Initialise a machine and container name resolver to properly format the 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.Printf("Scaling plan for service %s (%d → %d replicas):\n", svc.Name, currentReplicas, opts.replicas)
fmt.Println(plan.Format(resolver))
fmt.Println()
// Ask for confirmation before scaling down as it may cause data loss.
confirmed, err := confirm()
if err != nil {
return fmt.Errorf("confirm scaling: %w", err)
}
if !confirmed {
fmt.Println("Cancelled. No changes were made.")
return nil
}
}
title := fmt.Sprintf("Scaling service %s (%d → %d replicas)", svc.Name, currentReplicas, opts.replicas)
err = progress.RunWithTitle(ctx, func(ctx context.Context) error {
if _, err = deployment.Run(ctx); err != nil {
return fmt.Errorf("deploy service: %w", err)
}
return nil
}, uncli.ProgressOut(), title)
if err != nil {
return err
}
return nil
}
func confirm() (bool, error) {
var confirmed bool
form := huh.NewForm(
huh.NewGroup(
huh.NewConfirm().
Title(
"Do you want to continue?",
).
Affirmative("Yes!").
Negative("No").
Value(&confirmed),
),
).WithAccessible(true)
if err := form.Run(); err != nil {
return false, err
}
return confirmed, nil
}
+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
}
+7 -4
View File
@@ -2,14 +2,16 @@ package main
import (
"context"
"github.com/spf13/cobra"
"log/slog"
"os"
"os/signal"
"syscall"
"uncloud/internal/daemon"
"uncloud/internal/log"
"uncloud/internal/machine"
"github.com/psviderski/uncloud/internal/daemon"
"github.com/psviderski/uncloud/internal/log"
"github.com/psviderski/uncloud/internal/machine"
"github.com/psviderski/uncloud/internal/version"
"github.com/spf13/cobra"
)
func main() {
@@ -22,6 +24,7 @@ func main() {
cmd := &cobra.Command{
Use: "uncloudd",
Short: "Uncloud machine daemon.",
Version: version.String(),
SilenceUsage: true,
SilenceErrors: true,
RunE: func(cmd *cobra.Command, args []string) error {
+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

+59
View File
@@ -0,0 +1,59 @@
# User Guide
## Initialising a cluster
To begin setting up a new Uncloud cluster, create the desired nodes. Ensure that their firewall allows the required ports.
### Ports
Nodes running Uncloud with a standard configuration must have the following inbound ports allowed in the firewall:
* 51820/udp (WireGuard meshnet)
* 22/tcp (default SSH management port)
* 80/tcp (required for challenge if uncloud.run DNS is enabled, pass --no-dns during init to disable)
In addition, any ports for any workloads you want to expose will need to be open.
### Configuration
Uncloud stores its configuration in `~/.config/uncloud/config.yaml`. If you wish to reinitialise a cluster, simply remove it from this config.
### Initialisation
Begin by initialising the first node in your cluster with `uc machine init [USER@HOST:PORT]`. If you do not have a need for Caddy reverse proxy, you may disable this feature with `--no-caddy`.
This command will idempotently install Docker, uncloudd, uncloud-corrosion. If Caddy is enabled, it will set up a reverse proxy. If Uncloud DNS is enabled, it will create a DNS A record for the machine's public IP address under `*.[CLUSTER ID].cluster.uncloud.run`.
If you wish to uninstall Uncloud and its components, run `uncloud-uninstall`.
### Adding a node
Just like the initialisation of the first node, a node can be added to the cluster with `uc machine add`.
### DNS
Uncloud (uncloud.run) DNS can be managed with the `uc dns` subcommand.
* To reserve a domain name, run `uc dns reserve`
* To release a domain name, run `uc dns release`.
* To see the domain name, run `uc dns show`
### Running a service
Services on an Uncloud cluster can be managed with `uc service`.
You can run a service with two replicas that expose port 80 like the following:
```
uc service run -p 80/http --replicas 2 nginxdemos/hello
```
This requires the Caddy reverse proxy to be deployed. If it wasn't previously, it can be deployed with `uc caddy deploy`.
Since this doesn't specify a service name, a random one will be generated.
The service can be deleted by its service name:
```
uc service rm hello-gsdo
```
+1 -1
View File
@@ -11,7 +11,7 @@ import (
"go.uber.org/zap"
"net/netip"
"time"
"uncloud/internal/machine/network"
"github.com/psviderski/uncloud/internal/machine/network"
)
const (
+51 -31
View File
@@ -1,23 +1,30 @@
module uncloud
module github.com/psviderski/uncloud
go 1.23
go 1.23.0
toolchain go1.23.2
require (
github.com/BurntSushi/toml v1.4.0
github.com/Masterminds/semver v1.5.0
github.com/Masterminds/squirrel v1.5.4
github.com/caddyserver/caddy/v2 v2.8.4
github.com/cenkalti/backoff/v4 v4.3.0
github.com/charmbracelet/huh v0.6.0
github.com/charmbracelet/lipgloss v0.13.0
github.com/compose-spec/compose-go/v2 v2.4.5
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf
github.com/deckarep/golang-set/v2 v2.8.0
github.com/dgraph-io/badger/v3 v3.2103.5
github.com/distribution/reference v0.6.0
github.com/docker/cli v27.4.0-rc.4+incompatible
github.com/docker/cli v27.5.0+incompatible
github.com/docker/compose/v2 v2.31.0
github.com/docker/docker v27.4.0-rc.2+incompatible
github.com/docker/go-connections v0.5.0
github.com/docker/go-units v0.5.0
github.com/goccy/go-yaml v1.17.1
github.com/google/go-cmp v0.7.0
github.com/google/go-containerregistry v0.20.2
github.com/hashicorp/memberlist v0.5.1
github.com/hashicorp/serf v0.10.1
github.com/ipfs/boxo v0.21.0
@@ -27,7 +34,10 @@ require (
github.com/ipfs/go-ds-crdt v0.5.2
github.com/ipfs/go-ipld-format v0.6.0
github.com/ipfs/go-log/v2 v2.5.1
github.com/jmoiron/sqlx v1.4.0
github.com/lmittmann/tint v1.0.5
github.com/miekg/dns v1.1.65
github.com/opencontainers/go-digest v1.0.0
github.com/opencontainers/image-spec v1.1.0
github.com/siderolabs/discovery-api v0.1.4
github.com/siderolabs/discovery-client v0.1.9
@@ -37,15 +47,16 @@ require (
github.com/vishvananda/netlink v1.3.0
go.uber.org/zap v1.27.0
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba
golang.org/x/crypto v0.31.0
golang.org/x/net v0.32.0
golang.org/x/sync v0.10.0
golang.org/x/sys v0.28.0
golang.org/x/crypto v0.33.0
golang.org/x/net v0.35.0
golang.org/x/sync v0.11.0
golang.org/x/sys v0.31.0
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6
google.golang.org/genproto/googleapis/rpc v0.0.0-20241007155032-5fefd90f89a9
google.golang.org/grpc v1.68.0
google.golang.org/protobuf v1.36.0
google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576
google.golang.org/grpc v1.68.1
google.golang.org/protobuf v1.36.3
modernc.org/sqlite v1.36.3
)
require (
@@ -75,13 +86,11 @@ require (
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/charmbracelet/bubbles v0.20.0 // indirect
github.com/charmbracelet/bubbletea v1.1.1 // indirect
github.com/charmbracelet/lipgloss v0.13.0 // indirect
github.com/charmbracelet/x/ansi v0.3.2 // indirect
github.com/charmbracelet/x/exp/strings v0.0.0-20240919170804-a4978c8e603a // indirect
github.com/charmbracelet/x/term v0.2.0 // indirect
github.com/chzyer/readline v1.5.1 // indirect
github.com/cloudflare/cfssl v1.6.4 // indirect
github.com/compose-spec/compose-go/v2 v2.4.5 // indirect
github.com/containerd/console v1.0.4 // indirect
github.com/containerd/containerd v1.7.24 // indirect
github.com/containerd/containerd/api v1.7.19 // indirect
@@ -89,9 +98,10 @@ require (
github.com/containerd/errdefs v0.3.0 // indirect
github.com/containerd/log v0.1.0 // indirect
github.com/containerd/platforms v0.2.1 // indirect
github.com/containerd/stargz-snapshotter/estargz v0.16.3 // indirect
github.com/containerd/ttrpc v1.2.5 // indirect
github.com/containerd/typeurl/v2 v2.2.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect
github.com/dgraph-io/badger v1.6.2 // indirect
@@ -115,7 +125,7 @@ require (
github.com/go-logfmt/logfmt v0.6.0 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-sql-driver/mysql v1.7.1 // indirect
github.com/go-sql-driver/mysql v1.8.1 // indirect
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
github.com/go-viper/mapstructure/v2 v2.0.0 // indirect
github.com/godbus/dbus/v5 v5.1.0 // indirect
@@ -128,12 +138,11 @@ require (
github.com/google/btree v1.1.2 // indirect
github.com/google/cel-go v0.20.1 // indirect
github.com/google/flatbuffers v24.3.25+incompatible // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad // indirect
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/mux v1.8.1 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
github.com/hashicorp/go-msgpack v0.5.5 // indirect
@@ -189,11 +198,11 @@ require (
github.com/mdlayher/socket v0.5.1 // indirect
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
github.com/mholt/acmez/v2 v2.0.3 // indirect
github.com/miekg/dns v1.1.62 // indirect
github.com/miekg/pkcs11 v1.1.1 // indirect
github.com/minio/sha256-simd v1.0.1 // indirect
github.com/mitchellh/cli v1.1.5 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/go-ps v1.0.0 // indirect
github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
@@ -204,6 +213,8 @@ require (
github.com/moby/patternmatcher v0.6.0 // indirect
github.com/moby/sys/sequential v0.6.0 // indirect
github.com/moby/sys/signal v0.7.1 // indirect
github.com/moby/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/morikuni/aec v1.0.0 // indirect
github.com/mr-tron/base58 v1.2.0 // indirect
@@ -220,8 +231,8 @@ require (
github.com/multiformats/go-multistream v0.5.0 // indirect
github.com/multiformats/go-varint v0.0.7 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/onsi/ginkgo/v2 v2.22.0 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/pelletier/go-toml v1.9.5 // indirect
github.com/pires/go-proxyproto v0.7.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
@@ -235,6 +246,7 @@ require (
github.com/prometheus/procfs v0.15.1 // indirect
github.com/quic-go/qpack v0.5.1 // indirect
github.com/quic-go/quic-go v0.48.2 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/rs/xid v1.5.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
@@ -263,22 +275,27 @@ require (
github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea // indirect
github.com/tonistiigi/vt100 v0.0.0-20240514184818-90bafcd6abab // indirect
github.com/urfave/cli v1.22.16 // indirect
github.com/vbatts/tar-split v0.11.6 // indirect
github.com/vishvananda/netns v0.0.4 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
github.com/zeebo/blake3 v0.2.4 // indirect
go.etcd.io/bbolt v1.3.11 // indirect
go.opencensus.io v0.24.0 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.46.1 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.52.0 // indirect
go.opentelemetry.io/otel v1.32.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 // indirect
go.opentelemetry.io/otel v1.33.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.44.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.27.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0 // indirect
go.opentelemetry.io/otel/metric v1.32.0 // indirect
go.opentelemetry.io/otel/sdk v1.32.0 // indirect
go.opentelemetry.io/otel/metric v1.33.0 // indirect
go.opentelemetry.io/otel/sdk v1.33.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.32.0 // indirect
go.opentelemetry.io/otel/trace v1.32.0 // indirect
go.opentelemetry.io/proto/otlp v1.3.1 // indirect
go.opentelemetry.io/otel/trace v1.33.0 // indirect
go.opentelemetry.io/proto/otlp v1.4.0 // indirect
go.step.sm/cli-utils v0.9.0 // indirect
go.step.sm/crypto v0.45.0 // indirect
go.step.sm/linkedca v0.20.1 // indirect
@@ -288,16 +305,19 @@ require (
go.uber.org/zap/exp v0.2.0 // indirect
golang.org/x/crypto/x509roots/fallback v0.0.0-20240507223354-67b13616a595 // indirect
golang.org/x/exp v0.0.0-20241215155358-4a5509556b9e // indirect
golang.org/x/mod v0.22.0 // indirect
golang.org/x/term v0.27.0 // indirect
golang.org/x/text v0.21.0 // indirect
golang.org/x/mod v0.23.0 // indirect
golang.org/x/term v0.29.0 // indirect
golang.org/x/text v0.22.0 // indirect
golang.org/x/time v0.8.0 // indirect
golang.org/x/tools v0.28.0 // indirect
golang.org/x/tools v0.30.0 // indirect
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
google.golang.org/genproto v0.0.0-20240401170217-c3f982113cda // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259 // indirect
howett.net/plist v1.0.0 // indirect
lukechampine.com/blake3 v1.3.0 // indirect
modernc.org/libc v1.61.13 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.8.2 // indirect
)
+110 -61
View File
@@ -31,6 +31,8 @@ github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ
github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww=
github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0=
@@ -199,9 +201,8 @@ github.com/containerd/nydus-snapshotter v0.14.0 h1:6/eAi6d7MjaeLLuMO8Udfe5GVsDud
github.com/containerd/nydus-snapshotter v0.14.0/go.mod h1:TT4jv2SnIDxEBu4H2YOvWQHPOap031ydTaHTuvc5VQk=
github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A=
github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw=
github.com/containerd/stargz-snapshotter v0.15.1 h1:fpsP4kf/Z4n2EYnU0WT8ZCE3eiKDwikDhL6VwxIlgeA=
github.com/containerd/stargz-snapshotter/estargz v0.15.1 h1:eXJjw9RbkLFgioVaTG+G/ZW/0kEe2oEKCdS/ZxIyoCU=
github.com/containerd/stargz-snapshotter/estargz v0.15.1/go.mod h1:gr2RNwukQ/S9Nv33Lt6UC7xEx58C+LHRdoqbEKjz1Kk=
github.com/containerd/stargz-snapshotter/estargz v0.16.3 h1:7evrXtoh1mSbGj/pfRccTampEyKpjpOnS3CyiV1Ebr8=
github.com/containerd/stargz-snapshotter/estargz v0.16.3/go.mod h1:uyr4BfYfOj3G9WBVE8cOlQmXAbPN9VEQpBBeJIuOipU=
github.com/containerd/ttrpc v1.2.5 h1:IFckT1EFQoFBMG4c3sMdT8EP3/aKfumK1msY+Ze4oLU=
github.com/containerd/ttrpc v1.2.5/go.mod h1:YCXHsb32f+Sq5/72xHubdiJRQY9inL4a4ZQrAbN1q9o=
github.com/containerd/typeurl/v2 v2.2.0 h1:6NBDbQzr7I5LHgp34xAXYF5DOTQDn05X58lsPEmzLso=
@@ -218,8 +219,9 @@ github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSV
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc=
github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/creack/pty v1.1.21 h1:1/QdRyBaHHJP61QkWMXlOIBfsgdDeeKfK8SYVUWJKf0=
@@ -231,6 +233,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c h1:pFUpOrbxDR6AkioZ1ySsx5yxlDQZ8stG2b88gTPxgJU=
github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c/go.mod h1:6UhI8N9EjYm1c2odKpFpAYeR8dsBeM7PtzQhRgxRr9U=
github.com/deckarep/golang-set/v2 v2.8.0 h1:swm0rlPCmdWn9mESxKOjWk8hXSqoxOp+ZlfuyaAdFlQ=
github.com/deckarep/golang-set/v2 v2.8.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4=
github.com/decred/dcrd/crypto/blake256 v1.0.1 h1:7PltbUIQB7u/FfZ39+DGa/ShuMyJ5ilcvdfma9wOH6Y=
github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnNEcHYvcCuK6dPZSg=
@@ -253,8 +257,8 @@ github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5Qvfr
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/docker/buildx v0.18.0 h1:rSauXHeJt90NvtXrLK5J992Eb0UPJZs2vV3u1zTf1nE=
github.com/docker/buildx v0.18.0/go.mod h1:JGNSshOhHs5FhG3u51jXUf4lLOeD2QBIlJ2vaRB67p4=
github.com/docker/cli v27.4.0-rc.4+incompatible h1:nnrHCfd5eODviSGzCpIyYFcJovrq+/dVW4K4oqQu7WA=
github.com/docker/cli v27.4.0-rc.4+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/cli v27.5.0+incompatible h1:aMphQkcGtpHixwwhAXJT1rrK/detk2JIvDaFkLctbGM=
github.com/docker/cli v27.5.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/compose/v2 v2.31.0 h1:8Sm0c4MjIhksguxIA5koYMXoTJDAp/CaZ1cdZrMvMdw=
github.com/docker/compose/v2 v2.31.0/go.mod h1:oQq3UDEdsnB3AUO72AxaoeLbkCgmUu1+8tLzvmphmXA=
github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
@@ -330,8 +334,8 @@ github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ4
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-sql-driver/mysql v1.3.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI=
github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/go-stack/stack v1.6.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
@@ -339,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/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0=
github.com/goccy/go-yaml v1.17.1 h1:LI34wktB2xEE3ONG/2Ar54+/HJVBriAGJ55PHls4YuY=
github.com/goccy/go-yaml v1.17.1/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E=
@@ -393,8 +399,10 @@ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/go-containerregistry v0.20.2 h1:B1wPJ1SN/S7pB+ZAimcciVD+r+yV/l/DSArMxlbwseo=
github.com/google/go-containerregistry v0.20.2/go.mod h1:z38EKdKh4h7IP2gSfUUqEvalZBqs6AoLeWfUy34nQC8=
github.com/google/go-tpm v0.9.0 h1:sQF6YqWMi+SCXpsmS3fd21oPy/vSddwZry4JnmltHVk=
github.com/google/go-tpm v0.9.0/go.mod h1:FkNVkc6C+IsvDI9Jw1OveJmxGZUUaKxtrpOS47QWKfU=
github.com/google/go-tpm-tools v0.4.4 h1:oiQfAIkc6xTy9Fl5NKTeTJkBTlXdHsxAofmQyxBKY98=
@@ -404,8 +412,8 @@ github.com/google/go-tspi v0.3.0/go.mod h1:xfMGI3G0PhxCdNVcYr1C4C+EizojDg/TXuX5b
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8=
github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo=
github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad h1:a6HEuzUHeKH6hwfN/ZoQgRgVIWFJljSWa/zetS2WTvg=
github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o=
github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw=
@@ -427,8 +435,8 @@ github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0 h1:TmHmbvxPmaegwhDubVz0lICL0J5Ka2vwTzhoePEXsGE=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0/go.mod h1:qztMSjm835F2bXf+5HKAPIS5qsmQDqZna/PgVt4rWtI=
github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed h1:5upAirOpQc1Q53c0bnx2ufif5kANL7bfZWcc6VJWJd8=
github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
@@ -593,8 +601,8 @@ github.com/jinzhu/gorm v0.0.0-20170222002820-5409931a1bb8/go.mod h1:Vla75njaFJ8c
github.com/jinzhu/inflection v0.0.0-20170102125226-1c35d901db3d h1:jRQLvyVGL+iVtDElaEIDdKwpPqUIZJfzkNLV34htpEc=
github.com/jinzhu/inflection v0.0.0-20170102125226-1c35d901db3d/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jmoiron/sqlx v1.3.3 h1:j82X0bf7oQ27XeqxicSZsTU5suPwKElg3oyxNn43iTk=
github.com/jmoiron/sqlx v1.3.3/go.mod h1:2BljVx/86SuTyjE+aPYlHCTNvZrnJXghYGpNiXLBMCQ=
github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=
github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
@@ -635,8 +643,9 @@ github.com/lib/pq v0.0.0-20150723085316-0dad96c0b94f/go.mod h1:5WUZQaWbwv1U+lTRe
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8=
github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/libdns/libdns v0.2.2 h1:O6ws7bAfRPaBsgAYt8MDe2HcNBGC29hkZ9MX2eUSX3s=
github.com/libdns/libdns v0.2.2/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ=
github.com/libp2p/go-buffer-pool v0.1.0 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6cdF0Y8=
@@ -697,6 +706,8 @@ github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh
github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk=
github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
github.com/mattn/go-sqlite3 v1.6.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mdlayher/genetlink v1.3.2 h1:KdrNKe+CTu+IbZnm/GVUMXSqBBLqcGpRDa0xkQy56gw=
github.com/mdlayher/genetlink v1.3.2/go.mod h1:tcC3pkCrPUGIKKsCsp0B3AdaaKuHtaxoJRz3cc+528o=
@@ -710,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/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI=
github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ=
github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ=
github.com/miekg/dns v1.1.65 h1:0+tIPHzUW0GCge7IiK3guGP57VAw7hoPDfApjkMD1Fc=
github.com/miekg/dns v1.1.65/go.mod h1:Dzw9769uoKVaLuODMDZz9M6ynFU6Em65csPuoi8G0ck=
github.com/miekg/pkcs11 v1.0.2/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs=
github.com/miekg/pkcs11 v1.1.1 h1:Ugu9pdy6vAYku5DEpVWVFPYnzV+bxB+iRdbuFSu7TvU=
github.com/miekg/pkcs11 v1.1.1/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs=
@@ -731,6 +742,7 @@ github.com/mitchellh/cli v1.1.5/go.mod h1:v8+iFts2sPIKUV1ltktPXMCC8fumSKFItNcD2c
github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc=
github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg=
@@ -804,6 +816,8 @@ github.com/multiformats/go-varint v0.0.7/go.mod h1:r8PUYw/fD/SjBCiKOoDlGF6QawOEL
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.0/go.mod h1:oUhWkIvk5aDxtKvDDuw8gItl8pKl42LzjC9KZE0HfGg=
@@ -920,12 +934,14 @@ github.com/quic-go/webtransport-go v0.8.0 h1:HxSrwun11U+LlmwpgM1kEqIqH90IT4N8auv
github.com/quic-go/webtransport-go v0.8.0/go.mod h1:N99tjprW432Ut5ONql/aUhSLT0YVSlwHohQsuac9WaM=
github.com/raulk/go-watchdog v1.3.0 h1:oUmdlHxdkXRJlwfG0O9omj8ukerm8MEQavSiDTEtBsk=
github.com/raulk/go-watchdog v1.3.0/go.mod h1:fIvOnLbF0b0ZwkB9YU4mOW9Did//4vPZtDqv66NfsMU=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc=
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
@@ -1052,8 +1068,8 @@ github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljT
github.com/urfave/cli v1.22.10/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/urfave/cli v1.22.16 h1:MH0k6uJxdwdeWQTwhSO42Pwr4YLrNLwBtg1MRgTqPdQ=
github.com/urfave/cli v1.22.16/go.mod h1:EeJR6BKodywf4zciqrdw6hpCPk68JO9z5LazXZMn5Po=
github.com/vbatts/tar-split v0.11.5 h1:3bHCTIheBm1qFTcgh9oPu+nNBtX+XJIupG/vacinCts=
github.com/vbatts/tar-split v0.11.5/go.mod h1:yZbwRsSeGjusneWgA781EKej9HF8vme8okylkAeNKLk=
github.com/vbatts/tar-split v0.11.6 h1:4SjTW5+PU11n6fZenf2IPoV8/tz3AaYHMWjf23envGs=
github.com/vbatts/tar-split v0.11.6/go.mod h1:dqKNtesIOr2j2Qv3W/cHjnvk9I8+G7oAkFDFN6TCBEI=
github.com/vishvananda/netlink v1.3.0 h1:X7l42GfcV4S6E4vHTsw48qbrV+9PVojNfIhZcwQdrZk=
github.com/vishvananda/netlink v1.3.0/go.mod h1:i6NetklAujEcC6fK0JPjT8qSwWyO0HLn4UKG+hGqeJs=
github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8=
@@ -1064,6 +1080,13 @@ github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0 h1:GDDkbFiaK8jsSD
github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0/go.mod h1:x6AKhvSSexNrVSrViXSHUEbICjmGXhtgABaHIySUSGw=
github.com/weppos/publicsuffix-go v0.15.1-0.20210511084619-b1f36a2d6c0b h1:FsyNrX12e5BkplJq7wKOLk0+C6LZ+KGXvuEcKUYm5ss=
github.com/weppos/publicsuffix-go v0.15.1-0.20210511084619-b1f36a2d6c0b/go.mod h1:HYux0V0Zi04bHNwOHy4cXJVz/TQjYonnF6aoYhj+3QE=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo=
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
@@ -1085,32 +1108,34 @@ go.etcd.io/bbolt v1.3.11/go.mod h1:dksAq7YMXoljX0xu6VF5DMZGbhYYoLUalEiSySYAS4I=
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 h1:4Pp6oUg3+e/6M4C0A/3kJ2VYa++dsWVTtGgLVj5xtHg=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0/go.mod h1:Mjt1i1INqiaoZOMGR1RIUJN+i3ChKoFRqzrRQhlkbs0=
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.46.1 h1:gbhw/u49SS3gkPWiYweQNJGm/uJN5GkI/FrosxSHT7A=
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.46.1/go.mod h1:GnOaBaFQ2we3b9AGWJpsBa7v1S5RlQzlC3O7dRMxZhM=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.52.0 h1:9l89oX4ba9kHbBol3Xin3leYJ+252h0zszDtBwyKe2A=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.52.0/go.mod h1:XLZfZboOJWHNKUv7eH0inh0E9VV6eWDFB/9yJyTLPp0=
go.opentelemetry.io/otel v1.32.0 h1:WnBN+Xjcteh0zdk01SVqV55d/m62NJLJdIyb4y/WO5U=
go.opentelemetry.io/otel v1.32.0/go.mod h1:00DCVSB0RQcnzlwyTfqtxSm+DRr9hpYrHjNGiBHVQIg=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 h1:yd02MEjBdJkG3uabWP9apV+OuWRIXGDuJEUJbOHmCFU=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0/go.mod h1:umTcuxiv1n/s/S6/c2AT/g2CQ7u5C59sHDNmfSwgz7Q=
go.opentelemetry.io/otel v1.33.0 h1:/FerN9bax5LoK51X/sI0SVYrjSE0/yUL7DpxW4K3FWw=
go.opentelemetry.io/otel v1.33.0/go.mod h1:SUUkR6csvUQl+yjReHu5uM3EtVV7MBm5FHKRlNx4I8I=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.44.0 h1:jd0+5t/YynESZqsSyPz+7PAFdEop0dlN0+PkyHYo8oI=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.44.0/go.mod h1:U707O40ee1FpQGyhvqnzmCJm1Wh6OX6GGBVn0E6Uyyk=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.27.0 h1:R9DE4kQ4k+YtfLI2ULwX82VtNQ2J8yZmA7ZIF/D+7Mc=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.27.0/go.mod h1:OQFyQVrDlbe+R7xrEyDr/2Wr67Ol0hRUgsfA+V5A95s=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0 h1:Vh5HayB/0HHfOQA7Ctx69E/Y/DcQSMPpKANYVMQ7fBA=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0/go.mod h1:cpgtDBaqD/6ok/UG0jT15/uKjAY8mRA53diogHBg3UI=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0 h1:qFffATk0X+HD+f1Z8lswGiOQYKHRlzfmdJm0wEaVrFA=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0/go.mod h1:MOiCmryaYtc+V0Ei+Tx9o5S1ZjA7kzLucuVuyzBZloQ=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.27.0 h1:QY7/0NeRPKlzusf40ZE4t1VlMKbqSNT7cJRYzWuja0s=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.27.0/go.mod h1:HVkSiDhTM9BoUJU8qE6j2eSWLLXvi1USXjyd2BXT8PY=
go.opentelemetry.io/otel/metric v1.32.0 h1:xV2umtmNcThh2/a/aCP+h64Xx5wsj8qqnkYZktzNa0M=
go.opentelemetry.io/otel/metric v1.32.0/go.mod h1:jH7CIbbK6SH2V2wE16W05BHCtIDzauciCRLoc/SyMv8=
go.opentelemetry.io/otel/sdk v1.32.0 h1:RNxepc9vK59A8XsgZQouW8ue8Gkb4jpWtJm9ge5lEG4=
go.opentelemetry.io/otel/sdk v1.32.0/go.mod h1:LqgegDBjKMmb2GC6/PrTnteJG39I8/vJCAP9LlJXEjU=
go.opentelemetry.io/otel/metric v1.33.0 h1:r+JOocAyeRVXD8lZpjdQjzMadVZp2M4WmQ+5WtEnklQ=
go.opentelemetry.io/otel/metric v1.33.0/go.mod h1:L9+Fyctbp6HFTddIxClbQkjtubW6O9QS3Ann/M82u6M=
go.opentelemetry.io/otel/sdk v1.33.0 h1:iax7M131HuAm9QkZotNHEfstof92xM+N8sr3uHXc2IM=
go.opentelemetry.io/otel/sdk v1.33.0/go.mod h1:A1Q5oi7/9XaMlIWzPSxLRWOI8nG3FnzHJNbiENQuihM=
go.opentelemetry.io/otel/sdk/metric v1.32.0 h1:rZvFnvmvawYb0alrYkjraqJq0Z4ZUJAiyYCU9snn1CU=
go.opentelemetry.io/otel/sdk/metric v1.32.0/go.mod h1:PWeZlq0zt9YkYAp3gjKZ0eicRYvOh1Gd+X99x6GHpCQ=
go.opentelemetry.io/otel/trace v1.32.0 h1:WIC9mYrXf8TmY/EXuULKc8hR17vE+Hjv2cssQDe03fM=
go.opentelemetry.io/otel/trace v1.32.0/go.mod h1:+i4rkvCraA+tG6AzwloGaCtkx53Fa+L+V8e9a7YvhT8=
go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0=
go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8=
go.opentelemetry.io/otel/trace v1.33.0 h1:cCJuF7LRjUFso9LPnEAHJDB2pqzp+hbO8eu1qqW2d/s=
go.opentelemetry.io/otel/trace v1.33.0/go.mod h1:uIcdVUZMpTAmz0tI1z04GoVSezK37CbGV4fr1f2nBck=
go.opentelemetry.io/proto/otlp v1.4.0 h1:TA9WRvW6zMwP+Ssb6fLoUIuirti1gGbP28GcKG1jgeg=
go.opentelemetry.io/proto/otlp v1.4.0/go.mod h1:PPBWZIP98o2ElSqI35IHfu7hIhSwvc5N38Jw8pXuGFY=
go.step.sm/cli-utils v0.9.0 h1:55jYcsQbnArNqepZyAwcato6Zy2MoZDRkWW+jF+aPfQ=
go.step.sm/cli-utils v0.9.0/go.mod h1:Y/CRoWl1FVR9j+7PnAewufAwKmBOTzR6l9+7EYGAnp8=
go.step.sm/crypto v0.45.0 h1:Z0WYAaaOYrJmKP9sJkPW+6wy3pgN3Ija8ek/D4serjc=
@@ -1171,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.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.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
golang.org/x/crypto/x509roots/fallback v0.0.0-20240507223354-67b13616a595 h1:TgSqweA595vD0Zt86JzLv3Pb/syKg8gd5KMGGbJPYFw=
golang.org/x/crypto/x509roots/fallback v0.0.0-20240507223354-67b13616a595/go.mod h1:kNa9WdvYnzFwC79zRpLRMJbdEFlhyM5RPFBBZp/wWH8=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@@ -1189,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.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4=
golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM=
golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -1212,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.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.32.0 h1:ZqPmj8Kzc+Y6e0+skZsuACbx+wzMgo5MQsJh9Qd6aYI=
golang.org/x/net v0.32.0/go.mod h1:CwU0IoeOlnQQWJ6ioyFrfRuomB8GKF6KbYXZVyeXNfs=
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE=
golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
@@ -1227,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-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -1276,8 +1301,8 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@@ -1285,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.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
@@ -1297,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.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg=
golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -1320,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.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.28.0 h1:WuB6qZ4RPCQo5aP3WdKZS7i595EdWqWR8vqJTlwTVK8=
golang.org/x/tools v0.28.0/go.mod h1:dcIOrVd3mfQKTgrDVQHqCPMWy6lnhfhtX3hLXYVLfRw=
golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY=
golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY=
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -1344,10 +1369,10 @@ google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20240401170217-c3f982113cda h1:wu/KJm9KJwpfHWhkkZGohVC6KRrc1oJNr4jwtQMOQXw=
google.golang.org/genproto v0.0.0-20240401170217-c3f982113cda/go.mod h1:g2LLCvCeCSir/JJSWosk19BR4NVxGqHUC6rxIRsd7Aw=
google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 h1:hjSy6tcFQZ171igDaN5QHOw2n6vx40juYbC/x67CEhc=
google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:qpvKtACPCQhAdu3PyQgV4l3LMXZEtft7y8QcarRsp9I=
google.golang.org/genproto/googleapis/rpc v0.0.0-20241007155032-5fefd90f89a9 h1:QCqS/PdaHTSWGvupk2F/ehwHtGc0/GYkT+3GAcR1CCc=
google.golang.org/genproto/googleapis/rpc v0.0.0-20241007155032-5fefd90f89a9/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI=
google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 h1:CkkIfIt50+lT6NHAVoRYEyAvQGFM7xEwXUUywFvEb3Q=
google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576/go.mod h1:1R3kvZ1dtP3+4p4d3G8uJ8rFk/fWlScl38vanWACI08=
google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576 h1:8ZmaLZE4XWrtU3MyClkYqqtl6Oegr3235h7jxsDyqCY=
google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU=
google.golang.org/grpc v1.0.5/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
@@ -1355,8 +1380,8 @@ google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyac
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.68.0 h1:aHQeeJbo8zAkAa3pRzrVjZlbz6uSfeOXlJNQM0RAbz0=
google.golang.org/grpc v1.68.0/go.mod h1:fmSPC5AsjSBCK54MyHRx48kpOti1/jRfOlwEWywNjWA=
google.golang.org/grpc v1.68.1 h1:oI5oTa11+ng8r8XMMN7jAOmWfPZWbYpCFaMUTACxkM0=
google.golang.org/grpc v1.68.1/go.mod h1:+q1XYFJjShcqn0QZHvCyeR4CXPA+llXIeUIfIe00waw=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
@@ -1366,8 +1391,8 @@ google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.36.0 h1:mjIs9gYtt56AzC4ZaffQuh88TZurBGhIJMBZGSxNerQ=
google.golang.org/protobuf v1.36.0/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
google.golang.org/protobuf v1.36.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU=
google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/cenkalti/backoff.v2 v2.2.1 h1:eJ9UAg01/HIHG987TwxvnzK2MgxXq97YY6rYDpY9aII=
@@ -1413,3 +1438,27 @@ k8s.io/klog/v2 v2.110.1 h1:U/Af64HJf7FcwMcXyKm2RPM22WZzyR7OSpYj5tg3cL0=
k8s.io/klog/v2 v2.110.1/go.mod h1:YGtd1984u+GgbuZ7e08/yBuAfKLSO0+uR1Fhi6ExXjo=
lukechampine.com/blake3 v1.3.0 h1:sJ3XhFINmHSrYCgl958hscfIa3bw8x4DqMP3u1YvoYE=
lukechampine.com/blake3 v1.3.0/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k=
modernc.org/cc/v4 v4.24.4 h1:TFkx1s6dCkQpd6dKurBNmpo+G8Zl4Sq/ztJ+2+DEsh0=
modernc.org/cc/v4 v4.24.4/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
modernc.org/ccgo/v4 v4.23.16 h1:Z2N+kk38b7SfySC1ZkpGLN2vthNJP1+ZzGZIlH7uBxo=
modernc.org/ccgo/v4 v4.23.16/go.mod h1:nNma8goMTY7aQZQNTyN9AIoJfxav4nvTnvKThAeMDdo=
modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=
modernc.org/gc/v2 v2.6.3 h1:aJVhcqAte49LF+mGveZ5KPlsp4tdGdAOT4sipJXADjw=
modernc.org/gc/v2 v2.6.3/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
modernc.org/libc v1.61.13 h1:3LRd6ZO1ezsFiX1y+bHd1ipyEHIJKvuprv0sLTBwLW8=
modernc.org/libc v1.61.13/go.mod h1:8F/uJWL/3nNil0Lgt1Dpz+GgkApWh04N3el3hxJcA6E=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.8.2 h1:cL9L4bcoAObu4NkxOlKWBWtNHIsnnACGF/TbqQ6sbcI=
modernc.org/memory v1.8.2/go.mod h1:ZbjSvMO5NQ1A2i3bWeDiVMxIorXwdClKE/0SZ+BMotU=
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
modernc.org/sqlite v1.36.3 h1:qYMYlFR+rtLDUzuXoST1SDIdEPbX8xzuhdF90WsX1ss=
modernc.org/sqlite v1.36.3/go.mod h1:ADySlx7K4FdY5MaJcEv86hTJ0PjedAloTUuif0YS3ws=
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
-103
View File
@@ -1,103 +0,0 @@
package api
import (
"encoding/json"
"fmt"
"github.com/distribution/reference"
"reflect"
"uncloud/internal/machine/api/pb"
)
const (
ServiceModeReplicated = "replicated"
ServiceModeGlobal = "global"
)
type ServiceSpec struct {
Container ContainerSpec
// Mode is the replication mode of the service. Default is ServiceModeReplicated if empty.
Mode string
Name string
// Ports defines what service ports to publish to make the service accessible outside the cluster.
Ports []PortSpec
}
func (s *ServiceSpec) Validate() error {
if err := s.Container.Validate(); err != nil {
return err
}
switch s.Mode {
case "", ServiceModeGlobal, ServiceModeReplicated:
default:
return fmt.Errorf("invalid mode: %q", s.Mode)
}
// TODO: validate there is no conflict between ports.
return nil
}
func (s *ServiceSpec) Equals(spec ServiceSpec) bool {
// TODO: ignore order of ports.
return reflect.DeepEqual(*s, spec)
}
type ContainerSpec struct {
Command []string
Image string
// Run a custom init inside the container. If nil, use the daemon's configured settings.
Init *bool
// List of volumes to bind mount into the container.
Volumes []string
}
func (s *ContainerSpec) Validate() error {
if _, err := reference.ParseDockerRef(s.Image); err != nil {
return fmt.Errorf("invalid image: %w", err)
}
return nil
}
type Service struct {
ID string
Name string
Mode string
Containers []MachineContainer
}
type MachineContainer struct {
MachineID string
Container Container
}
func ServiceFromProto(s *pb.Service) (Service, error) {
var err error
containers := make([]MachineContainer, len(s.Containers))
for i, sc := range s.Containers {
containers[i], err = machineContainerFromProto(sc)
if err != nil {
return Service{}, err
}
}
return Service{
ID: s.Id,
Name: s.Name,
Mode: s.Mode,
Containers: containers,
}, nil
}
func machineContainerFromProto(sc *pb.Service_Container) (MachineContainer, error) {
var c Container
if err := json.Unmarshal(sc.Container, &c); err != nil {
return MachineContainer{}, fmt.Errorf("unmarshal container: %w", err)
}
return MachineContainer{
MachineID: sc.MachineId,
Container: c,
}, nil
}
+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
}
+235 -114
View File
@@ -5,87 +5,124 @@ import (
"errors"
"fmt"
"net/netip"
"uncloud/internal/cli/client"
"uncloud/internal/cli/client/connector"
"uncloud/internal/cli/config"
"uncloud/internal/fs"
"uncloud/internal/machine"
"uncloud/internal/machine/api/pb"
"uncloud/internal/sshexec"
"os"
"github.com/charmbracelet/huh"
"github.com/docker/cli/cli/streams"
"github.com/psviderski/uncloud/internal/cli/config"
"github.com/psviderski/uncloud/internal/fs"
"github.com/psviderski/uncloud/internal/machine"
"github.com/psviderski/uncloud/internal/machine/api/pb"
"github.com/psviderski/uncloud/internal/sshexec"
"github.com/psviderski/uncloud/pkg/api"
"github.com/psviderski/uncloud/pkg/client"
"github.com/psviderski/uncloud/pkg/client/connector"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/emptypb"
)
const defaultClusterName = "default"
const defaultContextName = "default"
type CLI struct {
config *config.Config
Config *config.Config
conn *config.MachineConnection
}
func New(configPath string) (*CLI, error) {
// New creates a new CLI instance with the given config path or remote machine connection.
// If the connection is provided, the config is ignored for all operations which is useful for interacting with
// a cluster without creating a config.
func New(configPath string, conn *config.MachineConnection) (*CLI, error) {
if conn != nil {
return &CLI{conn: conn}, nil
}
cfg, err := config.NewFromFile(configPath)
if err != nil {
return nil, fmt.Errorf("read Uncloud config: %w", err)
}
return &CLI{
config: cfg,
Config: cfg,
}, nil
}
func (cli *CLI) CreateCluster(name string) error {
if _, ok := cli.config.Clusters[name]; ok {
return fmt.Errorf("cluster %q already exists", name)
func (cli *CLI) CreateContext(name string) error {
if _, ok := cli.Config.Contexts[name]; ok {
return fmt.Errorf("context '%s' already exists", name)
}
cli.config.Clusters[name] = &config.Cluster{
cli.Config.Contexts[name] = &config.Context{
Name: name,
}
return cli.config.Save()
return cli.Config.Save()
}
func (cli *CLI) SetCurrentCluster(name string) error {
if _, ok := cli.config.Clusters[name]; !ok {
return client.ErrNotFound
func (cli *CLI) SetCurrentContext(name string) error {
if _, ok := cli.Config.Contexts[name]; !ok {
return api.ErrNotFound
}
cli.config.CurrentCluster = name
return cli.config.Save()
cli.Config.CurrentContext = name
return cli.Config.Save()
}
func (cli *CLI) ConnectCluster(ctx context.Context, clusterName string) (*client.Client, error) {
if len(cli.config.Clusters) == 0 {
return nil, errors.New(
"no clusters found in the Uncloud config. " +
"Please initialise a cluster with `uncloud machine init` first",
// ConnectCluster connects to a cluster using the given context name or the current context if not specified.
// If the CLI was initialised with a machine connection, the config is ignored and the connection is used instead.
func (cli *CLI) ConnectCluster(ctx context.Context, contextName string) (*client.Client, error) {
if cli.conn != nil {
return connectCluster(ctx, *cli.conn)
}
if len(cli.Config.Contexts) == 0 {
return nil, fmt.Errorf(
"no cluster contexts found in the Uncloud config (%s). "+
"Please initialise a cluster with 'uncloud machine init' first",
cli.Config.Path(),
)
}
if clusterName == "" {
if contextName == "" {
// If the cluster is not specified, use the current cluster if set.
if cli.config.CurrentCluster == "" {
return nil, errors.New(
"the current cluster is not set in the Uncloud config. " +
"Please specify a cluster with the --cluster flag or set current_cluster in the config",
)
}
if _, ok := cli.config.Clusters[cli.config.CurrentCluster]; !ok {
if cli.Config.CurrentContext == "" {
return nil, fmt.Errorf(
"current cluster %q not found in the config. "+
"Please specify a cluster with the --cluster flag or update current_cluster in the config",
cli.config.CurrentCluster,
"the current cluster context is not set in the Uncloud config (%s). "+
"Please specify the context with the '--context' flag or set 'current_context' in the config",
cli.Config.Path(),
)
}
clusterName = cli.config.CurrentCluster
if _, ok := cli.Config.Contexts[cli.Config.CurrentContext]; !ok {
return nil, fmt.Errorf(
"current cluster context '%s' not found in the Uncloud config (%s). "+
"Please specify the context with the '--context' flag or update 'current_context' in the config",
cli.Config.CurrentContext,
cli.Config.Path(),
)
}
contextName = cli.Config.CurrentContext
}
cfg, ok := cli.config.Clusters[clusterName]
cfg, ok := cli.Config.Contexts[contextName]
if !ok {
return nil, fmt.Errorf("cluster %q not found in the config", clusterName)
return nil, fmt.Errorf("cluster context '%s' not found in the Uncloud config (%s)",
contextName, cli.Config.Path())
}
if len(cfg.Connections) == 0 {
return nil, fmt.Errorf("no connection configurations found for cluster %q in the config", clusterName)
return nil, fmt.Errorf(
"no connection configurations found for cluster context '%s' in the Uncloud config (%s)",
contextName, cli.Config.Path(),
)
}
// TODO: iterate over all connections and try to connect to the cluster using the first successful connection.
conn := cfg.Connections[0]
c, err := connectCluster(ctx, conn)
if err != nil {
return nil, fmt.Errorf("connect to cluster (context '%s'): %w", contextName, err)
}
return c, nil
}
func connectCluster(ctx context.Context, conn config.MachineConnection) (*client.Client, error) {
if conn.SSH != "" {
user, host, port, err := conn.SSH.Parse()
if err != nil {
@@ -101,114 +138,179 @@ func (cli *CLI) ConnectCluster(ctx context.Context, clusterName string) (*client
KeyPath: keyPath,
}
return client.New(ctx, connector.NewSSHConnector(sshConfig))
} else if conn.TCP.IsValid() {
return client.New(ctx, connector.NewTCPConnector(conn.TCP))
} else if conn.TCP != nil && conn.TCP.IsValid() {
return client.New(ctx, connector.NewTCPConnector(*conn.TCP))
}
return nil, errors.New("no valid connection configuration found for the cluster")
return nil, errors.New("connection configuration is invalid")
}
func (cli *CLI) InitCluster(
ctx context.Context, remoteMachine *RemoteMachine, clusterName, machineName string, netPrefix netip.Prefix,
) error {
if remoteMachine != nil {
return cli.initRemoteMachine(ctx, *remoteMachine, clusterName, machineName, netPrefix)
type InitClusterOptions struct {
Context string
MachineName string
Network netip.Prefix
PublicIP *netip.Addr
RemoteMachine *RemoteMachine
Version string
}
// InitCluster initialises a new cluster on a remote machine and returns a client to interact with the cluster.
// The client should be closed after use by the caller.
func (cli *CLI) InitCluster(ctx context.Context, opts InitClusterOptions) (*client.Client, error) {
if opts.RemoteMachine != nil {
return cli.initRemoteMachine(ctx, opts)
}
// TODO: implement local machine initialisation
return fmt.Errorf("local machine initialisation is not implemented yet")
return nil, fmt.Errorf("local machine initialisation is not implemented yet")
}
func (cli *CLI) initRemoteMachine(
ctx context.Context, remoteMachine RemoteMachine, clusterName, machineName string, netPrefix netip.Prefix,
) error {
if clusterName == "" {
clusterName = defaultClusterName
func (cli *CLI) initRemoteMachine(ctx context.Context, opts InitClusterOptions) (*client.Client, error) {
contextName := opts.Context
if contextName == "" {
contextName = defaultContextName
}
if _, ok := cli.config.Clusters[clusterName]; ok {
return fmt.Errorf("cluster %q already exists", clusterName)
if _, ok := cli.Config.Contexts[contextName]; ok {
return nil, fmt.Errorf("cluster context '%s' already exists", contextName)
}
machineClient, err := cli.provisionRemoteMachine(ctx, remoteMachine)
machineClient, err := cli.provisionRemoteMachine(ctx, *opts.RemoteMachine, opts.Version)
if err != nil {
return err
return nil, err
}
defer machineClient.Close()
// Ensure machineClient is closed on error.
defer func() {
if err != nil {
machineClient.Close()
}
}()
// Check if the machine is already initialised as a cluster member and prompt the user to reset it first.
minfo, err := machineClient.Inspect(ctx, &emptypb.Empty{})
if err != nil {
return fmt.Errorf("inspect machine: %w", err)
return nil, fmt.Errorf("inspect machine: %w", err)
}
if minfo.Id != "" {
if err = cli.promptResetMachine(); err != nil {
return err
return 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, fmt.Errorf("check machine prerequisites: %w", err)
}
} else if !checkResp.Satisfied {
return nil, fmt.Errorf("machine prerequisites not satisfied: %s", checkResp.Error)
}
req := &pb.InitClusterRequest{
MachineName: machineName,
Network: pb.NewIPPrefix(netPrefix),
MachineName: opts.MachineName,
Network: pb.NewIPPrefix(opts.Network),
}
resp, err := machineClient.InitCluster(ctx, req)
if err != nil {
return fmt.Errorf("init cluster: %w", err)
}
fmt.Printf("Cluster %q initialised with machine %q\n", clusterName, resp.Machine.Name)
if err = cli.CreateCluster(clusterName); err != nil {
return fmt.Errorf("save cluster to config: %w", err)
}
// Set the current cluster to the just created one if it is the only cluster in the config.
if len(cli.config.Clusters) == 1 {
if err = cli.SetCurrentCluster(clusterName); err != nil {
return fmt.Errorf("set current cluster: %w", err)
if opts.PublicIP != nil {
if opts.PublicIP.IsValid() {
req.PublicIpConfig = &pb.InitClusterRequest_PublicIp{PublicIp: pb.NewIP(*opts.PublicIP)}
} else {
// Invalid or in other words zero IP means to automatically detect the public IP.
req.PublicIpConfig = &pb.InitClusterRequest_PublicIpAuto{PublicIpAuto: true}
}
}
// Save the machine's SSH connection details in the cluster config.
resp, err := machineClient.InitCluster(ctx, req)
if err != nil {
return nil, fmt.Errorf("init cluster: %w", err)
}
fmt.Printf("Cluster initialised with machine '%s' and saved as context '%s' in your local config (%s)\n",
resp.Machine.Name, contextName, cli.Config.Path())
if err = cli.CreateContext(contextName); err != nil {
return nil, fmt.Errorf("save cluster context to config: %w", err)
}
if err = cli.SetCurrentContext(contextName); err != nil {
return nil, fmt.Errorf("set current cluster context: %w", err)
}
fmt.Printf("Current cluster context is now '%s'.\n", contextName)
// Save the machine's SSH connection details in the context config.
connCfg := config.MachineConnection{
SSH: config.NewSSHDestination(remoteMachine.User, remoteMachine.Host, remoteMachine.Port),
SSHKeyFile: remoteMachine.KeyPath,
SSH: config.NewSSHDestination(opts.RemoteMachine.User, opts.RemoteMachine.Host, opts.RemoteMachine.Port),
SSHKeyFile: opts.RemoteMachine.KeyPath,
}
cli.config.Clusters[clusterName].Connections = append(cli.config.Clusters[clusterName].Connections, connCfg)
if err = cli.config.Save(); err != nil {
return fmt.Errorf("save config: %w", err)
cli.Config.Contexts[contextName].Connections = append(cli.Config.Contexts[contextName].Connections, connCfg)
if err = cli.Config.Save(); err != nil {
return nil, fmt.Errorf("save config: %w", err)
}
return nil
return machineClient, nil
}
func (cli *CLI) AddMachine(ctx context.Context, remoteMachine RemoteMachine, clusterName, machineName string) error {
c, err := cli.ConnectCluster(ctx, clusterName)
type AddMachineOptions struct {
Context string
MachineName string
PublicIP *netip.Addr
RemoteMachine RemoteMachine
Version string
}
// AddMachine provisions a remote machine and adds it to the cluster. It returns a cluster client and a machine client.
// The cluster client is connected to the existing machine in the cluster. It was used to add the new machine to the
// cluster. The machine client is connected to the new machine and can be used to interact with it.
// Both client should be closed after use by the caller.
func (cli *CLI) AddMachine(ctx context.Context, opts AddMachineOptions) (*client.Client, *client.Client, error) {
contextName := opts.Context
if contextName == "" {
contextName = cli.Config.CurrentContext
}
c, err := cli.ConnectCluster(ctx, contextName)
if err != nil {
return fmt.Errorf("connect to cluster: %w", err)
return nil, nil, fmt.Errorf("connect to cluster (context '%s'): %w", contextName, err)
}
defer func() {
_ = c.Close()
if err != nil {
c.Close()
}
}()
machineClient, err := cli.provisionRemoteMachine(ctx, remoteMachine)
machineClient, err := cli.provisionRemoteMachine(ctx, opts.RemoteMachine, opts.Version)
if err != nil {
return err
return nil, nil, err
}
defer machineClient.Close()
defer func() {
if err != nil {
machineClient.Close()
}
}()
// Check if the machine is already initialised as a cluster member and prompt the user to reset it first.
minfo, err := machineClient.Inspect(ctx, &emptypb.Empty{})
if err != nil {
return fmt.Errorf("inspect machine: %w", err)
return nil, nil, fmt.Errorf("inspect machine: %w", err)
}
if minfo.Id != "" {
if err = cli.promptResetMachine(); err != nil {
return err
return nil, nil, err
}
}
// Check machine meets all necessary system requirements before proceeding.
checkResp, err := machineClient.CheckPrerequisites(ctx, &emptypb.Empty{})
// TODO(lhf): remove Unimplemented check when v0.9.0 is released.
if err != nil {
if status.Convert(err).Code() != codes.Unimplemented {
return nil, nil, fmt.Errorf("check machine prerequisites: %w", err)
}
} else if !checkResp.Satisfied {
return nil, nil, fmt.Errorf("machine prerequisites not satisfied: %s", checkResp.Error)
}
tokenResp, err := machineClient.Token(ctx, &emptypb.Empty{})
if err != nil {
return fmt.Errorf("get remote machine token: %w", err)
return nil, nil, fmt.Errorf("get remote machine token: %w", err)
}
token, err := machine.ParseToken(tokenResp.Token)
if err != nil {
return 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.
@@ -217,21 +319,30 @@ func (cli *CLI) AddMachine(ctx context.Context, remoteMachine RemoteMachine, clu
endpoints[i] = pb.NewIPPort(addrPort)
}
addReq := &pb.AddMachineRequest{
Name: machineName,
Name: opts.MachineName,
Network: &pb.NetworkConfig{
Endpoints: endpoints,
PublicKey: token.PublicKey,
},
}
if opts.PublicIP != nil {
if opts.PublicIP.IsValid() {
addReq.PublicIp = pb.NewIP(*opts.PublicIP)
} else if token.PublicIP.IsValid() {
// Invalid or in other words zero IP means to use an automatically detected public IP from the token.
addReq.PublicIp = pb.NewIP(token.PublicIP)
}
}
addResp, err := c.AddMachine(ctx, addReq)
if err != nil {
return fmt.Errorf("add machine to cluster: %w", err)
return nil, nil, fmt.Errorf("add machine to cluster (context '%s'): %w", contextName, err)
}
// List other machines in the cluster to include them in the join request.
machines, err := c.ListMachines(ctx)
machines, err := c.ListMachines(ctx, nil)
if err != nil {
return fmt.Errorf("list cluster machines: %w", err)
return nil, nil, fmt.Errorf("list cluster machines: %w", err)
}
otherMachines := make([]*pb.MachineInfo, 0, len(machines)-1)
for _, m := range machines {
@@ -246,30 +357,34 @@ func (cli *CLI) AddMachine(ctx context.Context, remoteMachine RemoteMachine, clu
OtherMachines: otherMachines,
}
if _, err = machineClient.JoinCluster(ctx, joinReq); err != nil {
return fmt.Errorf("join cluster: %w", err)
return nil, nil, fmt.Errorf("join cluster: %w", err)
}
fmt.Printf("Machine %q added to cluster\n", addResp.Machine.Name)
// TODO: fix empty context name when using the current context (contextName == "").
fmt.Printf("Machine '%s' added to the cluster (context '%s').\n", addResp.Machine.Name, contextName)
// Save the machine's SSH connection details in the cluster config.
// Save the machine's SSH connection details in the context config.
connCfg := config.MachineConnection{
SSH: config.NewSSHDestination(remoteMachine.User, remoteMachine.Host, remoteMachine.Port),
SSHKeyFile: remoteMachine.KeyPath,
SSH: config.NewSSHDestination(opts.RemoteMachine.User, opts.RemoteMachine.Host, opts.RemoteMachine.Port),
SSHKeyFile: opts.RemoteMachine.KeyPath,
}
if clusterName == "" {
clusterName = cli.config.CurrentCluster
if contextName == "" {
contextName = cli.Config.CurrentContext
}
cli.config.Clusters[clusterName].Connections = append(cli.config.Clusters[clusterName].Connections, connCfg)
if err = cli.config.Save(); err != nil {
return fmt.Errorf("save config: %w", err)
cli.Config.Contexts[contextName].Connections = append(cli.Config.Contexts[contextName].Connections, connCfg)
if err = cli.Config.Save(); err != nil {
return nil, nil, fmt.Errorf("save config: %w", err)
}
return nil
return c, machineClient, nil
}
// provisionRemoteMachine installs the Uncloud daemon and dependencies on the remote machine over SSH and returns
// a machine API client to interact with the machine. The client should be closed after use by the caller.
func (cli *CLI) provisionRemoteMachine(ctx context.Context, remoteMachine RemoteMachine) (*client.Client, error) {
// The version parameter specifies the version of the Uncloud daemon to install. If empty, the latest version is used.
func (cli *CLI) provisionRemoteMachine(
ctx context.Context, remoteMachine RemoteMachine, version string,
) (*client.Client, error) {
// Provision the remote machine by installing the Uncloud daemon and dependencies over SSH.
sshClient, err := sshexec.Connect(remoteMachine.User, remoteMachine.Host, remoteMachine.Port, remoteMachine.KeyPath)
if err != nil {
@@ -280,7 +395,7 @@ func (cli *CLI) provisionRemoteMachine(ctx context.Context, remoteMachine Remote
}
exec := sshexec.NewRemote(sshClient)
// Install and run the Uncloud daemon and dependencies on the remote machine.
if err = provisionMachine(ctx, exec); err != nil {
if err = provisionMachine(ctx, exec, version); err != nil {
return nil, fmt.Errorf("provision machine: %w", err)
}
@@ -317,7 +432,7 @@ func (cli *CLI) promptResetMachine() error {
Negative("No").
Value(&confirm),
),
)
).WithAccessible(true)
if err := form.Run(); err != nil {
return fmt.Errorf("prompt user to confirm: %w", err)
}
@@ -326,5 +441,11 @@ func (cli *CLI) promptResetMachine() error {
return fmt.Errorf("remote machine is already initialised as a cluster member")
}
// TODO: implement resetting the remote machine.
return fmt.Errorf("resetting the remote machine is not implemented yet")
return fmt.Errorf("resetting the remote machine is not implemented yet. " +
"Please manually run 'uncloud-uninstall' on the remote machine to fully uninstall Uncloud from it")
}
// ProgressOut returns an output stream for progress writer.
func (cli *CLI) ProgressOut() *streams.Out {
return streams.NewOut(os.Stdout)
}
-30
View File
@@ -1,30 +0,0 @@
package client
import (
"context"
"google.golang.org/protobuf/types/known/emptypb"
"uncloud/internal/machine/api/pb"
)
func (cli *Client) InspectMachine(ctx context.Context, id string) (*pb.MachineMember, error) {
machines, err := cli.ListMachines(ctx)
if err != nil {
return nil, err
}
for _, m := range machines {
if m.Machine.Id == id || m.Machine.Name == id {
return m, nil
}
}
return nil, ErrNotFound
}
func (cli *Client) ListMachines(ctx context.Context) ([]*pb.MachineMember, error) {
resp, err := cli.ClusterClient.ListMachines(ctx, &emptypb.Empty{})
if err != nil {
return nil, err
}
return resp.Machines, nil
}
-104
View File
@@ -1,104 +0,0 @@
package client
import (
"context"
"fmt"
"github.com/docker/docker/api/types/container"
"strings"
"uncloud/internal/api"
)
// Operation represents a single atomic operation in a deployment process.
// Operations can be composed to form complex deployment strategies.
type Operation interface {
Execute(ctx context.Context, cli *Client) error
String() string
}
// RunContainerOperation creates and starts a new container on a specific machine.
type RunContainerOperation struct {
ServiceID string
Spec api.ServiceSpec
MachineID string
}
func (o *RunContainerOperation) Execute(ctx context.Context, cli *Client) error {
resp, err := cli.CreateContainer(ctx, o.ServiceID, o.Spec, o.MachineID)
if err != nil {
return fmt.Errorf("create container: %w", err)
}
if err = cli.StartContainer(ctx, o.ServiceID, resp.ID); err != nil {
return fmt.Errorf("start container: %w", err)
}
// TODO: wait for the container to become healthy
return nil
}
func (o *RunContainerOperation) String() string {
return fmt.Sprintf("RunContainerOperation[%s, %s, %s]", o.ServiceID, o.Spec.Name, o.MachineID)
}
// StopContainerOperation stops a container on a specific machine.
type StopContainerOperation struct {
ServiceID string
ContainerID string
MachineID string
}
func (o *StopContainerOperation) Execute(ctx context.Context, cli *Client) error {
if err := cli.StopContainer(ctx, o.ServiceID, o.ContainerID, container.StopOptions{}); err != nil {
return fmt.Errorf("stop container: %w", err)
}
return nil
}
func (o *StopContainerOperation) String() string {
return fmt.Sprintf("StopContainerOperation[%s, %s, %s]", o.ServiceID, o.ContainerID, o.MachineID)
}
// RemoveContainerOperation stops and removes a container from a specific machine.
type RemoveContainerOperation struct {
ServiceID string
ContainerID string
MachineID string
}
func (o *RemoveContainerOperation) Execute(ctx context.Context, cli *Client) error {
if err := cli.StopContainer(ctx, o.ServiceID, o.ContainerID, container.StopOptions{}); err != nil {
return fmt.Errorf("stop container: %w", err)
}
if err := cli.RemoveContainer(ctx, o.ServiceID, o.ContainerID, container.RemoveOptions{}); err != nil {
return fmt.Errorf("remove container: %w", err)
}
return nil
}
func (o *RemoveContainerOperation) String() string {
return fmt.Sprintf("RemoveContainerOperation[%s, %s, %s]", o.ServiceID, o.ContainerID, o.MachineID)
}
// SequenceOperation is a composite operation that executes a sequence of operations in order.
type SequenceOperation struct {
Operations []Operation
}
func (o *SequenceOperation) Execute(ctx context.Context, cli *Client) error {
for _, op := range o.Operations {
if err := op.Execute(ctx, cli); err != nil {
return err
}
}
return nil
}
func (o *SequenceOperation) String() string {
ops := make([]string, len(o.Operations))
for i, op := range o.Operations {
ops[i] = op.String()
}
return fmt.Sprintf("SequenceOperation[%s]", strings.Join(ops, ", "))
}
-186
View File
@@ -1,186 +0,0 @@
package client
import (
"context"
"errors"
"fmt"
"uncloud/internal/api"
"uncloud/internal/machine/api/pb"
"uncloud/internal/secret"
)
// Strategy defines how a service should be deployed or updated. Different implementations can provide various
// deployment patterns such as rolling updates, blue/green deployments, etc.
type Strategy interface {
// Plan returns the operation to reconcile the service to the desired state.
// If the service does not exist (new deployment), svc will be nil.
Plan(ctx context.Context, cli *Client, svc *api.Service, spec api.ServiceSpec) (Plan, error)
}
// RollingStrategy implements a rolling update deployment pattern where containers are updated one at a time
// to minimize service disruption.
type RollingStrategy struct{}
func (s *RollingStrategy) Plan(
ctx context.Context, cli *Client, svc *api.Service, spec api.ServiceSpec,
) (Plan, error) {
switch spec.Mode {
case "", api.ServiceModeReplicated:
return s.planReplicated(ctx, cli, svc, spec)
case api.ServiceModeGlobal:
return s.planGlobal(ctx, cli, svc, spec)
default:
return Plan{}, fmt.Errorf("unsupported service mode: %s", spec.Mode)
}
}
// planReplicated creates a plan for a replicated service deployment.
func (s *RollingStrategy) planReplicated(
ctx context.Context, cli *Client, svc *api.Service, spec api.ServiceSpec,
) (Plan, error) {
return Plan{}, errors.New("not implemented")
}
// planGlobal creates a plan for a global service deployment, ensuring one container runs on each available machine.
// For machines with an existing container, it attempts to start a new container before removing the old one if
// possible. If the new container would have port conflicts with the existing one, the old container is removed first.
// It handles multiple containers per machine (though this should not occur in normal operation) and skips machines
// that are down.
func (s *RollingStrategy) planGlobal(
ctx context.Context, cli *Client, svc *api.Service, spec api.ServiceSpec,
) (Plan, error) {
var plan Plan
// Map machineID to service containers on that machine. For the global mode, there should be at most one
// container per machine but we use a slice to handle multiple containers that may exist due to a bug
// or interruption in the previous deployment.
containersOnMachine := make(map[string][]api.MachineContainer)
if svc != nil {
plan.ServiceID = svc.ID
for _, c := range svc.Containers {
containersOnMachine[c.MachineID] = append(containersOnMachine[c.MachineID], c)
}
} else {
// Generate a new service ID for the first service deployment.
var err error
plan.ServiceID, err = secret.NewID()
if err != nil {
return plan, fmt.Errorf("generate service ID: %w", err)
}
}
machines, err := cli.ListMachines(ctx)
if err != nil {
return plan, fmt.Errorf("list machines: %w", err)
}
seqOp := &SequenceOperation{}
// TODO: figure out how to return a warning if there are machines down. Embed the machinesDown in the plan?
// WARNING: failed to run a service container on machine '%s' which is Down.
var machinesDown []*pb.MachineInfo
for _, m := range machines {
// Skip machines that are down but collect them to report a warning later.
if m.State == pb.MachineMember_DOWN {
machinesDown = append(machinesDown, m.Machine)
continue
}
containers := containersOnMachine[m.Machine.Id]
ops, err := reconcileGlobalContainer(containers, spec, plan.ServiceID, m.Machine.Id)
if err != nil {
return plan, err
}
seqOp.Operations = append(seqOp.Operations, ops...)
}
plan.Operation = seqOp
return plan, nil
}
// reconcileGlobalContainer returns a sequence of operations to reconcile containers on a machine for a global service.
// It ensures exactly one container with the desired spec is running on the machine by creating a new container and
// removing old ones. If there is a host port conflict, it stops the old container before starting a new one.
func reconcileGlobalContainer(
containers []api.MachineContainer, spec api.ServiceSpec, serviceID, machineID string,
) ([]Operation, error) {
var ops []Operation
if len(containers) == 0 {
// No containers on this machine, create a new one.
ops = append(ops, &RunContainerOperation{
ServiceID: serviceID,
Spec: spec,
MachineID: machineID,
})
return ops, nil
}
// Check if there is a container with the same spec already running. If so, remove the rest.
upToDate := false
for i, c := range containers {
if !c.Container.State.Running || c.Container.State.Paused {
// Skip containers that are not running.
continue
}
svcSpec, err := c.Container.ServiceSpec()
if err != nil {
return nil, fmt.Errorf("get service spec: %w", err)
}
if svcSpec.Equals(spec) {
// The container is already running with the same spec.
upToDate = true
for j, old := range containers {
if i == j {
continue
}
ops = append(ops, &RemoveContainerOperation{
ServiceID: serviceID,
ContainerID: old.Container.ID,
MachineID: old.MachineID,
})
}
break
}
}
if upToDate {
return ops, nil
}
// The machine has containers but none of them match the new spec.
// Stop the old running containers that have conflicting ports with the new spec before running a new one.
for _, c := range containers {
if c.Container.State.Running {
conflictingPorts, err := c.Container.ConflictingServicePorts(spec.Ports)
if err != nil {
return nil, fmt.Errorf("check conflicting ports: %w", err)
}
if len(conflictingPorts) > 0 {
// Stop the running container with conflicting ports.
ops = append(ops, &StopContainerOperation{
ServiceID: serviceID,
ContainerID: c.Container.ID,
MachineID: c.MachineID,
})
}
}
}
// Run a new container.
ops = append(ops, &RunContainerOperation{
ServiceID: serviceID,
Spec: spec,
MachineID: machineID,
})
// Remove the old containers.
for _, c := range containers {
ops = append(ops, &RemoveContainerOperation{
ServiceID: serviceID,
ContainerID: c.Container.ID,
MachineID: c.MachineID,
})
}
return ops, nil
}
-6
View File
@@ -1,6 +0,0 @@
package config
type Cluster struct {
Name string `toml:"-"`
Connections []MachineConnection `toml:"connections"`
}
+20 -12
View File
@@ -2,14 +2,15 @@ package config
import (
"fmt"
"github.com/BurntSushi/toml"
"os"
"path/filepath"
"github.com/goccy/go-yaml"
)
type Config struct {
Clusters map[string]*Cluster `toml:"clusters"`
CurrentCluster string `toml:"current_cluster"`
CurrentContext string `yaml:"current_context"`
Contexts map[string]*Context `yaml:"contexts"`
// path is the file path config is read from.
path string
@@ -18,10 +19,10 @@ type Config struct {
func NewFromFile(path string) (*Config, error) {
_, err := os.Stat(path)
if err != nil && !os.IsNotExist(err) {
return nil, fmt.Errorf("check file permissions %q: %w", path, err)
return nil, fmt.Errorf("check file permissions '%s': %w", path, err)
}
c := &Config{
Clusters: map[string]*Cluster{},
Contexts: map[string]*Context{},
path: path,
}
if os.IsNotExist(err) {
@@ -34,30 +35,37 @@ func NewFromFile(path string) (*Config, error) {
return c, nil
}
func (c *Config) Path() string {
return c.path
}
func (c *Config) Read() error {
_, err := toml.DecodeFile(c.path, c)
data, err := os.ReadFile(c.path)
if err != nil {
return fmt.Errorf("read config file %q: %w", c.path, err)
return fmt.Errorf("read config file '%s': %w", c.path, err)
}
if err = yaml.Unmarshal(data, c); err != nil {
return fmt.Errorf("parse config file '%s': %s", c.path, yaml.FormatError(err, true, true))
}
return nil
}
func (c *Config) Save() error {
dir, _ := filepath.Split(c.path)
if err := os.MkdirAll(dir, 0700); err != nil {
return fmt.Errorf("create config directory %q: %w", dir, err)
return fmt.Errorf("create config directory '%s': %w", dir, err)
}
f, err := os.OpenFile(c.path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil {
return fmt.Errorf("write config file %q: %w", c.path, err)
return fmt.Errorf("write config file '%s': %w", c.path, err)
}
encoder := toml.NewEncoder(f)
encoder.Indent = ""
encoder := yaml.NewEncoder(f, yaml.Indent(2), yaml.IndentSequence(true))
if err = encoder.Encode(c); err != nil {
_ = f.Close()
return fmt.Errorf("encode config file %q: %w", c.path, err)
return fmt.Errorf("encode config file '%s': %w", c.path, err)
}
return f.Close()
}
+9 -6
View File
@@ -5,7 +5,8 @@ import (
"net/netip"
"strconv"
"strings"
"uncloud/internal/secret"
"github.com/psviderski/uncloud/internal/secret"
)
const (
@@ -14,11 +15,13 @@ const (
)
type MachineConnection struct {
SSH SSHDestination `toml:"ssh,omitempty"`
TCP netip.AddrPort `toml:"tcp,omitempty"`
Host string `toml:"host,omitempty"`
PublicKey secret.Secret `toml:"public_key,omitempty"`
SSHKeyFile string `toml:"ssh_key_file,omitempty"`
SSH SSHDestination `yaml:"ssh,omitempty"`
SSHKeyFile string `yaml:"ssh_key_file,omitempty"`
// TCP is the address and port of the machine's API server.
// The pointer is used to omit the field when not set. Otherwise, yaml marshalling includes an empty object.
TCP *netip.AddrPort `yaml:"tcp,omitempty"`
Host string `yaml:"host,omitempty"`
PublicKey secret.Secret `yaml:"public_key,omitempty"`
}
// SSHDestination represents an SSH destination string in the canonical form of "user@host:port".
+6
View File
@@ -0,0 +1,6 @@
package config
type Context struct {
Name string `yaml:"-"`
Connections []MachineConnection `yaml:"connections"`
}
+23
View File
@@ -0,0 +1,23 @@
package cli
import (
"strings"
)
// ExpandCommaSeparatedValues takes a slice of strings and expands any comma-separated values into individual elements.
func ExpandCommaSeparatedValues(values []string) []string {
if len(values) == 0 {
return nil
}
var expanded []string
for _, value := range values {
for _, v := range strings.Split(value, ",") {
if v = strings.TrimSpace(v); v != "" {
expanded = append(expanded, v)
}
}
}
return expanded
}
+20 -16
View File
@@ -4,7 +4,9 @@ import (
"context"
"fmt"
"os"
"uncloud/internal/sshexec"
"strings"
"github.com/psviderski/uncloud/internal/sshexec"
)
// TODO: support pinning the script version to the CLI version.
@@ -17,36 +19,38 @@ type RemoteMachine struct {
KeyPath string
}
func installCmd(user string) string {
sudoPrefix := "sudo"
func installCmd(user string, version string) string {
sudoPrefix := ""
var env []string
// Add the SSH user (non-root) to the uncloud group to allow access to the Uncloud daemon unix socket.
env := "UNCLOUD_GROUP_ADD_USER=" + user
curlBashCmd := fmt.Sprintf(
"curl -fsSL %s | %s %s bash", sshexec.Quote(installScriptURL), sudoPrefix, sshexec.Quote(env),
)
if user == "root" {
curlBashCmd = fmt.Sprintf(
"curl -fsSL %s | bash", sshexec.Quote(installScriptURL),
)
if user != "root" {
sudoPrefix = "sudo"
env = append(env, "UNCLOUD_GROUP_ADD_USER="+sshexec.Quote(user))
}
if version != "" {
env = append(env, "UNCLOUD_VERSION="+sshexec.Quote(version))
}
envCmd := strings.Join(env, " ")
curlBashCmd := fmt.Sprintf("curl -fsSL %s | %s %s bash", sshexec.Quote(installScriptURL), sudoPrefix, envCmd)
return curlBashCmd
}
// provisionMachine provisions the remote machine by downloading the Uncloud install script from GitHub and running it.
func provisionMachine(ctx context.Context, exec sshexec.Executor) error {
// If version is specified, it will be passed to the install script as UNCLOUD_VERSION environment variable.
func provisionMachine(ctx context.Context, exec sshexec.Executor, version string) error {
user, err := exec.Run(ctx, "whoami")
if err != nil {
return fmt.Errorf("run whoami: %w", err)
}
installCmd := installCmd(user)
cmd := installCmd(user, version)
fmt.Println("Downloading Uncloud install script:", installScriptURL)
cmd := sshexec.QuoteCommand("bash", "-c", "set -o pipefail; "+installCmd)
cmd = sshexec.QuoteCommand("bash", "-c", "set -o pipefail; "+cmd)
if err = exec.Stream(ctx, cmd, os.Stdout, os.Stderr); err != nil {
return fmt.Errorf("download and run install script: %w", err)
}
+19 -2
View File
@@ -8,12 +8,29 @@ import (
func TestInstallCmd(t *testing.T) {
t.Run("root", func(t *testing.T) {
cmd := installCmd("root")
cmd := installCmd("root", "")
assert.NotContains(t, cmd, "sudo")
assert.NotContains(t, cmd, "UNCLOUD_GROUP_ADD_USER")
})
// Test with version
t.Run("root with version", func(t *testing.T) {
cmd := installCmd("root", "v1.2.3")
assert.NotContains(t, cmd, "sudo")
assert.NotContains(t, cmd, "UNCLOUD_GROUP_ADD_USER")
assert.Contains(t, cmd, "UNCLOUD_VERSION=v1.2.3")
})
t.Run("nonroot", func(t *testing.T) {
cmd := installCmd("nonroot")
cmd := installCmd("nonroot", "")
assert.Contains(t, cmd, "sudo")
assert.Contains(t, cmd, "UNCLOUD_GROUP_ADD_USER=nonroot")
})
t.Run("nonroot with version", func(t *testing.T) {
cmd := installCmd("nonroot", "v1.2.3")
assert.Contains(t, cmd, "sudo")
assert.Contains(t, cmd, "UNCLOUD_GROUP_ADD_USER=nonroot")
assert.Contains(t, cmd, "UNCLOUD_VERSION=v1.2.3")
})
}
+23
View File
@@ -0,0 +1,23 @@
package cli
import "github.com/charmbracelet/huh"
func Confirm() (bool, error) {
var confirmed bool
form := huh.NewForm(
huh.NewGroup(
huh.NewConfirm().
Title(
"Do you want to continue?",
).
Affirmative("Yes!").
Negative("No").
Value(&confirmed),
),
).WithAccessible(true)
if err := form.Run(); err != nil {
return false, err
}
return confirmed, nil
}
+1 -1
View File
@@ -5,7 +5,7 @@ import (
"fmt"
systemd "github.com/coreos/go-systemd/daemon"
"log/slog"
"uncloud/internal/machine"
"github.com/psviderski/uncloud/internal/machine"
)
type Daemon struct {
+3 -3
View File
@@ -3,10 +3,10 @@ package daemon
import (
"errors"
"fmt"
"github.com/psviderski/uncloud/internal/machine"
"github.com/psviderski/uncloud/internal/machine/network"
"net/netip"
"os"
"uncloud/internal/machine"
"uncloud/internal/machine/network"
)
// MachineToken returns the local machine's token that can be used for adding the machine to a cluster.
@@ -37,5 +37,5 @@ func MachineToken(dataDir string) (machine.Token, error) {
for i, ip := range ips {
endpoints[i] = netip.AddrPortFrom(ip, network.WireGuardPort)
}
return machine.NewToken(state.Network.PublicKey, endpoints), nil
return machine.NewToken(state.Network.PublicKey, publicIP, endpoints), nil
}
+34
View File
@@ -0,0 +1,34 @@
package dns
const (
RecordTypeA RecordType = "A"
RecordTypeAAAA RecordType = "AAAA"
)
type RecordType string
type DomainResponse struct {
Name string `json:"name,omitempty"`
Token string `json:"token,omitempty"`
}
type RecordRequest struct {
Name string `json:"name,omitempty"`
Type RecordType `json:"type,omitempty"`
Values []string `json:"values,omitempty"`
}
type RecordResponse struct {
RecordRequest
FQDN string `json:"fqdn,omitempty"`
}
type AuthErrorResponse struct {
Status int `json:"status,omitempty"`
Message string `json:"msg,omitempty"`
Data authErrorData `json:"data,omitempty"`
}
type authErrorData struct {
NoDomain bool `json:"noDomain,omitempty"`
}
+152
View File
@@ -0,0 +1,152 @@
package dns
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"log/slog"
"net/http"
)
// The dns package code is based on https://github.com/acorn-io/runtime/blob/main/pkg/dns.
// Client handles interactions with the Uncloud DNS API service.
type Client interface {
// ReserveDomain calls Uncloud DNS to reserve a new domain. It returns the domain, a token for authentication,
// and an error.
ReserveDomain(endpoint string) (string, string, error)
// CreateRecords calls Uncloud DNS to create or update DNS records based on the supplied RecordRequests
// for the specified domain.
CreateRecords(endpoint, domain, token string, records []RecordRequest) ([]RecordResponse, error)
}
// ErrAuthNoDomain indicates that a request failed authentication because the domain was not found.
// If encountered, a new domain needs to be reserved.
var ErrAuthNoDomain = errors.New("the supplied domain failed authentication")
// NewClient creates a new AcornDNS client
func NewClient() Client {
return &client{
c: http.DefaultClient,
}
}
type client struct {
c *http.Client
}
func (c *client) ReserveDomain(endpoint string) (string, string, error) {
url := fmt.Sprintf("%s/%s", endpoint, "domains")
req, err := c.request(http.MethodPost, url, nil, "")
if err != nil {
return "", "", err
}
resp := &DomainResponse{}
err = c.do(req, resp)
if err != nil {
return "", "", err
}
return resp.Name, resp.Token, nil
}
func (c *client) CreateRecords(endpoint, domain, token string, records []RecordRequest) ([]RecordResponse, error) {
url := fmt.Sprintf("%s/domains/%s/records", endpoint, domain)
var resp []RecordResponse
for _, recordRequest := range records {
body, err := jsonBody(recordRequest)
if err != nil {
return resp, err
}
req, err := c.request(http.MethodPost, url, body, token)
if err != nil {
return resp, err
}
var recordResp RecordResponse
if err = c.do(req, &recordResp); err != nil {
return resp, err
}
resp = append(resp, recordResp)
}
return resp, nil
}
func (c *client) request(method string, url string, body io.Reader, token string) (*http.Request, error) {
req, err := http.NewRequest(method, url, body)
if err != nil {
return nil, err
}
req.Header.Add("Content-Type", "application/json")
if token != "" {
bearer := "Bearer " + token
req.Header.Add("Authorization", bearer)
}
return req, nil
}
func (c *client) do(req *http.Request, responseBody any) error {
slog.Debug("Making request to DNS service.", "method", req.Method, "url", req.URL)
resp, err := c.c.Do(req)
if err != nil {
return err
}
slog.Debug("Response code for request to DNS service.",
"method", req.Method, "url", req.URL, "code", resp.StatusCode)
// When err is nil, resp contains a non-nil resp.Body which must be closed.
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("read response body: %w", err)
}
if resp.StatusCode == http.StatusUnauthorized {
var authError AuthErrorResponse
err = json.Unmarshal(body, &authError)
if err != nil {
return fmt.Errorf("unmarshal auth error response: %w", err)
}
if authError.Data.NoDomain {
return ErrAuthNoDomain
}
return errors.New("authentication failed")
}
if code := resp.StatusCode; code < 200 || code > 300 {
return fmt.Errorf("unexpected response status code: %d", code)
}
if responseBody != nil {
err = json.Unmarshal(body, responseBody)
if err != nil {
return fmt.Errorf("unmarshal response body (%s): %w", string(body), err)
}
}
return nil
}
func jsonBody(payload any) (io.Reader, error) {
buf := &bytes.Buffer{}
err := json.NewEncoder(buf).Encode(payload)
if err != nil {
return nil, err
}
return buf, nil
}
+10 -2
View File
@@ -45,11 +45,19 @@ func (h *SlogTextHandler) Enabled(ctx context.Context, level slog.Level) bool {
}
func (h *SlogTextHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
return h.TextHandler.WithAttrs(attrs)
return &SlogTextHandler{
TextHandler: h.TextHandler.WithAttrs(attrs).(*slog.TextHandler),
opts: h.opts,
w: h.w,
}
}
func (h *SlogTextHandler) WithGroup(name string) slog.Handler {
return h.TextHandler.WithGroup(name)
return &SlogTextHandler{
TextHandler: h.TextHandler.WithGroup(name).(*slog.TextHandler),
opts: h.opts,
w: h.w,
}
}
func (h *SlogTextHandler) Handle(ctx context.Context, r slog.Record) error {
+501 -69
View File
@@ -78,13 +78,63 @@ func (MachineMember_MembershipState) EnumDescriptor() ([]byte, []int) {
return file_internal_machine_api_pb_cluster_proto_rawDescGZIP(), []int{2, 0}
}
type DNSRecord_RecordType int32
const (
DNSRecord_UNSPECIFIED DNSRecord_RecordType = 0
DNSRecord_A DNSRecord_RecordType = 1
DNSRecord_AAAA DNSRecord_RecordType = 2
)
// Enum value maps for DNSRecord_RecordType.
var (
DNSRecord_RecordType_name = map[int32]string{
0: "UNSPECIFIED",
1: "A",
2: "AAAA",
}
DNSRecord_RecordType_value = map[string]int32{
"UNSPECIFIED": 0,
"A": 1,
"AAAA": 2,
}
)
func (x DNSRecord_RecordType) Enum() *DNSRecord_RecordType {
p := new(DNSRecord_RecordType)
*p = x
return p
}
func (x DNSRecord_RecordType) String() string {
return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
}
func (DNSRecord_RecordType) Descriptor() protoreflect.EnumDescriptor {
return file_internal_machine_api_pb_cluster_proto_enumTypes[1].Descriptor()
}
func (DNSRecord_RecordType) Type() protoreflect.EnumType {
return &file_internal_machine_api_pb_cluster_proto_enumTypes[1]
}
func (x DNSRecord_RecordType) Number() protoreflect.EnumNumber {
return protoreflect.EnumNumber(x)
}
// Deprecated: Use DNSRecord_RecordType.Descriptor instead.
func (DNSRecord_RecordType) EnumDescriptor() ([]byte, []int) {
return file_internal_machine_api_pb_cluster_proto_rawDescGZIP(), []int{8, 0}
}
type AddMachineRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
Network *NetworkConfig `protobuf:"bytes,2,opt,name=network,proto3" json:"network,omitempty"`
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
Network *NetworkConfig `protobuf:"bytes,2,opt,name=network,proto3" json:"network,omitempty"`
PublicIp *IP `protobuf:"bytes,3,opt,name=public_ip,json=publicIp,proto3" json:"public_ip,omitempty"`
}
func (x *AddMachineRequest) Reset() {
@@ -133,6 +183,13 @@ func (x *AddMachineRequest) GetNetwork() *NetworkConfig {
return nil
}
func (x *AddMachineRequest) GetPublicIp() *IP {
if x != nil {
return x.PublicIp
}
return nil
}
type AddMachineResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
@@ -282,6 +339,257 @@ func (x *ListMachinesResponse) GetMachines() []*MachineMember {
return nil
}
type Domain struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
}
func (x *Domain) Reset() {
*x = Domain{}
if protoimpl.UnsafeEnabled {
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *Domain) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Domain) ProtoMessage() {}
func (x *Domain) ProtoReflect() protoreflect.Message {
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[4]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Domain.ProtoReflect.Descriptor instead.
func (*Domain) Descriptor() ([]byte, []int) {
return file_internal_machine_api_pb_cluster_proto_rawDescGZIP(), []int{4}
}
func (x *Domain) GetName() string {
if x != nil {
return x.Name
}
return ""
}
type ReserveDomainRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Endpoint string `protobuf:"bytes,1,opt,name=endpoint,proto3" json:"endpoint,omitempty"`
}
func (x *ReserveDomainRequest) Reset() {
*x = ReserveDomainRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[5]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *ReserveDomainRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ReserveDomainRequest) ProtoMessage() {}
func (x *ReserveDomainRequest) ProtoReflect() protoreflect.Message {
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[5]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ReserveDomainRequest.ProtoReflect.Descriptor instead.
func (*ReserveDomainRequest) Descriptor() ([]byte, []int) {
return file_internal_machine_api_pb_cluster_proto_rawDescGZIP(), []int{5}
}
func (x *ReserveDomainRequest) GetEndpoint() string {
if x != nil {
return x.Endpoint
}
return ""
}
type CreateDomainRecordsRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Records []*DNSRecord `protobuf:"bytes,1,rep,name=records,proto3" json:"records,omitempty"`
}
func (x *CreateDomainRecordsRequest) Reset() {
*x = CreateDomainRecordsRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[6]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *CreateDomainRecordsRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*CreateDomainRecordsRequest) ProtoMessage() {}
func (x *CreateDomainRecordsRequest) ProtoReflect() protoreflect.Message {
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[6]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use CreateDomainRecordsRequest.ProtoReflect.Descriptor instead.
func (*CreateDomainRecordsRequest) Descriptor() ([]byte, []int) {
return file_internal_machine_api_pb_cluster_proto_rawDescGZIP(), []int{6}
}
func (x *CreateDomainRecordsRequest) GetRecords() []*DNSRecord {
if x != nil {
return x.Records
}
return nil
}
type CreateDomainRecordsResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Records []*DNSRecord `protobuf:"bytes,1,rep,name=records,proto3" json:"records,omitempty"`
}
func (x *CreateDomainRecordsResponse) Reset() {
*x = CreateDomainRecordsResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[7]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *CreateDomainRecordsResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*CreateDomainRecordsResponse) ProtoMessage() {}
func (x *CreateDomainRecordsResponse) ProtoReflect() protoreflect.Message {
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[7]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use CreateDomainRecordsResponse.ProtoReflect.Descriptor instead.
func (*CreateDomainRecordsResponse) Descriptor() ([]byte, []int) {
return file_internal_machine_api_pb_cluster_proto_rawDescGZIP(), []int{7}
}
func (x *CreateDomainRecordsResponse) GetRecords() []*DNSRecord {
if x != nil {
return x.Records
}
return nil
}
type DNSRecord struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
Type DNSRecord_RecordType `protobuf:"varint,2,opt,name=type,proto3,enum=api.DNSRecord_RecordType" json:"type,omitempty"`
Values []string `protobuf:"bytes,3,rep,name=values,proto3" json:"values,omitempty"`
}
func (x *DNSRecord) Reset() {
*x = DNSRecord{}
if protoimpl.UnsafeEnabled {
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[8]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *DNSRecord) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*DNSRecord) ProtoMessage() {}
func (x *DNSRecord) ProtoReflect() protoreflect.Message {
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[8]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use DNSRecord.ProtoReflect.Descriptor instead.
func (*DNSRecord) Descriptor() ([]byte, []int) {
return file_internal_machine_api_pb_cluster_proto_rawDescGZIP(), []int{8}
}
func (x *DNSRecord) GetName() string {
if x != nil {
return x.Name
}
return ""
}
func (x *DNSRecord) GetType() DNSRecord_RecordType {
if x != nil {
return x.Type
}
return DNSRecord_UNSPECIFIED
}
func (x *DNSRecord) GetValues() []string {
if x != nil {
return x.Values
}
return nil
}
var File_internal_machine_api_pb_cluster_proto protoreflect.FileDescriptor
var file_internal_machine_api_pb_cluster_proto_rawDesc = []byte{
@@ -289,48 +597,92 @@ var file_internal_machine_api_pb_cluster_proto_rawDesc = []byte{
0x6e, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x62, 0x2f, 0x63, 0x6c, 0x75, 0x73, 0x74, 0x65,
0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x03, 0x61, 0x70, 0x69, 0x1a, 0x1b, 0x67, 0x6f,
0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x65, 0x6d,
0x70, 0x74, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x25, 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,
0x70, 0x62, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f,
0x22, 0x55, 0x0a, 0x11, 0x41, 0x64, 0x64, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65,
0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20,
0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x2c, 0x0a, 0x07, 0x6e, 0x65, 0x74,
0x77, 0x6f, 0x72, 0x6b, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x61, 0x70, 0x69,
0x2e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x07,
0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x22, 0x40, 0x0a, 0x12, 0x41, 0x64, 0x64, 0x4d, 0x61,
0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2a, 0x0a,
0x07, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10,
0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f,
0x52, 0x07, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x22, 0xb4, 0x01, 0x0a, 0x0d, 0x4d, 0x61,
0x63, 0x68, 0x69, 0x6e, 0x65, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x12, 0x2a, 0x0a, 0x07, 0x6d,
0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x61,
0x70, 0x69, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x07,
0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x12, 0x38, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65,
0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x22, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d, 0x61, 0x63,
0x68, 0x69, 0x6e, 0x65, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x2e, 0x4d, 0x65, 0x6d, 0x62, 0x65,
0x72, 0x73, 0x68, 0x69, 0x70, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74,
0x65, 0x22, 0x3d, 0x0a, 0x0f, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, 0x68, 0x69, 0x70, 0x53,
0x74, 0x61, 0x74, 0x65, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10,
0x00, 0x12, 0x06, 0x0a, 0x02, 0x55, 0x50, 0x10, 0x01, 0x12, 0x0b, 0x0a, 0x07, 0x53, 0x55, 0x53,
0x50, 0x45, 0x43, 0x54, 0x10, 0x02, 0x12, 0x08, 0x0a, 0x04, 0x44, 0x4f, 0x57, 0x4e, 0x10, 0x03,
0x22, 0x46, 0x0a, 0x14, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73,
0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2e, 0x0a, 0x08, 0x6d, 0x61, 0x63, 0x68,
0x69, 0x6e, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x61, 0x70, 0x69,
0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x52, 0x08,
0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x32, 0x8b, 0x01, 0x0a, 0x07, 0x43, 0x6c, 0x75,
0x73, 0x74, 0x65, 0x72, 0x12, 0x3d, 0x0a, 0x0a, 0x41, 0x64, 0x64, 0x4d, 0x61, 0x63, 0x68, 0x69,
0x6e, 0x65, 0x12, 0x16, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x41, 0x64, 0x64, 0x4d, 0x61, 0x63, 0x68,
0x69, 0x6e, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x17, 0x2e, 0x61, 0x70, 0x69,
0x2e, 0x41, 0x64, 0x64, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f,
0x6e, 0x73, 0x65, 0x12, 0x41, 0x0a, 0x0c, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x61, 0x63, 0x68, 0x69,
0x6e, 0x65, 0x73, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f,
0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x19, 0x2e, 0x61, 0x70,
0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x52, 0x65,
0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x37, 0x5a, 0x35, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62,
0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x70, 0x73, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x6b, 0x69, 0x2f,
0x75, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c,
0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x62, 0x62,
0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
0x70, 0x62, 0x2f, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a,
0x25, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e,
0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x62, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65,
0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x7b, 0x0a, 0x11, 0x41, 0x64, 0x64, 0x4d, 0x61, 0x63,
0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x6e,
0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12,
0x2c, 0x0a, 0x07, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b,
0x32, 0x12, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x43, 0x6f,
0x6e, 0x66, 0x69, 0x67, 0x52, 0x07, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x12, 0x24, 0x0a,
0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x69, 0x70, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b,
0x32, 0x07, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x50, 0x52, 0x08, 0x70, 0x75, 0x62, 0x6c, 0x69,
0x63, 0x49, 0x70, 0x22, 0x40, 0x0a, 0x12, 0x41, 0x64, 0x64, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e,
0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2a, 0x0a, 0x07, 0x6d, 0x61, 0x63,
0x68, 0x69, 0x6e, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x61, 0x70, 0x69,
0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x07, 0x6d, 0x61,
0x63, 0x68, 0x69, 0x6e, 0x65, 0x22, 0xb4, 0x01, 0x0a, 0x0d, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e,
0x65, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x12, 0x2a, 0x0a, 0x07, 0x6d, 0x61, 0x63, 0x68, 0x69,
0x6e, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d,
0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x07, 0x6d, 0x61, 0x63, 0x68,
0x69, 0x6e, 0x65, 0x12, 0x38, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01,
0x28, 0x0e, 0x32, 0x22, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65,
0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x2e, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, 0x68, 0x69,
0x70, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x22, 0x3d, 0x0a,
0x0f, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, 0x68, 0x69, 0x70, 0x53, 0x74, 0x61, 0x74, 0x65,
0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x06, 0x0a,
0x02, 0x55, 0x50, 0x10, 0x01, 0x12, 0x0b, 0x0a, 0x07, 0x53, 0x55, 0x53, 0x50, 0x45, 0x43, 0x54,
0x10, 0x02, 0x12, 0x08, 0x0a, 0x04, 0x44, 0x4f, 0x57, 0x4e, 0x10, 0x03, 0x22, 0x46, 0x0a, 0x14,
0x4c, 0x69, 0x73, 0x74, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70,
0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2e, 0x0a, 0x08, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73,
0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d, 0x61, 0x63,
0x68, 0x69, 0x6e, 0x65, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x52, 0x08, 0x6d, 0x61, 0x63, 0x68,
0x69, 0x6e, 0x65, 0x73, 0x22, 0x1c, 0x0a, 0x06, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x12,
0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61,
0x6d, 0x65, 0x22, 0x32, 0x0a, 0x14, 0x52, 0x65, 0x73, 0x65, 0x72, 0x76, 0x65, 0x44, 0x6f, 0x6d,
0x61, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x65, 0x6e,
0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x65, 0x6e,
0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x22, 0x46, 0x0a, 0x1a, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65,
0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x52, 0x65, 0x71,
0x75, 0x65, 0x73, 0x74, 0x12, 0x28, 0x0a, 0x07, 0x72, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x18,
0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x44, 0x4e, 0x53, 0x52,
0x65, 0x63, 0x6f, 0x72, 0x64, 0x52, 0x07, 0x72, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x22, 0x47,
0x0a, 0x1b, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x52, 0x65,
0x63, 0x6f, 0x72, 0x64, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x28, 0x0a,
0x07, 0x72, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0e,
0x2e, 0x61, 0x70, 0x69, 0x2e, 0x44, 0x4e, 0x53, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x52, 0x07,
0x72, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x22, 0x96, 0x01, 0x0a, 0x09, 0x44, 0x4e, 0x53, 0x52,
0x65, 0x63, 0x6f, 0x72, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20,
0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x2d, 0x0a, 0x04, 0x74, 0x79, 0x70,
0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x19, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x44, 0x4e,
0x53, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x54, 0x79,
0x70, 0x65, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x76, 0x61, 0x6c, 0x75,
0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73,
0x22, 0x2e, 0x0a, 0x0a, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x54, 0x79, 0x70, 0x65, 0x12, 0x0f,
0x0a, 0x0b, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12,
0x05, 0x0a, 0x01, 0x41, 0x10, 0x01, 0x12, 0x08, 0x0a, 0x04, 0x41, 0x41, 0x41, 0x41, 0x10, 0x02,
0x32, 0x86, 0x03, 0x0a, 0x07, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x12, 0x3d, 0x0a, 0x0a,
0x41, 0x64, 0x64, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x12, 0x16, 0x2e, 0x61, 0x70, 0x69,
0x2e, 0x41, 0x64, 0x64, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65,
0x73, 0x74, 0x1a, 0x17, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x41, 0x64, 0x64, 0x4d, 0x61, 0x63, 0x68,
0x69, 0x6e, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x41, 0x0a, 0x0c, 0x4c,
0x69, 0x73, 0x74, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x12, 0x16, 0x2e, 0x67, 0x6f,
0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d,
0x70, 0x74, 0x79, 0x1a, 0x19, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x61,
0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x37,
0x0a, 0x0d, 0x52, 0x65, 0x73, 0x65, 0x72, 0x76, 0x65, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12,
0x19, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x52, 0x65, 0x73, 0x65, 0x72, 0x76, 0x65, 0x44, 0x6f, 0x6d,
0x61, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0b, 0x2e, 0x61, 0x70, 0x69,
0x2e, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x30, 0x0a, 0x09, 0x47, 0x65, 0x74, 0x44, 0x6f,
0x6d, 0x61, 0x69, 0x6e, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72,
0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x0b, 0x2e, 0x61,
0x70, 0x69, 0x2e, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x34, 0x0a, 0x0d, 0x52, 0x65, 0x6c,
0x65, 0x61, 0x73, 0x65, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f,
0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70,
0x74, 0x79, 0x1a, 0x0b, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12,
0x58, 0x0a, 0x13, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x52,
0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x12, 0x1f, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x72, 0x65,
0x61, 0x74, 0x65, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73,
0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x72,
0x65, 0x61, 0x74, 0x65, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64,
0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x37, 0x5a, 0x35, 0x67, 0x69, 0x74,
0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x70, 0x73, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73,
0x6b, 0x69, 0x2f, 0x75, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72,
0x6e, 0x61, 0x6c, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f,
0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
@@ -345,33 +697,52 @@ func file_internal_machine_api_pb_cluster_proto_rawDescGZIP() []byte {
return file_internal_machine_api_pb_cluster_proto_rawDescData
}
var file_internal_machine_api_pb_cluster_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
var file_internal_machine_api_pb_cluster_proto_msgTypes = make([]protoimpl.MessageInfo, 4)
var file_internal_machine_api_pb_cluster_proto_enumTypes = make([]protoimpl.EnumInfo, 2)
var file_internal_machine_api_pb_cluster_proto_msgTypes = make([]protoimpl.MessageInfo, 9)
var file_internal_machine_api_pb_cluster_proto_goTypes = []any{
(MachineMember_MembershipState)(0), // 0: api.MachineMember.MembershipState
(*AddMachineRequest)(nil), // 1: api.AddMachineRequest
(*AddMachineResponse)(nil), // 2: api.AddMachineResponse
(*MachineMember)(nil), // 3: api.MachineMember
(*ListMachinesResponse)(nil), // 4: api.ListMachinesResponse
(*NetworkConfig)(nil), // 5: api.NetworkConfig
(*MachineInfo)(nil), // 6: api.MachineInfo
(*emptypb.Empty)(nil), // 7: google.protobuf.Empty
(MachineMember_MembershipState)(0), // 0: api.MachineMember.MembershipState
(DNSRecord_RecordType)(0), // 1: api.DNSRecord.RecordType
(*AddMachineRequest)(nil), // 2: api.AddMachineRequest
(*AddMachineResponse)(nil), // 3: api.AddMachineResponse
(*MachineMember)(nil), // 4: api.MachineMember
(*ListMachinesResponse)(nil), // 5: api.ListMachinesResponse
(*Domain)(nil), // 6: api.Domain
(*ReserveDomainRequest)(nil), // 7: api.ReserveDomainRequest
(*CreateDomainRecordsRequest)(nil), // 8: api.CreateDomainRecordsRequest
(*CreateDomainRecordsResponse)(nil), // 9: api.CreateDomainRecordsResponse
(*DNSRecord)(nil), // 10: api.DNSRecord
(*NetworkConfig)(nil), // 11: api.NetworkConfig
(*IP)(nil), // 12: api.IP
(*MachineInfo)(nil), // 13: api.MachineInfo
(*emptypb.Empty)(nil), // 14: google.protobuf.Empty
}
var file_internal_machine_api_pb_cluster_proto_depIdxs = []int32{
5, // 0: api.AddMachineRequest.network:type_name -> api.NetworkConfig
6, // 1: api.AddMachineResponse.machine:type_name -> api.MachineInfo
6, // 2: api.MachineMember.machine:type_name -> api.MachineInfo
0, // 3: api.MachineMember.state:type_name -> api.MachineMember.MembershipState
3, // 4: api.ListMachinesResponse.machines:type_name -> api.MachineMember
1, // 5: api.Cluster.AddMachine:input_type -> api.AddMachineRequest
7, // 6: api.Cluster.ListMachines:input_type -> google.protobuf.Empty
2, // 7: api.Cluster.AddMachine:output_type -> api.AddMachineResponse
4, // 8: api.Cluster.ListMachines:output_type -> api.ListMachinesResponse
7, // [7:9] is the sub-list for method output_type
5, // [5:7] is the sub-list for method input_type
5, // [5:5] is the sub-list for extension type_name
5, // [5:5] is the sub-list for extension extendee
0, // [0:5] is the sub-list for field type_name
11, // 0: api.AddMachineRequest.network:type_name -> api.NetworkConfig
12, // 1: api.AddMachineRequest.public_ip:type_name -> api.IP
13, // 2: api.AddMachineResponse.machine:type_name -> api.MachineInfo
13, // 3: api.MachineMember.machine:type_name -> api.MachineInfo
0, // 4: api.MachineMember.state:type_name -> api.MachineMember.MembershipState
4, // 5: api.ListMachinesResponse.machines:type_name -> api.MachineMember
10, // 6: api.CreateDomainRecordsRequest.records:type_name -> api.DNSRecord
10, // 7: api.CreateDomainRecordsResponse.records:type_name -> api.DNSRecord
1, // 8: api.DNSRecord.type:type_name -> api.DNSRecord.RecordType
2, // 9: api.Cluster.AddMachine:input_type -> api.AddMachineRequest
14, // 10: api.Cluster.ListMachines:input_type -> google.protobuf.Empty
7, // 11: api.Cluster.ReserveDomain:input_type -> api.ReserveDomainRequest
14, // 12: api.Cluster.GetDomain:input_type -> google.protobuf.Empty
14, // 13: api.Cluster.ReleaseDomain:input_type -> google.protobuf.Empty
8, // 14: api.Cluster.CreateDomainRecords:input_type -> api.CreateDomainRecordsRequest
3, // 15: api.Cluster.AddMachine:output_type -> api.AddMachineResponse
5, // 16: api.Cluster.ListMachines:output_type -> api.ListMachinesResponse
6, // 17: api.Cluster.ReserveDomain:output_type -> api.Domain
6, // 18: api.Cluster.GetDomain:output_type -> api.Domain
6, // 19: api.Cluster.ReleaseDomain:output_type -> api.Domain
9, // 20: api.Cluster.CreateDomainRecords:output_type -> api.CreateDomainRecordsResponse
15, // [15:21] is the sub-list for method output_type
9, // [9:15] is the sub-list for method input_type
9, // [9:9] is the sub-list for extension type_name
9, // [9:9] is the sub-list for extension extendee
0, // [0:9] is the sub-list for field type_name
}
func init() { file_internal_machine_api_pb_cluster_proto_init() }
@@ -379,6 +750,7 @@ func file_internal_machine_api_pb_cluster_proto_init() {
if File_internal_machine_api_pb_cluster_proto != nil {
return
}
file_internal_machine_api_pb_common_proto_init()
file_internal_machine_api_pb_machine_proto_init()
if !protoimpl.UnsafeEnabled {
file_internal_machine_api_pb_cluster_proto_msgTypes[0].Exporter = func(v any, i int) any {
@@ -429,14 +801,74 @@ func file_internal_machine_api_pb_cluster_proto_init() {
return nil
}
}
file_internal_machine_api_pb_cluster_proto_msgTypes[4].Exporter = func(v any, i int) any {
switch v := v.(*Domain); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_internal_machine_api_pb_cluster_proto_msgTypes[5].Exporter = func(v any, i int) any {
switch v := v.(*ReserveDomainRequest); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_internal_machine_api_pb_cluster_proto_msgTypes[6].Exporter = func(v any, i int) any {
switch v := v.(*CreateDomainRecordsRequest); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_internal_machine_api_pb_cluster_proto_msgTypes[7].Exporter = func(v any, i int) any {
switch v := v.(*CreateDomainRecordsResponse); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_internal_machine_api_pb_cluster_proto_msgTypes[8].Exporter = func(v any, i int) any {
switch v := v.(*DNSRecord); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_internal_machine_api_pb_cluster_proto_rawDesc,
NumEnums: 1,
NumMessages: 4,
NumEnums: 2,
NumMessages: 9,
NumExtensions: 0,
NumServices: 1,
},
+35
View File
@@ -5,16 +5,23 @@ package api;
option go_package = "github.com/psviderski/uncloud/internal/machine/api/pb";
import "google/protobuf/empty.proto";
import "internal/machine/api/pb/common.proto";
import "internal/machine/api/pb/machine.proto";
service Cluster {
rpc AddMachine(AddMachineRequest) returns (AddMachineResponse);
rpc ListMachines(google.protobuf.Empty) returns (ListMachinesResponse);
rpc ReserveDomain(ReserveDomainRequest) returns (Domain);
rpc GetDomain(google.protobuf.Empty) returns (Domain);
rpc ReleaseDomain(google.protobuf.Empty) returns (Domain);
rpc CreateDomainRecords(CreateDomainRecordsRequest) returns (CreateDomainRecordsResponse);
}
message AddMachineRequest {
string name = 1;
NetworkConfig network = 2;
IP public_ip = 3;
}
message AddMachineResponse {
@@ -41,3 +48,31 @@ message MachineMember {
message ListMachinesResponse {
repeated MachineMember machines = 1;
}
message Domain {
string name = 1;
}
message ReserveDomainRequest {
string endpoint = 1;
}
message CreateDomainRecordsRequest {
repeated DNSRecord records = 1;
}
message CreateDomainRecordsResponse {
repeated DNSRecord records = 1;
}
message DNSRecord {
string name = 1;
enum RecordType {
UNSPECIFIED = 0;
A = 1;
AAAA = 2;
}
RecordType type = 2;
repeated string values = 3;
}
+154 -2
View File
@@ -20,8 +20,12 @@ import (
const _ = grpc.SupportPackageIsVersion9
const (
Cluster_AddMachine_FullMethodName = "/api.Cluster/AddMachine"
Cluster_ListMachines_FullMethodName = "/api.Cluster/ListMachines"
Cluster_AddMachine_FullMethodName = "/api.Cluster/AddMachine"
Cluster_ListMachines_FullMethodName = "/api.Cluster/ListMachines"
Cluster_ReserveDomain_FullMethodName = "/api.Cluster/ReserveDomain"
Cluster_GetDomain_FullMethodName = "/api.Cluster/GetDomain"
Cluster_ReleaseDomain_FullMethodName = "/api.Cluster/ReleaseDomain"
Cluster_CreateDomainRecords_FullMethodName = "/api.Cluster/CreateDomainRecords"
)
// ClusterClient is the client API for Cluster service.
@@ -30,6 +34,10 @@ const (
type ClusterClient interface {
AddMachine(ctx context.Context, in *AddMachineRequest, opts ...grpc.CallOption) (*AddMachineResponse, error)
ListMachines(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*ListMachinesResponse, error)
ReserveDomain(ctx context.Context, in *ReserveDomainRequest, opts ...grpc.CallOption) (*Domain, error)
GetDomain(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*Domain, error)
ReleaseDomain(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*Domain, error)
CreateDomainRecords(ctx context.Context, in *CreateDomainRecordsRequest, opts ...grpc.CallOption) (*CreateDomainRecordsResponse, error)
}
type clusterClient struct {
@@ -60,12 +68,56 @@ func (c *clusterClient) ListMachines(ctx context.Context, in *emptypb.Empty, opt
return out, nil
}
func (c *clusterClient) ReserveDomain(ctx context.Context, in *ReserveDomainRequest, opts ...grpc.CallOption) (*Domain, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(Domain)
err := c.cc.Invoke(ctx, Cluster_ReserveDomain_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *clusterClient) GetDomain(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*Domain, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(Domain)
err := c.cc.Invoke(ctx, Cluster_GetDomain_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *clusterClient) ReleaseDomain(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*Domain, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(Domain)
err := c.cc.Invoke(ctx, Cluster_ReleaseDomain_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *clusterClient) CreateDomainRecords(ctx context.Context, in *CreateDomainRecordsRequest, opts ...grpc.CallOption) (*CreateDomainRecordsResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(CreateDomainRecordsResponse)
err := c.cc.Invoke(ctx, Cluster_CreateDomainRecords_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
// ClusterServer is the server API for Cluster service.
// All implementations must embed UnimplementedClusterServer
// for forward compatibility.
type ClusterServer interface {
AddMachine(context.Context, *AddMachineRequest) (*AddMachineResponse, error)
ListMachines(context.Context, *emptypb.Empty) (*ListMachinesResponse, error)
ReserveDomain(context.Context, *ReserveDomainRequest) (*Domain, error)
GetDomain(context.Context, *emptypb.Empty) (*Domain, error)
ReleaseDomain(context.Context, *emptypb.Empty) (*Domain, error)
CreateDomainRecords(context.Context, *CreateDomainRecordsRequest) (*CreateDomainRecordsResponse, error)
mustEmbedUnimplementedClusterServer()
}
@@ -82,6 +134,18 @@ func (UnimplementedClusterServer) AddMachine(context.Context, *AddMachineRequest
func (UnimplementedClusterServer) ListMachines(context.Context, *emptypb.Empty) (*ListMachinesResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ListMachines not implemented")
}
func (UnimplementedClusterServer) ReserveDomain(context.Context, *ReserveDomainRequest) (*Domain, error) {
return nil, status.Errorf(codes.Unimplemented, "method ReserveDomain not implemented")
}
func (UnimplementedClusterServer) GetDomain(context.Context, *emptypb.Empty) (*Domain, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetDomain not implemented")
}
func (UnimplementedClusterServer) ReleaseDomain(context.Context, *emptypb.Empty) (*Domain, error) {
return nil, status.Errorf(codes.Unimplemented, "method ReleaseDomain not implemented")
}
func (UnimplementedClusterServer) CreateDomainRecords(context.Context, *CreateDomainRecordsRequest) (*CreateDomainRecordsResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method CreateDomainRecords not implemented")
}
func (UnimplementedClusterServer) mustEmbedUnimplementedClusterServer() {}
func (UnimplementedClusterServer) testEmbeddedByValue() {}
@@ -139,6 +203,78 @@ func _Cluster_ListMachines_Handler(srv interface{}, ctx context.Context, dec fun
return interceptor(ctx, in, info, handler)
}
func _Cluster_ReserveDomain_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ReserveDomainRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(ClusterServer).ReserveDomain(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: Cluster_ReserveDomain_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ClusterServer).ReserveDomain(ctx, req.(*ReserveDomainRequest))
}
return interceptor(ctx, in, info, handler)
}
func _Cluster_GetDomain_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.(ClusterServer).GetDomain(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: Cluster_GetDomain_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ClusterServer).GetDomain(ctx, req.(*emptypb.Empty))
}
return interceptor(ctx, in, info, handler)
}
func _Cluster_ReleaseDomain_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.(ClusterServer).ReleaseDomain(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: Cluster_ReleaseDomain_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ClusterServer).ReleaseDomain(ctx, req.(*emptypb.Empty))
}
return interceptor(ctx, in, info, handler)
}
func _Cluster_CreateDomainRecords_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(CreateDomainRecordsRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(ClusterServer).CreateDomainRecords(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: Cluster_CreateDomainRecords_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ClusterServer).CreateDomainRecords(ctx, req.(*CreateDomainRecordsRequest))
}
return interceptor(ctx, in, info, handler)
}
// Cluster_ServiceDesc is the grpc.ServiceDesc for Cluster service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
@@ -154,6 +290,22 @@ var Cluster_ServiceDesc = grpc.ServiceDesc{
MethodName: "ListMachines",
Handler: _Cluster_ListMachines_Handler,
},
{
MethodName: "ReserveDomain",
Handler: _Cluster_ReserveDomain_Handler,
},
{
MethodName: "GetDomain",
Handler: _Cluster_GetDomain_Handler,
},
{
MethodName: "ReleaseDomain",
Handler: _Cluster_ReleaseDomain_Handler,
},
{
MethodName: "CreateDomainRecords",
Handler: _Cluster_CreateDomainRecords_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "internal/machine/api/pb/cluster.proto",
File diff suppressed because it is too large Load Diff
+122 -14
View File
@@ -14,23 +14,37 @@ service Docker {
rpc StopContainer(StopContainerRequest) returns (google.protobuf.Empty);
rpc ListContainers(ListContainersRequest) returns (ListContainersResponse);
rpc RemoveContainer(RemoveContainerRequest) returns (google.protobuf.Empty);
rpc PullImage(PullImageRequest) returns (stream JSONMessage);
rpc InspectImage(InspectImageRequest) returns (InspectImageResponse);
// InspectRemoteImage returns the image metadata for an image in a remote registry using the machine's
// Docker auth credentials if necessary.
rpc InspectRemoteImage(InspectRemoteImageRequest) returns (InspectRemoteImageResponse);
rpc CreateVolume(CreateVolumeRequest) returns (CreateVolumeResponse);
rpc ListVolumes(ListVolumesRequest) returns (ListVolumesResponse);
rpc RemoveVolume(RemoveVolumeRequest) returns (google.protobuf.Empty);
rpc CreateServiceContainer(CreateServiceContainerRequest) returns (CreateContainerResponse);
rpc InspectServiceContainer(InspectContainerRequest) returns (ServiceContainer);
rpc ListServiceContainers(ListServiceContainersRequest) returns (ListServiceContainersResponse);
rpc RemoveServiceContainer(RemoveContainerRequest) returns (google.protobuf.Empty);
}
message CreateContainerRequest {
// JSON serialized container.Config.
// JSON serialised container.Config.
bytes config = 1;
// JSON serialized container.HostConfig.
// JSON serialised container.HostConfig.
bytes host_config = 2;
// JSON serialized network.NetworkingConfig.
// JSON serialised network.NetworkingConfig.
bytes network_config = 3;
// JSON serialized ocispec.Platform.
// JSON serialised ocispec.Platform.
bytes platform = 4;
string name = 5;
}
message CreateContainerResponse {
// JSON serialized container.CreateResponse.
// JSON serialised container.CreateResponse.
bytes response = 1;
}
@@ -39,51 +53,145 @@ message InspectContainerRequest {
}
message InspectContainerResponse {
// JSON serialized container.InspectResponse.
// JSON serialised container.InspectResponse.
bytes response = 1;
}
message StartContainerRequest {
string id = 1;
// JSON serialized container.StartOptions.
// JSON serialised container.StartOptions.
bytes options = 2;
}
message StopContainerRequest {
string id = 1;
// JSON serialized container.StopOptions.
// JSON serialised container.StopOptions.
bytes options = 2;
}
message ListContainersRequest {
// JSON serialized container.ListOptions.
// JSON serialised container.ListOptions.
bytes options = 1;
}
// ListContainersResponse structure allows broadcasting ListContainers requests to multiple machines.
message ListContainersResponse {
// Must contain only one repeated messages field to allow broadcasting ListContainers requests to multiple machines.
repeated MachineContainers messages = 1;
}
message MachineContainers {
Metadata metadata = 1;
// JSON serialized []container.ContainerJSON.
// JSON serialised []container.InspectResponse.
bytes containers = 2;
}
message RemoveContainerRequest {
string id = 1;
// JSON serialized container.RemoveOptions.
// JSON serialised container.RemoveOptions.
bytes options = 2;
}
message PullImageRequest {
string image = 1;
// JSON serialized image.PullOptions.
// JSON serialised image.PullOptions.
bytes options = 2;
}
message JSONMessage {
// JSON serialized jsonmessage.JSONMessage.
// JSON serialised jsonmessage.JSONMessage.
bytes message = 1;
}
message InspectImageRequest {
string id = 1;
}
message InspectImageResponse {
// Must contain only one repeated messages field to allow broadcasting InspectImage requests to multiple machines.
repeated Image messages = 1;
}
message Image {
Metadata metadata = 1;
// JSON serialised types.ImageInspect.
bytes image = 2;
}
message InspectRemoteImageRequest {
string id = 1;
}
message InspectRemoteImageResponse {
// Must contain only one repeated messages field to allow broadcasting InspectRemoteImage requests to multiple machines.
repeated RemoteImage messages = 1;
}
message RemoteImage {
Metadata metadata = 1;
// Image reference in the canonical form with the digest.
string reference = 2;
// Raw JSON manifest from the registry.
bytes manifest = 3;
}
message CreateVolumeRequest {
// JSON serialised volume.CreateOptions.
bytes options = 1;
}
message CreateVolumeResponse {
// JSON serialised volume.Volume.
bytes volume = 1;
}
message ListVolumesRequest {
// JSON serialised volume.ListOptions.
bytes options = 1;
}
message ListVolumesResponse {
// Must contain only one repeated messages field to allow broadcasting ListVolumes requests to multiple machines.
repeated MachineVolumes messages = 1;
}
message MachineVolumes {
Metadata metadata = 1;
// JSON serialised volume.ListResponse.
bytes response = 2;
}
message RemoveVolumeRequest {
string id = 1;
bool force = 2;
}
message CreateServiceContainerRequest {
string service_id = 1;
// JSON serialised api.ServiceSpec.
bytes service_spec = 2;
string container_name = 3;
}
message ServiceContainer {
// JSON serialised container.InspectResponse.
bytes container = 1;
// JSON serialised api.ServiceSpec.
bytes service_spec = 2;
}
message ListServiceContainersRequest {
string service_id = 1;
// JSON serialised container.ListOptions.
bytes options = 2;
}
message ListServiceContainersResponse {
// Must contain only one repeated messages field to allow broadcasting ListServiceContainers requests
// to multiple machines.
repeated MachineServiceContainers messages = 1;
}
message MachineServiceContainers {
Metadata metadata = 1;
repeated ServiceContainer containers = 2;
}
+353 -7
View File
@@ -20,13 +20,22 @@ import (
const _ = grpc.SupportPackageIsVersion9
const (
Docker_CreateContainer_FullMethodName = "/api.Docker/CreateContainer"
Docker_InspectContainer_FullMethodName = "/api.Docker/InspectContainer"
Docker_StartContainer_FullMethodName = "/api.Docker/StartContainer"
Docker_StopContainer_FullMethodName = "/api.Docker/StopContainer"
Docker_ListContainers_FullMethodName = "/api.Docker/ListContainers"
Docker_RemoveContainer_FullMethodName = "/api.Docker/RemoveContainer"
Docker_PullImage_FullMethodName = "/api.Docker/PullImage"
Docker_CreateContainer_FullMethodName = "/api.Docker/CreateContainer"
Docker_InspectContainer_FullMethodName = "/api.Docker/InspectContainer"
Docker_StartContainer_FullMethodName = "/api.Docker/StartContainer"
Docker_StopContainer_FullMethodName = "/api.Docker/StopContainer"
Docker_ListContainers_FullMethodName = "/api.Docker/ListContainers"
Docker_RemoveContainer_FullMethodName = "/api.Docker/RemoveContainer"
Docker_PullImage_FullMethodName = "/api.Docker/PullImage"
Docker_InspectImage_FullMethodName = "/api.Docker/InspectImage"
Docker_InspectRemoteImage_FullMethodName = "/api.Docker/InspectRemoteImage"
Docker_CreateVolume_FullMethodName = "/api.Docker/CreateVolume"
Docker_ListVolumes_FullMethodName = "/api.Docker/ListVolumes"
Docker_RemoveVolume_FullMethodName = "/api.Docker/RemoveVolume"
Docker_CreateServiceContainer_FullMethodName = "/api.Docker/CreateServiceContainer"
Docker_InspectServiceContainer_FullMethodName = "/api.Docker/InspectServiceContainer"
Docker_ListServiceContainers_FullMethodName = "/api.Docker/ListServiceContainers"
Docker_RemoveServiceContainer_FullMethodName = "/api.Docker/RemoveServiceContainer"
)
// DockerClient is the client API for Docker service.
@@ -40,6 +49,17 @@ type DockerClient interface {
ListContainers(ctx context.Context, in *ListContainersRequest, opts ...grpc.CallOption) (*ListContainersResponse, error)
RemoveContainer(ctx context.Context, in *RemoveContainerRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
PullImage(ctx context.Context, in *PullImageRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[JSONMessage], error)
InspectImage(ctx context.Context, in *InspectImageRequest, opts ...grpc.CallOption) (*InspectImageResponse, error)
// InspectRemoteImage returns the image metadata for an image in a remote registry using the machine's
// Docker auth credentials if necessary.
InspectRemoteImage(ctx context.Context, in *InspectRemoteImageRequest, opts ...grpc.CallOption) (*InspectRemoteImageResponse, error)
CreateVolume(ctx context.Context, in *CreateVolumeRequest, opts ...grpc.CallOption) (*CreateVolumeResponse, error)
ListVolumes(ctx context.Context, in *ListVolumesRequest, opts ...grpc.CallOption) (*ListVolumesResponse, error)
RemoveVolume(ctx context.Context, in *RemoveVolumeRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
CreateServiceContainer(ctx context.Context, in *CreateServiceContainerRequest, opts ...grpc.CallOption) (*CreateContainerResponse, error)
InspectServiceContainer(ctx context.Context, in *InspectContainerRequest, opts ...grpc.CallOption) (*ServiceContainer, error)
ListServiceContainers(ctx context.Context, in *ListServiceContainersRequest, opts ...grpc.CallOption) (*ListServiceContainersResponse, error)
RemoveServiceContainer(ctx context.Context, in *RemoveContainerRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
}
type dockerClient struct {
@@ -129,6 +149,96 @@ func (c *dockerClient) PullImage(ctx context.Context, in *PullImageRequest, opts
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
type Docker_PullImageClient = grpc.ServerStreamingClient[JSONMessage]
func (c *dockerClient) InspectImage(ctx context.Context, in *InspectImageRequest, opts ...grpc.CallOption) (*InspectImageResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(InspectImageResponse)
err := c.cc.Invoke(ctx, Docker_InspectImage_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *dockerClient) InspectRemoteImage(ctx context.Context, in *InspectRemoteImageRequest, opts ...grpc.CallOption) (*InspectRemoteImageResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(InspectRemoteImageResponse)
err := c.cc.Invoke(ctx, Docker_InspectRemoteImage_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *dockerClient) CreateVolume(ctx context.Context, in *CreateVolumeRequest, opts ...grpc.CallOption) (*CreateVolumeResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(CreateVolumeResponse)
err := c.cc.Invoke(ctx, Docker_CreateVolume_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *dockerClient) ListVolumes(ctx context.Context, in *ListVolumesRequest, opts ...grpc.CallOption) (*ListVolumesResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ListVolumesResponse)
err := c.cc.Invoke(ctx, Docker_ListVolumes_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *dockerClient) RemoveVolume(ctx context.Context, in *RemoveVolumeRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(emptypb.Empty)
err := c.cc.Invoke(ctx, Docker_RemoveVolume_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *dockerClient) CreateServiceContainer(ctx context.Context, in *CreateServiceContainerRequest, opts ...grpc.CallOption) (*CreateContainerResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(CreateContainerResponse)
err := c.cc.Invoke(ctx, Docker_CreateServiceContainer_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *dockerClient) InspectServiceContainer(ctx context.Context, in *InspectContainerRequest, opts ...grpc.CallOption) (*ServiceContainer, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ServiceContainer)
err := c.cc.Invoke(ctx, Docker_InspectServiceContainer_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *dockerClient) ListServiceContainers(ctx context.Context, in *ListServiceContainersRequest, opts ...grpc.CallOption) (*ListServiceContainersResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ListServiceContainersResponse)
err := c.cc.Invoke(ctx, Docker_ListServiceContainers_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *dockerClient) RemoveServiceContainer(ctx context.Context, in *RemoveContainerRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(emptypb.Empty)
err := c.cc.Invoke(ctx, Docker_RemoveServiceContainer_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
// DockerServer is the server API for Docker service.
// All implementations must embed UnimplementedDockerServer
// for forward compatibility.
@@ -140,6 +250,17 @@ type DockerServer interface {
ListContainers(context.Context, *ListContainersRequest) (*ListContainersResponse, error)
RemoveContainer(context.Context, *RemoveContainerRequest) (*emptypb.Empty, error)
PullImage(*PullImageRequest, grpc.ServerStreamingServer[JSONMessage]) error
InspectImage(context.Context, *InspectImageRequest) (*InspectImageResponse, error)
// InspectRemoteImage returns the image metadata for an image in a remote registry using the machine's
// Docker auth credentials if necessary.
InspectRemoteImage(context.Context, *InspectRemoteImageRequest) (*InspectRemoteImageResponse, error)
CreateVolume(context.Context, *CreateVolumeRequest) (*CreateVolumeResponse, error)
ListVolumes(context.Context, *ListVolumesRequest) (*ListVolumesResponse, error)
RemoveVolume(context.Context, *RemoveVolumeRequest) (*emptypb.Empty, error)
CreateServiceContainer(context.Context, *CreateServiceContainerRequest) (*CreateContainerResponse, error)
InspectServiceContainer(context.Context, *InspectContainerRequest) (*ServiceContainer, error)
ListServiceContainers(context.Context, *ListServiceContainersRequest) (*ListServiceContainersResponse, error)
RemoveServiceContainer(context.Context, *RemoveContainerRequest) (*emptypb.Empty, error)
mustEmbedUnimplementedDockerServer()
}
@@ -171,6 +292,33 @@ func (UnimplementedDockerServer) RemoveContainer(context.Context, *RemoveContain
func (UnimplementedDockerServer) PullImage(*PullImageRequest, grpc.ServerStreamingServer[JSONMessage]) error {
return status.Errorf(codes.Unimplemented, "method PullImage not implemented")
}
func (UnimplementedDockerServer) InspectImage(context.Context, *InspectImageRequest) (*InspectImageResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method InspectImage not implemented")
}
func (UnimplementedDockerServer) InspectRemoteImage(context.Context, *InspectRemoteImageRequest) (*InspectRemoteImageResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method InspectRemoteImage not implemented")
}
func (UnimplementedDockerServer) CreateVolume(context.Context, *CreateVolumeRequest) (*CreateVolumeResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method CreateVolume not implemented")
}
func (UnimplementedDockerServer) ListVolumes(context.Context, *ListVolumesRequest) (*ListVolumesResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ListVolumes not implemented")
}
func (UnimplementedDockerServer) RemoveVolume(context.Context, *RemoveVolumeRequest) (*emptypb.Empty, error) {
return nil, status.Errorf(codes.Unimplemented, "method RemoveVolume not implemented")
}
func (UnimplementedDockerServer) CreateServiceContainer(context.Context, *CreateServiceContainerRequest) (*CreateContainerResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method CreateServiceContainer not implemented")
}
func (UnimplementedDockerServer) InspectServiceContainer(context.Context, *InspectContainerRequest) (*ServiceContainer, error) {
return nil, status.Errorf(codes.Unimplemented, "method InspectServiceContainer not implemented")
}
func (UnimplementedDockerServer) ListServiceContainers(context.Context, *ListServiceContainersRequest) (*ListServiceContainersResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ListServiceContainers not implemented")
}
func (UnimplementedDockerServer) RemoveServiceContainer(context.Context, *RemoveContainerRequest) (*emptypb.Empty, error) {
return nil, status.Errorf(codes.Unimplemented, "method RemoveServiceContainer not implemented")
}
func (UnimplementedDockerServer) mustEmbedUnimplementedDockerServer() {}
func (UnimplementedDockerServer) testEmbeddedByValue() {}
@@ -311,6 +459,168 @@ func _Docker_PullImage_Handler(srv interface{}, stream grpc.ServerStream) error
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
type Docker_PullImageServer = grpc.ServerStreamingServer[JSONMessage]
func _Docker_InspectImage_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(InspectImageRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(DockerServer).InspectImage(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: Docker_InspectImage_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(DockerServer).InspectImage(ctx, req.(*InspectImageRequest))
}
return interceptor(ctx, in, info, handler)
}
func _Docker_InspectRemoteImage_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(InspectRemoteImageRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(DockerServer).InspectRemoteImage(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: Docker_InspectRemoteImage_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(DockerServer).InspectRemoteImage(ctx, req.(*InspectRemoteImageRequest))
}
return interceptor(ctx, in, info, handler)
}
func _Docker_CreateVolume_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(CreateVolumeRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(DockerServer).CreateVolume(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: Docker_CreateVolume_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(DockerServer).CreateVolume(ctx, req.(*CreateVolumeRequest))
}
return interceptor(ctx, in, info, handler)
}
func _Docker_ListVolumes_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ListVolumesRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(DockerServer).ListVolumes(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: Docker_ListVolumes_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(DockerServer).ListVolumes(ctx, req.(*ListVolumesRequest))
}
return interceptor(ctx, in, info, handler)
}
func _Docker_RemoveVolume_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(RemoveVolumeRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(DockerServer).RemoveVolume(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: Docker_RemoveVolume_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(DockerServer).RemoveVolume(ctx, req.(*RemoveVolumeRequest))
}
return interceptor(ctx, in, info, handler)
}
func _Docker_CreateServiceContainer_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(CreateServiceContainerRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(DockerServer).CreateServiceContainer(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: Docker_CreateServiceContainer_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(DockerServer).CreateServiceContainer(ctx, req.(*CreateServiceContainerRequest))
}
return interceptor(ctx, in, info, handler)
}
func _Docker_InspectServiceContainer_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(InspectContainerRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(DockerServer).InspectServiceContainer(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: Docker_InspectServiceContainer_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(DockerServer).InspectServiceContainer(ctx, req.(*InspectContainerRequest))
}
return interceptor(ctx, in, info, handler)
}
func _Docker_ListServiceContainers_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ListServiceContainersRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(DockerServer).ListServiceContainers(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: Docker_ListServiceContainers_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(DockerServer).ListServiceContainers(ctx, req.(*ListServiceContainersRequest))
}
return interceptor(ctx, in, info, handler)
}
func _Docker_RemoveServiceContainer_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(RemoveContainerRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(DockerServer).RemoveServiceContainer(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: Docker_RemoveServiceContainer_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(DockerServer).RemoveServiceContainer(ctx, req.(*RemoveContainerRequest))
}
return interceptor(ctx, in, info, handler)
}
// Docker_ServiceDesc is the grpc.ServiceDesc for Docker service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
@@ -342,6 +652,42 @@ var Docker_ServiceDesc = grpc.ServiceDesc{
MethodName: "RemoveContainer",
Handler: _Docker_RemoveContainer_Handler,
},
{
MethodName: "InspectImage",
Handler: _Docker_InspectImage_Handler,
},
{
MethodName: "InspectRemoteImage",
Handler: _Docker_InspectRemoteImage_Handler,
},
{
MethodName: "CreateVolume",
Handler: _Docker_CreateVolume_Handler,
},
{
MethodName: "ListVolumes",
Handler: _Docker_ListVolumes_Handler,
},
{
MethodName: "RemoveVolume",
Handler: _Docker_RemoveVolume_Handler,
},
{
MethodName: "CreateServiceContainer",
Handler: _Docker_CreateServiceContainer_Handler,
},
{
MethodName: "InspectServiceContainer",
Handler: _Docker_InspectServiceContainer_Handler,
},
{
MethodName: "ListServiceContainers",
Handler: _Docker_ListServiceContainers_Handler,
},
{
MethodName: "RemoveServiceContainer",
Handler: _Docker_RemoveServiceContainer_Handler,
},
},
Streams: []grpc.StreamDesc{
{
+312 -165
View File
@@ -21,20 +21,78 @@ const (
_ = 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 {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`
Network *NetworkConfig `protobuf:"bytes,3,opt,name=network,proto3" json:"network,omitempty"`
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`
Network *NetworkConfig `protobuf:"bytes,3,opt,name=network,proto3" json:"network,omitempty"`
PublicIp *IP `protobuf:"bytes,4,opt,name=public_ip,json=publicIp,proto3" json:"public_ip,omitempty"`
}
func (x *MachineInfo) Reset() {
*x = MachineInfo{}
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.StoreMessageInfo(mi)
}
@@ -47,7 +105,7 @@ func (x *MachineInfo) String() string {
func (*MachineInfo) ProtoMessage() {}
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 {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -60,7 +118,7 @@ func (x *MachineInfo) ProtoReflect() protoreflect.Message {
// Deprecated: Use MachineInfo.ProtoReflect.Descriptor instead.
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 {
@@ -84,6 +142,13 @@ func (x *MachineInfo) GetNetwork() *NetworkConfig {
return nil
}
func (x *MachineInfo) GetPublicIp() *IP {
if x != nil {
return x.PublicIp
}
return nil
}
type NetworkConfig struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
@@ -92,13 +157,13 @@ type NetworkConfig struct {
Subnet *IPPrefix `protobuf:"bytes,1,opt,name=subnet,proto3" json:"subnet,omitempty"`
ManagementIp *IP `protobuf:"bytes,2,opt,name=management_ip,json=managementIp,proto3" json:"management_ip,omitempty"`
Endpoints []*IPPort `protobuf:"bytes,3,rep,name=endpoints,proto3" json:"endpoints,omitempty"`
PublicKey []byte `protobuf:"bytes,4,opt,name=publicKey,proto3" json:"publicKey,omitempty"`
PublicKey []byte `protobuf:"bytes,4,opt,name=public_key,json=publicKey,proto3" json:"public_key,omitempty"`
}
func (x *NetworkConfig) Reset() {
*x = NetworkConfig{}
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.StoreMessageInfo(mi)
}
@@ -111,7 +176,7 @@ func (x *NetworkConfig) String() string {
func (*NetworkConfig) ProtoMessage() {}
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 {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -124,7 +189,7 @@ func (x *NetworkConfig) ProtoReflect() protoreflect.Message {
// Deprecated: Use NetworkConfig.ProtoReflect.Descriptor instead.
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 {
@@ -162,12 +227,17 @@ type InitClusterRequest struct {
MachineName string `protobuf:"bytes,1,opt,name=machineName,proto3" json:"machineName,omitempty"`
Network *IPPrefix `protobuf:"bytes,2,opt,name=network,proto3" json:"network,omitempty"`
// Types that are assignable to PublicIpConfig:
//
// *InitClusterRequest_PublicIp
// *InitClusterRequest_PublicIpAuto
PublicIpConfig isInitClusterRequest_PublicIpConfig `protobuf_oneof:"public_ip_config"`
}
func (x *InitClusterRequest) Reset() {
*x = InitClusterRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[2]
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -180,7 +250,7 @@ func (x *InitClusterRequest) String() string {
func (*InitClusterRequest) ProtoMessage() {}
func (x *InitClusterRequest) ProtoReflect() protoreflect.Message {
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[2]
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[3]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -193,7 +263,7 @@ func (x *InitClusterRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use InitClusterRequest.ProtoReflect.Descriptor instead.
func (*InitClusterRequest) Descriptor() ([]byte, []int) {
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{2}
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{3}
}
func (x *InitClusterRequest) GetMachineName() string {
@@ -210,6 +280,43 @@ func (x *InitClusterRequest) GetNetwork() *IPPrefix {
return nil
}
func (m *InitClusterRequest) GetPublicIpConfig() isInitClusterRequest_PublicIpConfig {
if m != nil {
return m.PublicIpConfig
}
return nil
}
func (x *InitClusterRequest) GetPublicIp() *IP {
if x, ok := x.GetPublicIpConfig().(*InitClusterRequest_PublicIp); ok {
return x.PublicIp
}
return nil
}
func (x *InitClusterRequest) GetPublicIpAuto() bool {
if x, ok := x.GetPublicIpConfig().(*InitClusterRequest_PublicIpAuto); ok {
return x.PublicIpAuto
}
return false
}
type isInitClusterRequest_PublicIpConfig interface {
isInitClusterRequest_PublicIpConfig()
}
type InitClusterRequest_PublicIp struct {
PublicIp *IP `protobuf:"bytes,3,opt,name=public_ip,json=publicIp,proto3,oneof"`
}
type InitClusterRequest_PublicIpAuto struct {
PublicIpAuto bool `protobuf:"varint,4,opt,name=public_ip_auto,json=publicIpAuto,proto3,oneof"`
}
func (*InitClusterRequest_PublicIp) isInitClusterRequest_PublicIpConfig() {}
func (*InitClusterRequest_PublicIpAuto) isInitClusterRequest_PublicIpConfig() {}
type InitClusterResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
@@ -221,7 +328,7 @@ type InitClusterResponse struct {
func (x *InitClusterResponse) Reset() {
*x = InitClusterResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[3]
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -234,7 +341,7 @@ func (x *InitClusterResponse) String() string {
func (*InitClusterResponse) ProtoMessage() {}
func (x *InitClusterResponse) ProtoReflect() protoreflect.Message {
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[3]
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[4]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -247,7 +354,7 @@ func (x *InitClusterResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use InitClusterResponse.ProtoReflect.Descriptor instead.
func (*InitClusterResponse) Descriptor() ([]byte, []int) {
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{3}
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{4}
}
func (x *InitClusterResponse) GetMachine() *MachineInfo {
@@ -269,7 +376,7 @@ type JoinClusterRequest struct {
func (x *JoinClusterRequest) Reset() {
*x = JoinClusterRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[4]
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[5]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -282,7 +389,7 @@ func (x *JoinClusterRequest) String() string {
func (*JoinClusterRequest) ProtoMessage() {}
func (x *JoinClusterRequest) ProtoReflect() protoreflect.Message {
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[4]
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[5]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -295,7 +402,7 @@ func (x *JoinClusterRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use JoinClusterRequest.ProtoReflect.Descriptor instead.
func (*JoinClusterRequest) Descriptor() ([]byte, []int) {
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{4}
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{5}
}
func (x *JoinClusterRequest) GetMachine() *MachineInfo {
@@ -323,7 +430,7 @@ type TokenResponse struct {
func (x *TokenResponse) Reset() {
*x = TokenResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[5]
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[6]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -336,7 +443,7 @@ func (x *TokenResponse) String() string {
func (*TokenResponse) ProtoMessage() {}
func (x *TokenResponse) ProtoReflect() protoreflect.Message {
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[5]
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[6]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -349,7 +456,7 @@ func (x *TokenResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use TokenResponse.ProtoReflect.Descriptor instead.
func (*TokenResponse) Descriptor() ([]byte, []int) {
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{5}
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{6}
}
func (x *TokenResponse) GetToken() string {
@@ -373,7 +480,7 @@ type Service struct {
func (x *Service) Reset() {
*x = Service{}
if protoimpl.UnsafeEnabled {
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[6]
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[7]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -386,7 +493,7 @@ func (x *Service) String() string {
func (*Service) ProtoMessage() {}
func (x *Service) ProtoReflect() protoreflect.Message {
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[6]
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[7]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -399,7 +506,7 @@ func (x *Service) ProtoReflect() protoreflect.Message {
// Deprecated: Use Service.ProtoReflect.Descriptor instead.
func (*Service) Descriptor() ([]byte, []int) {
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{6}
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{7}
}
func (x *Service) GetId() string {
@@ -441,7 +548,7 @@ type InspectServiceRequest struct {
func (x *InspectServiceRequest) Reset() {
*x = InspectServiceRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[7]
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[8]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -454,7 +561,7 @@ func (x *InspectServiceRequest) String() string {
func (*InspectServiceRequest) ProtoMessage() {}
func (x *InspectServiceRequest) ProtoReflect() protoreflect.Message {
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[7]
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[8]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -467,7 +574,7 @@ func (x *InspectServiceRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use InspectServiceRequest.ProtoReflect.Descriptor instead.
func (*InspectServiceRequest) Descriptor() ([]byte, []int) {
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{7}
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{8}
}
func (x *InspectServiceRequest) GetId() string {
@@ -488,7 +595,7 @@ type InspectServiceResponse struct {
func (x *InspectServiceResponse) Reset() {
*x = InspectServiceResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[8]
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[9]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -501,7 +608,7 @@ func (x *InspectServiceResponse) String() string {
func (*InspectServiceResponse) ProtoMessage() {}
func (x *InspectServiceResponse) ProtoReflect() protoreflect.Message {
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[8]
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[9]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -514,7 +621,7 @@ func (x *InspectServiceResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use InspectServiceResponse.ProtoReflect.Descriptor instead.
func (*InspectServiceResponse) Descriptor() ([]byte, []int) {
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{8}
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{9}
}
func (x *InspectServiceResponse) GetService() *Service {
@@ -537,7 +644,7 @@ type Service_Container struct {
func (x *Service_Container) Reset() {
*x = Service_Container{}
if protoimpl.UnsafeEnabled {
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[9]
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[10]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -550,7 +657,7 @@ func (x *Service_Container) String() string {
func (*Service_Container) ProtoMessage() {}
func (x *Service_Container) ProtoReflect() protoreflect.Message {
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[9]
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[10]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -563,7 +670,7 @@ func (x *Service_Container) ProtoReflect() protoreflect.Message {
// Deprecated: Use Service_Container.ProtoReflect.Descriptor instead.
func (*Service_Container) Descriptor() ([]byte, []int) {
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{6, 0}
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{7, 0}
}
func (x *Service_Container) GetMachineId() string {
@@ -590,87 +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,
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,
0x5f, 0x0a, 0x0b, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x0e,
0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x12,
0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61,
0x6d, 0x65, 0x12, 0x2c, 0x0a, 0x07, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x18, 0x03, 0x20,
0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72,
0x6b, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x07, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b,
0x22, 0xad, 0x01, 0x0a, 0x0d, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x43, 0x6f, 0x6e, 0x66,
0x69, 0x67, 0x12, 0x25, 0x0a, 0x06, 0x73, 0x75, 0x62, 0x6e, 0x65, 0x74, 0x18, 0x01, 0x20, 0x01,
0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x50, 0x50, 0x72, 0x65, 0x66, 0x69,
0x78, 0x52, 0x06, 0x73, 0x75, 0x62, 0x6e, 0x65, 0x74, 0x12, 0x2c, 0x0a, 0x0d, 0x6d, 0x61, 0x6e,
0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x70, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b,
0x32, 0x07, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x50, 0x52, 0x0c, 0x6d, 0x61, 0x6e, 0x61, 0x67,
0x65, 0x6d, 0x65, 0x6e, 0x74, 0x49, 0x70, 0x12, 0x29, 0x0a, 0x09, 0x65, 0x6e, 0x64, 0x70, 0x6f,
0x69, 0x6e, 0x74, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0b, 0x2e, 0x61, 0x70, 0x69,
0x2e, 0x49, 0x50, 0x50, 0x6f, 0x72, 0x74, 0x52, 0x09, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e,
0x74, 0x73, 0x12, 0x1c, 0x0a, 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x18,
0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79,
0x22, 0x5f, 0x0a, 0x12, 0x49, 0x6e, 0x69, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x52,
0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x20, 0x0a, 0x0b, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e,
0x65, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x6d, 0x61, 0x63,
0x68, 0x69, 0x6e, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x27, 0x0a, 0x07, 0x6e, 0x65, 0x74, 0x77,
0x6f, 0x72, 0x6b, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x61, 0x70, 0x69, 0x2e,
0x49, 0x50, 0x50, 0x72, 0x65, 0x66, 0x69, 0x78, 0x52, 0x07, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72,
0x6b, 0x22, 0x41, 0x0a, 0x13, 0x49, 0x6e, 0x69, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72,
0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2a, 0x0a, 0x07, 0x6d, 0x61, 0x63, 0x68,
0x69, 0x6e, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x61, 0x70, 0x69, 0x2e,
0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x07, 0x6d, 0x61, 0x63,
0x68, 0x69, 0x6e, 0x65, 0x22, 0x79, 0x0a, 0x12, 0x4a, 0x6f, 0x69, 0x6e, 0x43, 0x6c, 0x75, 0x73,
0x74, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2a, 0x0a, 0x07, 0x6d, 0x61,
0x63, 0x68, 0x69, 0x6e, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x61, 0x70,
0x69, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x07, 0x6d,
0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x12, 0x37, 0x0a, 0x0e, 0x6f, 0x74, 0x68, 0x65, 0x72, 0x5f,
0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x10,
0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f,
0x52, 0x0d, 0x6f, 0x74, 0x68, 0x65, 0x72, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x22,
0x25, 0x0a, 0x0d, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52,
0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0xc3, 0x01, 0x0a, 0x07, 0x53, 0x65, 0x72, 0x76, 0x69,
0x63, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02,
0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09,
0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x18, 0x03,
0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x12, 0x36, 0x0a, 0x0a, 0x63, 0x6f,
0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16,
0x2e, 0x61, 0x70, 0x69, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x43, 0x6f, 0x6e,
0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x0a, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65,
0x72, 0x73, 0x1a, 0x48, 0x0a, 0x09, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12,
0x1d, 0x0a, 0x0a, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20,
0x01, 0x28, 0x09, 0x52, 0x09, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x64, 0x12, 0x1c,
0x0a, 0x09, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28,
0x0c, 0x52, 0x09, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x22, 0x27, 0x0a, 0x15,
0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65,
0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28,
0x09, 0x52, 0x02, 0x69, 0x64, 0x22, 0x40, 0x0a, 0x16, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74,
0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12,
0x26, 0x0a, 0x07, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b,
0x32, 0x0c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x07,
0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x32, 0xc0, 0x02, 0x0a, 0x07, 0x4d, 0x61, 0x63, 0x68,
0x69, 0x6e, 0x65, 0x12, 0x40, 0x0a, 0x0b, 0x49, 0x6e, 0x69, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74,
0x65, 0x72, 0x12, 0x17, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x69, 0x74, 0x43, 0x6c, 0x75,
0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x61, 0x70,
0x69, 0x2e, 0x49, 0x6e, 0x69, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x73,
0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3e, 0x0a, 0x0b, 0x4a, 0x6f, 0x69, 0x6e, 0x43, 0x6c, 0x75,
0x73, 0x74, 0x65, 0x72, 0x12, 0x17, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4a, 0x6f, 0x69, 0x6e, 0x43,
0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e,
0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e,
0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x33, 0x0a, 0x05, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x16,
0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66,
0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x12, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x54, 0x6f, 0x6b,
0x65, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x33, 0x0a, 0x07, 0x49, 0x6e,
0x73, 0x70, 0x65, 0x63, 0x74, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70,
0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x10, 0x2e,
0x61, 0x70, 0x69, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x12,
0x49, 0x0a, 0x0e, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63,
0x65, 0x12, 0x1a, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53,
0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e,
0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69,
0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x37, 0x5a, 0x35, 0x67, 0x69,
0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x70, 0x73, 0x76, 0x69, 0x64, 0x65, 0x72,
0x73, 0x6b, 0x69, 0x2f, 0x75, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2f, 0x69, 0x6e, 0x74, 0x65,
0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x2f, 0x61, 0x70, 0x69,
0x2f, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
0x50, 0x0a, 0x1a, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x50, 0x72, 0x65, 0x72, 0x65, 0x71, 0x75, 0x69,
0x73, 0x69, 0x74, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1c, 0x0a,
0x09, 0x73, 0x61, 0x74, 0x69, 0x73, 0x66, 0x69, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08,
0x52, 0x09, 0x73, 0x61, 0x74, 0x69, 0x73, 0x66, 0x69, 0x65, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x65,
0x72, 0x72, 0x6f, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f,
0x72, 0x22, 0x85, 0x01, 0x0a, 0x0b, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66,
0x6f, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69,
0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52,
0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x2c, 0x0a, 0x07, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b,
0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4e, 0x65, 0x74,
0x77, 0x6f, 0x72, 0x6b, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x07, 0x6e, 0x65, 0x74, 0x77,
0x6f, 0x72, 0x6b, 0x12, 0x24, 0x0a, 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x69, 0x70,
0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x07, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x50, 0x52,
0x08, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x49, 0x70, 0x22, 0xae, 0x01, 0x0a, 0x0d, 0x4e, 0x65,
0x74, 0x77, 0x6f, 0x72, 0x6b, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x25, 0x0a, 0x06, 0x73,
0x75, 0x62, 0x6e, 0x65, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x61, 0x70,
0x69, 0x2e, 0x49, 0x50, 0x50, 0x72, 0x65, 0x66, 0x69, 0x78, 0x52, 0x06, 0x73, 0x75, 0x62, 0x6e,
0x65, 0x74, 0x12, 0x2c, 0x0a, 0x0d, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74,
0x5f, 0x69, 0x70, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x07, 0x2e, 0x61, 0x70, 0x69, 0x2e,
0x49, 0x50, 0x52, 0x0c, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x49, 0x70,
0x12, 0x29, 0x0a, 0x09, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x18, 0x03, 0x20,
0x03, 0x28, 0x0b, 0x32, 0x0b, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x50, 0x50, 0x6f, 0x72, 0x74,
0x52, 0x09, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x70,
0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52,
0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x22, 0xc3, 0x01, 0x0a, 0x12, 0x49,
0x6e, 0x69, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
0x74, 0x12, 0x20, 0x0a, 0x0b, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x4e, 0x61, 0x6d, 0x65,
0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x4e,
0x61, 0x6d, 0x65, 0x12, 0x27, 0x0a, 0x07, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x18, 0x02,
0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x50, 0x50, 0x72, 0x65,
0x66, 0x69, 0x78, 0x52, 0x07, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x12, 0x26, 0x0a, 0x09,
0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x69, 0x70, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32,
0x07, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x50, 0x48, 0x00, 0x52, 0x08, 0x70, 0x75, 0x62, 0x6c,
0x69, 0x63, 0x49, 0x70, 0x12, 0x26, 0x0a, 0x0e, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x69,
0x70, 0x5f, 0x61, 0x75, 0x74, 0x6f, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x48, 0x00, 0x52, 0x0c,
0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x49, 0x70, 0x41, 0x75, 0x74, 0x6f, 0x42, 0x12, 0x0a, 0x10,
0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x69, 0x70, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67,
0x22, 0x41, 0x0a, 0x13, 0x49, 0x6e, 0x69, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x52,
0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2a, 0x0a, 0x07, 0x6d, 0x61, 0x63, 0x68, 0x69,
0x6e, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d,
0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x07, 0x6d, 0x61, 0x63, 0x68,
0x69, 0x6e, 0x65, 0x22, 0x79, 0x0a, 0x12, 0x4a, 0x6f, 0x69, 0x6e, 0x43, 0x6c, 0x75, 0x73, 0x74,
0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2a, 0x0a, 0x07, 0x6d, 0x61, 0x63,
0x68, 0x69, 0x6e, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x61, 0x70, 0x69,
0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x07, 0x6d, 0x61,
0x63, 0x68, 0x69, 0x6e, 0x65, 0x12, 0x37, 0x0a, 0x0e, 0x6f, 0x74, 0x68, 0x65, 0x72, 0x5f, 0x6d,
0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x10, 0x2e,
0x61, 0x70, 0x69, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52,
0x0d, 0x6f, 0x74, 0x68, 0x65, 0x72, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x22, 0x25,
0x0a, 0x0d, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12,
0x14, 0x0a, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05,
0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0xc3, 0x01, 0x0a, 0x07, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63,
0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69,
0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52,
0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x18, 0x03, 0x20,
0x01, 0x28, 0x09, 0x52, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x12, 0x36, 0x0a, 0x0a, 0x63, 0x6f, 0x6e,
0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e,
0x61, 0x70, 0x69, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x43, 0x6f, 0x6e, 0x74,
0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x0a, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72,
0x73, 0x1a, 0x48, 0x0a, 0x09, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x1d,
0x0a, 0x0a, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01,
0x28, 0x09, 0x52, 0x09, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x64, 0x12, 0x1c, 0x0a,
0x09, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c,
0x52, 0x09, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x22, 0x27, 0x0a, 0x15, 0x49,
0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x71,
0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
0x52, 0x02, 0x69, 0x64, 0x22, 0x40, 0x0a, 0x16, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53,
0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x26,
0x0a, 0x07, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32,
0x0c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x07, 0x73,
0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x32, 0x8f, 0x03, 0x0a, 0x07, 0x4d, 0x61, 0x63, 0x68, 0x69,
0x6e, 0x65, 0x12, 0x4d, 0x0a, 0x12, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x50, 0x72, 0x65, 0x72, 0x65,
0x71, 0x75, 0x69, 0x73, 0x69, 0x74, 0x65, 0x73, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c,
0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79,
0x1a, 0x1f, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x50, 0x72, 0x65, 0x72,
0x65, 0x71, 0x75, 0x69, 0x73, 0x69, 0x74, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
0x65, 0x12, 0x40, 0x0a, 0x0b, 0x49, 0x6e, 0x69, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72,
0x12, 0x17, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x69, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74,
0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x61, 0x70, 0x69, 0x2e,
0x49, 0x6e, 0x69, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f,
0x6e, 0x73, 0x65, 0x12, 0x3e, 0x0a, 0x0b, 0x4a, 0x6f, 0x69, 0x6e, 0x43, 0x6c, 0x75, 0x73, 0x74,
0x65, 0x72, 0x12, 0x17, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4a, 0x6f, 0x69, 0x6e, 0x43, 0x6c, 0x75,
0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f,
0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d,
0x70, 0x74, 0x79, 0x12, 0x33, 0x0a, 0x05, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x16, 0x2e, 0x67,
0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45,
0x6d, 0x70, 0x74, 0x79, 0x1a, 0x12, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x54, 0x6f, 0x6b, 0x65, 0x6e,
0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x33, 0x0a, 0x07, 0x49, 0x6e, 0x73, 0x70,
0x65, 0x63, 0x74, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f,
0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x10, 0x2e, 0x61, 0x70,
0x69, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x49, 0x0a,
0x0e, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12,
0x1a, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72,
0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x61, 0x70,
0x69, 0x2e, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65,
0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x37, 0x5a, 0x35, 0x67, 0x69, 0x74, 0x68,
0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x70, 0x73, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x6b,
0x69, 0x2f, 0x75, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e,
0x61, 0x6c, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70,
0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
@@ -685,49 +811,54 @@ func file_internal_machine_api_pb_machine_proto_rawDescGZIP() []byte {
return file_internal_machine_api_pb_machine_proto_rawDescData
}
var file_internal_machine_api_pb_machine_proto_msgTypes = make([]protoimpl.MessageInfo, 10)
var file_internal_machine_api_pb_machine_proto_msgTypes = make([]protoimpl.MessageInfo, 11)
var file_internal_machine_api_pb_machine_proto_goTypes = []any{
(*MachineInfo)(nil), // 0: api.MachineInfo
(*NetworkConfig)(nil), // 1: api.NetworkConfig
(*InitClusterRequest)(nil), // 2: api.InitClusterRequest
(*InitClusterResponse)(nil), // 3: api.InitClusterResponse
(*JoinClusterRequest)(nil), // 4: api.JoinClusterRequest
(*TokenResponse)(nil), // 5: api.TokenResponse
(*Service)(nil), // 6: api.Service
(*InspectServiceRequest)(nil), // 7: api.InspectServiceRequest
(*InspectServiceResponse)(nil), // 8: api.InspectServiceResponse
(*Service_Container)(nil), // 9: api.Service.Container
(*IPPrefix)(nil), // 10: api.IPPrefix
(*IP)(nil), // 11: api.IP
(*IPPort)(nil), // 12: api.IPPort
(*emptypb.Empty)(nil), // 13: google.protobuf.Empty
(*CheckPrerequisitesResponse)(nil), // 0: api.CheckPrerequisitesResponse
(*MachineInfo)(nil), // 1: api.MachineInfo
(*NetworkConfig)(nil), // 2: api.NetworkConfig
(*InitClusterRequest)(nil), // 3: api.InitClusterRequest
(*InitClusterResponse)(nil), // 4: api.InitClusterResponse
(*JoinClusterRequest)(nil), // 5: api.JoinClusterRequest
(*TokenResponse)(nil), // 6: api.TokenResponse
(*Service)(nil), // 7: api.Service
(*InspectServiceRequest)(nil), // 8: api.InspectServiceRequest
(*InspectServiceResponse)(nil), // 9: api.InspectServiceResponse
(*Service_Container)(nil), // 10: api.Service.Container
(*IP)(nil), // 11: api.IP
(*IPPrefix)(nil), // 12: api.IPPrefix
(*IPPort)(nil), // 13: api.IPPort
(*emptypb.Empty)(nil), // 14: google.protobuf.Empty
}
var file_internal_machine_api_pb_machine_proto_depIdxs = []int32{
1, // 0: api.MachineInfo.network:type_name -> api.NetworkConfig
10, // 1: api.NetworkConfig.subnet:type_name -> api.IPPrefix
11, // 2: api.NetworkConfig.management_ip:type_name -> api.IP
12, // 3: api.NetworkConfig.endpoints:type_name -> api.IPPort
10, // 4: api.InitClusterRequest.network:type_name -> api.IPPrefix
0, // 5: api.InitClusterResponse.machine:type_name -> api.MachineInfo
0, // 6: api.JoinClusterRequest.machine:type_name -> api.MachineInfo
0, // 7: api.JoinClusterRequest.other_machines:type_name -> api.MachineInfo
9, // 8: api.Service.containers:type_name -> api.Service.Container
6, // 9: api.InspectServiceResponse.service:type_name -> api.Service
2, // 10: api.Machine.InitCluster:input_type -> api.InitClusterRequest
4, // 11: api.Machine.JoinCluster:input_type -> api.JoinClusterRequest
13, // 12: api.Machine.Token:input_type -> google.protobuf.Empty
13, // 13: api.Machine.Inspect:input_type -> google.protobuf.Empty
7, // 14: api.Machine.InspectService:input_type -> api.InspectServiceRequest
3, // 15: api.Machine.InitCluster:output_type -> api.InitClusterResponse
13, // 16: api.Machine.JoinCluster:output_type -> google.protobuf.Empty
5, // 17: api.Machine.Token:output_type -> api.TokenResponse
0, // 18: api.Machine.Inspect:output_type -> api.MachineInfo
8, // 19: api.Machine.InspectService:output_type -> api.InspectServiceResponse
15, // [15:20] is the sub-list for method output_type
10, // [10:15] is the sub-list for method input_type
10, // [10:10] is the sub-list for extension type_name
10, // [10:10] is the sub-list for extension extendee
0, // [0:10] is the sub-list for field type_name
2, // 0: api.MachineInfo.network:type_name -> api.NetworkConfig
11, // 1: api.MachineInfo.public_ip:type_name -> api.IP
12, // 2: api.NetworkConfig.subnet:type_name -> api.IPPrefix
11, // 3: api.NetworkConfig.management_ip:type_name -> api.IP
13, // 4: api.NetworkConfig.endpoints:type_name -> api.IPPort
12, // 5: api.InitClusterRequest.network:type_name -> api.IPPrefix
11, // 6: api.InitClusterRequest.public_ip:type_name -> api.IP
1, // 7: api.InitClusterResponse.machine:type_name -> api.MachineInfo
1, // 8: api.JoinClusterRequest.machine:type_name -> api.MachineInfo
1, // 9: api.JoinClusterRequest.other_machines:type_name -> api.MachineInfo
10, // 10: api.Service.containers:type_name -> api.Service.Container
7, // 11: api.InspectServiceResponse.service:type_name -> api.Service
14, // 12: api.Machine.CheckPrerequisites:input_type -> google.protobuf.Empty
3, // 13: api.Machine.InitCluster:input_type -> api.InitClusterRequest
5, // 14: api.Machine.JoinCluster:input_type -> api.JoinClusterRequest
14, // 15: api.Machine.Token:input_type -> google.protobuf.Empty
14, // 16: api.Machine.Inspect:input_type -> google.protobuf.Empty
8, // 17: api.Machine.InspectService:input_type -> api.InspectServiceRequest
0, // 18: api.Machine.CheckPrerequisites:output_type -> api.CheckPrerequisitesResponse
4, // 19: api.Machine.InitCluster:output_type -> api.InitClusterResponse
14, // 20: api.Machine.JoinCluster:output_type -> google.protobuf.Empty
6, // 21: api.Machine.Token:output_type -> api.TokenResponse
1, // 22: api.Machine.Inspect:output_type -> api.MachineInfo
9, // 23: api.Machine.InspectService:output_type -> api.InspectServiceResponse
18, // [18:24] is the sub-list for method output_type
12, // [12:18] is the sub-list for method input_type
12, // [12:12] is the sub-list for extension type_name
12, // [12:12] is the sub-list for extension extendee
0, // [0:12] is the sub-list for field type_name
}
func init() { file_internal_machine_api_pb_machine_proto_init() }
@@ -738,7 +869,7 @@ func file_internal_machine_api_pb_machine_proto_init() {
file_internal_machine_api_pb_common_proto_init()
if !protoimpl.UnsafeEnabled {
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:
return &v.state
case 1:
@@ -750,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 {
switch v := v.(*NetworkConfig); i {
switch v := v.(*MachineInfo); i {
case 0:
return &v.state
case 1:
@@ -762,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 {
switch v := v.(*InitClusterRequest); i {
switch v := v.(*NetworkConfig); i {
case 0:
return &v.state
case 1:
@@ -774,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 {
switch v := v.(*InitClusterResponse); i {
switch v := v.(*InitClusterRequest); i {
case 0:
return &v.state
case 1:
@@ -786,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 {
switch v := v.(*JoinClusterRequest); i {
switch v := v.(*InitClusterResponse); i {
case 0:
return &v.state
case 1:
@@ -798,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 {
switch v := v.(*TokenResponse); i {
switch v := v.(*JoinClusterRequest); i {
case 0:
return &v.state
case 1:
@@ -810,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 {
switch v := v.(*Service); i {
switch v := v.(*TokenResponse); i {
case 0:
return &v.state
case 1:
@@ -822,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 {
switch v := v.(*InspectServiceRequest); i {
switch v := v.(*Service); i {
case 0:
return &v.state
case 1:
@@ -834,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 {
switch v := v.(*InspectServiceResponse); i {
switch v := v.(*InspectServiceRequest); i {
case 0:
return &v.state
case 1:
@@ -846,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 {
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 {
case 0:
return &v.state
@@ -858,13 +1001,17 @@ func file_internal_machine_api_pb_machine_proto_init() {
}
}
}
file_internal_machine_api_pb_machine_proto_msgTypes[3].OneofWrappers = []any{
(*InitClusterRequest_PublicIp)(nil),
(*InitClusterRequest_PublicIpAuto)(nil),
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_internal_machine_api_pb_machine_proto_rawDesc,
NumEnums: 0,
NumMessages: 10,
NumMessages: 11,
NumExtensions: 0,
NumServices: 1,
},
+16 -1
View File
@@ -8,6 +8,8 @@ import "google/protobuf/empty.proto";
import "internal/machine/api/pb/common.proto";
service Machine {
// CheckPrerequisites verifies if the machine meets all necessary system requirements to participate in the cluster.
rpc CheckPrerequisites(google.protobuf.Empty) returns (CheckPrerequisitesResponse);
rpc InitCluster(InitClusterRequest) returns (InitClusterResponse);
rpc JoinCluster(JoinClusterRequest) returns (google.protobuf.Empty);
rpc Token(google.protobuf.Empty) returns (TokenResponse);
@@ -19,18 +21,31 @@ message MachineInfo {
string id = 1;
string name = 2;
NetworkConfig network = 3;
IP public_ip = 4;
}
message NetworkConfig {
IPPrefix subnet = 1;
IP management_ip = 2;
repeated IPPort endpoints = 3;
bytes publicKey = 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 {
string machineName = 1;
IPPrefix network = 2;
oneof public_ip_config {
IP public_ip = 3;
bool public_ip_auto = 4;
}
}
message InitClusterResponse {
+43 -5
View File
@@ -20,17 +20,19 @@ import (
const _ = grpc.SupportPackageIsVersion9
const (
Machine_InitCluster_FullMethodName = "/api.Machine/InitCluster"
Machine_JoinCluster_FullMethodName = "/api.Machine/JoinCluster"
Machine_Token_FullMethodName = "/api.Machine/Token"
Machine_Inspect_FullMethodName = "/api.Machine/Inspect"
Machine_InspectService_FullMethodName = "/api.Machine/InspectService"
Machine_CheckPrerequisites_FullMethodName = "/api.Machine/CheckPrerequisites"
Machine_InitCluster_FullMethodName = "/api.Machine/InitCluster"
Machine_JoinCluster_FullMethodName = "/api.Machine/JoinCluster"
Machine_Token_FullMethodName = "/api.Machine/Token"
Machine_Inspect_FullMethodName = "/api.Machine/Inspect"
Machine_InspectService_FullMethodName = "/api.Machine/InspectService"
)
// MachineClient is the client API for Machine service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
type MachineClient interface {
CheckPrerequisites(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*CheckPrerequisitesResponse, error)
InitCluster(ctx context.Context, in *InitClusterRequest, opts ...grpc.CallOption) (*InitClusterResponse, error)
JoinCluster(ctx context.Context, in *JoinClusterRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
Token(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*TokenResponse, error)
@@ -46,6 +48,16 @@ func NewMachineClient(cc grpc.ClientConnInterface) MachineClient {
return &machineClient{cc}
}
func (c *machineClient) CheckPrerequisites(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*CheckPrerequisitesResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(CheckPrerequisitesResponse)
err := c.cc.Invoke(ctx, Machine_CheckPrerequisites_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *machineClient) InitCluster(ctx context.Context, in *InitClusterRequest, opts ...grpc.CallOption) (*InitClusterResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(InitClusterResponse)
@@ -100,6 +112,7 @@ func (c *machineClient) InspectService(ctx context.Context, in *InspectServiceRe
// All implementations must embed UnimplementedMachineServer
// for forward compatibility.
type MachineServer interface {
CheckPrerequisites(context.Context, *emptypb.Empty) (*CheckPrerequisitesResponse, error)
InitCluster(context.Context, *InitClusterRequest) (*InitClusterResponse, error)
JoinCluster(context.Context, *JoinClusterRequest) (*emptypb.Empty, error)
Token(context.Context, *emptypb.Empty) (*TokenResponse, error)
@@ -115,6 +128,9 @@ type MachineServer interface {
// pointer dereference when methods are called.
type UnimplementedMachineServer struct{}
func (UnimplementedMachineServer) CheckPrerequisites(context.Context, *emptypb.Empty) (*CheckPrerequisitesResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method CheckPrerequisites not implemented")
}
func (UnimplementedMachineServer) InitCluster(context.Context, *InitClusterRequest) (*InitClusterResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method InitCluster not implemented")
}
@@ -151,6 +167,24 @@ func RegisterMachineServer(s grpc.ServiceRegistrar, srv MachineServer) {
s.RegisterService(&Machine_ServiceDesc, srv)
}
func _Machine_CheckPrerequisites_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(emptypb.Empty)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(MachineServer).CheckPrerequisites(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: Machine_CheckPrerequisites_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(MachineServer).CheckPrerequisites(ctx, req.(*emptypb.Empty))
}
return interceptor(ctx, in, info, handler)
}
func _Machine_InitCluster_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(InitClusterRequest)
if err := dec(in); err != nil {
@@ -248,6 +282,10 @@ var Machine_ServiceDesc = grpc.ServiceDesc{
ServiceName: "api.Machine",
HandlerType: (*MachineServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "CheckPrerequisites",
Handler: _Machine_CheckPrerequisites_Handler,
},
{
MethodName: "InitCluster",
Handler: _Machine_InitCluster_Handler,
+1 -1
View File
@@ -5,7 +5,7 @@ import (
"google.golang.org/grpc/status"
"google.golang.org/protobuf/encoding/protowire"
"google.golang.org/protobuf/proto"
"uncloud/internal/machine/api/pb"
"github.com/psviderski/uncloud/internal/machine/api/pb"
)
// One2ManyResponder converts upstream responses into messages from upstreams, so that multiple
+182
View File
@@ -0,0 +1,182 @@
package caddyconfig
import (
"encoding/json"
"errors"
"fmt"
"log/slog"
"maps"
"net"
"net/http"
"slices"
"strconv"
"time"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
"github.com/caddyserver/caddy/v2/modules/caddyhttp/reverseproxy"
"github.com/psviderski/uncloud/pkg/api"
)
func GenerateConfig(containers []api.ServiceContainer, verifyResponse string) (*caddy.Config, error) {
// Maps hostnames to lists of upstreams (container IP:port pairs).
httpHostUpstreams := make(map[string][]string)
httpsHostUpstreams := make(map[string][]string)
for _, ctr := range containers {
if !ctr.Healthy() {
continue
}
ip := ctr.UncloudNetworkIP()
if !ip.IsValid() {
// Container is not connected to the uncloud Docker network (could be host network).
continue
}
log := slog.With("container", ctr.ID)
ports, err := ctr.ServicePorts()
if err != nil {
log.Error("Failed to parse service ports for container.", "err", err)
continue
}
for _, port := range ports {
if port.Mode != api.PortModeIngress {
continue
}
switch port.Protocol {
case api.ProtocolHTTP:
upstream := net.JoinHostPort(ip.String(), strconv.Itoa(int(port.ContainerPort)))
httpHostUpstreams[port.Hostname] = append(httpHostUpstreams[port.Hostname], upstream)
case api.ProtocolHTTPS:
upstream := net.JoinHostPort(ip.String(), strconv.Itoa(int(port.ContainerPort)))
httpsHostUpstreams[port.Hostname] = append(httpsHostUpstreams[port.Hostname], upstream)
default:
// TODO: implement L4 ingress routing for TCP and UDP.
log.Error("Unsupported protocol for ingress port.", "port", port)
continue
}
}
}
var warnings []caddyconfig.Warning
servers := make(map[string]*caddyhttp.Server)
servers["http"] = &caddyhttp.Server{
Listen: []string{fmt.Sprintf(":%d", caddyhttp.DefaultHTTPPort)},
// All http requests to this server are logged to the default logger.
Logs: &caddyhttp.ServerLogConfig{},
Routes: append(
hostUpstreamsToRoutes(httpHostUpstreams, &warnings),
// Add a route to respond with a static verification response at the /.uncloud-verify path.
verificationRoute(verifyResponse, &warnings),
),
}
servers["https"] = &caddyhttp.Server{
Listen: []string{fmt.Sprintf(":%d", caddyhttp.DefaultHTTPSPort)},
// All https requests to this server are logged to the default logger.
Logs: &caddyhttp.ServerLogConfig{},
Routes: hostUpstreamsToRoutes(httpsHostUpstreams, &warnings),
}
httpApp := caddyhttp.App{
Servers: servers,
}
config := &caddy.Config{
AppsRaw: caddy.ModuleMap{
"http": caddyconfig.JSON(httpApp, &warnings),
},
}
var err error
if len(warnings) > 0 {
// warnings only contains errors from JSON marshaling, which are highly unlikely with correct code.
for _, w := range warnings {
err = errors.Join(err, errors.New(w.Message))
}
return nil, fmt.Errorf("marshal Caddy configuration: %w", err)
}
return config, nil
}
// hostUpstreamsToRoutes converts a map of hostnames to upstreams to a list of Caddy routes.
func hostUpstreamsToRoutes(hostUpstreams map[string][]string, warnings *[]caddyconfig.Warning) []caddyhttp.Route {
// Sort hostnames for deterministic output.
hostnames := slices.Collect(maps.Keys(hostUpstreams))
slices.Sort(hostnames)
routes := make([]caddyhttp.Route, 0, len(hostUpstreams))
for _, hostname := range hostnames {
upstreams := hostUpstreams[hostname]
upstreamPool := make([]*reverseproxy.Upstream, len(upstreams))
for i, upstream := range upstreams {
upstreamPool[i] = &reverseproxy.Upstream{
Dial: upstream,
}
}
handler := &reverseproxy.Handler{
HealthChecks: &reverseproxy.HealthChecks{
// Enable passive health checks to automatically detect unhealthy upstreams.
Passive: &reverseproxy.PassiveHealthChecks{
FailDuration: caddy.Duration(30 * time.Second),
},
},
LoadBalancing: &reverseproxy.LoadBalancing{
// Retry failed requests to skip over temporarily unavailable upstreams.
Retries: 3,
},
Upstreams: upstreamPool,
}
routes = append(routes, caddyhttp.Route{
MatcherSetsRaw: caddyhttp.RawMatcherSets{
{
"host": caddyconfig.JSON(caddyhttp.MatchHost{hostname}, warnings),
},
},
HandlersRaw: []json.RawMessage{
caddyconfig.JSONModuleObject(handler, "handler", "reverse_proxy", warnings),
},
})
}
return routes
}
// verificationRoute returns a Caddy route that responds with the given static response at the /.uncloud-verify path.
func verificationRoute(response string, warnings *[]caddyconfig.Warning) caddyhttp.Route {
// Return the following route:
// {
// "match": [
// {
// "path": [
// "/.uncloud-verify"
// ]
// }
// ],
// "handle": [
// {
// "handler": "static_response",
// "body": "<response>",
// "status_code": 200
// }
// ]
// }
staticResponse := caddyhttp.StaticResponse{
StatusCode: caddyhttp.WeakString(strconv.Itoa(http.StatusOK)),
Body: response,
}
return caddyhttp.Route{
MatcherSetsRaw: caddyhttp.RawMatcherSets{
{
"path": caddyconfig.JSON(caddyhttp.MatchPath{VerifyPath}, warnings),
},
},
HandlersRaw: []json.RawMessage{
caddyconfig.JSONModuleObject(staticResponse, "handler", "static_response", warnings),
},
}
}
+453
View File
@@ -0,0 +1,453 @@
package caddyconfig
import (
"strings"
"testing"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/network"
"github.com/psviderski/uncloud/internal/machine/docker"
"github.com/psviderski/uncloud/pkg/api"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestGenerateConfig(t *testing.T) {
configWithoutServices := `{
"servers": {
"http": {
"listen": [":80"],
"routes": [{
"match": [{"path": ["/.uncloud-verify"]}],
"handle": [{
"body": "verification-response-body",
"handler": "static_response",
"status_code": 200
}]
}],
"logs": {}
},
"https": {
"listen": [":443"],
"logs": {}
}
}
}`
tests := []struct {
name string
containers []api.ServiceContainer
want string
wantErr bool
}{
{
name: "empty containers",
containers: []api.ServiceContainer{},
want: configWithoutServices,
wantErr: false,
},
{
name: "HTTP container",
containers: []api.ServiceContainer{
newContainer("10.210.0.2", "app.example.com:8080/http"),
},
want: `{
"servers": {
"http": {
"listen": [":80"],
"routes": [
{
"match": [{"host": ["app.example.com"]}],
"handle": [{
"handler": "reverse_proxy",
"health_checks": {
"passive": {
"fail_duration": 30000000000
}
},
"load_balancing": {
"retries": 3
},
"upstreams": [{"dial": "10.210.0.2:8080"}]
}]
},
{
"match": [{"path": ["/.uncloud-verify"]}],
"handle": [{
"body": "verification-response-body",
"handler": "static_response",
"status_code": 200
}]
}
],
"logs": {}
},
"https": {
"listen": [":443"],
"logs": {}
}
}
}`,
wantErr: false,
},
{
name: "load balancing multiple containers",
containers: []api.ServiceContainer{
newContainer("10.210.0.2", "app.example.com:8080/http"),
newContainer("10.210.0.3", "app.example.com:8080/http"),
},
want: `{
"servers": {
"http": {
"listen": [":80"],
"routes": [
{
"match": [{"host": ["app.example.com"]}],
"handle": [{
"handler": "reverse_proxy",
"health_checks": {
"passive": {
"fail_duration": 30000000000
}
},
"load_balancing": {
"retries": 3
},
"upstreams": [
{"dial": "10.210.0.2:8080"},
{"dial": "10.210.0.3:8080"}
]
}]
},
{
"match": [{"path": ["/.uncloud-verify"]}],
"handle": [{
"body": "verification-response-body",
"handler": "static_response",
"status_code": 200
}]
}
],
"logs": {}
},
"https": {
"listen": [":443"],
"logs": {}
}
}
}`,
wantErr: false,
},
{
name: "HTTPS container",
containers: []api.ServiceContainer{
newContainer("10.210.0.2", "secure.example.com:8000/https"),
},
want: `{
"servers": {
"http": {
"listen": [":80"],
"routes": [
{
"match": [{"path": ["/.uncloud-verify"]}],
"handle": [{
"body": "verification-response-body",
"handler": "static_response",
"status_code": 200
}]
}
],
"logs": {}
},
"https": {
"listen": [":443"],
"routes": [
{
"match": [{"host": ["secure.example.com"]}],
"handle": [{
"handler": "reverse_proxy",
"health_checks": {
"passive": {
"fail_duration": 30000000000
}
},
"load_balancing": {
"retries": 3
},
"upstreams": [{"dial": "10.210.0.2:8000"}]
}]
}
],
"logs": {}
}
}
}`,
wantErr: false,
},
{
name: "mixed HTTP and HTTPS",
containers: []api.ServiceContainer{
newContainer("10.210.0.2",
"app.example.com:8080/http",
"web.example.com:8000/http"),
newContainer("10.210.0.3",
"app.example.com:8080/http",
"secure.example.com:8888/https"),
newContainer("10.210.0.4",
"web.example.com:8000/http",
"secure.example.com:8888/https"),
newContainer("10.210.0.5",
"app.example.com:8080/http",
"web.example.com:8000/http",
"secure.example.com:8888/https"),
},
want: `{
"servers": {
"http": {
"listen": [":80"],
"routes": [
{
"match": [{"host": ["app.example.com"]}],
"handle": [{
"handler": "reverse_proxy",
"health_checks": {
"passive": {
"fail_duration": 30000000000
}
},
"load_balancing": {
"retries": 3
},
"upstreams": [
{"dial": "10.210.0.2:8080"},
{"dial": "10.210.0.3:8080"},
{"dial": "10.210.0.5:8080"}
]
}]
},
{
"match": [{"host": ["web.example.com"]}],
"handle": [{
"handler": "reverse_proxy",
"health_checks": {
"passive": {
"fail_duration": 30000000000
}
},
"load_balancing": {
"retries": 3
},
"upstreams": [
{"dial": "10.210.0.2:8000"},
{"dial": "10.210.0.4:8000"},
{"dial": "10.210.0.5:8000"}
]
}]
},
{
"match": [{"path": ["/.uncloud-verify"]}],
"handle": [{
"body": "verification-response-body",
"handler": "static_response",
"status_code": 200
}]
}
],
"logs": {}
},
"https": {
"listen": [":443"],
"routes": [
{
"match": [{"host": ["secure.example.com"]}],
"handle": [{
"handler": "reverse_proxy",
"health_checks": {
"passive": {
"fail_duration": 30000000000
}
},
"load_balancing": {
"retries": 3
},
"upstreams": [
{"dial": "10.210.0.3:8888"},
{"dial": "10.210.0.4:8888"},
{"dial": "10.210.0.5:8888"}
]
}]
}
],
"logs": {}
}
}
}`,
wantErr: false,
},
{
name: "container without uncloud network ignored",
containers: []api.ServiceContainer{
newContainerWithoutNetwork("ignored.example.com:8080/http"),
},
want: configWithoutServices,
wantErr: false,
},
{
name: "container with invalid port ignored",
containers: []api.ServiceContainer{
newContainer("10.210.0.2", "invalid-port"),
},
want: configWithoutServices,
wantErr: false,
},
{
name: "containers with unsupported protocols and host mode ignored",
containers: []api.ServiceContainer{
newContainer("10.210.0.2", "5000/tcp"),
newContainer("10.210.0.3", "5000/udp"),
newContainer("10.210.0.4", "80:8080/tcp@host"),
},
want: configWithoutServices,
wantErr: false,
},
{
name: "restarting container ignored",
containers: []api.ServiceContainer{
newRestartingContainer("10.210.0.2", "app.example.com:8080/http"),
},
want: configWithoutServices,
wantErr: false,
},
{
name: "stopped container ignored",
containers: []api.ServiceContainer{
newStoppedContainer("10.210.0.2", "app.example.com:8080/http"),
},
want: configWithoutServices,
wantErr: false,
},
{
name: "mix of running, restarting, and stopped containers",
containers: []api.ServiceContainer{
newContainer("10.210.0.2", "app.example.com:8080/http"),
newRestartingContainer("10.210.0.3", "app.example.com:8080/http"),
newStoppedContainer("10.210.0.4", "app.example.com:8080/http"),
},
want: `{
"servers": {
"http": {
"listen": [":80"],
"routes": [
{
"match": [{"host": ["app.example.com"]}],
"handle": [{
"handler": "reverse_proxy",
"health_checks": {
"passive": {
"fail_duration": 30000000000
}
},
"load_balancing": {
"retries": 3
},
"upstreams": [{"dial": "10.210.0.2:8080"}]
}]
},
{
"match": [{"path": ["/.uncloud-verify"]}],
"handle": [{
"body": "verification-response-body",
"handler": "static_response",
"status_code": 200
}]
}
],
"logs": {}
},
"https": {
"listen": [":443"],
"logs": {}
}
}
}`,
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
config, err := GenerateConfig(tt.containers, "verification-response-body")
if tt.wantErr {
assert.Error(t, err)
return
}
require.NoError(t, err)
require.Len(t, config.AppsRaw, 1, "Expected one http app")
require.Contains(t, config.AppsRaw, "http", "Expected http app")
assert.JSONEq(t, tt.want, string(config.AppsRaw["http"]), "Generated Caddy app config doesn't match")
})
}
}
func newContainer(ip string, ports ...string) api.ServiceContainer {
portsLabel := strings.Join(ports, ",")
return api.ServiceContainer{Container: api.Container{ContainerJSON: types.ContainerJSON{
ContainerJSONBase: &types.ContainerJSONBase{
State: &types.ContainerState{
Running: true,
},
},
NetworkSettings: &types.NetworkSettings{
Networks: map[string]*network.EndpointSettings{
docker.NetworkName: {
IPAddress: ip,
},
},
},
Config: &container.Config{
Labels: map[string]string{
api.LabelServicePorts: portsLabel,
},
},
}}}
}
func newContainerWithoutNetwork(ports ...string) api.ServiceContainer {
portsLabel := strings.Join(ports, ",")
return api.ServiceContainer{Container: api.Container{ContainerJSON: types.ContainerJSON{
ContainerJSONBase: &types.ContainerJSONBase{
State: &types.ContainerState{
Running: true,
},
},
NetworkSettings: &types.NetworkSettings{
Networks: map[string]*network.EndpointSettings{
"other-network": {
IPAddress: "172.17.0.2",
},
},
},
Config: &container.Config{
Labels: map[string]string{
api.LabelServicePorts: portsLabel,
},
},
}}}
}
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
}
+125
View File
@@ -0,0 +1,125 @@
package caddyconfig
import (
"context"
"encoding/json"
"fmt"
"log/slog"
"os"
"path/filepath"
"github.com/psviderski/uncloud/internal/fs"
"github.com/psviderski/uncloud/internal/machine/store"
"github.com/psviderski/uncloud/pkg/api"
)
const (
CaddyGroup = "uncloud"
VerifyPath = "/.uncloud-verify"
)
// Controller monitors container changes in the cluster store and generates a configuration file for Caddy reverse
// proxy. The generated configuration allows Caddy to route external traffic to service containers across the internal
// network.
type Controller struct {
store *store.Store
path string
verifyResponse string
}
func NewController(store *store.Store, path string, verifyResponse string) (*Controller, error) {
dir := filepath.Dir(path)
if err := os.MkdirAll(dir, 0750); err != nil {
return nil, fmt.Errorf("create parent directory for Caddy configuration '%s': %w", dir, err)
}
if err := fs.Chown(dir, "", CaddyGroup); err != nil {
return nil, fmt.Errorf("change owner of parent directory for Caddy configuration '%s': %w", dir, err)
}
return &Controller{
store: store,
path: path,
verifyResponse: verifyResponse,
}, nil
}
func (c *Controller) Run(ctx context.Context) error {
containerRecords, changes, err := c.store.SubscribeContainers(ctx)
if err != nil {
return fmt.Errorf("subscribe to container changes: %w", err)
}
slog.Info("Subscribed to container changes in the cluster to generate Caddy configuration.")
containers, err := c.filterAvailableContainers(containerRecords)
if err != nil {
return fmt.Errorf("filter available containers: %w", err)
}
if err = c.generateConfig(containers); err != nil {
return fmt.Errorf("generate Caddy configuration: %w", err)
}
for {
select {
case _, ok := <-changes:
if !ok {
return fmt.Errorf("containers subscription failed")
}
slog.Debug("Cluster containers changed, updating Caddy configuration.")
containerRecords, err = c.store.ListContainers(ctx, store.ListOptions{})
if err != nil {
slog.Error("Failed to list containers.", "err", err)
continue
}
containers, err = c.filterAvailableContainers(containerRecords)
if err != nil {
slog.Error("Failed to filter available containers.", "err", err)
continue
}
if err = c.generateConfig(containers); err != nil {
slog.Error("Failed to generate Caddy configuration.", "err", err)
}
slog.Debug("Updated Caddy configuration.", "path", c.path)
case <-ctx.Done():
return nil
}
}
}
// filterAvailableContainers filters out containers from this machine that are likely unavailable. The availability
// is determined by the cluster membership state of the machine that the container is running on.
// TODO: implement machine membership check using Corrossion Admin client.
func (c *Controller) filterAvailableContainers(
containerRecords []store.ContainerRecord,
) ([]api.ServiceContainer, error) {
containers := make([]api.ServiceContainer, len(containerRecords))
for i, cr := range containerRecords {
containers[i] = api.ServiceContainer{
Container: cr.Container,
// TODO: restore ServiceSpec from the container record once it's saved in the store.
}
}
return containers, nil
}
func (c *Controller) generateConfig(containers []api.ServiceContainer) error {
config, err := GenerateConfig(containers, c.verifyResponse)
if err != nil {
return err
}
configBytes, err := json.MarshalIndent(config, "", " ")
if err != nil {
return fmt.Errorf("marshal Caddy configuration: %w", err)
}
if err = os.WriteFile(c.path, configBytes, 0640); err != nil {
return fmt.Errorf("write Caddy configuration to file '%s': %w", c.path, err)
}
if err = fs.Chown(c.path, "", CaddyGroup); err != nil {
return fmt.Errorf("change owner of Caddy configuration file '%s': %w", c.path, err)
}
return nil
}
-213
View File
@@ -1,213 +0,0 @@
package caddyfile
import (
"context"
"encoding/json"
"errors"
"fmt"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
"github.com/caddyserver/caddy/v2/modules/caddyhttp/reverseproxy"
"log/slog"
"net"
"os"
"path/filepath"
"strconv"
"uncloud/internal/api"
"uncloud/internal/fs"
"uncloud/internal/machine/docker"
"uncloud/internal/machine/store"
)
const CaddyGroup = "uncloud"
// 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
// network.
type Controller struct {
store *store.Store
path string
}
func NewController(store *store.Store, path string) (*Controller, error) {
dir := filepath.Dir(path)
if err := os.MkdirAll(dir, 0750); err != nil {
return nil, fmt.Errorf("create parent directory for Caddy configuration '%s': %w", dir, err)
}
if err := fs.Chown(dir, "", CaddyGroup); err != nil {
return nil, fmt.Errorf("change owner of parent directory for Caddy configuration '%s': %w", dir, err)
}
return &Controller{
store: store,
path: path,
}, nil
}
func (c *Controller) Run(ctx context.Context) error {
containerRecords, changes, err := c.store.SubscribeContainers(ctx)
if err != nil {
return fmt.Errorf("subscribe to container changes: %w", err)
}
slog.Info("Subscribed to container changes in the cluster to generate Caddy configuration.")
containers, err := c.filterAvailableContainers(containerRecords)
if err != nil {
return fmt.Errorf("filter available containers: %w", err)
}
if err = c.generateConfig(containers); err != nil {
return fmt.Errorf("generate Caddy configuration: %w", err)
}
for {
select {
case _, ok := <-changes:
if !ok {
return fmt.Errorf("containers subscription failed")
}
slog.Debug("Cluster containers changed, updating Caddy configuration.")
containerRecords, err = c.store.ListContainers(ctx, store.ListOptions{})
if err != nil {
slog.Error("Failed to list containers.", "err", err)
continue
}
containers, err = c.filterAvailableContainers(containerRecords)
if err != nil {
slog.Error("Failed to filter available containers.", "err", err)
continue
}
if err = c.generateConfig(containers); err != nil {
slog.Error("Failed to generate Caddy configuration.", "err", err)
}
slog.Debug("Updated Caddy configuration.", "path", c.path)
case <-ctx.Done():
return nil
}
}
}
// filterAvailableContainers filters out containers from this machine that are likely unavailable. The availability
// is determined by the cluster membership state of the machine that the container is running on.
// TODO: implement machine membership check using Corrossion Admin client.
func (c *Controller) filterAvailableContainers(containerRecords []store.ContainerRecord) ([]api.Container, error) {
containers := make([]api.Container, len(containerRecords))
for i, cr := range containerRecords {
containers[i] = cr.Container
}
return containers, nil
}
func (c *Controller) generateConfig(containers []api.Container) error {
// Maps hostnames to lists of upstreams (container IP:port pairs).
httpHostUpstreams := make(map[string][]string)
httpsHostUpstreams := make(map[string][]string)
for _, ctr := range containers {
logger := slog.With("container", ctr.ID)
network, ok := ctr.NetworkSettings.Networks[docker.NetworkName]
if !ok {
// Container is not connected to the uncloud Docker network (could be host network).
continue
}
if network.IPAddress == "" {
logger.Error("Container has no IPv4 address.")
continue
}
ports, err := ctr.ServicePorts()
if err != nil {
logger.Error("Failed to parse service ports for container.", "err", err)
continue
}
for _, port := range ports {
switch port.Protocol {
case api.ProtocolHTTP:
upstream := net.JoinHostPort(network.IPAddress, strconv.Itoa(int(port.ContainerPort)))
httpHostUpstreams[port.Hostname] = append(httpHostUpstreams[port.Hostname], upstream)
case api.ProtocolHTTPS:
upstream := net.JoinHostPort(network.IPAddress, strconv.Itoa(int(port.ContainerPort)))
httpsHostUpstreams[port.Hostname] = append(httpHostUpstreams[port.Hostname], upstream)
default:
if port.Mode == api.PortModeIngress {
// TODO: implement L4 ingress routing for TCP and UDP.
logger.Error("Unsupported protocol for ingress port.", "port", port)
continue
}
}
}
}
var warnings []caddyconfig.Warning
servers := make(map[string]*caddyhttp.Server)
servers["http"] = &caddyhttp.Server{
Listen: []string{fmt.Sprintf(":%d", caddyhttp.DefaultHTTPPort)},
Routes: hostUpstreamsToRoutes(httpHostUpstreams, &warnings),
}
servers["https"] = &caddyhttp.Server{
Listen: []string{fmt.Sprintf(":%d", caddyhttp.DefaultHTTPSPort)},
Routes: hostUpstreamsToRoutes(httpsHostUpstreams, &warnings),
}
httpApp := caddyhttp.App{
Servers: servers,
}
config := &caddy.Config{
AppsRaw: caddy.ModuleMap{
"http": caddyconfig.JSON(httpApp, &warnings),
},
}
var err error
if len(warnings) > 0 {
// warnings only contains errors from JSON marshaling, which are highly unlikely with correct code.
for _, w := range warnings {
err = errors.Join(err, errors.New(w.Message))
}
return fmt.Errorf("marshal Caddy configuration: %w", err)
}
configBytes, err := json.MarshalIndent(config, "", " ")
if err != nil {
return fmt.Errorf("marshal Caddy configuration: %w", err)
}
if err = os.WriteFile(c.path, configBytes, 0640); err != nil {
return fmt.Errorf("write Caddy configuration to file '%s': %w", c.path, err)
}
if err = fs.Chown(c.path, "", CaddyGroup); err != nil {
return fmt.Errorf("change owner of Caddy configuration file '%s': %w", c.path, err)
}
return nil
}
// hostUpstreamsToRoutes converts a map of hostnames to upstreams to a list of Caddy routes.
func hostUpstreamsToRoutes(hostUpstreams map[string][]string, warnings *[]caddyconfig.Warning) []caddyhttp.Route {
routes := make([]caddyhttp.Route, 0, len(hostUpstreams))
for hostname, upstreams := range hostUpstreams {
upstreamPool := make([]*reverseproxy.Upstream, len(upstreams))
for i, upstream := range upstreams {
upstreamPool[i] = &reverseproxy.Upstream{
Dial: upstream,
}
}
handler := &reverseproxy.Handler{
Upstreams: upstreamPool,
}
routes = append(routes, caddyhttp.Route{
MatcherSetsRaw: caddyhttp.RawMatcherSets{
{
"host": caddyconfig.JSON(caddyhttp.MatchHost{hostname}, warnings),
},
},
HandlersRaw: []json.RawMessage{
caddyconfig.JSONModuleObject(handler, "handler", "reverse_proxy", warnings),
},
})
}
return routes
}
+15 -26
View File
@@ -11,11 +11,11 @@ import (
"log/slog"
"net/netip"
"time"
"uncloud/internal/corrosion"
"uncloud/internal/machine/api/pb"
"uncloud/internal/machine/network"
"uncloud/internal/machine/store"
"uncloud/internal/secret"
"github.com/psviderski/uncloud/internal/corrosion"
"github.com/psviderski/uncloud/internal/machine/api/pb"
"github.com/psviderski/uncloud/internal/machine/network"
"github.com/psviderski/uncloud/internal/machine/store"
"github.com/psviderski/uncloud/internal/secret"
)
type Cluster struct {
@@ -110,6 +110,15 @@ func (c *Cluster) AddMachine(ctx context.Context, req *pb.AddMachineRequest) (*p
if len(req.Network.Endpoints) == 0 {
return nil, status.Error(codes.InvalidArgument, "endpoints not set")
}
if req.PublicIp != nil {
ip, err := req.PublicIp.ToAddr()
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "invalid public IP: %v", err)
}
if !ip.IsValid() {
return nil, status.Error(codes.InvalidArgument, "invalid public IP")
}
}
machines, err := c.store.ListMachines(ctx)
if err != nil {
@@ -174,6 +183,7 @@ func (c *Cluster) AddMachine(ctx context.Context, req *pb.AddMachineRequest) (*p
Endpoints: req.Network.Endpoints,
PublicKey: req.Network.PublicKey,
},
PublicIp: req.PublicIp,
}
// TODO: announce the new machine to the cluster members and achieve consensus.
// We should perhaps not proceed if this machine is in a minority partition.
@@ -232,24 +242,3 @@ func (c *Cluster) ListMachines(ctx context.Context, _ *emptypb.Empty) (*pb.ListM
return &pb.ListMachinesResponse{Machines: members}, nil
}
//func (c *Cluster) ListServices(ctx context.Context, _ *emptypb.Empty) (*pb.ListServicesResponse, error) {
// if err := c.checkInitialised(ctx); err != nil {
// return nil, err
// }
//
// return &pb.ListServicesResponse{
// Messages: []*pb.Services{
// {
// Services: []*pb.Service{
// {
// Name: "service1",
// },
// {
// Name: "service2",
// },
// },
// },
// },
// }, nil
//}
+159
View File
@@ -0,0 +1,159 @@
package cluster
import (
"context"
"encoding/json"
"errors"
"github.com/psviderski/uncloud/internal/dns"
"github.com/psviderski/uncloud/internal/machine/api/pb"
"github.com/psviderski/uncloud/internal/machine/store"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/emptypb"
)
// uncloudDNSKey is the key used to store the details of the reserved domain in the store.
const uncloudDNSKey = "uncloud_dns"
type uncloudDNSDomain struct {
// Endpoint is the API endpoint of the Uncloud DNS service where the domain is reserved.
Endpoint string
Name string
// TODO: encrypt the token in the store.
Token string
}
func (c *Cluster) ReserveDomain(ctx context.Context, req *pb.ReserveDomainRequest) (*pb.Domain, error) {
if err := c.checkInitialised(ctx); err != nil {
return nil, err
}
if req.Endpoint == "" {
return nil, status.Error(codes.InvalidArgument, "API endpoint not set")
}
if _, err := c.storedDomain(ctx); err == nil {
return nil, status.Errorf(codes.AlreadyExists, "domain already reserved")
} else {
if s := status.Convert(err); s.Code() != codes.NotFound {
return nil, err
}
}
dnsClient := dns.NewClient()
name, token, err := dnsClient.ReserveDomain(req.Endpoint)
if err != nil {
return nil, status.Errorf(codes.Internal, err.Error())
}
domain := uncloudDNSDomain{
Endpoint: req.Endpoint,
Name: name,
Token: token,
}
domainJSON, err := json.Marshal(domain)
if err != nil {
return nil, status.Errorf(codes.Internal, "marshal reserved domain for store: %v", err)
}
if err = c.store.Put(ctx, uncloudDNSKey, domainJSON); err != nil {
return nil, status.Errorf(codes.Internal, "store reserved domain: %v", err)
}
return &pb.Domain{Name: name}, nil
}
func (c *Cluster) GetDomain(ctx context.Context, _ *emptypb.Empty) (*pb.Domain, error) {
if err := c.checkInitialised(ctx); err != nil {
return nil, err
}
domain, err := c.storedDomain(ctx)
if err != nil {
return nil, err
}
return &pb.Domain{Name: domain.Name}, nil
}
func (c *Cluster) storedDomain(ctx context.Context) (uncloudDNSDomain, error) {
var domain uncloudDNSDomain
var domainJSON []byte
if err := c.store.Get(ctx, uncloudDNSKey, &domainJSON); err != nil {
if errors.Is(err, store.ErrKeyNotFound) {
return domain, status.Errorf(codes.NotFound, "domain not found")
}
return domain, status.Errorf(codes.Internal, "get domain from store: %v", err)
}
if err := json.Unmarshal(domainJSON, &domain); err != nil {
return domain, status.Errorf(codes.Internal, "unmarshal domain: %v", err)
}
return domain, nil
}
func (c *Cluster) ReleaseDomain(ctx context.Context, _ *emptypb.Empty) (*pb.Domain, error) {
if err := c.checkInitialised(ctx); err != nil {
return nil, err
}
domain, err := c.storedDomain(ctx)
if err != nil {
return nil, err
}
if err = c.store.Delete(ctx, uncloudDNSKey); err != nil {
return nil, status.Errorf(codes.Internal, "delete domain from store: %v", err)
}
// TODO: implement and call Uncloud DNS endpoint to release/delete the domain.
return &pb.Domain{Name: domain.Name}, nil
}
func (c *Cluster) CreateDomainRecords(
ctx context.Context, req *pb.CreateDomainRecordsRequest,
) (*pb.CreateDomainRecordsResponse, error) {
if err := c.checkInitialised(ctx); err != nil {
return nil, err
}
domain, err := c.storedDomain(ctx)
if err != nil {
return nil, err
}
dnsClient := dns.NewClient()
recordsReq := make([]dns.RecordRequest, len(req.Records))
for i, r := range req.Records {
recordsReq[i] = dns.RecordRequest{
Name: r.Name,
Type: dns.RecordType(r.Type.String()),
Values: r.Values,
}
}
recordsResp, err := dnsClient.CreateRecords(domain.Endpoint, domain.Name, domain.Token, recordsReq)
if err != nil {
return nil, err
}
resp := &pb.CreateDomainRecordsResponse{
Records: make([]*pb.DNSRecord, len(recordsResp)),
}
for i, r := range recordsResp {
resp.Records[i] = &pb.DNSRecord{
Name: r.FQDN,
Values: r.Values,
}
switch r.Type {
case dns.RecordTypeA:
resp.Records[i].Type = pb.DNSRecord_A
case dns.RecordTypeAAAA:
resp.Records[i].Type = pb.DNSRecord_AAAA
}
}
return resp, nil
}

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