Compare commits

..
193 Commits
Author SHA1 Message Date
Pavel Sviderski bee101aa3e fix asset template in .goreleaser config 2025-01-24 19:45:38 +10:00
Pavel Sviderski 21e70dd846 update quick start links on landing 2025-01-24 16:57:57 +10:00
Pavel Sviderski 91f870989a update README with basic Quick start 2025-01-24 16:56:40 +10:00
Pavel Sviderski bda1123ae6 add install_cli.sh script to be donwloaded at get.uncloud.run 2025-01-24 16:27:07 +10:00
Pavel Sviderski 190d0de61f fix warnings on landing 2025-01-23 14:58:05 +10:00
Pavel Sviderski f2e730796f enable Kit email subscription form 2025-01-23 14:49:41 +10:00
Pavel Sviderski bb77ff9246 format 2025-01-22 21:37:39 +10:00
Pavel Sviderski 5061097dff add landing section to follow the journey and subscribe via email 2025-01-22 21:26:49 +10:00
Pavel Sviderski 911522c388 comment out alpinejs 2025-01-22 17:18:26 +10:00
Pavel Sviderski 3ee7cb6e8e describe features on landing 2025-01-22 16:36:35 +10:00
Pavel Sviderski 2e00632959 update landing CSS 2025-01-21 19:38:49 +10:00
Pavel Sviderski 8e53fd46fa add how it works section with diagram 2025-01-21 19:38:38 +10:00
Pavel Sviderski 2835edadeb minor landing wording update 2025-01-21 17:57:33 +10:00
Pavel Sviderski f7c4490c10 add diagram for landing page with drawio source 2025-01-21 16:30:45 +10:00
Pavel Sviderski b3102480c0 rename site -> website 2025-01-20 13:41:55 +10:00
Pavel Sviderski cbe7c563cd add value proposition section: Cloud-like experience you control 2025-01-20 13:40:58 +10:00
Pavel Sviderski b016252821 add terminal demo to hero section 2025-01-16 15:50:40 +10:00
Pavel Sviderski 6c197085d0 update hero 2025-01-15 17:10:38 +10:00
Pavel Sviderski c29fa571e1 add favicons 2025-01-10 19:07:55 +10:00
Pavel Sviderski 092fe254aa fix navbar on mobile 2025-01-10 15:22:26 +10:00
Pavel Sviderski 6951221b88 github and get started links in navbar 2025-01-10 15:04:47 +10:00
Pavel Sviderski fa54584128 add logo and project name 2025-01-10 14:07:44 +10:00
Pavel Sviderski d922f7bfed reformat index.html 2025-01-10 12:42:13 +10:00
Pavel Sviderski 18ec186e81 init landing page template 2025-01-10 12:39:21 +10:00
Pavel Sviderski 2a898b3eee add -v|--volume to bind mount host path into a service container 2024-12-27 21:25:33 +10:00
Pavel Sviderski 042b44cbb6 fix port binding for host ports 2024-12-27 21:23:56 +10:00
Pavel Sviderski 3f7726e5fb fix SlogTextHandler to respect minimum log level 2024-12-24 17:07:37 +10:00
Pavel Sviderski 2b3f325075 generate caddy config with routes from published http(s) ports 2024-12-23 20:41:04 +10:00
Pavel Sviderski be2a26e626 remove example from run command 2024-12-19 14:19:11 +10:00
Pavel Sviderski 4479490db2 create custom NewSlogTextHandler log handler to override the json one patched by caddy pkg 2024-12-19 13:29:58 +10:00
Pavel Sviderski 7556f8cb12 generate basic caddy.json with empty http and https servers 2024-12-18 22:14:58 +10:00
Pavel Sviderski bfbcec34e8 add caddy/v2 dependency to generate a config 2024-12-17 17:52:30 +11:00
Pavel Sviderski 8eea34d8dc remove wrong TODO 2024-12-11 16:50:15 +10:00
Pavel Sviderski 6b80ef9db8 subscribe to container changes in Caddyfile controller 2024-12-11 15:14:32 +10:00
Pavel Sviderski 2134f3c1d9 add a stub for Caddyfile controller 2024-12-11 12:17:15 +10:00
Pavel Sviderski 0652123bc7 encode and decode published ports using container labels 2024-12-10 19:21:52 +10:00
Pavel Sviderski 4de7ecd747 add String() method for PortSpec to convert to publish format 2024-12-10 19:14:26 +10:00
Pavel Sviderski e8908ced9e parse --publish ports into spec 2024-12-10 17:57:26 +10:00
Pavel Sviderski deedbcb8a9 minor validation for port 2024-12-10 17:37:42 +10:00
Pavel Sviderski 8bfb3b80ed basic validation for hostname in port publishing 2024-12-10 17:24:29 +10:00
Pavel Sviderski 848abbd2f8 add validation for PortSpec 2024-12-10 16:35:47 +10:00
Pavel Sviderski 9cb4b35546 implement PortSpec parsing 2024-12-10 15:24:34 +10:00
Pavel Sviderski 26df6740e3 define PortSpec type for publishing ports 2024-12-09 20:25:34 +10:00
Pavel Sviderski 537789902b update publish flag description with lb and host formats 2024-12-09 17:06:33 +10:00
Pavel Sviderski 4088fdfd84 print image pull progress when running a service 2024-12-06 18:27:26 +10:00
Pavel Sviderski 01a71b4b38 run each container in global service in parallel 2024-12-06 16:00:10 +10:00
Pavel Sviderski 73d41c75b3 add compose-like progress for running service 2024-12-06 15:44:23 +10:00
Pavel Sviderski f85b5dd01d add service rm CLI command 2024-12-05 17:32:30 +10:00
Pavel Sviderski 1fb8a3d7a0 add ListServices method and ls CLI command 2024-12-05 17:21:54 +10:00
Pavel Sviderski 3ce9c8be04 fix run command, support 1 replica and global mode 2024-12-05 16:24:04 +10:00
Pavel Sviderski 3ac166e6cb move common api types to api package 2024-12-05 15:17:36 +10:00
Pavel Sviderski 88ce2d5181 implement RunService for replicated mode with only 1 replica 2024-12-05 14:01:56 +10:00
Pavel Sviderski 8bddbec7bd wait for test cluster to be ready (all machines up) 2024-12-05 13:45:46 +10:00
Pavel Sviderski a40fd613e8 refactor RemoveService using InspectService 2024-12-05 12:27:51 +10:00
Pavel Sviderski 99c9aabdfd implement more consistent InspectService that broadcasts Docker requests to all machines 2024-12-05 12:13:17 +10:00
Pavel Sviderski 5504951a8a add e2e test for global service creation and removal 2024-12-04 18:28:22 +10:00
Pavel Sviderski 2727c3c97e implement RunService in global mode and RemoveService 2024-12-04 18:26:59 +10:00
Pavel Sviderski 7d47d274a0 minor 2024-12-04 18:24:47 +10:00
Pavel Sviderski a4247b0097 refactor grpc proxy backends to use machine management IP in metadata, including local responses 2024-12-04 18:23:51 +10:00
Pavel Sviderski 11612a802c forward NotFound errors from StartContainer and RemoveContainer 2024-12-04 16:29:04 +10:00
Pavel Sviderski d7bad5ed46 add todo 2024-12-04 15:39:13 +10:00
Pavel Sviderski c0dff56378 fix filter unmarshalling for list containers 2024-12-04 15:37:52 +10:00
Pavel Sviderski 00dbbec62b refactor grpc-proxy backends to enhance responses from local backends with metadata in one2many mode 2024-12-04 14:47:31 +10:00
Pavel Sviderski 9755a6973d allow one2many for ListContainers 2024-12-04 12:43:52 +10:00
Pavel Sviderski 16120b80ba add ListContainers and RemoveContainer methods to Docker service 2024-12-04 12:15:55 +10:00
Pavel Sviderski 386e4e25f3 fix context in PullImage 2024-12-03 18:33:48 +10:00
Pavel Sviderski 43ddb5c363 add PullImage method to gRPC Docker service 2024-12-03 18:07:00 +10:00
Pavel Sviderski 417427b4d5 remove duplicate ErrNotFound in client 2024-12-03 15:47:33 +10:00
Pavel Sviderski 5da1fc5387 support providing TEST_CLUSTER_NAME for e2e tests 2024-12-03 15:46:19 +10:00
Pavel Sviderski 5621a84e99 add Mode to Service entity 2024-12-03 11:37:13 +10:00
Pavel Sviderski 8db7ac84a0 add subprocess corrosion service runner 2024-12-03 11:07:09 +10:00
Pavel Sviderski 77890b8a8a strip one proto layer in ListMachines method 2024-12-03 11:05:58 +10:00
Pavel Sviderski daada3bbe9 add service inspect command, introduce squirrel for sql query building 2024-12-03 10:56:29 +10:00
Pavel Sviderski 21d55dc69a minor refactor e2e cluster test 2024-12-02 20:37:01 +10:00
Pavel Sviderski 6eb373e4de init InspectService method 2024-12-02 20:36:31 +10:00
Pavel Sviderski 6a4a7f2e6a update e2e lifecycle test to test store reconciliation on every node 2024-12-02 17:21:06 +10:00
Pavel Sviderski 349dd0d2ef add basic e2e cluster lifecycle test 2024-11-29 20:40:49 +10:00
Pavel Sviderski cf5818d963 merge ucind cluster configuration to the user’s uncloud config 2024-11-29 13:30:15 +10:00
Pavel Sviderski faf3378659 ucind: wait for machine to become ready through polling API 2024-11-29 12:08:57 +10:00
Pavel Sviderski bb967461d0 add logging for starting/restarting corrosion service 2024-11-29 12:08:05 +10:00
Pavel Sviderski 0b5dd2f7ba fix network peer configuration when just joining the cluster before store is synced 2024-11-29 11:27:51 +10:00
Pavel Sviderski d5f0bca4a0 ucind: call cleanup on exit as well, e.g. main process crash 2024-11-29 09:53:20 +10:00
Pavel Sviderski bdd034d169 proper terminate all background processes in ucind containers 2024-11-29 09:46:29 +10:00
Pavel Sviderski 48767125f4 ucind: init cluster with created machines 2024-11-28 19:57:51 +10:00
Pavel Sviderski 035f44896d refactor cluster network related constants 2024-11-28 19:57:26 +10:00
Pavel Sviderski 3d5fe0a824 add support for tcp cluster connections in uncloud config 2024-11-28 18:53:47 +10:00
Pavel Sviderski 6d1c3aa5b2 add basic client TCPConnector to directly connect to a gRPC endpoint 2024-11-28 18:53:03 +10:00
Pavel Sviderski cd60d5178f ucind: create machines in containers with published API ports 2024-11-28 16:15:57 +10:00
Pavel Sviderski 7aa7a9fea1 init ucind CLI commands 2024-11-28 13:54:37 +10:00
Pavel Sviderski 73785007cb init ucind (Uncloud in Docker) provisioner 2024-11-28 13:54:10 +10:00
Pavel Sviderski 24a824b890 add make targets to build and push ucind image 2024-11-27 17:14:41 +10:00
Pavel Sviderski 085ee285e8 add make targets to build and push corrosion image 2024-11-27 16:53:57 +10:00
Pavel Sviderski 6daa1291b5 update TODO 2024-11-27 16:41:20 +10:00
Pavel Sviderski c3d3a4242e run corrosion in Docker as uncloud user, bind mount data dir 2024-11-27 16:35:35 +10:00
Pavel Sviderski 871fd8c035 proxy uncloud.sock socket to tcp port when running machine in container 2024-11-27 16:33:05 +10:00
Pavel Sviderski a6ccd6055a move fs and file ownership related funcs to fs package 2024-11-27 14:30:08 +10:00
Pavel Sviderski ca607ee82d pull corrosion image if doesn't exist 2024-11-27 13:37:22 +10:00
Pavel Sviderski 1c8e60aac0 workaround cgroup race in dind 2024-11-27 13:18:16 +10:00
Pavel Sviderski d02fc4d981 update Makefile to remove built binary 2024-11-27 10:38:25 +10:00
Pavel Sviderski 410f74425c update dind machine image to run dockerd in background 2024-11-27 10:35:49 +10:00
Pavel Sviderski 6e0df53f13 add in docker service runner for corrosion 2024-11-27 10:34:42 +10:00
Pavel Sviderski e9d07687f2 build corrosion image based on wolfi] 2024-11-26 20:15:31 +10:00
Pavel Sviderski 95175d10c5 init Dockerfile to run machine in container 2024-11-25 19:49:59 +10:00
Pavel Sviderski df25b0d098 add --mode flag to run command 2024-11-23 15:56:03 +10:00
Pavel Sviderski 5c1a3dd694 move generic docker types to top level docker package 2024-11-23 15:35:26 +10:00
Pavel Sviderski 6d39174b51 add Healthy method to Container that derives it from status string 2024-11-21 20:50:55 +10:00
Pavel Sviderski e851f25b12 delete removed Docker containers from store, sync only uncloud labeled 2024-11-19 19:38:11 +10:00
Pavel Sviderski 3c1d2201ce add methods to create, update, list, delete containers in store 2024-11-19 19:35:38 +10:00
Pavel Sviderski b4c2ec3a2a sync new and updated containers to cluster store, no deletion yet 2024-11-19 16:06:16 +10:00
Pavel Sviderski bf10b21da5 minor comment 2024-11-18 20:07:51 +10:00
Pavel Sviderski 2819ba63aa add sync_status field to containers table 2024-11-18 19:50:40 +10:00
Pavel Sviderski 54a8b09c9f subscribe to Docker container events with backoff 2024-11-18 14:43:42 +10:00
Pavel Sviderski 24371eb401 move Docker manager to docker package 2024-11-17 19:41:47 +10:00
Pavel Sviderski 9c9e143dc8 minor rename 2024-11-16 20:28:32 +10:00
Pavel Sviderski 19d53c4f67 move Docker management from network controller to standalone DockerManager 2024-11-16 20:25:34 +10:00
Pavel Sviderski 40f929847c add updated_at field to containers table 2024-11-16 07:37:43 +10:00
Pavel Sviderski e73e38957a add index on containers.machine_id 2024-11-16 07:00:09 +10:00
Pavel Sviderski e82b78df49 comment DB schema 2024-11-16 06:43:24 +10:00
Pavel Sviderski 741378d0b7 create containers table in Corrosion store 2024-11-15 21:35:33 +10:00
Pavel Sviderski d31595820d run service container in uncloud Docker network 2024-11-14 19:19:50 +10:00
Pavel Sviderski 035601d366 generate container name and assign service labels to container 2024-11-14 12:05:48 +10:00
Pavel Sviderski 5c96f75958 generate a name for the service 2024-11-14 11:56:29 +10:00
Pavel Sviderski 2c62e1f6df refactor machine name generation using secrets package 2024-11-14 11:47:22 +10:00
Pavel Sviderski 22bca16226 include alpha-numeric random string generation in secret package 2024-11-14 11:45:28 +10:00
Pavel Sviderski a4be45186d find first available machine for running a service 2024-11-14 09:12:25 +10:00
Pavel Sviderski 5e79697505 move RunService from CLI to the client, print output in the command 2024-11-13 19:52:46 +10:00
Pavel Sviderski 6736839ccb refactor machine ls command: move formatter logic to the command 2024-11-13 17:12:02 +10:00
Pavel Sviderski 913fb2da0c update ListMachines to include machine state from Corrosion membership 2024-11-13 16:48:55 +10:00
Pavel Sviderski bd0e7f44b2 init corrosion admin client in machine 2024-11-12 15:23:02 +10:00
Pavel Sviderski 81cf5f5605 refactor corrosion admin client to establish a connection on every command 2024-11-12 15:22:47 +10:00
Pavel Sviderski cedafd069d implement ClusterMembershipStates for corrosion admin client 2024-11-12 14:29:29 +10:00
Pavel Sviderski 93dfa5ca9d implement admin client for Corrosion admin API 2024-11-11 20:51:11 +10:00
Pavel Sviderski c70c8a6ec1 validate machine info before returning in ListMachine 2024-11-10 19:38:55 +10:00
Pavel Sviderski 5c74b09f45 allow running a service on a specified machine 2024-11-10 19:38:19 +10:00
Pavel Sviderski 1316dd3ffc fix proxy Director usage and close on shutdown 2024-11-08 15:50:28 +10:00
Pavel Sviderski 4493bb3c27 support gRPC proxy to remote backends 2024-11-08 15:49:21 +10:00
Pavel Sviderski b23699470b add EmptyResponse common message 2024-11-08 11:48:58 +10:00
Pavel Sviderski 34316c77cf add helper Empty message 2024-11-08 11:32:42 +10:00
Pavel Sviderski c2b466b046 add common Metadata message to be injected in reply messages by gRPC proxy 2024-11-08 11:28:04 +10:00
Pavel Sviderski d36429e6e5 replace API server with transparent gRPC proxy that routes requests to local machine API 2024-11-05 20:05:31 +10:00
Pavel Sviderski 17b96a8744 move default uncloud socket to /run/uncloud/uncloud.sock 2024-11-04 20:02:48 +10:00
Pavel Sviderski a54666a7ac update run command to run a stub service container on the connected node 2024-11-01 15:05:07 +10:00
Pavel Sviderski 5cb765d286 add Docker gRPC service to create and start a Docker container on machine 2024-11-01 15:04:06 +10:00
Pavel Sviderski 5bf3cba2d6 sketch run command 2024-10-31 20:07:45 +10:00
Pavel Sviderski 8a3f0032ae format machine command flags 2024-10-31 20:07:25 +10:00
Pavel Sviderski 5fe77c688b exclude Docker bridge br-[0-9a-f]{12} interfaces from machine routable IPs 2024-10-31 18:35:59 +10:00
Pavel Sviderski 61dc97228d use ls command instead of list by default 2024-10-31 18:35:12 +10:00
Pavel Sviderski e6df331fb8 fix options for machine list command 2024-10-31 18:32:11 +10:00
Pavel Sviderski 9dbcf1e902 do not build and deploy uncloud binary to dev machines 2024-10-31 18:08:36 +10:00
Pavel Sviderski 2f6c988125 do not rotate endpoints on daemon restart, handle endpoint changes on established reverse peer connections 2024-10-13 01:15:32 -04:00
Pavel Sviderski 411c3398ce upgrade to Go 1.23 2024-10-10 23:06:24 -04:00
Pavel Sviderski 3a644771ca watch for peer endpoint changes and preserve in machine state 2024-10-08 16:51:40 +10:00
Pavel Sviderski 2eff7d1e22 format 2024-10-07 20:06:56 +10:00
Pavel Sviderski f06dd865fa update existing WG peers instead of replacing to not interrupt and lose track of handshakes 2024-10-07 20:06:15 +10:00
Pavel Sviderski ee580df6af automatically rotate WG peer endpoints if can't establish a connection 2024-10-07 17:24:18 +10:00
Pavel Sviderski 2c0cf28509 fix peer status persistence, update status every second 2024-10-07 15:35:15 +10:00
Pavel Sviderski 7b98ed34df calculate WG peer connection status 2024-10-07 14:58:21 +10:00
Pavel Sviderski 5094149406 improve error message for adding machine that already exists in the cluster 2024-10-04 20:45:54 +10:00
Pavel Sviderski 2fd1ee203a fix: access to /run/uncloud.sock for a non-root user when initialising a fresh machine 2024-10-04 20:31:23 +10:00
Pavel Sviderski 336bdf9f4b add minor log 2024-10-04 18:46:52 +10:00
Pavel Sviderski c80c75e4e1 install.sh: download corrosion binary from github release 2024-10-04 18:37:24 +10:00
Pavel Sviderski e5bda92ec6 add opptimistic sleep on corrosion service start to wait for schema initialisation 2024-10-04 16:19:07 +10:00
Pavel Sviderski fd3ad91136 update network controller to reconfigure network peers on machine changes 2024-10-04 16:18:08 +10:00
Pavel Sviderski 94186b2ae7 allow to subscribe to machine changes in the cluster store 2024-10-04 16:14:59 +10:00
Pavel Sviderski 1bcb8836ef auto resubscribe a corrosion subscription if an error occurs 2024-10-03 16:13:03 +10:00
Pavel Sviderski 840943b3b0 split corrosion client subscription in a separate file 2024-10-03 14:10:09 +10:00
Pavel Sviderski 518eae9bd2 split corrosion queries in a separate file, make subscription more consistent with queries 2024-10-03 14:07:25 +10:00
Pavel Sviderski 06a4001080 refactor corrosion client to accept custom http client, fix backoff for every request 2024-10-03 13:58:28 +10:00
Pavel Sviderski b73577e1a0 implement subscription in corrosion client 2024-10-02 18:28:54 +10:00
Pavel Sviderski c3ba43a8fa delete old non-distributed cluster state, fix storing endpoints in store 2024-10-02 13:03:17 +10:00
Pavel Sviderski c275273617 fix install.sh to start uncloud.service after installing all systemd units 2024-10-02 12:44:38 +10:00
Pavel Sviderski b688742e5a remove the concept of cluster user (at least the current implementation) 2024-10-02 11:50:40 +10:00
Pavel Sviderski 4ec87db8e8 add init machine to store when initialising a cluster 2024-10-02 11:34:54 +10:00
Pavel Sviderski c6358474ac add comments 2024-10-02 09:25:12 +10:00
Pavel Sviderski 8fbf406007 relax relationship between corrosion service, init cluster with new store 2024-10-01 23:37:07 +10:00
Pavel Sviderski e123f7214e add exponential backoff for network errors in corrosion client 2024-10-01 23:15:36 +10:00
Pavel Sviderski 3287691a31 start corrosion service even when machine is not initialised to be able to init cluster 2024-10-01 15:04:20 +10:00
Pavel Sviderski 6f9fc69c2e fix corrosion data dir, add reload to systemd unit 2024-10-01 13:33:54 +10:00
Pavel Sviderski 0effba0adb create corrosion store when initialising machine 2024-09-30 23:10:56 +10:00
Pavel Sviderski 667ecadd30 init cluster store backed by corrosion 2024-09-30 20:46:41 +10:00
Pavel Sviderski 68845e7607 refactor corrosion service init for machine 2024-09-30 20:46:29 +10:00
Pavel Sviderski 6d4698fc1d implement Query in corrosion API client 2024-09-30 19:00:51 +10:00
Pavel Sviderski 708e14a4e0 add Exec and ExecMulti methods to corrosion API client 2024-09-29 10:25:54 +10:00
Pavel Sviderski c80de16ebe init corrosion API client 2024-09-28 20:42:57 +10:00
Pavel Sviderski 3e9718f08c update store schema when configuring corrosion, create machines table 2024-09-27 20:10:05 +10:00
Pavel Sviderski 69c9167355 configure corrosion bootstrap peers when initialising a machine 2024-09-26 20:36:13 +10:00
Pavel Sviderski b25938acc1 clean up corrosion systemd service 2024-09-26 19:14:25 +10:00
Pavel Sviderski 1ec3376be5 remove indentations from corrosion config 2024-09-26 18:06:45 +10:00
Pavel Sviderski aa236aac10 do not init corrosion data dir in install.sh 2024-09-26 17:00:48 +10:00
Pavel Sviderski 9fcc319c3a create corrosion config 2024-09-26 17:00:30 +10:00
Pavel Sviderski 2f7463ebe3 refactor corrosion into interface service 2024-09-25 23:59:56 +10:00
Pavel Sviderski 498d35e866 install.sh: install corrosion systemd service 2024-09-25 22:10:01 +10:00
Pavel Sviderski ba67b96734 update install.sh to download uncloudd binary from the latest GitHub release 2024-09-23 21:39:51 +10:00
118 changed files with 17413 additions and 1723 deletions
+11
View File
@@ -0,0 +1,11 @@
# Ignore everything and use an allowlist of what is allowed to not package any secrets by accident.
*
# Allow files and directories.
!cmd/
!internal/
!scripts/docker/
!go.*
# Ignore unnecessary files inside allowed directories.
**/.DS_Store
+6
View File
@@ -8,6 +8,9 @@
*.so
*.dylib
# OS X
.DS_Store
# Test binary, built with `go test -c`
*.test
@@ -26,3 +29,6 @@ go.work.sum
data
dist/
# Web
node_modules/
+1 -1
View File
@@ -45,7 +45,7 @@ archives:
{{ .Binary }}_
{{- if eq .Os "darwin"}}macos
{{- else}}{{.Os}}{{end}}_
{{- .Arch }}"
{{- .Arch }}
# use zip for windows archives
format_overrides:
- goos: windows
+1 -1
View File
@@ -1,5 +1,5 @@
[tools]
go = "1.22"
go = "1.23"
[env]
_.file = ".env"
+51
View File
@@ -0,0 +1,51 @@
ARG ALPINE_VERSION=3.20.3
FROM golang:1.23.2-alpine AS uncloudd
ARG TARGETOS
ARG TARGETARCH
ENV CGO_ENABLED=0
WORKDIR /build
# Download and cache dependencies and only redownload them in subsequent builds if they change.
COPY go.mod go.sum ./
RUN go mod download && go mod verify
COPY . .
RUN GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -o uncloudd cmd/uncloudd/main.go
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 \
&& 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
COPY --from=corrosion-download /usr/local/bin/corrosion /usr/local/bin/corrosion
CMD ["corrosion", "agent"]
FROM alpine:${ALPINE_VERSION} AS corrosion-image-tarball
ARG CORROSION_IMAGE="ghcr.io/psviderski/corrosion:latest"
RUN apk --no-cache add crane
RUN crane pull "${CORROSION_IMAGE}" /corrosion.tar
# Uncloud-in-Docker (ucind) image for running Uncloud test clusters using Docker.
FROM docker:27.3.1-dind AS ucind
# Create system group and user 'uncloud'.
RUN addgroup -S uncloud && adduser -SHD -h /nonexistent -G uncloud -g "" uncloud
RUN apk --no-cache add \
socat \
wireguard-tools
COPY --from=corrosion-image-tarball /corrosion.tar /images/corrosion.tar
COPY scripts/docker/dind scripts/docker/entrypoint.sh /usr/local/bin/
COPY --from=uncloudd /build/uncloudd /usr/local/bin/
ENTRYPOINT ["entrypoint.sh"]
CMD ["uncloudd"]
+37 -9
View File
@@ -1,16 +1,44 @@
.PHONY: build
uncloud-dev:
CORROSION_IMAGE ?= ghcr.io/psviderski/corrosion:latest
UCIND_IMAGE ?= ghcr.io/psviderski/ucind:latest
update-dev:
GOOS=linux GOARCH=amd64 go build -o uncloudd-linux-amd64 ./cmd/uncloudd && \
scp uncloudd-linux-amd64 spy@192.168.40.243:~/ && \
ssh spy@192.168.40.243 sudo install ./uncloudd-linux-amd64 /usr/local/bin/uncloudd
scp uncloudd-linux-amd64 spy@192.168.40.176:~/ && \
ssh spy@192.168.40.176 sudo install ./uncloudd-linux-amd64 /usr/local/bin/uncloudd
GOOS=linux GOARCH=amd64 go build -o uncloud-linux-amd64 ./cmd/uncloud && \
scp uncloud-linux-amd64 spy@192.168.40.243:~/ && \
ssh spy@192.168.40.243 sudo install ./uncloud-linux-amd64 /usr/local/bin/uncloud
scp uncloud-linux-amd64 spy@192.168.40.176:~/ && \
ssh spy@192.168.40.176 sudo install ./uncloud-linux-amd64 /usr/local/bin/uncloud
ssh spy@192.168.40.176 sudo install ./uncloudd-linux-amd64 /usr/local/bin/uncloudd && \
rm uncloudd-linux-amd64
update-restart-dev:
GOOS=linux GOARCH=amd64 go build -o uncloudd-linux-amd64 ./cmd/uncloudd && \
scp uncloudd-linux-amd64 spy@192.168.40.243:~/ && \
ssh spy@192.168.40.243 "sudo install ./uncloudd-linux-amd64 /usr/local/bin/uncloudd && sudo systemctl restart uncloud" && \
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
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"
.PHONY: proto
proto:
protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative internal/machine/api/pb/*.proto
protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative \
--proto_path=. --proto_path=internal/machine/api/vendor internal/machine/api/pb/*.proto
.PHONY: corrosion-image
corrosion-image:
docker build -t "$(CORROSION_IMAGE)" --target corrosion .
.PHONY: corrosion-image-push
corrosion-image-push: corrosion-image
docker push "$(CORROSION_IMAGE)"
.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)"
+42 -7
View File
@@ -1,9 +1,44 @@
# Uncloud
<div align="center">
<img src="./website/images/logo.svg" height="100" alt="Uncloud logo"/>
<h1>Uncloud</h2>
A lightweight Docker clustering tool for running web apps on your own servers — from cloud VMs to bare metal.<br>
Replace Heroku and Render, no Kubernetes required.
</div>
Uncloud is a lightweight tool for deploying and managing containerised applications across a network of Docker hosts. It
bridges the gap between simple Docker deployments and complex cloud platforms and container orchestrators like
Kubernetes.
## Quick start
Our goal is to become the go-to self-hosted alternative to platforms like [Fly.io](https://fly.io) and
[Render](https://render.com/). Whether you're using bare metal servers, Raspberry Pis, or cloud VMs, Uncloud transforms
your hardware into a personal cloud platform.
1. Install Uncloud CLI:
```bash
curl -fsSL https://raw.githubusercontent.com/psviderski/uncloud/main/scripts/install_cli.sh | sh
```
2. Initialize your first machine:
```bash
uc machine init root@your-server-ip
```
3. Create a DNS A record pointing `app.example.com` to your server's IP address, then deploy your app with
automatic HTTPS:
```bash
uc run --name my-app -p app.example.com:8000/https registry/app
```
That's it! Your app is now running and accessible at https://app.example.com ✨
## Project status
Uncloud is an open source project I'm ([@psviderski](https://github.com/psviderski)) actively developing. I'd love to
share this journey with you. [Subscribe](https://uncloud.run/) to follow the progress, get early insights into new
features, and be the first to know when it's ready for production use.
I'd also love your input:
- 🐛 Found a bug? [Open an issue](https://github.com/psviderski/uncloud/issues)
- 💡 Have ideas? [Join the discussion](https://github.com/psviderski/uncloud/discussions)
## Motivation
TBD
+34
View File
@@ -0,0 +1,34 @@
package cluster
import (
"fmt"
"github.com/spf13/cobra"
"uncloud/internal/ucind"
)
func NewCreateCommand() *cobra.Command {
opts := ucind.CreateClusterOptions{}
cmd := &cobra.Command{
Use: "create [NAME]",
Short: "Create a new cluster.",
Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
p := cmd.Context().Value("provisioner").(*ucind.Provisioner)
name := DefaultClusterName
if len(args) > 0 {
name = args[0]
}
if _, err := p.CreateCluster(cmd.Context(), name, opts); err != nil {
return fmt.Errorf("create cluster '%s': %w", name, err)
}
fmt.Printf("Cluster '%s' created.\n", name)
return nil
},
}
cmd.Flags().IntVarP(&opts.Machines, "machines", "m", 1, "Number of machines to create.")
return cmd
}
+30
View File
@@ -0,0 +1,30 @@
package cluster
import (
"fmt"
"github.com/spf13/cobra"
"uncloud/internal/ucind"
)
func NewRemoveCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "rm [NAME]",
Short: "Remove a cluster.",
Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
p := cmd.Context().Value("provisioner").(*ucind.Provisioner)
name := DefaultClusterName
if len(args) > 0 {
name = args[0]
}
if err := p.RemoveCluster(cmd.Context(), name); err != nil {
return fmt.Errorf("remove cluster '%s': %w", name, err)
}
fmt.Printf("Cluster '%s' removed.\n", name)
return nil
},
}
return cmd
}
+20
View File
@@ -0,0 +1,20 @@
package cluster
import (
"github.com/spf13/cobra"
)
const DefaultClusterName = "ucind-default"
func NewRootCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "cluster",
Short: "Manage local Docker-based clusters.",
}
cmd.AddCommand(
NewCreateCommand(),
//NewListCommand(),
NewRemoveCommand(),
)
return cmd
}
+55
View File
@@ -0,0 +1,55 @@
package main
import (
"context"
"fmt"
"github.com/docker/docker/client"
"github.com/spf13/cobra"
"os"
"strings"
"uncloud/cmd/ucind/cluster"
"uncloud/internal/ucind"
)
func main() {
var configPath string
cmd := &cobra.Command{
Use: "ucind",
Short: "A CLI tool for running Uncloud test clusters using Docker.",
Long: "A CLI tool for running Uncloud test clusters using Docker.\n" +
"Machines in a ucind cluster are Docker containers running a ucind image. All machines within a cluster " +
"are connected to the same Docker network.",
SilenceUsage: true,
SilenceErrors: true,
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
if err != nil {
return fmt.Errorf("create Docker client: %w", err)
}
if strings.HasPrefix(configPath, "~/") {
home, err := os.UserHomeDir()
if err != nil {
return fmt.Errorf("get user home directory to resolve %q: %w", configPath, err)
}
configPath = strings.Replace(configPath, "~", home, 1)
}
configUpdater := ucind.NewConfigUpdater(configPath)
p := ucind.NewProvisioner(cli, configUpdater)
// Persist the provisioner in the context so it can be used by subcommands.
cmd.SetContext(context.WithValue(cmd.Context(), "provisioner", p))
return nil
},
}
// 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.AddCommand(
cluster.NewRootCommand(),
)
cobra.CheckErr(cmd.Execute())
}
+3 -3
View File
@@ -36,14 +36,14 @@ func NewAddCommand() *cobra.Command {
return uncli.AddMachine(cmd.Context(), remoteMachine, opts.cluster, opts.name)
},
}
cmd.Flags().StringVarP(&opts.name, "name", "n", "", "Assign a name to the machine")
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_*)",
"path to SSH private key for SSH remote login. (default ~/.ssh/id_*)",
)
cmd.Flags().StringVarP(
&opts.cluster, "cluster", "c", "",
"Name of the cluster to add the machine to (default is the current cluster)",
"Name of the cluster to add the machine to. (default is the current cluster)",
)
return cmd
}
+11 -16
View File
@@ -6,14 +6,12 @@ import (
"net/netip"
"uncloud/internal/cli"
"uncloud/internal/cli/config"
"uncloud/internal/machine/network"
"uncloud/internal/machine/cluster"
)
type initOptions struct {
name string
network string
userPublicKey string
name string
network string
sshKey string
cluster string
}
@@ -21,10 +19,10 @@ type initOptions struct {
func NewInitCommand() *cobra.Command {
opts := initOptions{}
cmd := &cobra.Command{
Use: "init [USER@HOST:PORT]",
Args: cobra.MaximumNArgs(1),
// TODO: include usage examples of initialising a local and remote machine.
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.
Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
uncli := cmd.Context().Value("cli").(*cli.CLI)
@@ -49,21 +47,18 @@ func NewInitCommand() *cobra.Command {
return uncli.InitCluster(cmd.Context(), remoteMachine, opts.cluster, opts.name, netPrefix)
},
}
cmd.Flags().StringVarP(&opts.name, "name", "n", "", "Assign a name to the machine")
cmd.Flags().StringVarP(&opts.name, "name", "n", "", "Assign a name to the machine.")
cmd.Flags().StringVar(
&opts.network, "network", network.DefaultNetwork.String(),
"IPv4 network CIDR to use for machines and services",
&opts.network, "network", cluster.DefaultNetwork.String(),
"IPv4 network CIDR to use for machines and services.",
)
//cmd.Flags().StringVar(&opts.userPublicKey, "user-pubkey", "",
// "User's public key which will be able to access the cluster (hex-encoded)")
cmd.Flags().StringVarP(
&opts.sshKey, "ssh-key", "i", "",
"path to SSH private key for SSH remote login (default ~/.ssh/id_*)",
"path to SSH private key for SSH remote login. (default ~/.ssh/id_*)",
)
cmd.Flags().StringVarP(
&opts.cluster, "cluster", "c", "",
"Name of the cluster in the local config if initialising a remote machine",
"Name of the cluster in the local config if initialising a remote machine.",
)
return cmd
+61 -7
View File
@@ -1,24 +1,78 @@
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"
)
func NewListCommand() *cobra.Command {
opts := addOptions{}
var cluster string
cmd := &cobra.Command{
Use: "list",
Aliases: []string{"ls"},
Short: "List machines in a cluster",
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 uncli.ListMachines(cmd.Context(), opts.cluster)
return list(cmd.Context(), uncli, cluster)
},
}
cmd.Flags().StringVarP(
&opts.cluster, "cluster", "c", "",
"Name of the cluster (default is the current cluster)",
&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()
machines, err := client.ListMachines(ctx)
if err != nil {
return fmt.Errorf("list machines: %w", err)
}
// 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 {
return fmt.Errorf("write header: %w", err)
}
// Print rows.
for _, member := range machines {
m := member.Machine
subnet, _ := m.Network.Subnet.ToPrefix()
subnet = netip.PrefixFrom(network.MachineIP(subnet), subnet.Bits())
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, ", "),
); err != nil {
return fmt.Errorf("write row: %w", err)
}
}
return tw.Flush()
}
// capitalise returns a string where the first character is upper case, and the rest is lower case.
func capitalise(s string) string {
if s == "" {
return ""
}
return strings.ToUpper(s[:1]) + strings.ToLower(s[1:])
}
+1 -1
View File
@@ -31,7 +31,7 @@ func NewTokenCommand() *cobra.Command {
}
cmd.Flags().StringVarP(&opts.dataDir, "data-dir", "d", machine.DefaultDataDir,
"Directory for storing persistent machine state")
"Directory for storing persistent machine state.")
_ = cmd.MarkFlagDirname("data-dir")
return cmd
+7 -1
View File
@@ -7,6 +7,7 @@ import (
"os"
"strings"
"uncloud/cmd/uncloud/machine"
"uncloud/cmd/uncloud/service"
"uncloud/internal/cli"
)
@@ -36,11 +37,16 @@ func main() {
}
// 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")
"path to the Uncloud configuration file.")
_ = cmd.MarkPersistentFlagFilename("uncloud-config", "toml")
cmd.AddCommand(
machine.NewRootCommand(),
service.NewRootCommand(),
service.NewInspectCommand(),
service.NewListCommand(),
service.NewRmCommand(),
service.NewRunCommand(),
)
cobra.CheckErr(cmd.Execute())
}
+95
View File
@@ -0,0 +1,95 @@
package service
import (
"context"
"fmt"
"github.com/docker/docker/pkg/stringid"
"github.com/docker/go-units"
"os"
"text/tabwriter"
"time"
"github.com/spf13/cobra"
"uncloud/internal/cli"
)
type inspectOptions struct {
service string
cluster string
}
func NewInspectCommand() *cobra.Command {
opts := inspectOptions{}
cmd := &cobra.Command{
Use: "inspect",
Short: "Display detailed information on a service.",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
uncli := cmd.Context().Value("cli").(*cli.CLI)
opts.service = args[0]
return inspect(cmd.Context(), uncli, opts)
},
}
cmd.Flags().StringVarP(
&opts.cluster, "cluster", "c", "",
"Name of the cluster. (default is the current cluster)",
)
return cmd
}
func inspect(ctx context.Context, uncli *cli.CLI, opts inspectOptions) error {
client, err := uncli.ConnectCluster(ctx, opts.cluster)
if err != nil {
return fmt.Errorf("connect to cluster: %w", err)
}
defer client.Close()
svc, err := client.InspectService(ctx, opts.service)
if err != nil {
return fmt.Errorf("inspect service: %w", err)
}
machines, err := client.ListMachines(ctx)
if err != nil {
return fmt.Errorf("list machines: %w", err)
}
machinesNamesByID := make(map[string]string)
for _, m := range machines {
machinesNamesByID[m.Machine.Id] = m.Machine.Name
}
fmt.Printf("ID: %s\n", svc.ID)
fmt.Printf("Name: %s\n", svc.Name)
fmt.Printf("Mode: %s\n", svc.Mode)
fmt.Println()
// Print the list of containers in a table format.
tw := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', 0)
if _, err = fmt.Fprintln(tw, "CONTAINER ID\tIMAGE\tCREATED\tSTATUS\tMACHINE"); err != nil {
return fmt.Errorf("write header: %w", err)
}
for _, ctr := range svc.Containers {
createdAt := time.Unix(ctr.Container.Created, 0)
created := units.HumanDuration(time.Now().UTC().Sub(createdAt)) + " ago"
machine := machinesNamesByID[ctr.MachineID]
if machine == "" {
machine = ctr.MachineID
}
_, err = fmt.Fprintf(
tw,
"%s\t%s\t%s\t%s\t%s\n",
stringid.TruncateID(ctr.Container.ID),
ctr.Container.Image,
created,
ctr.Container.Status,
machine,
)
if err != nil {
return fmt.Errorf("write row: %w", err)
}
}
return tw.Flush()
}
+53
View File
@@ -0,0 +1,53 @@
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()
}
+50
View File
@@ -0,0 +1,50 @@
package service
import (
"context"
"fmt"
"github.com/spf13/cobra"
"uncloud/internal/cli"
)
type rmOptions struct {
services []string
cluster string
}
func NewRmCommand() *cobra.Command {
opts := rmOptions{}
cmd := &cobra.Command{
Use: "rm SERVICE [SERVICE...]",
Aliases: []string{"remove", "delete"},
Short: "Remove one or more services.",
Args: cobra.MinimumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
uncli := cmd.Context().Value("cli").(*cli.CLI)
opts.services = args
return rm(cmd.Context(), uncli, opts)
},
}
cmd.Flags().StringVarP(
&opts.cluster, "cluster", "c", "",
"Name of the cluster. (default is the current cluster)",
)
return cmd
}
func rm(ctx context.Context, uncli *cli.CLI, opts rmOptions) error {
client, err := uncli.ConnectCluster(ctx, opts.cluster)
if err != nil {
return fmt.Errorf("connect to cluster: %w", err)
}
defer client.Close()
for _, s := range opts.services {
if err = client.RemoveService(ctx, s); err != nil {
return fmt.Errorf("remove service %q: %w", s, err)
}
fmt.Printf("Service %q removed.\n", s)
}
return nil
}
+18
View File
@@ -0,0 +1,18 @@
package service
import (
"github.com/spf13/cobra"
)
func NewRootCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "service",
Short: "Manage services in an Uncloud cluster.",
}
cmd.AddCommand(
NewListCommand(),
NewRmCommand(),
NewRunCommand(),
)
return cmd
}
+115
View File
@@ -0,0 +1,115 @@
package service
import (
"context"
"fmt"
"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
cluster string
}
func NewRunCommand() *cobra.Command {
opts := runOptions{}
cmd := &cobra.Command{
Use: "run IMAGE [COMMAND...]",
Short: "Run a service.",
Args: cobra.MinimumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
uncli := cmd.Context().Value("cli").(*cli.CLI)
opts.image = args[0]
if len(args) > 1 {
opts.command = args[1:]
}
return run(cmd.Context(), uncli, opts)
},
}
// 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().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).",
api.ServiceModeReplicated, api.ServiceModeGlobal))
cmd.Flags().StringVarP(&opts.name, "name", "n", "",
"Assign a name to the service. A random name is generated if not specified.")
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"+
"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 53:5353/udp@host Bind UDP port 5353 to host port 53")
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.")
cmd.Flags().StringVarP(
&opts.cluster, "cluster", "c", "",
"Name of the cluster to run the service in. (default is the current cluster)",
)
return cmd
}
func run(ctx context.Context, uncli *cli.CLI, opts runOptions) error {
switch opts.mode {
case "", api.ServiceModeReplicated, api.ServiceModeGlobal:
default:
return fmt.Errorf("invalid replication mode: %q", opts.mode)
}
ports := make([]api.PortSpec, len(opts.publish))
for i, publishPort := range opts.publish {
port, err := api.ParsePortSpec(publishPort)
if err != nil {
return fmt.Errorf("invalid service port '%s': %w", publishPort, err)
}
ports[i] = port
}
// TODO: parse and validate opts.volumes to fail fast if invalid.
spec := api.ServiceSpec{
Container: api.ContainerSpec{
Command: opts.command,
Image: opts.image,
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)
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 nil
}
+5 -1
View File
@@ -8,11 +8,15 @@ import (
"os/signal"
"syscall"
"uncloud/internal/daemon"
"uncloud/internal/log"
"uncloud/internal/machine"
)
func main() {
slog.SetLogLoggerLevel(slog.LevelDebug)
logger := slog.New(log.NewSlogTextHandler(os.Stderr, &slog.HandlerOptions{
Level: slog.LevelDebug,
}))
slog.SetDefault(logger)
var dataDir string
cmd := &cobra.Command{
+151 -26
View File
@@ -1,16 +1,23 @@
module uncloud
go 1.22.2
go 1.23
toolchain go1.22.5
toolchain go1.23.2
require (
github.com/BurntSushi/toml v1.4.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/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf
github.com/dgraph-io/badger/v3 v3.2103.5
github.com/docker/docker v27.3.0+incompatible
github.com/distribution/reference v0.6.0
github.com/docker/cli v27.4.0-rc.4+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/hashicorp/memberlist v0.5.1
github.com/hashicorp/serf v0.10.1
github.com/ipfs/boxo v0.21.0
@@ -21,34 +28,48 @@ require (
github.com/ipfs/go-ipld-format v0.6.0
github.com/ipfs/go-log/v2 v2.5.1
github.com/lmittmann/tint v1.0.5
github.com/opencontainers/image-spec v1.1.0
github.com/siderolabs/discovery-api v0.1.4
github.com/siderolabs/discovery-client v0.1.9
github.com/siderolabs/grpc-proxy v0.5.1
github.com/spf13/cobra v1.8.1
github.com/stretchr/testify v1.10.0
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.27.0
golang.org/x/sync v0.8.0
golang.org/x/sys v0.25.0
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.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6
google.golang.org/grpc v1.66.0
google.golang.org/protobuf v1.34.2
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
)
require (
filippo.io/edwards25519 v1.1.0 // indirect
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 // indirect
github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 // indirect
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/semver/v3 v3.2.1 // indirect
github.com/Masterminds/sprig/v3 v3.2.3 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/antlr4-go/antlr/v4 v4.13.0 // indirect
github.com/armon/circbuf v0.0.0-20190214190532-5111143e8da2 // indirect
github.com/armon/go-metrics v0.4.1 // indirect
github.com/armon/go-radix v1.0.0 // indirect
github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b // indirect
github.com/atotto/clipboard v0.1.4 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bgentry/speakeasy v0.2.0 // indirect
github.com/buger/goterm v1.0.4 // indirect
github.com/caddyserver/certmagic v0.21.4 // indirect
github.com/caddyserver/zerossl v0.1.3 // indirect
github.com/catppuccin/go v0.2.0 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cespare/xxhash v1.1.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/charmbracelet/bubbles v0.20.0 // indirect
@@ -57,28 +78,60 @@ require (
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/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
github.com/containerd/continuity v0.4.4 // indirect
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/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/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
github.com/dgraph-io/badger/v2 v2.2007.4 // indirect
github.com/dgraph-io/ristretto v0.1.1 // indirect
github.com/distribution/reference v0.6.0 // indirect
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 // indirect
github.com/docker/buildx v0.18.0 // indirect
github.com/docker/distribution v2.8.3+incompatible // indirect
github.com/docker/docker-credential-helpers v0.8.2 // indirect
github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c // indirect
github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/docker/go-metrics v0.0.1 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
github.com/fatih/color v1.17.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fvbommel/sortorder v1.1.0 // indirect
github.com/go-jose/go-jose/v3 v3.0.3 // indirect
github.com/go-kit/kit v0.13.0 // indirect
github.com/go-kit/log v0.2.1 // indirect
github.com/go-logfmt/logfmt v0.6.0 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-sql-driver/mysql v1.7.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
github.com/gofrs/flock v0.12.1 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/glog v1.2.2 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/google/btree v1.1.2 // indirect
github.com/google/cel-go v0.20.1 // indirect
github.com/google/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/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/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
@@ -86,12 +139,14 @@ require (
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/go-sockaddr v1.0.6 // indirect
github.com/hashicorp/go-syslog v1.0.0 // indirect
github.com/hashicorp/go-version v1.7.0 // indirect
github.com/hashicorp/golang-lru v1.0.2 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/hashicorp/logutils v1.0.0 // indirect
github.com/hashicorp/mdns v1.0.5 // indirect
github.com/huandu/xstrings v1.5.0 // indirect
github.com/imdario/mergo v0.3.16 // indirect
github.com/in-toto/in-toto-golang v0.5.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/ipfs/bbloom v0.0.4 // indirect
github.com/ipfs/go-block-format v0.2.0 // indirect
@@ -101,30 +156,52 @@ require (
github.com/ipld/go-codec-dagpb v1.6.0 // indirect
github.com/ipld/go-ipld-prime v0.21.0 // indirect
github.com/ishidawataru/sctp v0.0.0-20230406120618-7ff4192f6ff2 // indirect
github.com/jackc/chunkreader/v2 v2.0.1 // indirect
github.com/jackc/pgconn v1.14.3 // indirect
github.com/jackc/pgio v1.0.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgproto3/v2 v2.3.3 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/jackc/pgtype v1.14.0 // indirect
github.com/jackc/pgx/v4 v4.18.3 // indirect
github.com/jbenet/goprocess v0.1.4 // indirect
github.com/josharian/native v1.1.0 // indirect
github.com/klauspost/compress v1.17.9 // indirect
github.com/klauspost/cpuid/v2 v2.2.8 // indirect
github.com/klauspost/compress v1.17.11 // indirect
github.com/klauspost/cpuid/v2 v2.2.9 // indirect
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect
github.com/libdns/libdns v0.2.2 // indirect
github.com/libp2p/go-buffer-pool v0.1.0 // indirect
github.com/libp2p/go-libp2p v0.35.4 // indirect
github.com/libp2p/go-libp2p-pubsub v0.11.0 // indirect
github.com/libp2p/go-msgio v0.3.0 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/manifoldco/promptui v0.9.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/mattn/go-shellwords v1.0.12 // indirect
github.com/mdlayher/genetlink v1.3.2 // indirect
github.com/mdlayher/netlink v1.7.2 // indirect
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-ps v1.0.0 // indirect
github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/moby/buildkit v0.17.2 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/locker v1.0.1 // indirect
github.com/moby/patternmatcher v0.6.0 // indirect
github.com/moby/sys/sequential v0.6.0 // indirect
github.com/moby/sys/signal v0.7.1 // indirect
github.com/moby/term v0.5.0 // indirect
github.com/morikuni/aec v1.0.0 // indirect
github.com/mr-tron/base58 v1.2.0 // indirect
@@ -140,37 +217,85 @@ require (
github.com/multiformats/go-multihash v0.2.3 // indirect
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/onsi/ginkgo/v2 v2.22.0 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.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
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/polydawn/refmt v0.89.0 // indirect
github.com/posener/complete v1.2.3 // indirect
github.com/prometheus/client_golang v1.20.5 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.61.0 // indirect
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/rivo/uniseg v0.4.7 // indirect
github.com/rs/xid v1.5.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 // indirect
github.com/secure-systems-lab/go-securesystemslib v0.4.0 // indirect
github.com/shibumi/go-pathspec v1.3.0 // indirect
github.com/shopspring/decimal v1.4.0 // indirect
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
github.com/siderolabs/gen v0.4.8 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/slackhq/nebula v1.6.1 // indirect
github.com/smallstep/certificates v0.26.1 // indirect
github.com/smallstep/nosql v0.6.1 // indirect
github.com/smallstep/pkcs7 v0.0.0-20231024181729-3b98ecc1ca81 // indirect
github.com/smallstep/scep v0.0.0-20231024192529-aee96d7ad34d // indirect
github.com/smallstep/truststore v0.13.0 // indirect
github.com/spaolacci/murmur3 v1.1.0 // indirect
github.com/spf13/cast v1.7.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/stoewer/go-strcase v1.2.0 // indirect
github.com/tailscale/tscert v0.0.0-20240517230440-bbccfbf48933 // indirect
github.com/theupdateframework/notary v0.7.0 // indirect
github.com/tonistiigi/dchapes-mode v0.0.0-20241001053921-ca0759fec205 // indirect
github.com/tonistiigi/fsutil v0.0.0-20241028165955-397af5306b5c // indirect
github.com/tonistiigi/go-csvvalue v0.0.0-20240710180619-ddb21b71c0b4 // indirect
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/vishvananda/netns v0.0.4 // 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/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.30.0 // indirect
go.opentelemetry.io/otel/metric v1.30.0 // indirect
go.opentelemetry.io/otel/trace v1.30.0 // indirect
go.opentelemetry.io/otel v1.32.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/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/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.step.sm/cli-utils v0.9.0 // indirect
go.step.sm/crypto v0.45.0 // indirect
go.step.sm/linkedca v0.20.1 // indirect
go.uber.org/automaxprocs v1.5.3 // indirect
go.uber.org/mock v0.5.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect
golang.org/x/mod v0.21.0 // indirect
golang.org/x/net v0.29.0 // indirect
golang.org/x/text v0.18.0 // indirect
golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 // indirect
golang.org/x/tools v0.25.0 // indirect
go.uber.org/zap/exp v0.2.0 // indirect
golang.org/x/crypto/x509roots/fallback v0.0.0-20240507223354-67b13616a595 // indirect
golang.org/x/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/time v0.8.0 // indirect
golang.org/x/tools v0.28.0 // indirect
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240827150818-7e3bb234dfed // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240827150818-7e3bb234dfed // indirect
gotest.tools/v3 v3.5.1 // indirect
google.golang.org/genproto v0.0.0-20240401170217-c3f982113cda // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 // 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
)
+604 -61
View File
File diff suppressed because it is too large Load Diff
+86
View File
@@ -0,0 +1,86 @@
package api
import (
"github.com/docker/docker/api/types"
"regexp"
"strings"
)
const (
LabelManaged = "uncloud.managed"
LabelServiceID = "uncloud.service.id"
LabelServiceName = "uncloud.service.name"
LabelServiceMode = "uncloud.service.mode"
LabelServicePorts = "uncloud.service.ports"
)
type Container struct {
types.Container
}
// ServiceID returns the ID of the service this container belongs to.
func (c *Container) ServiceID() string {
return c.Labels[LabelServiceID]
}
// ServiceName returns the name of the service this container belongs to.
func (c *Container) ServiceName() string {
return c.Labels[LabelServiceName]
}
// ServiceMode returns the replication mode of the service this container belongs to.
func (c *Container) ServiceMode() string {
return c.Labels[LabelServiceMode]
}
// ServicePorts returns the ports this container publishes as part of its service.
func (c *Container) ServicePorts() ([]PortSpec, error) {
encoded, ok := c.Labels[LabelServicePorts]
if !ok {
return nil, nil
}
publishPorts := strings.Split(encoded, ",")
ports := make([]PortSpec, len(publishPorts))
for i, p := range publishPorts {
port, err := ParsePortSpec(strings.TrimSpace(p))
if err != nil {
return nil, err
}
ports[i] = port
}
return ports, nil
}
// runningStatusRegex matches the status string of a running container.
// - "Up 3 minutes (healthy)" -> groups: ["Up 3 minutes (healthy)", "healthy"]
// - "Up 5 seconds" -> groups: ["Up 5 seconds", ""]
// - "Up 2 hours (unhealthy)" -> groups: ["Up 2 hours (unhealthy)", "unhealthy"]
// - "Up 1 minute (health: starting)" -> groups: ["Up 1 minute (health: starting)", "health: starting"]
// - "Restarting (0) 5 seconds ago" -> no match
// See https://github.com/moby/moby/blob/c130ce1f5d1e38b98a97044a39557de43bc0d58f/container/state.go#L77-L90
// for more details on how the status string for a running container is formatted.
var runningStatusRegex = regexp.MustCompile(`^Up [^(]+(?:\(([^)]+)\))?$`)
// Healthy determines if the container is running and healthy based on its status string.
// A running container with no health check configured is considered healthy.
func (c *Container) Healthy() bool {
if c.State != "running" {
return false
}
matches := runningStatusRegex.FindStringSubmatch(c.Status)
// Not "Up" or invalid format.
if matches == nil {
return false
}
// If there's no health status (no health check configured so no parentheses), container is considered healthy.
if matches[1] == "" {
return true
}
// If the health status in parentheses is "healthy", the container is considered healthy.
return matches[1] == types.Healthy
}
+92
View File
@@ -0,0 +1,92 @@
package api
import (
"github.com/docker/docker/api/types"
"github.com/stretchr/testify/assert"
"testing"
)
func TestContainer_Healthy(t *testing.T) {
t.Parallel()
t.Run("exited", func(t *testing.T) {
t.Parallel()
c := &Container{Container: types.Container{
State: "exited",
Status: "Exited (0) 2 minutes ago",
}}
assert.False(t, c.Healthy())
})
t.Run("running with no health check", func(t *testing.T) {
t.Parallel()
c := &Container{Container: types.Container{
State: "running",
Status: "Up 5 minutes",
}}
assert.True(t, c.Healthy())
})
t.Run("running and healthy", func(t *testing.T) {
t.Parallel()
c := &Container{Container: types.Container{
State: "running",
Status: "Up 3 minutes (healthy)",
}}
assert.True(t, c.Healthy())
})
t.Run("running but unhealthy", func(t *testing.T) {
t.Parallel()
c := &Container{Container: types.Container{
State: "running",
Status: "Up 2 hours (unhealthy)",
}}
assert.False(t, c.Healthy())
})
t.Run("running with health starting", func(t *testing.T) {
t.Parallel()
c := &Container{Container: types.Container{
State: "running",
Status: "Up 1 minute (health: starting)",
}}
assert.False(t, c.Healthy())
})
t.Run("invalid up format no time", func(t *testing.T) {
t.Parallel()
c := &Container{Container: types.Container{
State: "running",
Status: "Up",
}}
assert.False(t, c.Healthy())
})
t.Run("invalid up format empty parentheses", func(t *testing.T) {
t.Parallel()
c := &Container{Container: types.Container{
State: "running",
Status: "Up 5 minutes ()",
}}
assert.False(t, c.Healthy())
})
t.Run("malformed status", func(t *testing.T) {
t.Parallel()
c := &Container{Container: types.Container{
State: "running",
Status: "Invalid status",
}}
assert.False(t, c.Healthy())
})
t.Run("restarting", func(t *testing.T) {
t.Parallel()
c := &Container{Container: types.Container{
State: "running",
Status: "Restarting (0) 5 seconds ago",
}}
assert.False(t, c.Healthy())
})
}
+268
View File
@@ -0,0 +1,268 @@
package api
import (
"fmt"
"net/netip"
"strconv"
"strings"
)
const (
PortModeIngress = "ingress"
PortModeHost = "host"
ProtocolHTTP = "http"
ProtocolHTTPS = "https"
ProtocolTCP = "tcp"
ProtocolUDP = "udp"
)
type PortSpec struct {
// Hostname specifies the DNS name that will route to this service. Only valid in ingress mode.
Hostname string
// HostIP is the host IP to bind the PublishedPort to. Only valid in host mode.
HostIP netip.Addr
// PublishedPort is the port number exposed outside the container.
// In ingress mode, this is the load balancer port. In host mode, this is the port bound on the host.
PublishedPort uint16
// ContainerPort is the port inside the container that the service listens on.
ContainerPort uint16
// Protocol specifies the network protocol.
Protocol string
// Mode specifies how the port is published.
Mode string
}
func (p *PortSpec) Validate() error {
if p.ContainerPort == 0 {
return fmt.Errorf("container port must be non-zero")
}
switch p.Protocol {
case "":
return fmt.Errorf("protocol must be specified")
case ProtocolHTTP, ProtocolHTTPS, ProtocolTCP, ProtocolUDP:
default:
return fmt.Errorf("invalid protocol '%s', supported protocols: '%s', '%s', '%s', '%s'",
p.Protocol, ProtocolHTTP, ProtocolHTTPS, ProtocolTCP, ProtocolUDP)
}
switch p.Mode {
case "":
return fmt.Errorf("mode must be specified")
case PortModeIngress:
if p.HostIP.IsValid() {
return fmt.Errorf("host IP cannot be specified in %s mode", PortModeIngress)
}
if p.Hostname != "" {
if p.Protocol != ProtocolHTTP && p.Protocol != ProtocolHTTPS {
return fmt.Errorf("hostname is only valid with '%s' or '%s' protocols", ProtocolHTTP, ProtocolHTTPS)
}
if err := validateHostname(p.Hostname); err != nil {
return fmt.Errorf("invalid hostname '%s': %w", p.Hostname, err)
}
}
if p.Hostname == "" && (p.Protocol == ProtocolHTTP || p.Protocol == ProtocolHTTPS) {
return fmt.Errorf("hostname is required with '%s' or '%s' protocols", ProtocolHTTP, ProtocolHTTPS)
}
case PortModeHost:
if p.PublishedPort == 0 {
return fmt.Errorf("published port is required in %s mode", PortModeHost)
}
if p.Protocol != ProtocolTCP && p.Protocol != ProtocolUDP {
return fmt.Errorf("unsupported protocol '%s' in %s mode, only '%s' and '%s' are supported",
p.Protocol, PortModeHost, ProtocolTCP, ProtocolUDP)
}
if p.Hostname != "" {
return fmt.Errorf("hostname cannot be specified in %s mode", PortModeHost)
}
default:
return fmt.Errorf("invalid mode: '%s'", p.Mode)
}
return nil
}
// String returns the port specification in the -p/--publish flag format.
// Format:
// [hostname:][load_balancer_port:]container_port/protocol for ingress mode (default) or
// [host_ip:]:host_port:container_port/protocol@host for host mode.
func (p *PortSpec) String() (string, error) {
if err := p.Validate(); err != nil {
return "", err
}
var parts []string
switch p.Mode {
case "", PortModeIngress: // [hostname:][load_balancer_port:]container_port/protocol
if p.Hostname != "" {
parts = append(parts, p.Hostname)
}
if p.PublishedPort != 0 {
parts = append(parts, fmt.Sprint(p.PublishedPort))
}
parts = append(parts, fmt.Sprint(p.ContainerPort))
return fmt.Sprintf("%s/%s", strings.Join(parts, ":"), p.Protocol), nil
case PortModeHost: // [host_ip:]:host_port:container_port/protocol@host
if p.HostIP.IsValid() {
if p.HostIP.Is6() {
parts = append(parts, fmt.Sprintf("[%s]", p.HostIP))
} else {
parts = append(parts, p.HostIP.String())
}
}
parts = append(parts, fmt.Sprint(p.PublishedPort))
parts = append(parts, fmt.Sprint(p.ContainerPort))
return fmt.Sprintf("%s/%s@host", strings.Join(parts, ":"), p.Protocol), nil
default:
return "", fmt.Errorf("not implemented for mode: '%s'", p.Mode)
}
}
func ParsePortSpec(port string) (PortSpec, error) {
spec := PortSpec{
Protocol: ProtocolTCP, // Default protocol.
Mode: PortModeIngress, // Default mode.
}
// Split off mode first.
parts := strings.Split(port, "@")
if len(parts) > 2 {
return spec, fmt.Errorf("too many '@' symbols")
}
if len(parts) == 2 {
if parts[1] != PortModeHost {
return spec, fmt.Errorf("invalid mode: '%s', only 'host' mode is supported", parts[1])
}
spec.Mode = PortModeHost
}
port = parts[0]
// Parse protocol.
parts = strings.Split(port, "/")
if len(parts) > 2 {
return spec, fmt.Errorf("too many '/' symbols")
}
specifiedProtocol := ""
if len(parts) == 2 {
protocol := parts[1]
switch protocol {
case ProtocolHTTP, ProtocolHTTPS, ProtocolTCP, ProtocolUDP:
spec.Protocol = protocol
specifiedProtocol = protocol
default:
return spec, fmt.Errorf("unsupported protocol: '%s'", protocol)
}
}
port = parts[0]
// Parse hostname/host IP and ports.
parts = splitPortParts(port)
var err error
switch len(parts) {
case 1: // Just container port.
if spec.ContainerPort, err = parsePort(parts[0]); err != nil {
return spec, fmt.Errorf("invalid container port '%s': %w", parts[0], err)
}
case 2: // hostname:container_port or [load_balancer_port|host_port]:container_port
if spec.ContainerPort, err = parsePort(parts[1]); err != nil {
return spec, fmt.Errorf("invalid container port '%s': %w", parts[1], err)
}
if parts[0] == "" {
return spec, fmt.Errorf("hostname or published port must be specified, format: " +
"hostname:container_port or published_port:container_port")
}
// Try to parse the first part as port.
if publishedPort, err := parsePort(parts[0]); err == nil {
spec.PublishedPort = publishedPort
} else {
// It's a hostname.
if spec.Mode == PortModeHost {
return spec, fmt.Errorf("hostname cannot be specified in host mode")
}
spec.Hostname = parts[0]
}
case 3: // hostname:load_balancer_port:container_port or host_ip:host_port:container_port
if spec.ContainerPort, err = parsePort(parts[2]); err != nil {
return spec, fmt.Errorf("invalid container port '%s': %w", parts[2], err)
}
if spec.PublishedPort, err = parsePort(parts[1]); err != nil {
return spec, fmt.Errorf("invalid published port '%s': %w", parts[1], err)
}
if spec.Mode == PortModeHost {
// In host mode, the first part must be IP.
ip := parts[0]
// Strip brackets from IPv6 address if present.
if strings.Contains(ip, ":") {
if !strings.HasPrefix(ip, "[") {
return spec, fmt.Errorf(
"invalid host IP '%s': IPv6 address must be enclosed in square brackets", ip)
}
if !strings.HasSuffix(ip, "]") {
return spec, fmt.Errorf("invalid host IP '%s': missing closing bracket", ip)
}
ip = ip[1 : len(ip)-1]
}
if spec.HostIP, err = netip.ParseAddr(ip); err != nil {
return spec, fmt.Errorf("invalid host IP '%s': %w", parts[0], err)
}
} else {
if parts[0] == "" {
return spec, fmt.Errorf("hostname must not be empty")
}
spec.Hostname = parts[0]
}
default:
return spec, fmt.Errorf("unexpected number of parts in port spec: %d", len(parts))
}
if spec.Hostname != "" {
if specifiedProtocol == "" {
spec.Protocol = ProtocolHTTPS
} else if specifiedProtocol != ProtocolHTTP && specifiedProtocol != ProtocolHTTPS {
return spec, fmt.Errorf("hostname is only valid with '%s' or '%s' protocols, specified: '%s'",
ProtocolHTTP, ProtocolHTTPS, specifiedProtocol)
}
}
return spec, spec.Validate()
}
// splitPortParts splits a port specification [hostname|host_ip:][published_port:]container_port into its parts.
func splitPortParts(port string) []string {
parts := strings.Split(port, ":")
n := len(parts)
if n > 3 {
// Host IP may contain colons if it's IPv6, so we need to join the first n-2 parts.
return append([]string{strings.Join(parts[:n-2], ":")}, parts[n-2:]...)
}
return parts
}
func parsePort(s string) (uint16, error) {
port, err := strconv.ParseUint(s, 10, 16)
if err != nil {
return 0, err
}
return uint16(port), nil
}
func validateHostname(hostname string) error {
if hostname == "" {
return fmt.Errorf("must not be empty")
}
if !strings.Contains(hostname, ".") {
return fmt.Errorf("must be a valid domain name containing at least one dot")
}
return nil
}
+698
View File
@@ -0,0 +1,698 @@
package api
import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"net/netip"
"testing"
)
func TestPortSpec_Validate(t *testing.T) {
t.Parallel()
tests := []struct {
name string
spec PortSpec
wantErr string
}{
// Valid ingress mode.
{
name: "ingress mode tcp",
spec: PortSpec{
ContainerPort: 8080,
Protocol: ProtocolTCP,
Mode: PortModeIngress,
},
},
{
name: "ingress mode with published tcp port",
spec: PortSpec{
PublishedPort: 80,
ContainerPort: 8080,
Protocol: ProtocolTCP,
Mode: PortModeIngress,
},
},
{
name: "ingress mode udp",
spec: PortSpec{
ContainerPort: 8080,
Protocol: ProtocolUDP,
Mode: PortModeIngress,
},
},
{
name: "ingress mode with published udp port",
spec: PortSpec{
PublishedPort: 80,
ContainerPort: 8080,
Protocol: ProtocolUDP,
Mode: PortModeIngress,
},
},
{
name: "ingress mode with hostname and http",
spec: PortSpec{
Hostname: "app.example.com",
ContainerPort: 8080,
Protocol: ProtocolHTTP,
Mode: PortModeIngress,
},
},
{
name: "ingress mode with hostname and https",
spec: PortSpec{
Hostname: "app.example.com",
ContainerPort: 8080,
Protocol: ProtocolHTTPS,
Mode: PortModeIngress,
},
},
{
name: "ingress mode with hostname and published port",
spec: PortSpec{
Hostname: "app.example.com",
PublishedPort: 6443,
ContainerPort: 8080,
Protocol: ProtocolHTTPS,
Mode: PortModeIngress,
},
},
// Valid host mode.
{
name: "host mode tcp",
spec: PortSpec{
PublishedPort: 80,
ContainerPort: 8080,
Protocol: ProtocolUDP,
Mode: PortModeHost,
},
},
{
name: "host mode udp",
spec: PortSpec{
PublishedPort: 80,
ContainerPort: 8080,
Protocol: ProtocolUDP,
Mode: PortModeHost,
},
},
{
name: "host mode with IPv4",
spec: PortSpec{
HostIP: netip.MustParseAddr("127.0.0.1"),
PublishedPort: 80,
ContainerPort: 8080,
Protocol: ProtocolTCP,
Mode: PortModeHost,
},
},
{
name: "host mode with IPv6",
spec: PortSpec{
HostIP: netip.MustParseAddr("2001:db8::1234:5678"),
PublishedPort: 80,
ContainerPort: 8080,
Protocol: ProtocolUDP,
Mode: PortModeHost,
},
},
// Error cases.
{
name: "missing container port",
spec: PortSpec{
Protocol: ProtocolTCP,
},
wantErr: "container port must be non-zero",
},
{
name: "invalid protocol",
spec: PortSpec{
ContainerPort: 8080,
Protocol: "invalid",
},
wantErr: "invalid protocol 'invalid'",
},
{
name: "invalid mode",
spec: PortSpec{
ContainerPort: 8080,
Protocol: ProtocolTCP,
Mode: "invalid",
},
wantErr: "invalid mode: 'invalid'",
},
{
name: "hostname with non-http protocol",
spec: PortSpec{
Hostname: "app.example.com",
ContainerPort: 8080,
Protocol: ProtocolTCP,
Mode: PortModeIngress,
},
wantErr: "hostname is only valid with 'http' or 'https' protocols",
},
{
name: "invalid hostname",
spec: PortSpec{
Hostname: "app",
ContainerPort: 8080,
Protocol: ProtocolHTTPS,
Mode: PortModeIngress,
},
wantErr: "invalid hostname 'app': must be a valid domain name containing at least one dot",
},
{
name: "missing hostname with http",
spec: PortSpec{
ContainerPort: 8080,
Protocol: ProtocolHTTP,
Mode: PortModeIngress,
},
wantErr: "hostname is required with 'http' or 'https' protocols",
},
{
name: "missing hostname with https",
spec: PortSpec{
ContainerPort: 8080,
Protocol: ProtocolHTTPS,
Mode: PortModeIngress,
},
wantErr: "hostname is required with 'http' or 'https' protocols",
},
{
name: "host IP in ingress mode",
spec: PortSpec{
HostIP: netip.MustParseAddr("127.0.0.1"),
ContainerPort: 8080,
Protocol: ProtocolTCP,
Mode: PortModeIngress,
},
wantErr: "host IP cannot be specified in ingress mode",
},
{
name: "zero published port in host mode",
spec: PortSpec{
ContainerPort: 8080,
Protocol: ProtocolTCP,
Mode: PortModeHost,
},
wantErr: "published port is required in host mode",
},
{
name: "hostname in host mode",
spec: PortSpec{
Hostname: "app.example.com",
PublishedPort: 80,
ContainerPort: 8080,
Protocol: ProtocolTCP,
Mode: PortModeHost,
},
wantErr: "hostname cannot be specified in host mode",
},
{
name: "http in host mode",
spec: PortSpec{
PublishedPort: 80,
ContainerPort: 8080,
Protocol: ProtocolHTTP,
Mode: PortModeHost,
},
wantErr: "unsupported protocol 'http' in host mode",
},
{
name: "https in host mode",
spec: PortSpec{
PublishedPort: 80,
ContainerPort: 8080,
Protocol: ProtocolHTTPS,
Mode: PortModeHost,
},
wantErr: "unsupported protocol 'https' in host mode",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
err := tt.spec.Validate()
if tt.wantErr != "" {
require.Error(t, err, tt.wantErr)
assert.Contains(t, err.Error(), tt.wantErr)
return
}
require.NoError(t, err)
})
}
}
func TestPortSpec_String(t *testing.T) {
tests := []struct {
name string
spec PortSpec
expected string
}{
// Ingress mode.
{
name: "container port only",
spec: PortSpec{
ContainerPort: 8080,
Protocol: ProtocolTCP,
Mode: PortModeIngress,
},
expected: "8080/tcp",
},
{
name: "container port udp",
spec: PortSpec{
ContainerPort: 8080,
Protocol: ProtocolUDP,
Mode: PortModeIngress,
},
expected: "8080/udp",
},
{
name: "published and container port",
spec: PortSpec{
ContainerPort: 8080,
PublishedPort: 80,
Protocol: ProtocolTCP,
Mode: PortModeIngress,
},
expected: "80:8080/tcp",
},
{
name: "hostname and container port https",
spec: PortSpec{
Hostname: "app.example.com",
ContainerPort: 8080,
Protocol: ProtocolHTTPS,
Mode: PortModeIngress,
},
expected: "app.example.com:8080/https",
},
{
name: "hostname and container port http",
spec: PortSpec{
Hostname: "app.example.com",
ContainerPort: 8080,
Protocol: ProtocolHTTP,
Mode: PortModeIngress,
},
expected: "app.example.com:8080/http",
},
{
name: "hostname and published and container port https",
spec: PortSpec{
Hostname: "app.example.com",
PublishedPort: 6443,
ContainerPort: 8080,
Protocol: ProtocolHTTPS,
Mode: PortModeIngress,
},
expected: "app.example.com:6443:8080/https",
},
{
name: "hostname and published and container port http",
spec: PortSpec{
Hostname: "app.example.com",
PublishedPort: 6443,
ContainerPort: 8080,
Protocol: ProtocolHTTP,
Mode: PortModeIngress,
},
expected: "app.example.com:6443:8080/http",
},
// Host mode.
{
name: "host mode tcp",
spec: PortSpec{
PublishedPort: 80,
ContainerPort: 8080,
Protocol: ProtocolTCP,
Mode: PortModeHost,
},
expected: "80:8080/tcp@host",
},
{
name: "host mode udp",
spec: PortSpec{
PublishedPort: 80,
ContainerPort: 8080,
Protocol: ProtocolUDP,
Mode: PortModeHost,
},
expected: "80:8080/udp@host",
},
{
name: "host mode with IPv4 tcp",
spec: PortSpec{
HostIP: netip.MustParseAddr("127.0.0.1"),
PublishedPort: 80,
ContainerPort: 8080,
Protocol: ProtocolTCP,
Mode: PortModeHost,
},
expected: "127.0.0.1:80:8080/tcp@host",
},
{
name: "host mode with IPv4 udp",
spec: PortSpec{
HostIP: netip.MustParseAddr("127.0.0.1"),
PublishedPort: 80,
ContainerPort: 8080,
Protocol: ProtocolUDP,
Mode: PortModeHost,
},
expected: "127.0.0.1:80:8080/udp@host",
},
{
name: "host mode with IPv6",
spec: PortSpec{
HostIP: netip.MustParseAddr("2001:db8::1234:5678"),
PublishedPort: 80,
ContainerPort: 8080,
Protocol: ProtocolTCP,
Mode: PortModeHost,
},
expected: "[2001:db8::1234:5678]:80:8080/tcp@host",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := tt.spec.String()
require.NoError(t, err)
assert.Equal(t, tt.expected, result)
})
}
}
func TestParsePortSpec(t *testing.T) {
t.Parallel()
tests := []struct {
name string
port string
expected PortSpec
wantErr string
}{
// Ingress mode (default).
{
name: "container port only",
port: "8080",
expected: PortSpec{
ContainerPort: 8080,
Protocol: ProtocolTCP,
Mode: PortModeIngress,
},
},
{
name: "published and container port",
port: "8000:8080",
expected: PortSpec{
PublishedPort: 8000,
ContainerPort: 8080,
Protocol: ProtocolTCP,
Mode: PortModeIngress,
},
},
{
name: "published port zero",
port: "0:8080",
expected: PortSpec{
ContainerPort: 8080,
Protocol: ProtocolTCP,
Mode: PortModeIngress,
},
},
{
name: "tcp protocol explicit",
port: "8080/tcp",
expected: PortSpec{
ContainerPort: 8080,
Protocol: ProtocolTCP,
Mode: PortModeIngress,
},
},
{
name: "udp protocol",
port: "8080/udp",
expected: PortSpec{
ContainerPort: 8080,
Protocol: ProtocolUDP,
Mode: PortModeIngress,
},
},
{
name: "published port udp",
port: "8000:8080/udp",
expected: PortSpec{
PublishedPort: 8000,
ContainerPort: 8080,
Protocol: ProtocolUDP,
Mode: PortModeIngress,
},
},
{
name: "hostname and container port",
port: "app.example.com:8080",
expected: PortSpec{
Hostname: "app.example.com",
ContainerPort: 8080,
Protocol: ProtocolHTTPS,
Mode: PortModeIngress,
},
},
{
name: "hostname and container port http",
port: "app.example.com:8080/http",
expected: PortSpec{
Hostname: "app.example.com",
ContainerPort: 8080,
Protocol: ProtocolHTTP,
Mode: PortModeIngress,
},
},
{
name: "hostname and published port",
port: "app.example.com:6443:8080",
expected: PortSpec{
Hostname: "app.example.com",
PublishedPort: 6443,
ContainerPort: 8080,
Protocol: ProtocolHTTPS,
Mode: PortModeIngress,
},
},
{
name: "hostname and published port http",
port: "app.example.com:8000:8080/http",
expected: PortSpec{
Hostname: "app.example.com",
PublishedPort: 8000,
ContainerPort: 8080,
Protocol: ProtocolHTTP,
Mode: PortModeIngress,
},
},
// Host mode.
{
name: "host mode published with protocol",
port: "80:8080/udp@host",
expected: PortSpec{
PublishedPort: 80,
ContainerPort: 8080,
Protocol: ProtocolUDP,
Mode: PortModeHost,
},
},
{
name: "host mode with IPv4",
port: "127.0.0.1:80:8080@host",
expected: PortSpec{
HostIP: netip.MustParseAddr("127.0.0.1"),
PublishedPort: 80,
ContainerPort: 8080,
Protocol: ProtocolTCP,
Mode: PortModeHost,
},
},
{
name: "host mode with IPv6",
port: "[2001:db8::1234:5678]:80:8080@host",
expected: PortSpec{
HostIP: netip.MustParseAddr("2001:db8::1234:5678"),
PublishedPort: 80,
ContainerPort: 8080,
Protocol: ProtocolTCP,
Mode: PortModeHost,
},
},
{
name: "host mode with IP and protocol",
port: "127.0.0.1:80:8080/udp@host",
expected: PortSpec{
HostIP: netip.MustParseAddr("127.0.0.1"),
PublishedPort: 80,
ContainerPort: 8080,
Protocol: ProtocolUDP,
Mode: PortModeHost,
},
},
// Error cases.
{
name: "empty",
port: "",
wantErr: "invalid container port",
},
{
name: "invalid container port",
port: "invalid",
wantErr: "invalid container port",
},
{
name: "container port zero",
port: "0",
wantErr: "container port must be non-zero",
},
{
name: "out of range container port",
port: "100500",
wantErr: "invalid container port",
},
{
name: "just protocol",
port: "/tcp",
wantErr: "invalid container port",
},
{
name: "just mode",
port: "@host",
wantErr: "invalid container port",
},
{
name: "multiple @ symbols",
port: "8080@host@host",
wantErr: "too many '@' symbols",
},
{
name: "invalid mode",
port: "8080@invalid",
wantErr: "invalid mode: 'invalid'",
},
{
name: "multiple protocols",
port: "8080/tcp/udp",
wantErr: "too many '/' symbols",
},
{
name: "invalid protocol",
port: "8080/invalid",
wantErr: "unsupported protocol: 'invalid'",
},
{
name: "invalid published port",
port: "app.example.com:invalid:8080",
wantErr: "invalid published port",
},
{
name: "missing hostname with http",
port: "8080/http",
wantErr: "hostname is required",
},
{
name: "missing hostname with http",
port: "8080/https",
wantErr: "hostname is required",
},
{
name: "missing hostname with published https port",
port: "8000:8080/https",
wantErr: "hostname is required",
},
{
name: "invalid hostname",
port: "app:8080/http",
wantErr: "invalid hostname 'app': must be a valid domain name containing at least one dot",
},
{
name: "hostname with tcp protocol",
port: "app.example.com:8080/tcp",
wantErr: "hostname is only valid with 'http' or 'https' protocols",
},
{
name: "missing published port in host mode",
port: "8080@host",
wantErr: "published port is required in host mode",
},
{
name: "missing published port with protocol in host mode",
port: "8080/udp@host",
wantErr: "published port is required in host mode",
},
{
name: "invalid host IPv4",
port: "300.0.0.1:80:8080@host",
wantErr: "invalid host IP",
},
{
name: "invalid host IPv6",
port: "[:::1]:80:8080@host",
wantErr: "invalid host IP",
},
{
name: "missing closing bracket in IPv6",
port: "[::1:80:8080@host",
wantErr: "invalid host IP",
},
{
name: "missing brackets in IPv6",
port: "2001:db8::1234:5678:80:8080@host",
wantErr: "invalid host IP",
},
{
name: "http in host mode",
port: "80:8080/http@host",
wantErr: "unsupported protocol 'http' in host mode, only 'tcp' and 'udp' are supported",
},
{
name: "https in host mode",
port: "80:8080/https@host",
wantErr: "unsupported protocol 'https' in host mode, only 'tcp' and 'udp' are supported",
},
{
name: "hostname in host mode",
port: "app.example.com:8080@host",
wantErr: "hostname cannot be specified in host mode",
},
{
name: "hostname with invalid published port",
port: "app.example.com:invalid:8080@host",
wantErr: "invalid published port",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
spec, err := ParsePortSpec(tt.port)
if tt.wantErr != "" {
require.Error(t, err, "Expected error: %s, got nil, spec: %+v", tt.wantErr, spec)
assert.Contains(t, err.Error(), tt.wantErr)
return
}
require.NoError(t, err)
assert.Equal(t, tt.expected, spec)
})
}
}
+98
View File
@@ -0,0 +1,98 @@
package api
import (
"encoding/json"
"fmt"
"github.com/distribution/reference"
"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
}
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 {
_, err := reference.ParseDockerRef(s.Image)
if 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
}
+65 -125
View File
@@ -7,24 +7,16 @@ import (
"github.com/charmbracelet/huh"
"google.golang.org/protobuf/types/known/emptypb"
"net/netip"
"os"
"strings"
"text/tabwriter"
"uncloud/internal/cli/client"
"uncloud/internal/cli/client/connector"
"uncloud/internal/cli/config"
"uncloud/internal/machine"
"uncloud/internal/machine/api/pb"
"uncloud/internal/secret"
"uncloud/internal/sshexec"
)
const defaultClusterName = "default"
var (
ErrNotFound = errors.New("not found")
)
type CLI struct {
config *config.Config
}
@@ -39,35 +31,19 @@ func New(configPath string) (*CLI, error) {
}, nil
}
func (cli *CLI) CreateCluster(name string, userPrivateKey secret.Secret) error {
func (cli *CLI) CreateCluster(name string) error {
if _, ok := cli.config.Clusters[name]; ok {
return fmt.Errorf("cluster %q already exists", name)
}
if userPrivateKey == nil {
user, err := client.NewUser(nil)
if err != nil {
return fmt.Errorf("generate user: %w", err)
}
userPrivateKey = user.PrivateKey()
}
cli.config.Clusters[name] = &config.Cluster{
Name: name,
UserPrivateKey: userPrivateKey,
Name: name,
}
return cli.config.Save()
}
func (cli *CLI) CreateDefaultCluster() error {
if err := cli.CreateCluster(defaultClusterName, nil); err != nil {
return err
}
return cli.SetCurrentCluster(defaultClusterName)
}
func (cli *CLI) SetCurrentCluster(name string) error {
if _, ok := cli.config.Clusters[name]; !ok {
return ErrNotFound
return client.ErrNotFound
}
cli.config.CurrentCluster = name
return cli.config.Save()
@@ -108,16 +84,21 @@ func (cli *CLI) ConnectCluster(ctx context.Context, clusterName string) (*client
// TODO: iterate over all connections and try to connect to the cluster using the first successful connection.
conn := cfg.Connections[0]
user, host, port, err := conn.SSH.Parse()
if err != nil {
return nil, fmt.Errorf("parse SSH connection %q: %w", conn.SSH, err)
if conn.SSH != "" {
user, host, port, err := conn.SSH.Parse()
if err != nil {
return nil, fmt.Errorf("parse SSH connection %q: %w", conn.SSH, err)
}
sshConfig := &connector.SSHConnectorConfig{
User: user,
Host: host,
Port: port,
}
return client.New(ctx, connector.NewSSHConnector(sshConfig))
} else if conn.TCP.IsValid() {
return client.New(ctx, connector.NewTCPConnector(conn.TCP))
}
sshConfig := &connector.SSHConnectorConfig{
User: user,
Host: host,
Port: port,
}
return client.New(ctx, connector.NewSSHConnector(sshConfig))
return nil, errors.New("no valid connection configuration found for the cluster")
}
func (cli *CLI) InitCluster(
@@ -139,29 +120,10 @@ func (cli *CLI) initRemoteMachine(
if _, ok := cli.config.Clusters[clusterName]; ok {
return fmt.Errorf("cluster %q already exists", clusterName)
}
user, err := client.NewUser(nil)
if err != nil {
return fmt.Errorf("generate cluster user: %w", err)
}
// 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)
machineClient, err := cli.provisionRemoteMachine(ctx, remoteMachine)
if err != nil {
return fmt.Errorf(
"SSH login to remote machine %s: %w",
config.NewSSHDestination(remoteMachine.User, remoteMachine.Host, remoteMachine.Port), err,
)
}
exec := sshexec.NewRemote(sshClient)
// Install and run the Uncloud daemon and dependencies on the remote machine.
if err = provisionMachine(ctx, exec); err != nil {
return fmt.Errorf("provision machine: %w", err)
}
// Create a machine API client over the SSH connection to the remote machine.
machineClient, err := client.New(ctx, connector.NewSSHConnectorFromClient(sshClient))
if err != nil {
return fmt.Errorf("connect to remote machine: %w", err)
return err
}
defer machineClient.Close()
@@ -179,12 +141,6 @@ func (cli *CLI) initRemoteMachine(
req := &pb.InitClusterRequest{
MachineName: machineName,
Network: pb.NewIPPrefix(netPrefix),
User: &pb.User{
Network: &pb.NetworkConfig{
ManagementIp: pb.NewIP(user.ManagementIP()),
PublicKey: user.PublicKey(),
},
},
}
resp, err := machineClient.InitCluster(ctx, req)
if err != nil {
@@ -192,7 +148,7 @@ func (cli *CLI) initRemoteMachine(
}
fmt.Printf("Cluster %q initialised with machine %q\n", clusterName, resp.Machine.Name)
if err = cli.CreateCluster(clusterName, user.PrivateKey()); err != nil {
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.
@@ -221,24 +177,9 @@ func (cli *CLI) AddMachine(ctx context.Context, remoteMachine RemoteMachine, clu
_ = c.Close()
}()
// 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)
machineClient, err := cli.provisionRemoteMachine(ctx, remoteMachine)
if err != nil {
return fmt.Errorf(
"SSH login to remote machine %s: %w",
config.NewSSHDestination(remoteMachine.User, remoteMachine.Host, remoteMachine.Port), err,
)
}
exec := sshexec.NewRemote(sshClient)
// Install and run the Uncloud daemon and dependencies on the remote machine.
if err = provisionMachine(ctx, exec); err != nil {
return fmt.Errorf("provision machine: %w", err)
}
// Create a machine API client over the SSH connection to the remote machine.
machineClient, err := client.New(ctx, connector.NewSSHConnectorFromClient(sshClient))
if err != nil {
return fmt.Errorf("connect to remote machine: %w", err)
return err
}
defer machineClient.Close()
@@ -280,14 +221,14 @@ func (cli *CLI) AddMachine(ctx context.Context, remoteMachine RemoteMachine, clu
}
// List other machines in the cluster to include them in the join request.
listResp, err := c.ListMachines(ctx, &emptypb.Empty{})
machines, err := c.ListMachines(ctx)
if err != nil {
return fmt.Errorf("list cluster machines: %w", err)
}
otherMachines := make([]*pb.MachineInfo, 0, len(listResp.Machines)-1)
for _, m := range listResp.Machines {
if m.Id != addResp.Machine.Id {
otherMachines = append(otherMachines, m)
otherMachines := make([]*pb.MachineInfo, 0, len(machines)-1)
for _, m := range machines {
if m.Machine.Id != addResp.Machine.Id {
otherMachines = append(otherMachines, m.Machine)
}
}
@@ -317,6 +258,44 @@ func (cli *CLI) AddMachine(ctx context.Context, remoteMachine RemoteMachine, clu
return 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) {
// 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 {
return nil, fmt.Errorf(
"SSH login to remote machine %s: %w",
config.NewSSHDestination(remoteMachine.User, remoteMachine.Host, remoteMachine.Port), err,
)
}
exec := sshexec.NewRemote(sshClient)
// Install and run the Uncloud daemon and dependencies on the remote machine.
if err = provisionMachine(ctx, exec); err != nil {
return nil, fmt.Errorf("provision machine: %w", err)
}
var machineClient *client.Client
if remoteMachine.User == "root" {
// Create a machine API client over the established SSH connection to the remote machine.
machineClient, err = client.New(ctx, connector.NewSSHConnectorFromClient(sshClient))
} else {
// Since the user is not root, we need to establish a new SSH connection to make the user's addition
// to the uncloud group effective, thus allowing access to the Uncloud daemon Unix socket.
sshConfig := &connector.SSHConnectorConfig{
User: remoteMachine.User,
Host: remoteMachine.Host,
Port: remoteMachine.Port,
KeyPath: remoteMachine.KeyPath,
}
machineClient, err = client.New(ctx, connector.NewSSHConnector(sshConfig))
}
if err != nil {
return nil, fmt.Errorf("connect to remote machine: %w", err)
}
return machineClient, nil
}
func (cli *CLI) promptResetMachine() error {
var confirm bool
form := huh.NewForm(
@@ -340,42 +319,3 @@ func (cli *CLI) promptResetMachine() error {
// TODO: implement resetting the remote machine.
return fmt.Errorf("resetting the remote machine is not implemented yet")
}
func (cli *CLI) ListMachines(ctx context.Context, clusterName string) error {
c, err := cli.ConnectCluster(ctx, clusterName)
if err != nil {
return fmt.Errorf("connect to cluster: %w", err)
}
defer func() {
_ = c.Close()
}()
listResp, err := c.ListMachines(ctx, &emptypb.Empty{})
if err != nil {
return fmt.Errorf("list machines: %w", err)
}
// Print the list of machines in a table format.
tw := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', 0)
// Print header.
// TODO: print ADDRESS instead of SUBNET which is a machine IP with prefix.
if _, err = fmt.Fprintln(tw, "NAME\tSUBNET\tPUBLIC KEY\tENDPOINTS"); err != nil {
return fmt.Errorf("write header: %w", err)
}
// Print rows.
for _, m := range listResp.Machines {
subnet, _ := m.Network.Subnet.ToPrefix()
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\n", m.Name, subnet, publicKey, strings.Join(endpoints, ", "),
); err != nil {
return fmt.Errorf("write row: %w", err)
}
}
return tw.Flush()
}
+17 -3
View File
@@ -4,10 +4,15 @@ import (
"context"
"errors"
"fmt"
"github.com/docker/cli/cli/streams"
"google.golang.org/grpc"
"os"
"uncloud/internal/machine/api/pb"
"uncloud/internal/machine/docker"
)
var ErrNotFound = errors.New("not found")
// Client is a client for the machine API.
type Client struct {
connector Connector
@@ -15,8 +20,12 @@ type Client struct {
pb.MachineClient
pb.ClusterClient
*DockerClient
}
// DockerClient is a type alias for the Docker client to embed it in Client with a more specific name.
type DockerClient = docker.Client
// Connector is an interface for establishing a connection to the machine API.
type Connector interface {
Connect(ctx context.Context) (*grpc.ClientConn, error)
@@ -37,10 +46,15 @@ func New(ctx context.Context, connector Connector) (*Client, error) {
c.MachineClient = pb.NewMachineClient(c.conn)
c.ClusterClient = pb.NewClusterClient(c.conn)
c.DockerClient = docker.NewClient(c.conn)
return c, nil
}
func (c *Client) Close() error {
err := c.conn.Close()
return errors.Join(err, c.connector.Close())
func (cli *Client) Close() error {
return errors.Join(cli.conn.Close(), cli.connector.Close())
}
// progressOut returns an output stream for progress writer.
func (cli *Client) progressOut() *streams.Out {
return streams.NewOut(os.Stdout)
}
+15
View File
@@ -0,0 +1,15 @@
package client
import (
"context"
"google.golang.org/protobuf/types/known/emptypb"
"uncloud/internal/machine/api/pb"
)
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
}
+5 -5
View File
@@ -18,7 +18,7 @@ type SSHConnectorConfig struct {
Port int
KeyPath string
APISockPath string
SockPath string
}
// SSHConnector establishes a connection to the machine API through an SSH tunnel to the machine.
@@ -49,12 +49,12 @@ func (c *SSHConnector) Connect(ctx context.Context) (*grpc.ClientConn, error) {
}
}
apiSockPath := c.config.APISockPath
if apiSockPath == "" {
apiSockPath = machine.DefaultAPISockPath
sockPath := c.config.SockPath
if sockPath == "" {
sockPath = machine.DefaultUncloudSockPath
}
conn, err := grpc.NewClient(
"unix://"+apiSockPath,
"unix://"+sockPath,
grpc.WithTransportCredentials(insecure.NewCredentials()),
grpc.WithContextDialer(
func(ctx context.Context, addr string) (net.Conn, error) {
+33
View File
@@ -0,0 +1,33 @@
package connector
import (
"context"
"fmt"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"net/netip"
)
// TCPConnector establishes a connection to the machine API through a direct TCP connection to an API endpoint.
type TCPConnector struct {
apiAddr netip.AddrPort
}
func NewTCPConnector(apiAddr netip.AddrPort) *TCPConnector {
return &TCPConnector{apiAddr: apiAddr}
}
func (c *TCPConnector) Connect(_ context.Context) (*grpc.ClientConn, error) {
conn, err := grpc.NewClient(
c.apiAddr.String(),
grpc.WithTransportCredentials(insecure.NewCredentials()),
)
if err != nil {
return nil, fmt.Errorf("create machine API client: %w", err)
}
return conn, nil
}
func (c *TCPConnector) Close() error {
return nil
}
+678
View File
@@ -0,0 +1,678 @@
package client
import (
"context"
"errors"
"fmt"
"github.com/distribution/reference"
"github.com/docker/compose/v2/pkg/progress"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/network"
dockerclient "github.com/docker/docker/client"
"github.com/docker/docker/pkg/jsonmessage"
"github.com/docker/go-connections/nat"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
"slices"
"strconv"
"strings"
"sync"
"uncloud/internal/api"
"uncloud/internal/machine/api/pb"
machinedocker "uncloud/internal/machine/docker"
"uncloud/internal/secret"
)
type RunServiceResponse struct {
ID string
Name string
Containers []MachineContainerID
}
type MachineContainerID struct {
MachineID string
ContainerID string
}
func (cli *Client) RunService(ctx context.Context, spec api.ServiceSpec) (RunServiceResponse, error) {
var resp RunServiceResponse
if err := spec.Validate(); err != nil {
return resp, fmt.Errorf("invalid service spec: %w", err)
}
img, err := reference.ParseDockerRef(spec.Container.Image)
if err != nil {
return resp, fmt.Errorf("invalid image: %w", err)
}
if spec.Name == "" {
// Generate a random service name from the image if not specified.
// Get the image name without the repository and tag/digest parts.
imageName := reference.FamiliarName(img)
// Get the last part of the image name (path), e.g. "nginx" from "bitnami/nginx".
if i := strings.LastIndex(imageName, "/"); i != -1 {
imageName = imageName[i+1:]
}
// Append a random suffix to the image name to generate an optimistically unique service name.
suffix, err := secret.RandomAlphaNumeric(4)
if err != nil {
return resp, fmt.Errorf("generate random suffix: %w", err)
}
spec.Name = fmt.Sprintf("%s-%s", imageName, suffix)
} else {
// Optimistically check if a service with the specified name already exists.
_, err := cli.InspectService(ctx, spec.Name)
if err == nil {
return resp, fmt.Errorf("service with name '%s' already exists", spec.Name)
}
if !errors.Is(err, ErrNotFound) {
return resp, fmt.Errorf("inspect service: %w", err)
}
}
serviceID, err := secret.NewID()
if err != nil {
return resp, fmt.Errorf("generate service ID: %w", err)
}
err = progress.RunWithTitle(ctx, func(ctx context.Context) error {
switch spec.Mode {
case "", api.ServiceModeReplicated:
resp, err = cli.runReplicatedService(ctx, serviceID, spec)
case api.ServiceModeGlobal:
resp, err = cli.runGlobalService(ctx, serviceID, spec)
default:
return fmt.Errorf("invalid mode: %q", spec.Mode)
}
return err
}, cli.progressOut(), "Running service "+spec.Name)
return resp, err
}
func (cli *Client) runReplicatedService(ctx context.Context, id string, spec api.ServiceSpec) (RunServiceResponse, error) {
resp := RunServiceResponse{
ID: id,
Name: spec.Name,
}
// Find a machine to run a service replica on.
machines, err := cli.ListMachines(ctx)
if err != nil {
return resp, fmt.Errorf("list machines: %w", err)
}
// TODO: support selecting a particular machine by ID or name through the user options.
//var machine *pb.MachineMember
//if opts.Machine != "" {
// // Check if the machine ID or name exists if it's explicitly specified.
// for _, m := range machines {
// if m.Machine.Name == opts.Machine || m.Machine.Id == opts.Machine {
// machine = m
// break
// }
// }
// if machine == nil {
// return resp, fmt.Errorf("machine %q not found", opts.Machine)
// }
//}
m := firstAvailableMachine(machines)
if m == nil {
return resp, errors.New("no available machine to run the service")
}
runResp, err := cli.runContainer(ctx, id, spec, m.Machine)
if err != nil {
return resp, fmt.Errorf("run container: %w", err)
}
resp.Containers = append(resp.Containers, MachineContainerID{
MachineID: m.Machine.Id,
ContainerID: runResp.ID,
})
return resp, nil
}
func firstAvailableMachine(machines []*pb.MachineMember) *pb.MachineMember {
// Find the first UP machine.
for _, m := range machines {
if m.State == pb.MachineMember_UP {
return m
}
}
// There is no UP machine, try to find the first SUSPECT machine.
for _, m := range machines {
if m.State == pb.MachineMember_SUSPECT {
return m
}
}
return nil
}
func (cli *Client) runGlobalService(ctx context.Context, id string, spec api.ServiceSpec) (RunServiceResponse, error) {
resp := RunServiceResponse{
ID: id,
Name: spec.Name,
}
machines, err := cli.ListMachines(ctx)
if err != nil {
return resp, fmt.Errorf("list machines: %w", err)
}
wg := sync.WaitGroup{}
errCh := make(chan error)
mu := sync.Mutex{}
// Run a service container on each available machine.
for _, m := range machines {
if m.State != pb.MachineMember_UP && m.State != pb.MachineMember_SUSPECT {
// TODO: return failed machines in the response.
fmt.Printf("WARNING: failed to run a service container on machine '%s' which is Down.\n", m.Machine.Name)
continue
}
wg.Add(1)
go func() {
defer wg.Done()
runResp, err := cli.runContainer(ctx, id, spec, m.Machine)
if err != nil {
errCh <- fmt.Errorf("run container on machine '%s': %w", m.Machine.Name, err)
return
}
mu.Lock()
resp.Containers = append(resp.Containers, MachineContainerID{
MachineID: m.Machine.Id,
ContainerID: runResp.ID,
})
mu.Unlock()
}()
}
go func() {
wg.Wait()
close(errCh)
}()
err = nil
for e := range errCh {
err = errors.Join(err, e)
}
return resp, err
}
func (cli *Client) runContainer(
ctx context.Context, serviceID string, spec api.ServiceSpec, machine *pb.MachineInfo,
) (container.CreateResponse, error) {
var resp container.CreateResponse
// Proxy Docker gRPC requests to the selected machine.
machineIP, _ := machine.Network.ManagementIp.ToAddr()
md := metadata.Pairs("machines", machineIP.String())
ctx = metadata.NewOutgoingContext(ctx, md)
suffix, err := secret.RandomAlphaNumeric(4)
if err != nil {
return resp, fmt.Errorf("generate random suffix: %w", err)
}
containerName := fmt.Sprintf("%s-%s", spec.Name, suffix)
config := &container.Config{
Cmd: spec.Container.Command,
Image: spec.Container.Image,
Labels: map[string]string{
api.LabelServiceID: serviceID,
api.LabelServiceName: spec.Name,
api.LabelManaged: "",
},
}
if spec.Mode == api.ServiceModeGlobal {
config.Labels[api.LabelServiceMode] = api.ServiceModeGlobal
}
if len(spec.Ports) > 0 {
encodedPorts := make([]string, len(spec.Ports))
for i, p := range spec.Ports {
encodedPorts[i], err = p.String()
if err != nil {
return resp, fmt.Errorf("encode service port spec: %w", err)
}
}
config.Labels[api.LabelServicePorts] = strings.Join(encodedPorts, ",")
}
portBindings := make(nat.PortMap)
for _, p := range spec.Ports {
if p.Mode != api.PortModeHost {
continue
}
port := nat.Port(fmt.Sprintf("%d/%s", p.ContainerPort, p.Protocol))
portBindings[port] = []nat.PortBinding{
{
HostPort: strconv.Itoa(int(p.PublishedPort)),
},
}
if p.HostIP.IsValid() {
portBindings[port][0].HostIP = p.HostIP.String()
}
}
hostConfig := &container.HostConfig{
Binds: spec.Container.Volumes,
Init: spec.Container.Init,
PortBindings: portBindings,
}
netConfig := &network.NetworkingConfig{
EndpointsConfig: map[string]*network.EndpointSettings{
machinedocker.NetworkName: {},
},
}
pw := progress.ContextWriter(ctx)
eventID := fmt.Sprintf("Container %s on %s", containerName, machine.Name)
pw.Event(progress.CreatingEvent(eventID))
resp, err = cli.CreateContainer(ctx, config, hostConfig, netConfig, nil, containerName)
if err != nil {
if !dockerclient.IsErrNotFound(err) {
return resp, fmt.Errorf("create container: %w", err)
}
// Pull the missing image and create the container again.
if err = cli.pullImageWithProgress(ctx, config.Image, machine.Name, eventID); err != nil {
return resp, err
}
if resp, err = cli.CreateContainer(ctx, config, hostConfig, netConfig, nil, containerName); err != nil {
return resp, fmt.Errorf("create container: %w", err)
}
}
pw.Event(progress.CreatedEvent(eventID))
pw.Event(progress.StartingEvent(eventID))
if err = cli.StartContainer(ctx, resp.ID, container.StartOptions{}); err != nil {
return resp, fmt.Errorf("start container: %w", err)
}
pw.Event(progress.StartedEvent(eventID))
return resp, nil
}
func (cli *Client) pullImageWithProgress(ctx context.Context, image, machineName, parentEventID string) error {
pw := progress.ContextWriter(ctx)
eventID := fmt.Sprintf("Image %s on %s", image, machineName)
pw.Event(progress.Event{
ID: eventID,
ParentID: parentEventID,
Status: progress.Working,
StatusText: "Pulling",
})
pullCh, err := cli.PullImage(ctx, image)
if err != nil {
pw.Event(progress.Event{
ID: eventID,
ParentID: parentEventID,
Text: "Error",
Status: progress.Error,
StatusText: errors.Unwrap(err).Error(),
})
return fmt.Errorf("pull image: %w", err)
}
// Wait for pull to complete by reading all progress messages and converting them to events.
for msg := range pullCh {
if msg.Err != nil {
err = msg.Err
} else {
if msg.Message.Error != nil {
err = errors.New(msg.Message.Error.Message)
}
}
if err != nil {
pw.Event(progress.Event{
ID: eventID,
ParentID: parentEventID,
Text: "Error",
Status: progress.Error,
StatusText: errors.Unwrap(err).Error(),
})
return fmt.Errorf("pull image: %w", err)
}
// TODO: add like in compose: --quiet-pull Pull without printing progress information
e := toPullProgressEvent(msg.Message)
if e != nil {
e.ID = fmt.Sprintf("%s on %s", e.ID, machineName)
e.ParentID = eventID
// Grand children events are not printed by the tty progress writer but they are still required
// to calculate the progress line of their parent.
pw.Event(*e)
}
}
pw.Event(progress.Event{
ID: eventID,
ParentID: parentEventID,
Status: progress.Done,
StatusText: "Pulled",
})
return nil
}
// toPullProgressEvent converts a JSON progress message from the Docker API to a progress event.
// It's based on toPullProgressEvent from Docker Compose.
func toPullProgressEvent(jm jsonmessage.JSONMessage) *progress.Event {
if jm.ID == "" || jm.Progress == nil {
return nil
}
var (
total int64
percent int
current int64
)
text := jm.Progress.String()
stat := progress.Working
switch jm.Status {
case "Preparing", "Waiting", "Pulling fs layer":
percent = 0
case "Downloading", "Extracting", "Verifying Checksum":
current = jm.Progress.Current
total = jm.Progress.Total
if jm.Progress.Total > 0 {
percent = int(jm.Progress.Current * 100 / jm.Progress.Total)
}
case "Download complete", "Already exists", "Pull complete":
stat = progress.Done
percent = 100
}
if strings.Contains(jm.Status, "Image is up to date") ||
strings.Contains(jm.Status, "Downloaded newer image") {
stat = progress.Done
percent = 100
}
return &progress.Event{
ID: jm.ID,
Current: current,
Total: total,
Percent: percent,
Text: jm.Status,
Status: stat,
StatusText: text,
}
}
// InspectService returns detailed information about a service and its containers.
// The id parameter can be either a service ID or name.
func (cli *Client) InspectService(ctx context.Context, id string) (api.Service, error) {
var svc api.Service
machines, err := cli.ListMachines(ctx)
if err != nil {
return svc, fmt.Errorf("list machines: %w", err)
}
// Broadcast the container list request to all available machines.
machineIDByManagementIP := make(map[string]string)
md := metadata.New(nil)
for _, m := range machines {
if m.State == pb.MachineMember_UP || m.State == pb.MachineMember_SUSPECT {
machineIP, _ := m.Machine.Network.ManagementIp.ToAddr()
md.Append("machines", machineIP.String())
machineIDByManagementIP[machineIP.String()] = m.Machine.Id
}
// TODO: warning about machines that are DOWN.
}
listCtx := metadata.NewOutgoingContext(ctx, md)
// List only uncloud-managed containers that belong to some service.
opts := container.ListOptions{
All: true,
Filters: filters.NewArgs(
filters.Arg("label", api.LabelServiceID),
filters.Arg("label", api.LabelManaged),
),
}
machineContainers, err := cli.ListContainers(listCtx, opts)
if err != nil {
return svc, fmt.Errorf("list containers: %w", err)
}
// Collect all containers on all machines that belong to the specified service.
foundByID := false
var containers []api.MachineContainer
for _, mc := range machineContainers {
// Metadata can be nil if the request was broadcasted to only one machine.
if mc.Metadata == nil && len(machineContainers) > 1 {
return svc, errors.New("something went wrong with gRPC proxy: metadata is missing for a machine response")
}
if mc.Metadata != nil && mc.Metadata.Error != "" {
// TODO: return failed machines in the response.
fmt.Printf("WARNING: failed to list containers on machine '%s': %s\n",
mc.Metadata.Machine, mc.Metadata.Error)
continue
}
machineID := ""
if mc.Metadata == nil {
// ListContainers was proxied to only one machine.
for _, v := range machineIDByManagementIP {
machineID = v
break
}
} else {
var ok bool
machineID, ok = machineIDByManagementIP[mc.Metadata.Machine]
if !ok {
return svc, fmt.Errorf("machine name not found for management IP: %s", mc.Metadata.Machine)
}
}
for _, c := range mc.Containers {
ctr := api.Container{Container: c}
if ctr.ServiceID() == id || ctr.ServiceName() == id {
containers = append(containers, api.MachineContainer{
MachineID: machineID,
Container: ctr,
})
if ctr.ServiceID() == id {
foundByID = true
}
}
}
}
if len(containers) == 0 {
return svc, ErrNotFound
}
// Containers from different services may share the same service name (distributed and eventually consistent store
// may not prevent this), or a service name might match another service's ID. In these cases, matching by ID takes
// priority over matching by name.
if foundByID {
containers = slices.DeleteFunc(containers, func(mc api.MachineContainer) bool {
return mc.Container.ServiceID() != id
})
} else {
// Matched only by name but there could be multiple services with the same name.
serviceID := containers[0].Container.ServiceID()
for _, mc := range containers[1:] {
if mc.Container.ServiceID() != serviceID {
return svc, fmt.Errorf("multiple services found with name: %s", id)
}
}
}
svc = api.Service{
ID: containers[0].Container.ServiceID(),
Name: containers[0].Container.ServiceName(),
Mode: containers[0].Container.ServiceMode(),
Containers: containers,
}
if svc.Mode == "" {
svc.Mode = api.ServiceModeReplicated
}
return svc, nil
}
// InspectServiceFromStore returns detailed information about a service and its containers from the distributed store.
// Due to eventual consistency of the store, the returned information may not reflect the most recent changes.
// The id parameter can be either a service ID or name.
func (cli *Client) InspectServiceFromStore(ctx context.Context, id string) (api.Service, error) {
var svc api.Service
resp, err := cli.MachineClient.InspectService(ctx, &pb.InspectServiceRequest{Id: id})
if err != nil {
if s, ok := status.FromError(err); ok {
if s.Code() == codes.NotFound {
return svc, ErrNotFound
}
}
return svc, err
}
svc, err = api.ServiceFromProto(resp.Service)
if err != nil {
return svc, fmt.Errorf("from proto: %w", err)
}
return svc, nil
}
// RemoveService removes all containers on all machines that belong to the specified service.
// The id parameter can be either a service ID or name.
func (cli *Client) RemoveService(ctx context.Context, id string) error {
svc, err := cli.InspectService(ctx, id)
if err != nil {
return err
}
machines, err := cli.ListMachines(ctx)
if err != nil {
return fmt.Errorf("list machines: %w", err)
}
machineManagementIPByID := make(map[string]string)
for _, m := range machines {
machineIP, _ := m.Machine.Network.ManagementIp.ToAddr()
machineManagementIPByID[m.Machine.Id] = machineIP.String()
}
wg := sync.WaitGroup{}
errCh := make(chan error)
// Remove all containers on all machines that belong to the service.
for _, mc := range svc.Containers {
wg.Add(1)
go func() {
defer wg.Done()
machineIP, ok := machineManagementIPByID[mc.MachineID]
if !ok {
errCh <- fmt.Errorf("machine not found by ID: %s", mc.MachineID)
return
}
removeCtx := metadata.NewOutgoingContext(ctx, metadata.Pairs("machines", machineIP))
// TODO: gracefully stop the container before removing it without force.
err := cli.RemoveContainer(removeCtx, mc.Container.ID, container.RemoveOptions{Force: true})
if err != nil {
if !dockerclient.IsErrNotFound(err) {
errCh <- fmt.Errorf("remove container '%s': %w", mc.Container.ID, err)
}
}
}()
}
go func() {
wg.Wait()
close(errCh)
}()
err = nil
for e := range errCh {
err = errors.Join(err, e)
}
return err
}
// ListServices returns a list of all services and their containers.
func (cli *Client) ListServices(ctx context.Context) ([]api.Service, error) {
machines, err := cli.ListMachines(ctx)
if err != nil {
return nil, fmt.Errorf("list machines: %w", err)
}
// Broadcast the container list request to all available machines.
md := metadata.New(nil)
for _, m := range machines {
if m.State == pb.MachineMember_UP || m.State == pb.MachineMember_SUSPECT {
machineIP, _ := m.Machine.Network.ManagementIp.ToAddr()
md.Append("machines", machineIP.String())
}
// TODO: warning about machines that are DOWN.
}
listCtx := metadata.NewOutgoingContext(ctx, md)
// List only uncloud-managed containers that belong to some service.
opts := container.ListOptions{
All: true,
Filters: filters.NewArgs(
filters.Arg("label", api.LabelServiceID),
filters.Arg("label", api.LabelManaged),
),
}
machineContainers, err := cli.ListContainers(listCtx, opts)
if err != nil {
return nil, fmt.Errorf("list containers: %w", err)
}
// TODO: optimise by extracting services from the list of all containers instead of inspecting each service.
// Most of the code can be reused in both InspectService and ListServices.
servicesByID := make(map[string]api.Service)
for _, mc := range machineContainers {
if mc.Metadata != nil && mc.Metadata.Error != "" {
// TODO: return failed machines in the response.
fmt.Printf("WARNING: failed to list containers on machine '%s': %s\n",
mc.Metadata.Machine, mc.Metadata.Error)
continue
}
for _, c := range mc.Containers {
ctr := api.Container{Container: c}
if _, ok := servicesByID[ctr.ServiceID()]; ok {
continue
}
svc, err := cli.InspectService(ctx, ctr.ServiceID())
if err != nil {
if errors.Is(err, ErrNotFound) {
continue
}
return nil, fmt.Errorf("inspect service: %w", err)
}
servicesByID[ctr.ServiceID()] = svc
}
}
services := make([]api.Service, 0, len(servicesByID))
for _, svc := range servicesByID {
services = append(services, svc)
}
return services, nil
}
-4
View File
@@ -1,10 +1,6 @@
package config
import "uncloud/internal/secret"
type Cluster struct {
Name string `toml:"-"`
Connections []MachineConnection `toml:"connections"`
// UserPrivateKey is the user's WireGuard private key used to connect to cluster machines.
UserPrivateKey secret.Secret `toml:"user_private_key"`
}
+2
View File
@@ -2,6 +2,7 @@ package config
import (
"net"
"net/netip"
"strconv"
"strings"
"uncloud/internal/secret"
@@ -14,6 +15,7 @@ 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"`
}
+258
View File
@@ -0,0 +1,258 @@
package corrosion
import (
"encoding/binary"
"encoding/json"
"errors"
"fmt"
"io"
"net"
"net/netip"
"time"
)
// AdminClient is a client for the Corrosion admin API.
type AdminClient struct {
sockPath string
}
func NewAdminClient(sockPath string) (*AdminClient, error) {
return &AdminClient{sockPath: sockPath}, nil
}
type Response struct {
JSON map[string]any
// Err is set if the response is an error or if an error occurred while processing the response.
Err error
}
// SendCommand sends a command to the Corrosion admin API and returns a channel that will receive responses.
// The channel will be closed after sending the last or error response. The caller must read from the channel until
// it is closed.
func (c *AdminClient) SendCommand(cmd []byte) (<-chan Response, error) {
conn, err := net.Dial("unix", c.sockPath)
if err != nil {
return nil, fmt.Errorf("connect to admin socket: %w", err)
}
if _, err = conn.Write(encodeFrame(cmd)); err != nil {
conn.Close()
return nil, fmt.Errorf("send command: %w", err)
}
ch := make(chan Response)
go func() {
defer close(ch)
defer conn.Close()
for {
r := Response{}
data, err := readFrame(conn)
if err != nil {
r.Err = err
ch <- r
return
}
var decoded any
if err = json.Unmarshal(data, &decoded); err != nil {
r.Err = fmt.Errorf("unmarshal response: %w", err)
ch <- r
return
}
switch v := decoded.(type) {
case string:
if v == "Success" {
return
}
// Ignore other strings.
case map[string]any:
if errData, ok := v["Error"].(map[string]any); ok {
if errMsg, ok := errData["msg"].(string); ok {
r.Err = errors.New(errMsg)
} else {
r.Err = fmt.Errorf("invalid error response: %v", errData)
}
ch <- r
return
} else if jsonData, ok := v["Json"].(map[string]any); ok {
r.JSON = jsonData
ch <- r
}
// Ignore other maps.
default:
// Ignore other types.
}
}
}()
return ch, nil
}
// encodeFrame encodes a length_delimited Tokio frame by prefacing frame data with a frame head that specifies
// the length of the frame.
func encodeFrame(data []byte) []byte {
encoded := make([]byte, 4+len(data))
binary.BigEndian.PutUint32(encoded, uint32(len(data)))
copy(encoded[4:], data)
return encoded
}
// readFrame reads a length_delimited Tokio frame from the connection by extracting the frame data that follows
// the frame head.
func readFrame(conn net.Conn) ([]byte, error) {
// Read the frame head (4 bytes).
head := make([]byte, 4)
if _, err := io.ReadFull(conn, head); err != nil {
return nil, fmt.Errorf("read frame head: %w", err)
}
// Read the frame data (length specified in the frame head).
length := binary.BigEndian.Uint32(head)
data := make([]byte, length)
if _, err := io.ReadFull(conn, data); err != nil {
return nil, fmt.Errorf("read frame data: %w", err)
}
return data, nil
}
type ClusterMembershipState struct {
ID string
Addr netip.AddrPort
State string
Timestamp time.Time
}
var (
// MembershipStateAlive indicates that the member is active.
MembershipStateAlive string = "Alive"
// MembershipStateSuspect indicates that the member is active, but at least one cluster member suspects its down.
// For all purposes, a Suspect member is treated as if it were Alive until either it refutes the suspicion
// (becoming Alive) or fails to do so (being declared Down).
MembershipStateSuspect string = "Suspect"
// MembershipStateDown indicates that the member is confirmed Down. A member that reaches this state can't join
// the cluster with the same identity until the cluster forgets this knowledge.
MembershipStateDown string = "Down"
)
func parseClusterMembershipState(json map[string]any) (ClusterMembershipState, error) {
// Example JSON:
// {
// "id": {
// "addr": "[fdcc:1d51:6bae:6bb2:53c0:8796:1be0:b783]:51001",
// "cluster_id": 0,
// "id": "10d69d6f-6578-4dcf-a285-e860e40c5f06",
// "ts": 7435936225798880256
// },
// "incarnation": 0,
// "state": "Down"
// }
var state ClusterMembershipState
var err error
idObj, ok := json["id"].(map[string]any)
if !ok {
return state, fmt.Errorf("missing or invalid 'id' field")
}
// ID
if id, ok := idObj["id"].(string); ok {
state.ID = id
} else {
return state, fmt.Errorf("missing or invalid 'id' field")
}
// Addr
if addr, ok := idObj["addr"].(string); ok {
state.Addr, err = netip.ParseAddrPort(addr)
if err != nil {
return state, fmt.Errorf("parse 'addr' field: %w", err)
}
} else {
return state, fmt.Errorf("missing or invalid 'addr' field")
}
// State
if stateStr, ok := json["state"].(string); ok {
switch stateStr {
case MembershipStateAlive, MembershipStateSuspect, MembershipStateDown:
state.State = stateStr
default:
return state, fmt.Errorf("invalid 'state' field: %s", stateStr)
}
} else {
return state, fmt.Errorf("missing or invalid 'state' field")
}
// Timestamp
if ts, ok := idObj["ts"].(float64); ok {
state.Timestamp = ntp64ToTime(uint64(ts))
} else {
return state, fmt.Errorf("missing or invalid 'ts' field")
}
return state, nil
}
// nt64ToTime converts a 64-bit NTP timestamp relative to the Unix epoch (1st Jan 1970) to time.Time.
// See for more details: https://datatracker.ietf.org/doc/html/rfc5905#section-6
func ntp64ToTime(ntp uint64) time.Time {
// The NTP timestamp returned from Corrosion is relative to the Unix epoch (1st Jan 1970)
// so no need to subtract the 70 years offset.
secs := uint32(ntp >> 32)
frac := uint32(ntp)
// Convert the fraction to nanoseconds: frac * 1e9 / 2^32
nsecs := (uint64(frac) * 1000_000_000) >> 32
return time.Unix(int64(secs), int64(nsecs))
}
// ClusterMembershipStates returns the current membership SWIM states of all cluster members.
// If latest is true, only the latest state of each member is returned.
func (c *AdminClient) ClusterMembershipStates(latest bool) ([]ClusterMembershipState, error) {
respCh, err := c.SendCommand([]byte("{\"Cluster\":\"MembershipStates\"}"))
if err != nil {
return nil, err
}
var (
states []ClusterMembershipState
latestStates map[string]ClusterMembershipState
parseErr error
)
if latest {
latestStates = make(map[string]ClusterMembershipState)
}
for r := range respCh {
if r.Err != nil {
// It's safe to return here because the channel is closed after the first error response.
return nil, r.Err
}
s, err := parseClusterMembershipState(r.JSON)
if err != nil {
// Do not return early to drain the channel.
parseErr = errors.Join(parseErr, err)
} else {
if latest {
if existing, ok := latestStates[s.ID]; !ok || existing.Timestamp.Before(s.Timestamp) {
latestStates[s.ID] = s
}
} else {
states = append(states, s)
}
}
}
if latest {
states = make([]ClusterMembershipState, 0, len(latestStates))
for _, s := range latestStates {
states = append(states, s)
}
}
return states, parseErr
}
+233
View File
@@ -0,0 +1,233 @@
package corrosion
import (
"context"
"crypto/tls"
"errors"
"fmt"
"github.com/cenkalti/backoff/v4"
"golang.org/x/net/http2"
"log/slog"
"net"
"net/http"
"net/netip"
"net/url"
"time"
)
const (
// http2ConnectTimeout is the maximum amount of time an HTTP2 client will wait for a connection to be established.
http2ConnectTimeout = 3 * time.Second
// http2MaxRetryTime is the maximum amount of time an HTTP2 client will retry a request.
http2MaxRetryTime = 10 * time.Second
// resubscribeMaxRetryTime is the maximum amount of time an API client will retry resubscribing to a query after
// an error occurs.
resubscribeMaxRetryTime = 60 * time.Second
)
// APIClient is a client for the Corrosion API.
type APIClient struct {
baseURL *url.URL
client *http.Client
newResubBackoff func() backoff.BackOff
}
// NewAPIClient creates a new Corrosion API client. The client retries on network errors using an exponential backoff
// policy with a maximum interval of 1 second and a maximum elapsed time of 10 seconds.
// It automatically resubscribes to active subscriptions if an error occurs using an exponential backoff policy with a
// maximum interval of 1 second and a maximum elapsed time of 60 seconds.
// Use the WithHTTP2Client option to provide a custom HTTP client and the WithResubscribeBackoff option to change the
// backoff policy for resubscribing to a query.
func NewAPIClient(addr netip.AddrPort, opts ...APIClientOption) (*APIClient, error) {
baseURL, err := url.Parse(fmt.Sprintf("http://%s", addr))
if err != nil {
return nil, fmt.Errorf("invalid URL: %w", err)
}
c := &APIClient{
baseURL: baseURL,
client: &http.Client{
Transport: &RetryRoundTripper{
Base: &http2.Transport{
AllowHTTP: true,
DialTLSContext: func(ctx context.Context, network, addr string, _ *tls.Config) (net.Conn, error) {
dialer := &net.Dialer{
Timeout: http2ConnectTimeout,
}
return dialer.DialContext(ctx, network, addr)
},
},
NewBackoff: func() backoff.BackOff {
return backoff.NewExponentialBackOff(
backoff.WithInitialInterval(100*time.Millisecond),
backoff.WithMaxInterval(1*time.Second),
backoff.WithMaxElapsedTime(http2MaxRetryTime),
)
},
},
},
newResubBackoff: func() backoff.BackOff {
return backoff.NewExponentialBackOff(
backoff.WithInitialInterval(100*time.Millisecond),
backoff.WithMaxInterval(1*time.Second),
backoff.WithMaxElapsedTime(resubscribeMaxRetryTime),
)
},
}
for _, opt := range opts {
opt(c)
}
return c, nil
}
type APIClientOption func(*APIClient)
func WithHTTP2Client(client *http.Client) APIClientOption {
return func(c *APIClient) {
c.client = client
}
}
// WithResubscribeBackoff sets the backoff policy for resubscribing to a query if an error occurs.
// Pass nil to disable resubscribing.
func WithResubscribeBackoff(newBackoff func() backoff.BackOff) APIClientOption {
return func(c *APIClient) {
c.newResubBackoff = newBackoff
}
}
type RetryRoundTripper struct {
Base http.RoundTripper
// NewBackoff creates a new backoff policy for each request.
NewBackoff func() backoff.BackOff
}
func (rt *RetryRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
roundTrip := func() (*http.Response, error) {
resp, err := rt.Base.RoundTrip(req)
if err != nil {
var opErr *net.OpError
if errors.As(err, &opErr) {
// Not certain, but I expect operational errors should generally be retryable.
slog.Debug("Retrying corrosion API request due to network error.", "error", err)
return nil, err
}
// Don't retry on other errors.
return nil, backoff.Permanent(err)
}
// Success, don't retry.
return resp, err
}
boff := backoff.WithContext(rt.NewBackoff(), req.Context())
return backoff.RetryWithData(roundTrip, boff)
}
type RetrySubscription struct {
ctx context.Context
cancel context.CancelFunc
client *APIClient
sub *Subscription
changes chan *ChangeEvent
lastChangeID uint64
err error
backoff backoff.BackOff
}
func NewRetrySubscription(ctx context.Context, client *APIClient, sub *Subscription, boff backoff.BackOff) *RetrySubscription {
ctx, cancel := context.WithCancel(ctx)
if boff == nil {
boff = backoff.NewExponentialBackOff(
backoff.WithInitialInterval(100*time.Millisecond),
backoff.WithMaxInterval(1*time.Second),
backoff.WithMaxElapsedTime(60*time.Second),
)
}
return &RetrySubscription{
ctx: ctx,
cancel: cancel,
client: client,
sub: sub,
backoff: backoff.WithContext(boff, ctx),
}
}
func (rs *RetrySubscription) ID() string {
return rs.sub.ID()
}
func (rs *RetrySubscription) Changes() (<-chan *ChangeEvent, error) {
if rs.changes != nil {
return rs.changes, nil
}
// Ensure the rows are consumed if they have been requested.
changes, err := rs.sub.Changes()
if err != nil {
return nil, err
}
rs.lastChangeID = rs.sub.lastChangeID
go func() {
defer close(rs.changes)
var change *ChangeEvent
for {
select {
case change = <-changes:
case <-rs.ctx.Done():
return
}
if change != nil {
select {
case rs.changes <- change:
case <-rs.ctx.Done():
return
}
rs.lastChangeID = change.ChangeID
continue
}
// The underlying subscription has been closed due to an error or context cancellation.
// Return if the context is done or try to resubscribe otherwise.
if rs.ctx.Err() != nil {
return
}
if err = rs.resubscribe(); err != nil {
// resubscribe returns a permanent error after unsuccessful retries.
rs.err = fmt.Errorf("resubscribe to query: %w", err)
return
}
changes, err = rs.sub.Changes()
if err != nil {
// Unexpected error but report it anyway.
rs.err = err
return
}
}
}()
return rs.changes, nil
}
func (rs *RetrySubscription) resubscribe() error {
return backoff.Retry(func() error {
slog.Debug("Resubscribing to Corrosion query.", "id", rs.ID(), "from_change", rs.lastChangeID)
sub, err := rs.client.ResubscribeContext(rs.ctx, rs.ID(), rs.lastChangeID)
if err != nil {
return err
}
rs.sub = sub
return nil
}, rs.backoff)
}
func (rs *RetrySubscription) Err() error {
return rs.err
}
func (rs *RetrySubscription) Close() {
rs.cancel()
}
+327
View File
@@ -0,0 +1,327 @@
package corrosion
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
)
type Statement struct {
Query string `json:"query"`
Params []any `json:"params"`
}
type ExecResponse struct {
Results []ExecResult `json:"results"`
Time float64 `json:"time"`
Version *uint `json:"version"`
}
type ExecResult struct {
RowsAffected uint `json:"rows_affected"`
Time float64 `json:"time"`
Error *string `json:"error"`
}
// ExecContext writes changes to the Corrosion database for propagation through the cluster. The args are for any
// placeholder parameters in the query. Corrosion does not sync schema changes made using this method. Use Corrosion's
// schema_files to create and update the cluster's database schema.
func (c *APIClient) ExecContext(ctx context.Context, query string, args ...any) (*ExecResult, error) {
statements := []Statement{
{
Query: query,
Params: args,
},
}
resp, err := c.ExecMultiContext(ctx, statements...)
if err != nil {
return nil, err
}
if len(resp.Results) == 0 {
return nil, fmt.Errorf("no results: %+v", resp)
}
return &resp.Results[0], nil
}
// ExecMultiContext writes changes to the Corrosion database for propagation through the cluster.
// Unlike ExecContext, this method allows multiple statements to be executed in a single transaction.
func (c *APIClient) ExecMultiContext(ctx context.Context, statements ...Statement) (*ExecResponse, error) {
body, err := json.Marshal(statements)
if err != nil {
return nil, fmt.Errorf("marshal queries: %w", err)
}
transactionsURL := c.baseURL.JoinPath("/v1/transactions").String()
req, err := http.NewRequestWithContext(ctx, "POST", transactionsURL, bytes.NewReader(body))
if err != nil {
return nil, fmt.Errorf("create request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
resp, err := c.client.Do(req)
if err != nil {
return nil, fmt.Errorf("send request: %w", err)
}
defer resp.Body.Close()
var execResp ExecResponse
if resp.StatusCode == http.StatusOK {
if err = json.NewDecoder(resp.Body).Decode(&execResp); err != nil {
return nil, fmt.Errorf("decode response: %w", err)
}
// The response may still contain DB errors even if the status code is OK. Return them along with the response.
var errs []error
for _, result := range execResp.Results {
if result.Error != nil {
errs = append(errs, errors.New(*result.Error))
}
}
return &execResp, errors.Join(errs...)
} else if resp.StatusCode == http.StatusInternalServerError {
// If the response is an Internal Server Error, the response body may contain the error encoded as ExecResponse.
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("read response body: %w", err)
}
if err = json.Unmarshal(respBody, &execResp); err != nil {
return nil, fmt.Errorf("internal server error: %s", respBody)
}
if len(execResp.Results) > 0 && execResp.Results[0].Error != nil {
return nil, errors.New(*execResp.Results[0].Error)
}
return nil, fmt.Errorf("internal server error: %s", respBody)
}
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("read response body: %w", err)
}
return nil, fmt.Errorf("unexpected status code %d: %s", resp.StatusCode, respBody)
}
type QueryEvent struct {
Columns []string `json:"columns"`
Row *RowEvent `json:"row"`
EOQ *EndOfQuery `json:"eoq"`
Change *ChangeEvent `json:"change"`
// Error is a server-side error that occurred during query execution. It's considered fatal for the client
// as it cannot be recovered from server-side.
Error *string `json:"error"`
}
type EndOfQuery struct {
Time float64 `json:"time"`
ChangeID *uint64 `json:"change_id"`
}
type RowEvent struct {
RowID uint64
Values []json.RawMessage
}
func (re *RowEvent) UnmarshalJSON(data []byte) error {
var raw []json.RawMessage
if err := json.Unmarshal(data, &raw); err != nil {
return fmt.Errorf("invalid row event: %w", err)
}
if len(raw) != 2 {
return fmt.Errorf("invalid row event: expected an array of 2 elements")
}
if err := json.Unmarshal(raw[0], &re.RowID); err != nil {
return fmt.Errorf("invalid row event: %w", err)
}
if err := json.Unmarshal(raw[1], &re.Values); err != nil {
return fmt.Errorf("invalid row event: %w", err)
}
return nil
}
func (re *RowEvent) MarshalJSON() ([]byte, error) {
return json.Marshal([]any{re.RowID, re.Values})
}
// QueryContext executes a query that returns rows, typically a SELECT.
// The args are for any placeholder parameters in the query.
func (c *APIClient) QueryContext(ctx context.Context, query string, args ...any) (*Rows, error) {
statement := Statement{
Query: query,
Params: args,
}
body, err := json.Marshal(statement)
if err != nil {
return nil, fmt.Errorf("marshal query: %w", err)
}
queriesURL := c.baseURL.JoinPath("/v1/queries").String()
req, err := http.NewRequestWithContext(ctx, "POST", queriesURL, bytes.NewReader(body))
if err != nil {
return nil, fmt.Errorf("create request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
resp, err := c.client.Do(req)
if err != nil {
return nil, fmt.Errorf("send request: %w", err)
}
if resp.StatusCode != http.StatusOK {
respBody, err := io.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
return nil, fmt.Errorf("read response body: %w", err)
}
return nil, fmt.Errorf("unexpected status code %d: %s", resp.StatusCode, respBody)
}
rows, err := newRows(ctx, resp.Body, true)
if err != nil {
resp.Body.Close()
return nil, fmt.Errorf("parse query response: %w", err)
}
return rows, nil
}
// Rows is the result of a query. Its cursor starts before the first row of the result set.
// Use [Rows.Next] to advance from row to row.
type Rows struct {
ctx context.Context
body io.ReadCloser
decoder *json.Decoder
eoq *EndOfQuery
closeOnEOQ bool
columns []string
row RowEvent
err error
}
func newRows(ctx context.Context, body io.ReadCloser, closeOnEOQ bool) (*Rows, error) {
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
}
decoder := json.NewDecoder(body)
var e QueryEvent
if err := decoder.Decode(&e); err != nil {
return nil, fmt.Errorf("decode query event: %w", err)
}
if e.Columns == nil {
return nil, fmt.Errorf("expected columns event, got: %+v", e)
}
return &Rows{
ctx: ctx,
body: body,
decoder: decoder,
closeOnEOQ: closeOnEOQ,
columns: e.Columns,
}, nil
}
// Columns returns the column names.
func (rs *Rows) Columns() []string {
return rs.columns
}
// Next prepares the next result row for reading with the [Rows.Scan] method. It returns true on success, or false
// if there is no next result row or an error happened while preparing it. [Rows.Err] should be consulted to distinguish
// between the two cases.
//
// Every call to [Rows.Scan], even the first one, must be preceded by a call to [Rows.Next].
func (rs *Rows) Next() bool {
select {
case <-rs.ctx.Done():
rs.err = rs.ctx.Err()
_ = rs.Close()
return false
default:
}
var e QueryEvent
if err := rs.decoder.Decode(&e); err != nil {
rs.err = fmt.Errorf("decode query event: %w", err)
_ = rs.Close()
return false
}
// Server-side query error.
if e.Error != nil {
rs.err = fmt.Errorf("query error: %s", *e.Error)
_ = rs.Close()
return false
}
if e.Row != nil {
if len(e.Row.Values) != len(rs.columns) {
rs.err = fmt.Errorf("expected %d column values, got %d", len(rs.columns), len(e.Row.Values))
_ = rs.Close()
return false
}
rs.row = *e.Row
return true
}
if e.EOQ != nil {
rs.eoq = e.EOQ
// Rows could be used as part of a subscription, so don't close the body if so.
if rs.closeOnEOQ {
_ = rs.Close()
}
return false
}
rs.err = fmt.Errorf("expected row or eof event, got: %+v", e)
_ = rs.Close()
return false
}
// Err returns the error, if any, that was encountered during iteration.
// Err may be called after an explicit or implicit [Rows.Close].
func (rs *Rows) Err() error {
return rs.err
}
// Scan copies the columns in the current row into the values pointed at by dest.
// The number of values in dest must be the same as the number of columns in [Rows].
// Scan converts JSON-encoded column values to the provided Go types using [json.Unmarshal].
func (rs *Rows) Scan(dest ...any) error {
if rs.err != nil {
return rs.err
}
if len(dest) != len(rs.columns) {
return fmt.Errorf("expected %d values, got %d", len(rs.columns), len(dest))
}
for i, v := range rs.row.Values {
if err := json.Unmarshal(v, dest[i]); err != nil {
return fmt.Errorf("unmarshal column value #%d: %w", i, err)
}
}
return nil
}
// Time returns the time taken to execute the query in seconds. It's only available after all rows have been consumed.
// It doesn't include the time to send the query, receive the response, or iterate over the rows.
func (rs *Rows) Time() (float64, error) {
if rs.eoq == nil {
if rs.Err() != nil {
return 0, fmt.Errorf("time is not available: %w", rs.Err())
}
return 0, errors.New("time is not available until all rows are consumed")
}
return rs.eoq.Time, nil
}
// Close closes the [Rows], preventing further enumeration. If [Rows.Next] is called and returns false,
// the [Rows] are closed automatically and it will suffice to check the result of [Rows.Err].
// Close is idempotent and does not affect the result of [Rows.Err].
func (rs *Rows) Close() error {
return rs.body.Close()
}
+323
View File
@@ -0,0 +1,323 @@
package corrosion
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"github.com/cenkalti/backoff/v4"
"io"
"log/slog"
"net/http"
"strconv"
)
type ChangeType string
var (
ChangeTypeInsert ChangeType = "insert"
ChangeTypeUpdate ChangeType = "update"
ChangeTypeDelete ChangeType = "delete"
)
type ChangeEvent struct {
Type ChangeType
RowID uint64
Values []json.RawMessage
ChangeID uint64
}
func (ce *ChangeEvent) UnmarshalJSON(data []byte) error {
var raw []json.RawMessage
if err := json.Unmarshal(data, &raw); err != nil {
return fmt.Errorf("invalid change event: %w", err)
}
if len(raw) != 4 {
return fmt.Errorf("invalid change event: expected an array of 4 elements")
}
if err := json.Unmarshal(raw[0], &ce.Type); err != nil {
return fmt.Errorf("invalid change event type: %w", err)
}
if err := json.Unmarshal(raw[1], &ce.RowID); err != nil {
return fmt.Errorf("invalid change event row ID: %w", err)
}
if err := json.Unmarshal(raw[2], &ce.Values); err != nil {
return fmt.Errorf("invalid change event values: %w", err)
}
if err := json.Unmarshal(raw[3], &ce.ChangeID); err != nil {
return fmt.Errorf("invalid change event change ID: %w", err)
}
return nil
}
func (ce *ChangeEvent) MarshalJSON() ([]byte, error) {
return json.Marshal([]any{ce.Type, ce.RowID, ce.Values, ce.ChangeID})
}
// Scan copies the column values in the change event into the values pointed at by dest.
// The number of values in dest must be the same as the number of columns in the change.
// Scan converts JSON-encoded column values to the provided Go types using [json.Unmarshal].
func (ce *ChangeEvent) Scan(dest ...any) error {
if len(dest) != len(ce.Values) {
return fmt.Errorf("expected %d values, got %d", len(ce.Values), len(dest))
}
for i, v := range ce.Values {
if err := json.Unmarshal(v, dest[i]); err != nil {
return fmt.Errorf("unmarshal column value #%d: %w", i, err)
}
}
return nil
}
// Subscription receives updates from the Corrosion database for a desired SQL query.
type Subscription struct {
ctx context.Context
cancel context.CancelFunc
id string
rows *Rows
body io.ReadCloser
decoder *json.Decoder
resubscribe func(ctx context.Context, fromChange uint64) (*Subscription, error)
changes chan *ChangeEvent
lastChangeID uint64
err error
}
func newSubscription(
ctx context.Context,
id string,
rows *Rows,
body io.ReadCloser,
decoder *json.Decoder,
resubscribe func(ctx context.Context, fromChange uint64) (*Subscription, error),
) *Subscription {
ctx, cancel := context.WithCancel(ctx)
if decoder == nil {
decoder = json.NewDecoder(body)
}
return &Subscription{
ctx: ctx,
cancel: cancel,
id: id,
rows: rows,
body: body,
decoder: decoder,
resubscribe: resubscribe,
}
}
// ID returns the subscription ID.
func (s *Subscription) ID() string {
return s.id
}
// Rows returns the rows of the query or nil if skipRows was true when creating the subscription or if the subscription
// was created with [APIClient.ResubscribeContext].
func (s *Subscription) Rows() *Rows {
return s.rows
}
// Changes returns a channel that receives change events for the query. Changes are not available until all rows
// are consumed. The channel is closed when the context is done, or an error occurs while reading the changes,
// or when the subscription is closed explicitly. If it's closed due to an error, [Subscription.Err] will return
// the error.
func (s *Subscription) Changes() (<-chan *ChangeEvent, error) {
if s.changes != nil {
return s.changes, nil
}
if s.rows != nil {
if s.rows.eoq == nil {
return nil, errors.New("changes are not available until all rows are consumed")
}
s.lastChangeID = *s.rows.eoq.ChangeID
}
s.changes = make(chan *ChangeEvent)
go func() {
// Close the body when the context is done to unblock the decoder in the following goroutine.
<-s.ctx.Done()
s.body.Close()
}()
go s.handleChangeEvents()
return s.changes, nil
}
func (s *Subscription) handleChangeEvents() {
defer s.cancel()
defer close(s.changes)
for {
select {
case <-s.ctx.Done():
return
default:
}
var e QueryEvent
var err error
if err = s.decoder.Decode(&e); err != nil {
// Do not report an error that occurred due to context cancellation, just return.
if s.ctx.Err() != nil {
return
}
err = fmt.Errorf("decode query event: %w", err)
} else if e.Error != nil {
err = fmt.Errorf("query error: %s", *e.Error)
} else if e.Change == nil {
err = fmt.Errorf("expected change event, got: %+v", e)
} else if s.lastChangeID != 0 && e.Change.ChangeID != s.lastChangeID+1 {
// If skipRows is true, the last change ID is unknown.
err = fmt.Errorf("missed a change: expected change ID %d, got %d",
s.lastChangeID+1, e.Change.ChangeID)
}
if err == nil {
s.lastChangeID = e.Change.ChangeID
select {
case s.changes <- e.Change:
case <-s.ctx.Done():
return
}
} else {
// Report the error if resubscribing is disabled.
if s.resubscribe == nil {
s.err = err
return
}
slog.Info("Resubscribing to Corrosion query due to an error.",
"err", err, "id", s.id, "from_change", s.lastChangeID)
sub, sErr := s.resubscribe(s.ctx, s.lastChangeID)
if sErr != nil {
// resubscribe returns a permanent error after unsuccessful retries.
s.err = fmt.Errorf("resubscribe to query with backoff: %w", sErr)
return
}
// Reset the subscription to the new one.
s.rows = nil
s.body = sub.body
s.decoder = sub.decoder
// Do not close the sub to not close the body.
sub.cancel()
}
}
}
// Err returns the error, if any, that was encountered during fetching changes.
// Err may be called after an explicit or implicit [Subscription.Close].
func (s *Subscription) Err() error {
return s.err
}
func (s *Subscription) Close() error {
s.cancel()
return s.body.Close()
}
// SubscribeContext creates a subscription to receive updates for a desired SQL query. If skipRows is false,
// Subscription.Rows must be consumed before Subscription.Changes can be called. If skipRows is true, Subscription.Rows
// will return nil.
func (c *APIClient) SubscribeContext(
ctx context.Context, query string, args []any, skipRows bool,
) (*Subscription, error) {
statement := Statement{
Query: query,
Params: args,
}
body, err := json.Marshal(statement)
if err != nil {
return nil, fmt.Errorf("marshal query: %w", err)
}
subURL := c.baseURL.JoinPath("/v1/subscriptions")
if skipRows {
q := subURL.Query()
q.Set("skip_rows", "true")
subURL.RawQuery = q.Encode()
}
req, err := http.NewRequestWithContext(ctx, "POST", subURL.String(), bytes.NewReader(body))
if err != nil {
return nil, fmt.Errorf("create request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
resp, err := c.client.Do(req)
if err != nil {
return nil, fmt.Errorf("send request: %w", err)
}
if resp.StatusCode != http.StatusOK {
respBody, err := io.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
return nil, fmt.Errorf("read response body: %w", err)
}
return nil, fmt.Errorf("unexpected status code %d: %s", resp.StatusCode, respBody)
}
id := resp.Header.Get("corro-query-id")
if id == "" {
resp.Body.Close()
return nil, errors.New("missing corro-query-id header in response")
}
if skipRows {
return newSubscription(ctx, id, nil, resp.Body, nil, c.resubscribeWithBackoffFn(id)), nil
}
rows, err := newRows(ctx, resp.Body, false)
if err != nil {
resp.Body.Close()
return nil, fmt.Errorf("parse query response: %w", err)
}
return newSubscription(ctx, id, rows, rows.body, rows.decoder, c.resubscribeWithBackoffFn(id)), nil
}
func (c *APIClient) resubscribeWithBackoffFn(id string) func(context.Context, uint64) (*Subscription, error) {
if c.newResubBackoff == nil {
return nil
}
return func(ctx context.Context, fromChange uint64) (*Subscription, error) {
return backoff.RetryWithData(func() (*Subscription, error) {
slog.Debug("Retrying to resubscribe to Corrosion query.", "id", id, "from_change", fromChange)
return c.ResubscribeContext(ctx, id, fromChange)
}, c.newResubBackoff())
}
}
func (c *APIClient) ResubscribeContext(ctx context.Context, id string, fromChange uint64) (*Subscription, error) {
subURL := c.baseURL.JoinPath("/v1/subscriptions", id)
q := subURL.Query()
q.Set("from", strconv.FormatUint(fromChange, 10))
subURL.RawQuery = q.Encode()
req, err := http.NewRequestWithContext(ctx, "GET", subURL.String(), nil)
if err != nil {
return nil, fmt.Errorf("create request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
resp, err := c.client.Do(req)
if err != nil {
return nil, fmt.Errorf("send request: %w", err)
}
if resp.StatusCode != http.StatusOK {
respBody, err := io.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
return nil, fmt.Errorf("read response body: %w", err)
}
return nil, fmt.Errorf("unexpected status code %d: %s", resp.StatusCode, respBody)
}
return newSubscription(ctx, id, nil, resp.Body, nil, c.resubscribeWithBackoffFn(id)), nil
}
+1 -1
View File
@@ -35,7 +35,7 @@ func (d *Daemon) Run(ctx context.Context) error {
case <-d.machine.Started():
_, err := systemd.SdNotify(false, systemd.SdNotifyReady)
if err != nil {
slog.Error("Failed to notify systemd that the daemon is ready.", "error", err)
slog.Error("Failed to notify systemd that the daemon is ready.", "err", err)
}
case <-ctx.Done():
}
+49
View File
@@ -0,0 +1,49 @@
package docker
import (
"context"
"errors"
"fmt"
"github.com/cenkalti/backoff/v4"
"github.com/docker/docker/client"
"log/slog"
"time"
)
// WaitDaemonReady waits for the Docker daemon to start and be ready to serve requests.
func WaitDaemonReady(ctx context.Context, cli *client.Client) error {
// Retry to ping the Docker daemon until it's ready or the context is canceled.
boff := backoff.WithContext(backoff.NewExponentialBackOff(
backoff.WithInitialInterval(100*time.Millisecond),
backoff.WithMaxInterval(1*time.Second),
backoff.WithMaxElapsedTime(0),
), ctx)
waitingLogged := false
ping := func() error {
_, err := cli.Ping(ctx)
if err == nil {
if waitingLogged {
slog.Info("Docker daemon is ready.")
}
return nil
}
if !client.IsErrConnectionFailed(err) {
return backoff.Permanent(fmt.Errorf("connect to Docker daemon: %w", err))
}
if !waitingLogged {
slog.Info("Waiting for Docker daemon to start and be ready.")
waitingLogged = true
}
return err
}
if err := backoff.Retry(ping, boff); err != nil {
if errors.Is(err, context.Canceled) {
return nil
}
return fmt.Errorf("ping Docker: %w", err)
}
return nil
}
+58
View File
@@ -0,0 +1,58 @@
package fs
import (
"fmt"
"os"
"os/user"
"strconv"
)
// LookupUIDGID returns the user and group IDs for the given username.
func LookupUIDGID(username string) (uid, gid int, err error) {
usr, err := user.Lookup(username)
if err != nil {
err = fmt.Errorf("lookup user %q: %w", username, err)
return
}
uid, err = strconv.Atoi(usr.Uid)
if err != nil {
err = fmt.Errorf("parse %q user ID (UID) %q: %w", username, usr.Uid, err)
return
}
gid, err = strconv.Atoi(usr.Gid)
if err != nil {
err = fmt.Errorf("parse %q user group ID (GID) %q: %w", username, usr.Gid, err)
return
}
return
}
func Chown(path, username, group string) error {
uid, gid := -1, -1
if username != "" {
usr, err := user.Lookup(username)
if err != nil {
return fmt.Errorf("lookup user %q: %w", username, err)
}
uid, err = strconv.Atoi(usr.Uid)
if err != nil {
return fmt.Errorf("parse %q user ID (UID) %q: %w", username, usr.Uid, err)
}
}
if group != "" {
grp, err := user.LookupGroup(group)
if err != nil {
return fmt.Errorf("lookup group %q: %w", group, err)
}
gid, err = strconv.Atoi(grp.Gid)
if err != nil {
return fmt.Errorf("parse %q group ID (GID) %q: %w", group, grp.Gid, err)
}
}
if err := os.Chown(path, uid, gid); err != nil {
return fmt.Errorf("chown %q: %w", path, err)
}
return nil
}
+74
View File
@@ -0,0 +1,74 @@
package log
import (
"context"
"fmt"
"io"
"log/slog"
"sync"
)
// SlogTextHandler extends the standard [slog.TextHandler] to provide custom formatting that is very similar
// to the default slog handler. It prefixes each log entry with a right-padded level indicator and message,
// while excluding the default time, level, and message attributes from the structured output.
//
// The output format is:
// LEVEL MESSAGE key1=value1 key2=value2
type SlogTextHandler struct {
*slog.TextHandler
opts slog.HandlerOptions
mu sync.Mutex
w io.Writer
}
func NewSlogTextHandler(w io.Writer, opts *slog.HandlerOptions) *SlogTextHandler {
if opts == nil {
opts = &slog.HandlerOptions{}
}
// Remove time, level, and message from the default attributes.
opts.ReplaceAttr = func(groups []string, a slog.Attr) slog.Attr {
if a.Key == slog.TimeKey || a.Key == slog.LevelKey || a.Key == slog.MessageKey {
return slog.Attr{}
}
return a
}
return &SlogTextHandler{
TextHandler: slog.NewTextHandler(w, opts),
opts: *opts,
w: w,
}
}
func (h *SlogTextHandler) Enabled(ctx context.Context, level slog.Level) bool {
return h.TextHandler.Enabled(ctx, level)
}
func (h *SlogTextHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
return h.TextHandler.WithAttrs(attrs)
}
func (h *SlogTextHandler) WithGroup(name string) slog.Handler {
return h.TextHandler.WithGroup(name)
}
func (h *SlogTextHandler) Handle(ctx context.Context, r slog.Record) error {
// Only log entries that are at or above the minimum level (INFO by default).
minLevel := slog.LevelInfo
if h.opts.Level != nil {
minLevel = h.opts.Level.Level()
}
if r.Level < minLevel {
return nil
}
h.mu.Lock()
defer h.mu.Unlock()
levelMsg := fmt.Sprintf("%-5s %s ", r.Level.String(), r.Message)
if _, err := h.w.Write([]byte(levelMsg)); err != nil {
return err
}
return h.TextHandler.Handle(ctx, r)
}
-8
View File
@@ -1,7 +1,6 @@
package pb
import (
"fmt"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
@@ -40,10 +39,3 @@ func (c *NetworkConfig) Validate() error {
return nil
}
func (r *AddMachineRequest) Validate() error {
if r.Network == nil {
return fmt.Errorf("network not set")
}
return r.Network.Validate()
}
+187 -619
View File
@@ -21,6 +21,63 @@ const (
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type MachineMember_MembershipState int32
const (
MachineMember_UNKNOWN MachineMember_MembershipState = 0
// The member is active.
MachineMember_UP MachineMember_MembershipState = 1
// The member is active, but at least one cluster member suspects its down. For all purposes,
// a SUSPECT member is treated as if it were UP until either it refutes the suspicion (becoming UP)
// or fails to do so (being declared DOWN).
MachineMember_SUSPECT MachineMember_MembershipState = 2
// The member is confirmed DOWN.
MachineMember_DOWN MachineMember_MembershipState = 3
)
// Enum value maps for MachineMember_MembershipState.
var (
MachineMember_MembershipState_name = map[int32]string{
0: "UNKNOWN",
1: "UP",
2: "SUSPECT",
3: "DOWN",
}
MachineMember_MembershipState_value = map[string]int32{
"UNKNOWN": 0,
"UP": 1,
"SUSPECT": 2,
"DOWN": 3,
}
)
func (x MachineMember_MembershipState) Enum() *MachineMember_MembershipState {
p := new(MachineMember_MembershipState)
*p = x
return p
}
func (x MachineMember_MembershipState) String() string {
return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
}
func (MachineMember_MembershipState) Descriptor() protoreflect.EnumDescriptor {
return file_internal_machine_api_pb_cluster_proto_enumTypes[0].Descriptor()
}
func (MachineMember_MembershipState) Type() protoreflect.EnumType {
return &file_internal_machine_api_pb_cluster_proto_enumTypes[0]
}
func (x MachineMember_MembershipState) Number() protoreflect.EnumNumber {
return protoreflect.EnumNumber(x)
}
// Deprecated: Use MachineMember_MembershipState.Descriptor instead.
func (MachineMember_MembershipState) EnumDescriptor() ([]byte, []int) {
return file_internal_machine_api_pb_cluster_proto_rawDescGZIP(), []int{2, 0}
}
type AddMachineRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
@@ -123,18 +180,73 @@ func (x *AddMachineResponse) GetMachine() *MachineInfo {
return nil
}
type MachineMember struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Machine *MachineInfo `protobuf:"bytes,1,opt,name=machine,proto3" json:"machine,omitempty"`
State MachineMember_MembershipState `protobuf:"varint,2,opt,name=state,proto3,enum=api.MachineMember_MembershipState" json:"state,omitempty"`
}
func (x *MachineMember) Reset() {
*x = MachineMember{}
if protoimpl.UnsafeEnabled {
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *MachineMember) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*MachineMember) ProtoMessage() {}
func (x *MachineMember) ProtoReflect() protoreflect.Message {
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[2]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use MachineMember.ProtoReflect.Descriptor instead.
func (*MachineMember) Descriptor() ([]byte, []int) {
return file_internal_machine_api_pb_cluster_proto_rawDescGZIP(), []int{2}
}
func (x *MachineMember) GetMachine() *MachineInfo {
if x != nil {
return x.Machine
}
return nil
}
func (x *MachineMember) GetState() MachineMember_MembershipState {
if x != nil {
return x.State
}
return MachineMember_UNKNOWN
}
type ListMachinesResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Machines []*MachineInfo `protobuf:"bytes,1,rep,name=machines,proto3" json:"machines,omitempty"`
Machines []*MachineMember `protobuf:"bytes,1,rep,name=machines,proto3" json:"machines,omitempty"`
}
func (x *ListMachinesResponse) Reset() {
*x = ListMachinesResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[2]
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -147,7 +259,7 @@ func (x *ListMachinesResponse) String() string {
func (*ListMachinesResponse) ProtoMessage() {}
func (x *ListMachinesResponse) ProtoReflect() protoreflect.Message {
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[2]
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[3]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -160,419 +272,16 @@ func (x *ListMachinesResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use ListMachinesResponse.ProtoReflect.Descriptor instead.
func (*ListMachinesResponse) Descriptor() ([]byte, []int) {
return file_internal_machine_api_pb_cluster_proto_rawDescGZIP(), []int{2}
}
func (x *ListMachinesResponse) GetMachines() []*MachineInfo {
if x != nil {
return x.Machines
}
return nil
}
type ListMachineEndpointsRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
}
func (x *ListMachineEndpointsRequest) Reset() {
*x = ListMachineEndpointsRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *ListMachineEndpointsRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ListMachineEndpointsRequest) ProtoMessage() {}
func (x *ListMachineEndpointsRequest) ProtoReflect() protoreflect.Message {
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[3]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ListMachineEndpointsRequest.ProtoReflect.Descriptor instead.
func (*ListMachineEndpointsRequest) Descriptor() ([]byte, []int) {
return file_internal_machine_api_pb_cluster_proto_rawDescGZIP(), []int{3}
}
func (x *ListMachineEndpointsRequest) GetId() string {
if x != nil {
return x.Id
}
return ""
}
type ListMachineEndpointsResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Endpoints *MachineEndpoints `protobuf:"bytes,1,opt,name=endpoints,proto3" json:"endpoints,omitempty"`
}
func (x *ListMachineEndpointsResponse) Reset() {
*x = ListMachineEndpointsResponse{}
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 *ListMachineEndpointsResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ListMachineEndpointsResponse) ProtoMessage() {}
func (x *ListMachineEndpointsResponse) 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 ListMachineEndpointsResponse.ProtoReflect.Descriptor instead.
func (*ListMachineEndpointsResponse) Descriptor() ([]byte, []int) {
return file_internal_machine_api_pb_cluster_proto_rawDescGZIP(), []int{4}
}
func (x *ListMachineEndpointsResponse) GetEndpoints() *MachineEndpoints {
if x != nil {
return x.Endpoints
}
return nil
}
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"`
}
func (x *MachineInfo) Reset() {
*x = MachineInfo{}
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 *MachineInfo) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*MachineInfo) ProtoMessage() {}
func (x *MachineInfo) 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 MachineInfo.ProtoReflect.Descriptor instead.
func (*MachineInfo) Descriptor() ([]byte, []int) {
return file_internal_machine_api_pb_cluster_proto_rawDescGZIP(), []int{5}
}
func (x *MachineInfo) GetId() string {
if x != nil {
return x.Id
}
return ""
}
func (x *MachineInfo) GetName() string {
if x != nil {
return x.Name
}
return ""
}
func (x *MachineInfo) GetNetwork() *NetworkConfig {
if x != nil {
return x.Network
}
return nil
}
type NetworkConfig struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
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"`
}
func (x *NetworkConfig) Reset() {
*x = NetworkConfig{}
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 *NetworkConfig) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*NetworkConfig) ProtoMessage() {}
func (x *NetworkConfig) 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 NetworkConfig.ProtoReflect.Descriptor instead.
func (*NetworkConfig) Descriptor() ([]byte, []int) {
return file_internal_machine_api_pb_cluster_proto_rawDescGZIP(), []int{6}
}
func (x *NetworkConfig) GetSubnet() *IPPrefix {
if x != nil {
return x.Subnet
}
return nil
}
func (x *NetworkConfig) GetManagementIp() *IP {
if x != nil {
return x.ManagementIp
}
return nil
}
func (x *NetworkConfig) GetEndpoints() []*IPPort {
if x != nil {
return x.Endpoints
}
return nil
}
func (x *NetworkConfig) GetPublicKey() []byte {
if x != nil {
return x.PublicKey
}
return nil
}
type MachineEndpoints struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
Endpoints []*IPPort `protobuf:"bytes,2,rep,name=endpoints,proto3" json:"endpoints,omitempty"`
}
func (x *MachineEndpoints) Reset() {
*x = MachineEndpoints{}
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 *MachineEndpoints) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*MachineEndpoints) ProtoMessage() {}
func (x *MachineEndpoints) 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 MachineEndpoints.ProtoReflect.Descriptor instead.
func (*MachineEndpoints) Descriptor() ([]byte, []int) {
return file_internal_machine_api_pb_cluster_proto_rawDescGZIP(), []int{7}
}
func (x *MachineEndpoints) GetId() string {
if x != nil {
return x.Id
}
return ""
}
func (x *MachineEndpoints) GetEndpoints() []*IPPort {
if x != nil {
return x.Endpoints
}
return nil
}
type User struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Network *NetworkConfig `protobuf:"bytes,1,opt,name=network,proto3" json:"network,omitempty"`
}
func (x *User) Reset() {
*x = User{}
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 *User) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*User) ProtoMessage() {}
func (x *User) 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 User.ProtoReflect.Descriptor instead.
func (*User) Descriptor() ([]byte, []int) {
return file_internal_machine_api_pb_cluster_proto_rawDescGZIP(), []int{8}
}
func (x *User) GetNetwork() *NetworkConfig {
if x != nil {
return x.Network
}
return nil
}
type State struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Network *IPPrefix `protobuf:"bytes,1,opt,name=Network,proto3" json:"Network,omitempty"`
// The machine configuration in the state is the source of truth set by the administrator.
// The machine itself can't update it.
Machines map[string]*MachineInfo `protobuf:"bytes,2,rep,name=machines,proto3" json:"machines,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
Endpoints map[string]*MachineEndpoints `protobuf:"bytes,3,rep,name=endpoints,proto3" json:"endpoints,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
Users []*User `protobuf:"bytes,4,rep,name=users,proto3" json:"users,omitempty"`
}
func (x *State) Reset() {
*x = State{}
if protoimpl.UnsafeEnabled {
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[9]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *State) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*State) ProtoMessage() {}
func (x *State) ProtoReflect() protoreflect.Message {
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[9]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use State.ProtoReflect.Descriptor instead.
func (*State) Descriptor() ([]byte, []int) {
return file_internal_machine_api_pb_cluster_proto_rawDescGZIP(), []int{9}
}
func (x *State) GetNetwork() *IPPrefix {
if x != nil {
return x.Network
}
return nil
}
func (x *State) GetMachines() map[string]*MachineInfo {
func (x *ListMachinesResponse) GetMachines() []*MachineMember {
if x != nil {
return x.Machines
}
return nil
}
func (x *State) GetEndpoints() map[string]*MachineEndpoints {
if x != nil {
return x.Endpoints
}
return nil
}
func (x *State) GetUsers() []*User {
if x != nil {
return x.Users
}
return nil
}
var File_internal_machine_api_pb_cluster_proto protoreflect.FileDescriptor
var file_internal_machine_api_pb_cluster_proto_rawDesc = []byte{
@@ -580,98 +289,48 @@ 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, 0x24, 0x69, 0x6e, 0x74, 0x65, 0x72,
0x70, 0x74, 0x79, 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, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 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, 0x44, 0x0a, 0x14, 0x4c, 0x69, 0x73, 0x74,
0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
0x12, 0x2c, 0x0a, 0x08, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03,
0x28, 0x0b, 0x32, 0x10, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65,
0x49, 0x6e, 0x66, 0x6f, 0x52, 0x08, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x22, 0x2d,
0x0a, 0x1b, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x45, 0x6e, 0x64,
0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a,
0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x22, 0x53, 0x0a,
0x1c, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x45, 0x6e, 0x64, 0x70,
0x6f, 0x69, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x33, 0x0a,
0x09, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b,
0x32, 0x15, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x45, 0x6e,
0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x52, 0x09, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e,
0x74, 0x73, 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, 0x4d, 0x0a, 0x10, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x45, 0x6e,
0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20,
0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x29, 0x0a, 0x09, 0x65, 0x6e, 0x64, 0x70, 0x6f,
0x69, 0x6e, 0x74, 0x73, 0x18, 0x02, 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, 0x22, 0x34, 0x0a, 0x04, 0x55, 0x73, 0x65, 0x72, 0x12, 0x2c, 0x0a, 0x07, 0x6e, 0x65,
0x74, 0x77, 0x6f, 0x72, 0x6b, 0x18, 0x01, 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, 0xe4, 0x02, 0x0a, 0x05, 0x53, 0x74, 0x61,
0x74, 0x65, 0x12, 0x27, 0x0a, 0x07, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x18, 0x01, 0x20,
0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x50, 0x50, 0x72, 0x65, 0x66,
0x69, 0x78, 0x52, 0x07, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x12, 0x34, 0x0a, 0x08, 0x6d,
0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x18, 0x2e,
0x61, 0x70, 0x69, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e,
0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x08, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65,
0x73, 0x12, 0x37, 0x0a, 0x09, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x18, 0x03,
0x20, 0x03, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x65,
0x2e, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52,
0x09, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x12, 0x1f, 0x0a, 0x05, 0x75, 0x73,
0x65, 0x72, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x09, 0x2e, 0x61, 0x70, 0x69, 0x2e,
0x55, 0x73, 0x65, 0x72, 0x52, 0x05, 0x75, 0x73, 0x65, 0x72, 0x73, 0x1a, 0x4d, 0x0a, 0x0d, 0x4d,
0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03,
0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x26,
0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e,
0x61, 0x70, 0x69, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52,
0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x1a, 0x53, 0x0a, 0x0e, 0x45, 0x6e,
0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03,
0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x2b,
0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e,
0x61, 0x70, 0x69, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x45, 0x6e, 0x64, 0x70, 0x6f,
0x69, 0x6e, 0x74, 0x73, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x32,
0xe8, 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, 0x12, 0x5b, 0x0a,
0x14, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x45, 0x6e, 0x64, 0x70,
0x6f, 0x69, 0x6e, 0x74, 0x73, 0x12, 0x20, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74,
0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73,
0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69,
0x73, 0x74, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e,
0x74, 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, 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,
}
var (
@@ -686,53 +345,33 @@ 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_msgTypes = make([]protoimpl.MessageInfo, 12)
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_goTypes = []any{
(*AddMachineRequest)(nil), // 0: api.AddMachineRequest
(*AddMachineResponse)(nil), // 1: api.AddMachineResponse
(*ListMachinesResponse)(nil), // 2: api.ListMachinesResponse
(*ListMachineEndpointsRequest)(nil), // 3: api.ListMachineEndpointsRequest
(*ListMachineEndpointsResponse)(nil), // 4: api.ListMachineEndpointsResponse
(*MachineInfo)(nil), // 5: api.MachineInfo
(*NetworkConfig)(nil), // 6: api.NetworkConfig
(*MachineEndpoints)(nil), // 7: api.MachineEndpoints
(*User)(nil), // 8: api.User
(*State)(nil), // 9: api.State
nil, // 10: api.State.MachinesEntry
nil, // 11: api.State.EndpointsEntry
(*IPPrefix)(nil), // 12: api.IPPrefix
(*IP)(nil), // 13: api.IP
(*IPPort)(nil), // 14: api.IPPort
(*emptypb.Empty)(nil), // 15: google.protobuf.Empty
(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
}
var file_internal_machine_api_pb_cluster_proto_depIdxs = []int32{
6, // 0: api.AddMachineRequest.network:type_name -> api.NetworkConfig
5, // 1: api.AddMachineResponse.machine:type_name -> api.MachineInfo
5, // 2: api.ListMachinesResponse.machines:type_name -> api.MachineInfo
7, // 3: api.ListMachineEndpointsResponse.endpoints:type_name -> api.MachineEndpoints
6, // 4: api.MachineInfo.network:type_name -> api.NetworkConfig
12, // 5: api.NetworkConfig.subnet:type_name -> api.IPPrefix
13, // 6: api.NetworkConfig.management_ip:type_name -> api.IP
14, // 7: api.NetworkConfig.endpoints:type_name -> api.IPPort
14, // 8: api.MachineEndpoints.endpoints:type_name -> api.IPPort
6, // 9: api.User.network:type_name -> api.NetworkConfig
12, // 10: api.State.Network:type_name -> api.IPPrefix
10, // 11: api.State.machines:type_name -> api.State.MachinesEntry
11, // 12: api.State.endpoints:type_name -> api.State.EndpointsEntry
8, // 13: api.State.users:type_name -> api.User
5, // 14: api.State.MachinesEntry.value:type_name -> api.MachineInfo
7, // 15: api.State.EndpointsEntry.value:type_name -> api.MachineEndpoints
0, // 16: api.Cluster.AddMachine:input_type -> api.AddMachineRequest
15, // 17: api.Cluster.ListMachines:input_type -> google.protobuf.Empty
3, // 18: api.Cluster.ListMachineEndpoints:input_type -> api.ListMachineEndpointsRequest
1, // 19: api.Cluster.AddMachine:output_type -> api.AddMachineResponse
2, // 20: api.Cluster.ListMachines:output_type -> api.ListMachinesResponse
4, // 21: api.Cluster.ListMachineEndpoints:output_type -> api.ListMachineEndpointsResponse
19, // [19:22] is the sub-list for method output_type
16, // [16:19] is the sub-list for method input_type
16, // [16:16] is the sub-list for extension type_name
16, // [16:16] is the sub-list for extension extendee
0, // [0:16] is the sub-list for field type_name
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
}
func init() { file_internal_machine_api_pb_cluster_proto_init() }
@@ -740,7 +379,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 {
switch v := v.(*AddMachineRequest); i {
@@ -767,7 +406,7 @@ func file_internal_machine_api_pb_cluster_proto_init() {
}
}
file_internal_machine_api_pb_cluster_proto_msgTypes[2].Exporter = func(v any, i int) any {
switch v := v.(*ListMachinesResponse); i {
switch v := v.(*MachineMember); i {
case 0:
return &v.state
case 1:
@@ -779,79 +418,7 @@ func file_internal_machine_api_pb_cluster_proto_init() {
}
}
file_internal_machine_api_pb_cluster_proto_msgTypes[3].Exporter = func(v any, i int) any {
switch v := v.(*ListMachineEndpointsRequest); 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[4].Exporter = func(v any, i int) any {
switch v := v.(*ListMachineEndpointsResponse); 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.(*MachineInfo); 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.(*NetworkConfig); 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.(*MachineEndpoints); 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.(*User); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_internal_machine_api_pb_cluster_proto_msgTypes[9].Exporter = func(v any, i int) any {
switch v := v.(*State); i {
switch v := v.(*ListMachinesResponse); i {
case 0:
return &v.state
case 1:
@@ -868,13 +435,14 @@ func file_internal_machine_api_pb_cluster_proto_init() {
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_internal_machine_api_pb_cluster_proto_rawDesc,
NumEnums: 0,
NumMessages: 12,
NumEnums: 1,
NumMessages: 4,
NumExtensions: 0,
NumServices: 1,
},
GoTypes: file_internal_machine_api_pb_cluster_proto_goTypes,
DependencyIndexes: file_internal_machine_api_pb_cluster_proto_depIdxs,
EnumInfos: file_internal_machine_api_pb_cluster_proto_enumTypes,
MessageInfos: file_internal_machine_api_pb_cluster_proto_msgTypes,
}.Build()
File_internal_machine_api_pb_cluster_proto = out.File
+19 -43
View File
@@ -5,12 +5,11 @@ 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 ListMachineEndpoints(ListMachineEndpointsRequest) returns (ListMachineEndpointsResponse);
}
message AddMachineRequest {
@@ -22,46 +21,23 @@ message AddMachineResponse {
MachineInfo machine = 1;
}
message MachineMember {
MachineInfo machine = 1;
enum MembershipState {
UNKNOWN = 0;
// The member is active.
UP = 1;
// The member is active, but at least one cluster member suspects its down. For all purposes,
// a SUSPECT member is treated as if it were UP until either it refutes the suspicion (becoming UP)
// or fails to do so (being declared DOWN).
SUSPECT = 2;
// The member is confirmed DOWN.
DOWN = 3;
}
MembershipState state = 2;
}
message ListMachinesResponse {
repeated MachineInfo machines = 1;
repeated MachineMember machines = 1;
}
message ListMachineEndpointsRequest {
string id = 1;
}
message ListMachineEndpointsResponse {
MachineEndpoints endpoints = 1;
}
message MachineInfo {
string id = 1;
string name = 2;
NetworkConfig network = 3;
}
message NetworkConfig {
IPPrefix subnet = 1;
IP management_ip = 2;
repeated IPPort endpoints = 3;
bytes publicKey = 4;
}
message MachineEndpoints {
string id = 1;
repeated IPPort endpoints = 2;
}
message User {
NetworkConfig network = 1;
}
message State {
IPPrefix Network = 1;
// The machine configuration in the state is the source of truth set by the administrator.
// The machine itself can't update it.
map<string, MachineInfo> machines = 2;
map<string, MachineEndpoints> endpoints = 3;
repeated User users = 4;
}
+2 -40
View File
@@ -20,9 +20,8 @@ import (
const _ = grpc.SupportPackageIsVersion9
const (
Cluster_AddMachine_FullMethodName = "/api.Cluster/AddMachine"
Cluster_ListMachines_FullMethodName = "/api.Cluster/ListMachines"
Cluster_ListMachineEndpoints_FullMethodName = "/api.Cluster/ListMachineEndpoints"
Cluster_AddMachine_FullMethodName = "/api.Cluster/AddMachine"
Cluster_ListMachines_FullMethodName = "/api.Cluster/ListMachines"
)
// ClusterClient is the client API for Cluster service.
@@ -31,7 +30,6 @@ 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)
ListMachineEndpoints(ctx context.Context, in *ListMachineEndpointsRequest, opts ...grpc.CallOption) (*ListMachineEndpointsResponse, error)
}
type clusterClient struct {
@@ -62,23 +60,12 @@ func (c *clusterClient) ListMachines(ctx context.Context, in *emptypb.Empty, opt
return out, nil
}
func (c *clusterClient) ListMachineEndpoints(ctx context.Context, in *ListMachineEndpointsRequest, opts ...grpc.CallOption) (*ListMachineEndpointsResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ListMachineEndpointsResponse)
err := c.cc.Invoke(ctx, Cluster_ListMachineEndpoints_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)
ListMachineEndpoints(context.Context, *ListMachineEndpointsRequest) (*ListMachineEndpointsResponse, error)
mustEmbedUnimplementedClusterServer()
}
@@ -95,9 +82,6 @@ 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) ListMachineEndpoints(context.Context, *ListMachineEndpointsRequest) (*ListMachineEndpointsResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ListMachineEndpoints not implemented")
}
func (UnimplementedClusterServer) mustEmbedUnimplementedClusterServer() {}
func (UnimplementedClusterServer) testEmbeddedByValue() {}
@@ -155,24 +139,6 @@ func _Cluster_ListMachines_Handler(srv interface{}, ctx context.Context, dec fun
return interceptor(ctx, in, info, handler)
}
func _Cluster_ListMachineEndpoints_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ListMachineEndpointsRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(ClusterServer).ListMachineEndpoints(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: Cluster_ListMachineEndpoints_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ClusterServer).ListMachineEndpoints(ctx, req.(*ListMachineEndpointsRequest))
}
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)
@@ -188,10 +154,6 @@ var Cluster_ServiceDesc = grpc.ServiceDesc{
MethodName: "ListMachines",
Handler: _Cluster_ListMachines_Handler,
},
{
MethodName: "ListMachineEndpoints",
Handler: _Cluster_ListMachineEndpoints_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "internal/machine/api/pb/cluster.proto",
+260 -37
View File
@@ -7,6 +7,7 @@
package pb
import (
status "google.golang.org/genproto/googleapis/rpc/status"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
@@ -20,6 +21,171 @@ const (
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
// Common metadata message nested in all reply message types, injected by the gRPC proxy to provide information
// about the machine that responded to the request.
type Metadata struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
// Address of the machine the response came from.
Machine string `protobuf:"bytes,1,opt,name=machine,proto3" json:"machine,omitempty"`
// error is set if the request to upstream failed. The rest of the response is undefined.
Error string `protobuf:"bytes,2,opt,name=error,proto3" json:"error,omitempty"`
// error as a gRPC Status message.
Status *status.Status `protobuf:"bytes,3,opt,name=status,proto3" json:"status,omitempty"`
}
func (x *Metadata) Reset() {
*x = Metadata{}
if protoimpl.UnsafeEnabled {
mi := &file_internal_machine_api_pb_common_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *Metadata) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Metadata) ProtoMessage() {}
func (x *Metadata) ProtoReflect() protoreflect.Message {
mi := &file_internal_machine_api_pb_common_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 Metadata.ProtoReflect.Descriptor instead.
func (*Metadata) Descriptor() ([]byte, []int) {
return file_internal_machine_api_pb_common_proto_rawDescGZIP(), []int{0}
}
func (x *Metadata) GetMachine() string {
if x != nil {
return x.Machine
}
return ""
}
func (x *Metadata) GetError() string {
if x != nil {
return x.Error
}
return ""
}
func (x *Metadata) GetStatus() *status.Status {
if x != nil {
return x.Status
}
return nil
}
// A helper message for marshalling the metadata field and injecting it into the response by the gRPC proxy.
type Empty struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Metadata *Metadata `protobuf:"bytes,1,opt,name=metadata,proto3" json:"metadata,omitempty"`
}
func (x *Empty) Reset() {
*x = Empty{}
if protoimpl.UnsafeEnabled {
mi := &file_internal_machine_api_pb_common_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *Empty) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Empty) ProtoMessage() {}
func (x *Empty) ProtoReflect() protoreflect.Message {
mi := &file_internal_machine_api_pb_common_proto_msgTypes[1]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Empty.ProtoReflect.Descriptor instead.
func (*Empty) Descriptor() ([]byte, []int) {
return file_internal_machine_api_pb_common_proto_rawDescGZIP(), []int{1}
}
func (x *Empty) GetMetadata() *Metadata {
if x != nil {
return x.Metadata
}
return nil
}
// EmptyResponse is a response message to be returned by the gRPC proxy when a request to the upstream failed.
// The nested Empty.Metadata message should contain an error.
type EmptyResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Messages []*Empty `protobuf:"bytes,1,rep,name=messages,proto3" json:"messages,omitempty"`
}
func (x *EmptyResponse) Reset() {
*x = EmptyResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_internal_machine_api_pb_common_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *EmptyResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*EmptyResponse) ProtoMessage() {}
func (x *EmptyResponse) ProtoReflect() protoreflect.Message {
mi := &file_internal_machine_api_pb_common_proto_msgTypes[2]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use EmptyResponse.ProtoReflect.Descriptor instead.
func (*EmptyResponse) Descriptor() ([]byte, []int) {
return file_internal_machine_api_pb_common_proto_rawDescGZIP(), []int{2}
}
func (x *EmptyResponse) GetMessages() []*Empty {
if x != nil {
return x.Messages
}
return nil
}
type IP struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
@@ -31,7 +197,7 @@ type IP struct {
func (x *IP) Reset() {
*x = IP{}
if protoimpl.UnsafeEnabled {
mi := &file_internal_machine_api_pb_common_proto_msgTypes[0]
mi := &file_internal_machine_api_pb_common_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -44,7 +210,7 @@ func (x *IP) String() string {
func (*IP) ProtoMessage() {}
func (x *IP) ProtoReflect() protoreflect.Message {
mi := &file_internal_machine_api_pb_common_proto_msgTypes[0]
mi := &file_internal_machine_api_pb_common_proto_msgTypes[3]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -57,7 +223,7 @@ func (x *IP) ProtoReflect() protoreflect.Message {
// Deprecated: Use IP.ProtoReflect.Descriptor instead.
func (*IP) Descriptor() ([]byte, []int) {
return file_internal_machine_api_pb_common_proto_rawDescGZIP(), []int{0}
return file_internal_machine_api_pb_common_proto_rawDescGZIP(), []int{3}
}
func (x *IP) GetIp() []byte {
@@ -79,7 +245,7 @@ type IPPort struct {
func (x *IPPort) Reset() {
*x = IPPort{}
if protoimpl.UnsafeEnabled {
mi := &file_internal_machine_api_pb_common_proto_msgTypes[1]
mi := &file_internal_machine_api_pb_common_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -92,7 +258,7 @@ func (x *IPPort) String() string {
func (*IPPort) ProtoMessage() {}
func (x *IPPort) ProtoReflect() protoreflect.Message {
mi := &file_internal_machine_api_pb_common_proto_msgTypes[1]
mi := &file_internal_machine_api_pb_common_proto_msgTypes[4]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -105,7 +271,7 @@ func (x *IPPort) ProtoReflect() protoreflect.Message {
// Deprecated: Use IPPort.ProtoReflect.Descriptor instead.
func (*IPPort) Descriptor() ([]byte, []int) {
return file_internal_machine_api_pb_common_proto_rawDescGZIP(), []int{1}
return file_internal_machine_api_pb_common_proto_rawDescGZIP(), []int{4}
}
func (x *IPPort) GetIp() *IP {
@@ -134,7 +300,7 @@ type IPPrefix struct {
func (x *IPPrefix) Reset() {
*x = IPPrefix{}
if protoimpl.UnsafeEnabled {
mi := &file_internal_machine_api_pb_common_proto_msgTypes[2]
mi := &file_internal_machine_api_pb_common_proto_msgTypes[5]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -147,7 +313,7 @@ func (x *IPPrefix) String() string {
func (*IPPrefix) ProtoMessage() {}
func (x *IPPrefix) ProtoReflect() protoreflect.Message {
mi := &file_internal_machine_api_pb_common_proto_msgTypes[2]
mi := &file_internal_machine_api_pb_common_proto_msgTypes[5]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -160,7 +326,7 @@ func (x *IPPrefix) ProtoReflect() protoreflect.Message {
// Deprecated: Use IPPrefix.ProtoReflect.Descriptor instead.
func (*IPPrefix) Descriptor() ([]byte, []int) {
return file_internal_machine_api_pb_common_proto_rawDescGZIP(), []int{2}
return file_internal_machine_api_pb_common_proto_rawDescGZIP(), []int{5}
}
func (x *IPPrefix) GetIp() *IP {
@@ -182,20 +348,34 @@ var File_internal_machine_api_pb_common_proto protoreflect.FileDescriptor
var file_internal_machine_api_pb_common_proto_rawDesc = []byte{
0x0a, 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, 0x12, 0x03, 0x61, 0x70, 0x69, 0x22, 0x14, 0x0a, 0x02, 0x49,
0x50, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x02, 0x69,
0x70, 0x22, 0x35, 0x0a, 0x06, 0x49, 0x50, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x17, 0x0a, 0x02, 0x69,
0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x07, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x50,
0x52, 0x02, 0x69, 0x70, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x02, 0x20, 0x01,
0x28, 0x0d, 0x52, 0x04, 0x70, 0x6f, 0x72, 0x74, 0x22, 0x37, 0x0a, 0x08, 0x49, 0x50, 0x50, 0x72,
0x65, 0x66, 0x69, 0x78, 0x12, 0x17, 0x0a, 0x02, 0x69, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b,
0x32, 0x07, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x50, 0x52, 0x02, 0x69, 0x70, 0x12, 0x12, 0x0a,
0x04, 0x62, 0x69, 0x74, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x04, 0x62, 0x69, 0x74,
0x73, 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,
0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x03, 0x61, 0x70, 0x69, 0x1a, 0x17, 0x67, 0x6f, 0x6f,
0x67, 0x6c, 0x65, 0x2f, 0x72, 0x70, 0x63, 0x2f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x2e, 0x70,
0x72, 0x6f, 0x74, 0x6f, 0x22, 0x66, 0x0a, 0x08, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61,
0x12, 0x18, 0x0a, 0x07, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28,
0x09, 0x52, 0x07, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72,
0x72, 0x6f, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72,
0x12, 0x2a, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b,
0x32, 0x12, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x74,
0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x32, 0x0a, 0x05,
0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x29, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74,
0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d, 0x65,
0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61,
0x22, 0x37, 0x0a, 0x0d, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
0x65, 0x12, 0x26, 0x0a, 0x08, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x18, 0x01, 0x20,
0x03, 0x28, 0x0b, 0x32, 0x0a, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x52,
0x08, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x22, 0x14, 0x0a, 0x02, 0x49, 0x50, 0x12,
0x0e, 0x0a, 0x02, 0x69, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x02, 0x69, 0x70, 0x22,
0x35, 0x0a, 0x06, 0x49, 0x50, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x17, 0x0a, 0x02, 0x69, 0x70, 0x18,
0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x07, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x50, 0x52, 0x02,
0x69, 0x70, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d,
0x52, 0x04, 0x70, 0x6f, 0x72, 0x74, 0x22, 0x37, 0x0a, 0x08, 0x49, 0x50, 0x50, 0x72, 0x65, 0x66,
0x69, 0x78, 0x12, 0x17, 0x0a, 0x02, 0x69, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x07,
0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x50, 0x52, 0x02, 0x69, 0x70, 0x12, 0x12, 0x0a, 0x04, 0x62,
0x69, 0x74, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x04, 0x62, 0x69, 0x74, 0x73, 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 (
@@ -210,20 +390,27 @@ func file_internal_machine_api_pb_common_proto_rawDescGZIP() []byte {
return file_internal_machine_api_pb_common_proto_rawDescData
}
var file_internal_machine_api_pb_common_proto_msgTypes = make([]protoimpl.MessageInfo, 3)
var file_internal_machine_api_pb_common_proto_msgTypes = make([]protoimpl.MessageInfo, 6)
var file_internal_machine_api_pb_common_proto_goTypes = []any{
(*IP)(nil), // 0: api.IP
(*IPPort)(nil), // 1: api.IPPort
(*IPPrefix)(nil), // 2: api.IPPrefix
(*Metadata)(nil), // 0: api.Metadata
(*Empty)(nil), // 1: api.Empty
(*EmptyResponse)(nil), // 2: api.EmptyResponse
(*IP)(nil), // 3: api.IP
(*IPPort)(nil), // 4: api.IPPort
(*IPPrefix)(nil), // 5: api.IPPrefix
(*status.Status)(nil), // 6: google.rpc.Status
}
var file_internal_machine_api_pb_common_proto_depIdxs = []int32{
0, // 0: api.IPPort.ip:type_name -> api.IP
0, // 1: api.IPPrefix.ip:type_name -> api.IP
2, // [2:2] is the sub-list for method output_type
2, // [2:2] is the sub-list for method input_type
2, // [2:2] is the sub-list for extension type_name
2, // [2:2] is the sub-list for extension extendee
0, // [0:2] is the sub-list for field type_name
6, // 0: api.Metadata.status:type_name -> google.rpc.Status
0, // 1: api.Empty.metadata:type_name -> api.Metadata
1, // 2: api.EmptyResponse.messages:type_name -> api.Empty
3, // 3: api.IPPort.ip:type_name -> api.IP
3, // 4: api.IPPrefix.ip:type_name -> api.IP
5, // [5:5] is the sub-list for method output_type
5, // [5:5] 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
}
func init() { file_internal_machine_api_pb_common_proto_init() }
@@ -233,7 +420,7 @@ func file_internal_machine_api_pb_common_proto_init() {
}
if !protoimpl.UnsafeEnabled {
file_internal_machine_api_pb_common_proto_msgTypes[0].Exporter = func(v any, i int) any {
switch v := v.(*IP); i {
switch v := v.(*Metadata); i {
case 0:
return &v.state
case 1:
@@ -245,7 +432,7 @@ func file_internal_machine_api_pb_common_proto_init() {
}
}
file_internal_machine_api_pb_common_proto_msgTypes[1].Exporter = func(v any, i int) any {
switch v := v.(*IPPort); i {
switch v := v.(*Empty); i {
case 0:
return &v.state
case 1:
@@ -257,6 +444,42 @@ func file_internal_machine_api_pb_common_proto_init() {
}
}
file_internal_machine_api_pb_common_proto_msgTypes[2].Exporter = func(v any, i int) any {
switch v := v.(*EmptyResponse); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_internal_machine_api_pb_common_proto_msgTypes[3].Exporter = func(v any, i int) any {
switch v := v.(*IP); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_internal_machine_api_pb_common_proto_msgTypes[4].Exporter = func(v any, i int) any {
switch v := v.(*IPPort); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_internal_machine_api_pb_common_proto_msgTypes[5].Exporter = func(v any, i int) any {
switch v := v.(*IPPrefix); i {
case 0:
return &v.state
@@ -275,7 +498,7 @@ func file_internal_machine_api_pb_common_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_internal_machine_api_pb_common_proto_rawDesc,
NumEnums: 0,
NumMessages: 3,
NumMessages: 6,
NumExtensions: 0,
NumServices: 0,
},
+25
View File
@@ -4,6 +4,31 @@ package api;
option go_package = "github.com/psviderski/uncloud/internal/machine/api/pb";
// Vendored at internal/machine/api/vendor/google/rpc/status.proto.
import "google/rpc/status.proto";
// Common metadata message nested in all reply message types, injected by the gRPC proxy to provide information
// about the machine that responded to the request.
message Metadata {
// Address of the machine the response came from.
string machine = 1;
// error is set if the request to upstream failed. The rest of the response is undefined.
string error = 2;
// error as a gRPC Status message.
google.rpc.Status status = 3;
}
// A helper message for marshalling the metadata field and injecting it into the response by the gRPC proxy.
message Empty {
Metadata metadata = 1;
}
// EmptyResponse is a response message to be returned by the gRPC proxy when a request to the upstream failed.
// The nested Empty.Metadata message should contain an error.
message EmptyResponse {
repeated Empty messages = 1;
}
message IP {
bytes ip = 1;
}
+785
View File
@@ -0,0 +1,785 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.34.2
// protoc v5.27.3
// source: internal/machine/api/pb/docker.proto
package pb
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
emptypb "google.golang.org/protobuf/types/known/emptypb"
reflect "reflect"
sync "sync"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type CreateContainerRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
// JSON serialized container.Config.
Config []byte `protobuf:"bytes,1,opt,name=config,proto3" json:"config,omitempty"`
// JSON serialized container.HostConfig.
HostConfig []byte `protobuf:"bytes,2,opt,name=host_config,json=hostConfig,proto3" json:"host_config,omitempty"`
// JSON serialized network.NetworkingConfig.
NetworkConfig []byte `protobuf:"bytes,3,opt,name=network_config,json=networkConfig,proto3" json:"network_config,omitempty"`
// JSON serialized ocispec.Platform.
Platform []byte `protobuf:"bytes,4,opt,name=platform,proto3" json:"platform,omitempty"`
Name string `protobuf:"bytes,5,opt,name=name,proto3" json:"name,omitempty"`
}
func (x *CreateContainerRequest) Reset() {
*x = CreateContainerRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *CreateContainerRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*CreateContainerRequest) ProtoMessage() {}
func (x *CreateContainerRequest) ProtoReflect() protoreflect.Message {
mi := &file_internal_machine_api_pb_docker_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 CreateContainerRequest.ProtoReflect.Descriptor instead.
func (*CreateContainerRequest) Descriptor() ([]byte, []int) {
return file_internal_machine_api_pb_docker_proto_rawDescGZIP(), []int{0}
}
func (x *CreateContainerRequest) GetConfig() []byte {
if x != nil {
return x.Config
}
return nil
}
func (x *CreateContainerRequest) GetHostConfig() []byte {
if x != nil {
return x.HostConfig
}
return nil
}
func (x *CreateContainerRequest) GetNetworkConfig() []byte {
if x != nil {
return x.NetworkConfig
}
return nil
}
func (x *CreateContainerRequest) GetPlatform() []byte {
if x != nil {
return x.Platform
}
return nil
}
func (x *CreateContainerRequest) GetName() string {
if x != nil {
return x.Name
}
return ""
}
type CreateContainerResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
// JSON serialized container.CreateResponse.
Response []byte `protobuf:"bytes,1,opt,name=response,proto3" json:"response,omitempty"`
}
func (x *CreateContainerResponse) Reset() {
*x = CreateContainerResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *CreateContainerResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*CreateContainerResponse) ProtoMessage() {}
func (x *CreateContainerResponse) ProtoReflect() protoreflect.Message {
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[1]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use CreateContainerResponse.ProtoReflect.Descriptor instead.
func (*CreateContainerResponse) Descriptor() ([]byte, []int) {
return file_internal_machine_api_pb_docker_proto_rawDescGZIP(), []int{1}
}
func (x *CreateContainerResponse) GetResponse() []byte {
if x != nil {
return x.Response
}
return nil
}
type StartContainerRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
// JSON serialized container.StartOptions.
Options []byte `protobuf:"bytes,2,opt,name=options,proto3" json:"options,omitempty"`
}
func (x *StartContainerRequest) Reset() {
*x = StartContainerRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *StartContainerRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*StartContainerRequest) ProtoMessage() {}
func (x *StartContainerRequest) ProtoReflect() protoreflect.Message {
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[2]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use StartContainerRequest.ProtoReflect.Descriptor instead.
func (*StartContainerRequest) Descriptor() ([]byte, []int) {
return file_internal_machine_api_pb_docker_proto_rawDescGZIP(), []int{2}
}
func (x *StartContainerRequest) GetId() string {
if x != nil {
return x.Id
}
return ""
}
func (x *StartContainerRequest) GetOptions() []byte {
if x != nil {
return x.Options
}
return nil
}
type ListContainersRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
// JSON serialized container.ListOptions.
Options []byte `protobuf:"bytes,1,opt,name=options,proto3" json:"options,omitempty"`
}
func (x *ListContainersRequest) Reset() {
*x = ListContainersRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *ListContainersRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ListContainersRequest) ProtoMessage() {}
func (x *ListContainersRequest) ProtoReflect() protoreflect.Message {
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[3]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ListContainersRequest.ProtoReflect.Descriptor instead.
func (*ListContainersRequest) Descriptor() ([]byte, []int) {
return file_internal_machine_api_pb_docker_proto_rawDescGZIP(), []int{3}
}
func (x *ListContainersRequest) GetOptions() []byte {
if x != nil {
return x.Options
}
return nil
}
// ListContainersResponse structure allows broadcasting ListContainers requests to multiple machines.
type ListContainersResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Messages []*MachineContainers `protobuf:"bytes,1,rep,name=messages,proto3" json:"messages,omitempty"`
}
func (x *ListContainersResponse) Reset() {
*x = ListContainersResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *ListContainersResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ListContainersResponse) ProtoMessage() {}
func (x *ListContainersResponse) ProtoReflect() protoreflect.Message {
mi := &file_internal_machine_api_pb_docker_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 ListContainersResponse.ProtoReflect.Descriptor instead.
func (*ListContainersResponse) Descriptor() ([]byte, []int) {
return file_internal_machine_api_pb_docker_proto_rawDescGZIP(), []int{4}
}
func (x *ListContainersResponse) GetMessages() []*MachineContainers {
if x != nil {
return x.Messages
}
return nil
}
type MachineContainers struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Metadata *Metadata `protobuf:"bytes,1,opt,name=metadata,proto3" json:"metadata,omitempty"`
// JSON serialized []container.Summary.
Containers []byte `protobuf:"bytes,2,opt,name=containers,proto3" json:"containers,omitempty"`
}
func (x *MachineContainers) Reset() {
*x = MachineContainers{}
if protoimpl.UnsafeEnabled {
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[5]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *MachineContainers) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*MachineContainers) ProtoMessage() {}
func (x *MachineContainers) ProtoReflect() protoreflect.Message {
mi := &file_internal_machine_api_pb_docker_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 MachineContainers.ProtoReflect.Descriptor instead.
func (*MachineContainers) Descriptor() ([]byte, []int) {
return file_internal_machine_api_pb_docker_proto_rawDescGZIP(), []int{5}
}
func (x *MachineContainers) GetMetadata() *Metadata {
if x != nil {
return x.Metadata
}
return nil
}
func (x *MachineContainers) GetContainers() []byte {
if x != nil {
return x.Containers
}
return nil
}
type RemoveContainerRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
// JSON serialized container.RemoveOptions.
Options []byte `protobuf:"bytes,2,opt,name=options,proto3" json:"options,omitempty"`
}
func (x *RemoveContainerRequest) Reset() {
*x = RemoveContainerRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[6]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *RemoveContainerRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*RemoveContainerRequest) ProtoMessage() {}
func (x *RemoveContainerRequest) ProtoReflect() protoreflect.Message {
mi := &file_internal_machine_api_pb_docker_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 RemoveContainerRequest.ProtoReflect.Descriptor instead.
func (*RemoveContainerRequest) Descriptor() ([]byte, []int) {
return file_internal_machine_api_pb_docker_proto_rawDescGZIP(), []int{6}
}
func (x *RemoveContainerRequest) GetId() string {
if x != nil {
return x.Id
}
return ""
}
func (x *RemoveContainerRequest) GetOptions() []byte {
if x != nil {
return x.Options
}
return nil
}
type PullImageRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Image string `protobuf:"bytes,1,opt,name=image,proto3" json:"image,omitempty"`
// JSON serialized image.PullOptions.
Options []byte `protobuf:"bytes,2,opt,name=options,proto3" json:"options,omitempty"`
}
func (x *PullImageRequest) Reset() {
*x = PullImageRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[7]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *PullImageRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*PullImageRequest) ProtoMessage() {}
func (x *PullImageRequest) ProtoReflect() protoreflect.Message {
mi := &file_internal_machine_api_pb_docker_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 PullImageRequest.ProtoReflect.Descriptor instead.
func (*PullImageRequest) Descriptor() ([]byte, []int) {
return file_internal_machine_api_pb_docker_proto_rawDescGZIP(), []int{7}
}
func (x *PullImageRequest) GetImage() string {
if x != nil {
return x.Image
}
return ""
}
func (x *PullImageRequest) GetOptions() []byte {
if x != nil {
return x.Options
}
return nil
}
type JSONMessage struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
// JSON serialized jsonmessage.JSONMessage.
Message []byte `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"`
}
func (x *JSONMessage) Reset() {
*x = JSONMessage{}
if protoimpl.UnsafeEnabled {
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[8]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *JSONMessage) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*JSONMessage) ProtoMessage() {}
func (x *JSONMessage) ProtoReflect() protoreflect.Message {
mi := &file_internal_machine_api_pb_docker_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 JSONMessage.ProtoReflect.Descriptor instead.
func (*JSONMessage) Descriptor() ([]byte, []int) {
return file_internal_machine_api_pb_docker_proto_rawDescGZIP(), []int{8}
}
func (x *JSONMessage) GetMessage() []byte {
if x != nil {
return x.Message
}
return nil
}
var File_internal_machine_api_pb_docker_proto protoreflect.FileDescriptor
var file_internal_machine_api_pb_docker_proto_rawDesc = []byte{
0x0a, 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, 0x64, 0x6f, 0x63, 0x6b, 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, 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, 0xa8,
0x01, 0x0a, 0x16, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e,
0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x63, 0x6f, 0x6e,
0x66, 0x69, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69,
0x67, 0x12, 0x1f, 0x0a, 0x0b, 0x68, 0x6f, 0x73, 0x74, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67,
0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0a, 0x68, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66,
0x69, 0x67, 0x12, 0x25, 0x0a, 0x0e, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x5f, 0x63, 0x6f,
0x6e, 0x66, 0x69, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0d, 0x6e, 0x65, 0x74, 0x77,
0x6f, 0x72, 0x6b, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x6c, 0x61,
0x74, 0x66, 0x6f, 0x72, 0x6d, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, 0x70, 0x6c, 0x61,
0x74, 0x66, 0x6f, 0x72, 0x6d, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x05, 0x20,
0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x35, 0x0a, 0x17, 0x43, 0x72, 0x65,
0x61, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70,
0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
0x22, 0x41, 0x0a, 0x15, 0x53, 0x74, 0x61, 0x72, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e,
0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18,
0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x6f, 0x70, 0x74,
0x69, 0x6f, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x6f, 0x70, 0x74, 0x69,
0x6f, 0x6e, 0x73, 0x22, 0x31, 0x0a, 0x15, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61,
0x69, 0x6e, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x07,
0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x6f,
0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x4c, 0x0a, 0x16, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f,
0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
0x12, 0x32, 0x0a, 0x08, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03,
0x28, 0x0b, 0x32, 0x16, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65,
0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x52, 0x08, 0x6d, 0x65, 0x73, 0x73,
0x61, 0x67, 0x65, 0x73, 0x22, 0x5e, 0x0a, 0x11, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x43,
0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x12, 0x29, 0x0a, 0x08, 0x6d, 0x65, 0x74,
0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x61, 0x70,
0x69, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61,
0x64, 0x61, 0x74, 0x61, 0x12, 0x1e, 0x0a, 0x0a, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65,
0x72, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0a, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69,
0x6e, 0x65, 0x72, 0x73, 0x22, 0x42, 0x0a, 0x16, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x43, 0x6f,
0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e,
0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x18,
0x0a, 0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52,
0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x42, 0x0a, 0x10, 0x50, 0x75, 0x6c, 0x6c,
0x49, 0x6d, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x14, 0x0a, 0x05,
0x69, 0x6d, 0x61, 0x67, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x69, 0x6d, 0x61,
0x67, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x02, 0x20,
0x01, 0x28, 0x0c, 0x52, 0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x27, 0x0a, 0x0b,
0x4a, 0x53, 0x4f, 0x4e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x6d,
0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x6d, 0x65,
0x73, 0x73, 0x61, 0x67, 0x65, 0x32, 0xe7, 0x02, 0x0a, 0x06, 0x44, 0x6f, 0x63, 0x6b, 0x65, 0x72,
0x12, 0x4c, 0x0a, 0x0f, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69,
0x6e, 0x65, 0x72, 0x12, 0x1b, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65,
0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
0x1a, 0x1c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6e,
0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x44,
0x0a, 0x0e, 0x53, 0x74, 0x61, 0x72, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72,
0x12, 0x1a, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x53, 0x74, 0x61, 0x72, 0x74, 0x43, 0x6f, 0x6e, 0x74,
0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67,
0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45,
0x6d, 0x70, 0x74, 0x79, 0x12, 0x49, 0x0a, 0x0e, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x74,
0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x12, 0x1a, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73,
0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65,
0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, 0x6e,
0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12,
0x46, 0x0a, 0x0f, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e,
0x65, 0x72, 0x12, 0x1b, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x43,
0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a,
0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75,
0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x36, 0x0a, 0x09, 0x50, 0x75, 0x6c, 0x6c, 0x49,
0x6d, 0x61, 0x67, 0x65, 0x12, 0x15, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x50, 0x75, 0x6c, 0x6c, 0x49,
0x6d, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x10, 0x2e, 0x61, 0x70,
0x69, 0x2e, 0x4a, 0x53, 0x4f, 0x4e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x30, 0x01, 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 (
file_internal_machine_api_pb_docker_proto_rawDescOnce sync.Once
file_internal_machine_api_pb_docker_proto_rawDescData = file_internal_machine_api_pb_docker_proto_rawDesc
)
func file_internal_machine_api_pb_docker_proto_rawDescGZIP() []byte {
file_internal_machine_api_pb_docker_proto_rawDescOnce.Do(func() {
file_internal_machine_api_pb_docker_proto_rawDescData = protoimpl.X.CompressGZIP(file_internal_machine_api_pb_docker_proto_rawDescData)
})
return file_internal_machine_api_pb_docker_proto_rawDescData
}
var file_internal_machine_api_pb_docker_proto_msgTypes = make([]protoimpl.MessageInfo, 9)
var file_internal_machine_api_pb_docker_proto_goTypes = []any{
(*CreateContainerRequest)(nil), // 0: api.CreateContainerRequest
(*CreateContainerResponse)(nil), // 1: api.CreateContainerResponse
(*StartContainerRequest)(nil), // 2: api.StartContainerRequest
(*ListContainersRequest)(nil), // 3: api.ListContainersRequest
(*ListContainersResponse)(nil), // 4: api.ListContainersResponse
(*MachineContainers)(nil), // 5: api.MachineContainers
(*RemoveContainerRequest)(nil), // 6: api.RemoveContainerRequest
(*PullImageRequest)(nil), // 7: api.PullImageRequest
(*JSONMessage)(nil), // 8: api.JSONMessage
(*Metadata)(nil), // 9: api.Metadata
(*emptypb.Empty)(nil), // 10: google.protobuf.Empty
}
var file_internal_machine_api_pb_docker_proto_depIdxs = []int32{
5, // 0: api.ListContainersResponse.messages:type_name -> api.MachineContainers
9, // 1: api.MachineContainers.metadata:type_name -> api.Metadata
0, // 2: api.Docker.CreateContainer:input_type -> api.CreateContainerRequest
2, // 3: api.Docker.StartContainer:input_type -> api.StartContainerRequest
3, // 4: api.Docker.ListContainers:input_type -> api.ListContainersRequest
6, // 5: api.Docker.RemoveContainer:input_type -> api.RemoveContainerRequest
7, // 6: api.Docker.PullImage:input_type -> api.PullImageRequest
1, // 7: api.Docker.CreateContainer:output_type -> api.CreateContainerResponse
10, // 8: api.Docker.StartContainer:output_type -> google.protobuf.Empty
4, // 9: api.Docker.ListContainers:output_type -> api.ListContainersResponse
10, // 10: api.Docker.RemoveContainer:output_type -> google.protobuf.Empty
8, // 11: api.Docker.PullImage:output_type -> api.JSONMessage
7, // [7:12] is the sub-list for method output_type
2, // [2:7] is the sub-list for method input_type
2, // [2:2] is the sub-list for extension type_name
2, // [2:2] is the sub-list for extension extendee
0, // [0:2] is the sub-list for field type_name
}
func init() { file_internal_machine_api_pb_docker_proto_init() }
func file_internal_machine_api_pb_docker_proto_init() {
if File_internal_machine_api_pb_docker_proto != nil {
return
}
file_internal_machine_api_pb_common_proto_init()
if !protoimpl.UnsafeEnabled {
file_internal_machine_api_pb_docker_proto_msgTypes[0].Exporter = func(v any, i int) any {
switch v := v.(*CreateContainerRequest); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_internal_machine_api_pb_docker_proto_msgTypes[1].Exporter = func(v any, i int) any {
switch v := v.(*CreateContainerResponse); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_internal_machine_api_pb_docker_proto_msgTypes[2].Exporter = func(v any, i int) any {
switch v := v.(*StartContainerRequest); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_internal_machine_api_pb_docker_proto_msgTypes[3].Exporter = func(v any, i int) any {
switch v := v.(*ListContainersRequest); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_internal_machine_api_pb_docker_proto_msgTypes[4].Exporter = func(v any, i int) any {
switch v := v.(*ListContainersResponse); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_internal_machine_api_pb_docker_proto_msgTypes[5].Exporter = func(v any, i int) any {
switch v := v.(*MachineContainers); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_internal_machine_api_pb_docker_proto_msgTypes[6].Exporter = func(v any, i int) any {
switch v := v.(*RemoveContainerRequest); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_internal_machine_api_pb_docker_proto_msgTypes[7].Exporter = func(v any, i int) any {
switch v := v.(*PullImageRequest); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_internal_machine_api_pb_docker_proto_msgTypes[8].Exporter = func(v any, i int) any {
switch v := v.(*JSONMessage); 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_docker_proto_rawDesc,
NumEnums: 0,
NumMessages: 9,
NumExtensions: 0,
NumServices: 1,
},
GoTypes: file_internal_machine_api_pb_docker_proto_goTypes,
DependencyIndexes: file_internal_machine_api_pb_docker_proto_depIdxs,
MessageInfos: file_internal_machine_api_pb_docker_proto_msgTypes,
}.Build()
File_internal_machine_api_pb_docker_proto = out.File
file_internal_machine_api_pb_docker_proto_rawDesc = nil
file_internal_machine_api_pb_docker_proto_goTypes = nil
file_internal_machine_api_pb_docker_proto_depIdxs = nil
}
+72
View File
@@ -0,0 +1,72 @@
syntax = "proto3";
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";
service Docker {
rpc CreateContainer(CreateContainerRequest) returns (CreateContainerResponse);
rpc StartContainer(StartContainerRequest) returns (google.protobuf.Empty);
rpc ListContainers(ListContainersRequest) returns (ListContainersResponse);
rpc RemoveContainer(RemoveContainerRequest) returns (google.protobuf.Empty);
rpc PullImage(PullImageRequest) returns (stream JSONMessage);
}
message CreateContainerRequest {
// JSON serialized container.Config.
bytes config = 1;
// JSON serialized container.HostConfig.
bytes host_config = 2;
// JSON serialized network.NetworkingConfig.
bytes network_config = 3;
// JSON serialized ocispec.Platform.
bytes platform = 4;
string name = 5;
}
message CreateContainerResponse {
// JSON serialized container.CreateResponse.
bytes response = 1;
}
message StartContainerRequest {
string id = 1;
// JSON serialized container.StartOptions.
bytes options = 2;
}
message ListContainersRequest {
// JSON serialized container.ListOptions.
bytes options = 1;
}
// ListContainersResponse structure allows broadcasting ListContainers requests to multiple machines.
message ListContainersResponse {
repeated MachineContainers messages = 1;
}
message MachineContainers {
Metadata metadata = 1;
// JSON serialized []container.Summary.
bytes containers = 2;
}
message RemoveContainerRequest {
string id = 1;
// JSON serialized container.RemoveOptions.
bytes options = 2;
}
message PullImageRequest {
string image = 1;
// JSON serialized image.PullOptions.
bytes options = 2;
}
message JSONMessage {
// JSON serialized jsonmessage.JSONMessage.
bytes message = 1;
}
+278
View File
@@ -0,0 +1,278 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.5.1
// - protoc v5.27.3
// source: internal/machine/api/pb/docker.proto
package pb
import (
context "context"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
emptypb "google.golang.org/protobuf/types/known/emptypb"
)
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
// Requires gRPC-Go v1.64.0 or later.
const _ = grpc.SupportPackageIsVersion9
const (
Docker_CreateContainer_FullMethodName = "/api.Docker/CreateContainer"
Docker_StartContainer_FullMethodName = "/api.Docker/StartContainer"
Docker_ListContainers_FullMethodName = "/api.Docker/ListContainers"
Docker_RemoveContainer_FullMethodName = "/api.Docker/RemoveContainer"
Docker_PullImage_FullMethodName = "/api.Docker/PullImage"
)
// DockerClient is the client API for Docker 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 DockerClient interface {
CreateContainer(ctx context.Context, in *CreateContainerRequest, opts ...grpc.CallOption) (*CreateContainerResponse, error)
StartContainer(ctx context.Context, in *StartContainerRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
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)
}
type dockerClient struct {
cc grpc.ClientConnInterface
}
func NewDockerClient(cc grpc.ClientConnInterface) DockerClient {
return &dockerClient{cc}
}
func (c *dockerClient) CreateContainer(ctx context.Context, in *CreateContainerRequest, opts ...grpc.CallOption) (*CreateContainerResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(CreateContainerResponse)
err := c.cc.Invoke(ctx, Docker_CreateContainer_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *dockerClient) StartContainer(ctx context.Context, in *StartContainerRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(emptypb.Empty)
err := c.cc.Invoke(ctx, Docker_StartContainer_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *dockerClient) ListContainers(ctx context.Context, in *ListContainersRequest, opts ...grpc.CallOption) (*ListContainersResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ListContainersResponse)
err := c.cc.Invoke(ctx, Docker_ListContainers_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *dockerClient) RemoveContainer(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_RemoveContainer_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *dockerClient) PullImage(ctx context.Context, in *PullImageRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[JSONMessage], error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
stream, err := c.cc.NewStream(ctx, &Docker_ServiceDesc.Streams[0], Docker_PullImage_FullMethodName, cOpts...)
if err != nil {
return nil, err
}
x := &grpc.GenericClientStream[PullImageRequest, JSONMessage]{ClientStream: stream}
if err := x.ClientStream.SendMsg(in); err != nil {
return nil, err
}
if err := x.ClientStream.CloseSend(); err != nil {
return nil, err
}
return x, nil
}
// 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]
// DockerServer is the server API for Docker service.
// All implementations must embed UnimplementedDockerServer
// for forward compatibility.
type DockerServer interface {
CreateContainer(context.Context, *CreateContainerRequest) (*CreateContainerResponse, error)
StartContainer(context.Context, *StartContainerRequest) (*emptypb.Empty, error)
ListContainers(context.Context, *ListContainersRequest) (*ListContainersResponse, error)
RemoveContainer(context.Context, *RemoveContainerRequest) (*emptypb.Empty, error)
PullImage(*PullImageRequest, grpc.ServerStreamingServer[JSONMessage]) error
mustEmbedUnimplementedDockerServer()
}
// UnimplementedDockerServer must be embedded to have
// forward compatible implementations.
//
// NOTE: this should be embedded by value instead of pointer to avoid a nil
// pointer dereference when methods are called.
type UnimplementedDockerServer struct{}
func (UnimplementedDockerServer) CreateContainer(context.Context, *CreateContainerRequest) (*CreateContainerResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method CreateContainer not implemented")
}
func (UnimplementedDockerServer) StartContainer(context.Context, *StartContainerRequest) (*emptypb.Empty, error) {
return nil, status.Errorf(codes.Unimplemented, "method StartContainer not implemented")
}
func (UnimplementedDockerServer) ListContainers(context.Context, *ListContainersRequest) (*ListContainersResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ListContainers not implemented")
}
func (UnimplementedDockerServer) RemoveContainer(context.Context, *RemoveContainerRequest) (*emptypb.Empty, error) {
return nil, status.Errorf(codes.Unimplemented, "method RemoveContainer not implemented")
}
func (UnimplementedDockerServer) PullImage(*PullImageRequest, grpc.ServerStreamingServer[JSONMessage]) error {
return status.Errorf(codes.Unimplemented, "method PullImage not implemented")
}
func (UnimplementedDockerServer) mustEmbedUnimplementedDockerServer() {}
func (UnimplementedDockerServer) testEmbeddedByValue() {}
// UnsafeDockerServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to DockerServer will
// result in compilation errors.
type UnsafeDockerServer interface {
mustEmbedUnimplementedDockerServer()
}
func RegisterDockerServer(s grpc.ServiceRegistrar, srv DockerServer) {
// If the following call pancis, it indicates UnimplementedDockerServer was
// embedded by pointer and is nil. This will cause panics if an
// unimplemented method is ever invoked, so we test this at initialization
// time to prevent it from happening at runtime later due to I/O.
if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
t.testEmbeddedByValue()
}
s.RegisterService(&Docker_ServiceDesc, srv)
}
func _Docker_CreateContainer_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(CreateContainerRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(DockerServer).CreateContainer(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: Docker_CreateContainer_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(DockerServer).CreateContainer(ctx, req.(*CreateContainerRequest))
}
return interceptor(ctx, in, info, handler)
}
func _Docker_StartContainer_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(StartContainerRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(DockerServer).StartContainer(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: Docker_StartContainer_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(DockerServer).StartContainer(ctx, req.(*StartContainerRequest))
}
return interceptor(ctx, in, info, handler)
}
func _Docker_ListContainers_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ListContainersRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(DockerServer).ListContainers(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: Docker_ListContainers_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(DockerServer).ListContainers(ctx, req.(*ListContainersRequest))
}
return interceptor(ctx, in, info, handler)
}
func _Docker_RemoveContainer_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).RemoveContainer(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: Docker_RemoveContainer_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(DockerServer).RemoveContainer(ctx, req.(*RemoveContainerRequest))
}
return interceptor(ctx, in, info, handler)
}
func _Docker_PullImage_Handler(srv interface{}, stream grpc.ServerStream) error {
m := new(PullImageRequest)
if err := stream.RecvMsg(m); err != nil {
return err
}
return srv.(DockerServer).PullImage(m, &grpc.GenericServerStream[PullImageRequest, JSONMessage]{ServerStream: stream})
}
// 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]
// 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)
var Docker_ServiceDesc = grpc.ServiceDesc{
ServiceName: "api.Docker",
HandlerType: (*DockerServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "CreateContainer",
Handler: _Docker_CreateContainer_Handler,
},
{
MethodName: "StartContainer",
Handler: _Docker_StartContainer_Handler,
},
{
MethodName: "ListContainers",
Handler: _Docker_ListContainers_Handler,
},
{
MethodName: "RemoveContainer",
Handler: _Docker_RemoveContainer_Handler,
},
},
Streams: []grpc.StreamDesc{
{
StreamName: "PullImage",
Handler: _Docker_PullImage_Handler,
ServerStreams: true,
},
},
Metadata: "internal/machine/api/pb/docker.proto",
}
+564 -97
View File
@@ -21,6 +21,140 @@ const (
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
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"`
}
func (x *MachineInfo) Reset() {
*x = MachineInfo{}
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 *MachineInfo) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*MachineInfo) ProtoMessage() {}
func (x *MachineInfo) 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 MachineInfo.ProtoReflect.Descriptor instead.
func (*MachineInfo) Descriptor() ([]byte, []int) {
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{0}
}
func (x *MachineInfo) GetId() string {
if x != nil {
return x.Id
}
return ""
}
func (x *MachineInfo) GetName() string {
if x != nil {
return x.Name
}
return ""
}
func (x *MachineInfo) GetNetwork() *NetworkConfig {
if x != nil {
return x.Network
}
return nil
}
type NetworkConfig struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
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"`
}
func (x *NetworkConfig) Reset() {
*x = NetworkConfig{}
if protoimpl.UnsafeEnabled {
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *NetworkConfig) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*NetworkConfig) ProtoMessage() {}
func (x *NetworkConfig) ProtoReflect() protoreflect.Message {
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 {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use NetworkConfig.ProtoReflect.Descriptor instead.
func (*NetworkConfig) Descriptor() ([]byte, []int) {
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{1}
}
func (x *NetworkConfig) GetSubnet() *IPPrefix {
if x != nil {
return x.Subnet
}
return nil
}
func (x *NetworkConfig) GetManagementIp() *IP {
if x != nil {
return x.ManagementIp
}
return nil
}
func (x *NetworkConfig) GetEndpoints() []*IPPort {
if x != nil {
return x.Endpoints
}
return nil
}
func (x *NetworkConfig) GetPublicKey() []byte {
if x != nil {
return x.PublicKey
}
return nil
}
type InitClusterRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
@@ -28,13 +162,12 @@ 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"`
User *User `protobuf:"bytes,3,opt,name=user,proto3" json:"user,omitempty"`
}
func (x *InitClusterRequest) Reset() {
*x = InitClusterRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[0]
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -47,7 +180,7 @@ func (x *InitClusterRequest) String() string {
func (*InitClusterRequest) ProtoMessage() {}
func (x *InitClusterRequest) ProtoReflect() protoreflect.Message {
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[0]
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 {
@@ -60,7 +193,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{0}
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{2}
}
func (x *InitClusterRequest) GetMachineName() string {
@@ -77,13 +210,6 @@ func (x *InitClusterRequest) GetNetwork() *IPPrefix {
return nil
}
func (x *InitClusterRequest) GetUser() *User {
if x != nil {
return x.User
}
return nil
}
type InitClusterResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
@@ -95,7 +221,7 @@ type InitClusterResponse struct {
func (x *InitClusterResponse) Reset() {
*x = InitClusterResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[1]
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -108,7 +234,7 @@ func (x *InitClusterResponse) String() string {
func (*InitClusterResponse) ProtoMessage() {}
func (x *InitClusterResponse) ProtoReflect() protoreflect.Message {
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[1]
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 {
@@ -121,7 +247,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{1}
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{3}
}
func (x *InitClusterResponse) GetMachine() *MachineInfo {
@@ -143,7 +269,7 @@ type JoinClusterRequest struct {
func (x *JoinClusterRequest) Reset() {
*x = JoinClusterRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[2]
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -156,7 +282,7 @@ func (x *JoinClusterRequest) String() string {
func (*JoinClusterRequest) ProtoMessage() {}
func (x *JoinClusterRequest) ProtoReflect() protoreflect.Message {
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[2]
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 {
@@ -169,7 +295,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{2}
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{4}
}
func (x *JoinClusterRequest) GetMachine() *MachineInfo {
@@ -197,7 +323,7 @@ type TokenResponse struct {
func (x *TokenResponse) Reset() {
*x = TokenResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[3]
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[5]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -210,7 +336,7 @@ func (x *TokenResponse) String() string {
func (*TokenResponse) ProtoMessage() {}
func (x *TokenResponse) ProtoReflect() protoreflect.Message {
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[3]
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 {
@@ -223,7 +349,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{3}
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{5}
}
func (x *TokenResponse) GetToken() string {
@@ -233,6 +359,227 @@ func (x *TokenResponse) GetToken() string {
return ""
}
type Service 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"`
Mode string `protobuf:"bytes,3,opt,name=mode,proto3" json:"mode,omitempty"`
Containers []*Service_Container `protobuf:"bytes,4,rep,name=containers,proto3" json:"containers,omitempty"`
}
func (x *Service) Reset() {
*x = Service{}
if protoimpl.UnsafeEnabled {
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[6]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *Service) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Service) ProtoMessage() {}
func (x *Service) ProtoReflect() protoreflect.Message {
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 {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Service.ProtoReflect.Descriptor instead.
func (*Service) Descriptor() ([]byte, []int) {
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{6}
}
func (x *Service) GetId() string {
if x != nil {
return x.Id
}
return ""
}
func (x *Service) GetName() string {
if x != nil {
return x.Name
}
return ""
}
func (x *Service) GetMode() string {
if x != nil {
return x.Mode
}
return ""
}
func (x *Service) GetContainers() []*Service_Container {
if x != nil {
return x.Containers
}
return nil
}
type InspectServiceRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
}
func (x *InspectServiceRequest) Reset() {
*x = InspectServiceRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[7]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *InspectServiceRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*InspectServiceRequest) ProtoMessage() {}
func (x *InspectServiceRequest) ProtoReflect() protoreflect.Message {
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[7]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use InspectServiceRequest.ProtoReflect.Descriptor instead.
func (*InspectServiceRequest) Descriptor() ([]byte, []int) {
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{7}
}
func (x *InspectServiceRequest) GetId() string {
if x != nil {
return x.Id
}
return ""
}
type InspectServiceResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Service *Service `protobuf:"bytes,1,opt,name=service,proto3" json:"service,omitempty"`
}
func (x *InspectServiceResponse) Reset() {
*x = InspectServiceResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[8]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *InspectServiceResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*InspectServiceResponse) ProtoMessage() {}
func (x *InspectServiceResponse) ProtoReflect() protoreflect.Message {
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 {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use InspectServiceResponse.ProtoReflect.Descriptor instead.
func (*InspectServiceResponse) Descriptor() ([]byte, []int) {
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{8}
}
func (x *InspectServiceResponse) GetService() *Service {
if x != nil {
return x.Service
}
return nil
}
type Service_Container struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
MachineId string `protobuf:"bytes,1,opt,name=machine_id,json=machineId,proto3" json:"machine_id,omitempty"`
// JSON encoded Docker types.Container.
Container []byte `protobuf:"bytes,2,opt,name=container,proto3" json:"container,omitempty"`
}
func (x *Service_Container) Reset() {
*x = Service_Container{}
if protoimpl.UnsafeEnabled {
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[9]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *Service_Container) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Service_Container) ProtoMessage() {}
func (x *Service_Container) ProtoReflect() protoreflect.Message {
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 {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// 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}
}
func (x *Service_Container) GetMachineId() string {
if x != nil {
return x.MachineId
}
return ""
}
func (x *Service_Container) GetContainer() []byte {
if x != nil {
return x.Container
}
return nil
}
var File_internal_machine_api_pb_machine_proto protoreflect.FileDescriptor
var file_internal_machine_api_pb_machine_proto_rawDesc = []byte{
@@ -242,52 +589,88 @@ var file_internal_machine_api_pb_machine_proto_rawDesc = []byte{
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, 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, 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, 0x63, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72,
0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x7e, 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, 0x1d, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18,
0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x09, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x55, 0x73, 0x65, 0x72,
0x52, 0x04, 0x75, 0x73, 0x65, 0x72, 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,
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, 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, 0x32, 0xf5, 0x01, 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, 0x42, 0x37, 0x5a, 0x35, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f,
0x6d, 0x2f, 0x70, 0x73, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x6b, 0x69, 0x2f, 0x75, 0x6e, 0x63,
0x6c, 0x6f, 0x75, 0x64, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x6d, 0x61,
0x63, 0x68, 0x69, 0x6e, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72,
0x6f, 0x74, 0x6f, 0x33,
0x52, 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,
}
var (
@@ -302,36 +685,49 @@ 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, 4)
var file_internal_machine_api_pb_machine_proto_msgTypes = make([]protoimpl.MessageInfo, 10)
var file_internal_machine_api_pb_machine_proto_goTypes = []any{
(*InitClusterRequest)(nil), // 0: api.InitClusterRequest
(*InitClusterResponse)(nil), // 1: api.InitClusterResponse
(*JoinClusterRequest)(nil), // 2: api.JoinClusterRequest
(*TokenResponse)(nil), // 3: api.TokenResponse
(*IPPrefix)(nil), // 4: api.IPPrefix
(*User)(nil), // 5: api.User
(*MachineInfo)(nil), // 6: api.MachineInfo
(*emptypb.Empty)(nil), // 7: google.protobuf.Empty
(*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
}
var file_internal_machine_api_pb_machine_proto_depIdxs = []int32{
4, // 0: api.InitClusterRequest.network:type_name -> api.IPPrefix
5, // 1: api.InitClusterRequest.user:type_name -> api.User
6, // 2: api.InitClusterResponse.machine:type_name -> api.MachineInfo
6, // 3: api.JoinClusterRequest.machine:type_name -> api.MachineInfo
6, // 4: api.JoinClusterRequest.other_machines:type_name -> api.MachineInfo
0, // 5: api.Machine.InitCluster:input_type -> api.InitClusterRequest
2, // 6: api.Machine.JoinCluster:input_type -> api.JoinClusterRequest
7, // 7: api.Machine.Token:input_type -> google.protobuf.Empty
7, // 8: api.Machine.Inspect:input_type -> google.protobuf.Empty
1, // 9: api.Machine.InitCluster:output_type -> api.InitClusterResponse
7, // 10: api.Machine.JoinCluster:output_type -> google.protobuf.Empty
3, // 11: api.Machine.Token:output_type -> api.TokenResponse
6, // 12: api.Machine.Inspect:output_type -> api.MachineInfo
9, // [9:13] is the sub-list for method output_type
5, // [5:9] 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
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
}
func init() { file_internal_machine_api_pb_machine_proto_init() }
@@ -340,10 +736,9 @@ func file_internal_machine_api_pb_machine_proto_init() {
return
}
file_internal_machine_api_pb_common_proto_init()
file_internal_machine_api_pb_cluster_proto_init()
if !protoimpl.UnsafeEnabled {
file_internal_machine_api_pb_machine_proto_msgTypes[0].Exporter = func(v any, i int) any {
switch v := v.(*InitClusterRequest); i {
switch v := v.(*MachineInfo); i {
case 0:
return &v.state
case 1:
@@ -355,7 +750,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.(*InitClusterResponse); i {
switch v := v.(*NetworkConfig); i {
case 0:
return &v.state
case 1:
@@ -367,7 +762,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.(*JoinClusterRequest); i {
switch v := v.(*InitClusterRequest); i {
case 0:
return &v.state
case 1:
@@ -379,6 +774,30 @@ 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 {
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[4].Exporter = func(v any, i int) any {
switch v := v.(*JoinClusterRequest); 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[5].Exporter = func(v any, i int) any {
switch v := v.(*TokenResponse); i {
case 0:
return &v.state
@@ -390,6 +809,54 @@ func file_internal_machine_api_pb_machine_proto_init() {
return nil
}
}
file_internal_machine_api_pb_machine_proto_msgTypes[6].Exporter = func(v any, i int) any {
switch v := v.(*Service); 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[7].Exporter = func(v any, i int) any {
switch v := v.(*InspectServiceRequest); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_internal_machine_api_pb_machine_proto_msgTypes[8].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[9].Exporter = func(v any, i int) any {
switch v := v.(*Service_Container); 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{
@@ -397,7 +864,7 @@ func file_internal_machine_api_pb_machine_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_internal_machine_api_pb_machine_proto_rawDesc,
NumEnums: 0,
NumMessages: 4,
NumMessages: 10,
NumExtensions: 0,
NumServices: 1,
},
+36 -2
View File
@@ -6,19 +6,31 @@ 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/cluster.proto";
service Machine {
rpc InitCluster(InitClusterRequest) returns (InitClusterResponse);
rpc JoinCluster(JoinClusterRequest) returns (google.protobuf.Empty);
rpc Token(google.protobuf.Empty) returns (TokenResponse);
rpc Inspect(google.protobuf.Empty) returns (MachineInfo);
rpc InspectService(InspectServiceRequest) returns (InspectServiceResponse);
}
message MachineInfo {
string id = 1;
string name = 2;
NetworkConfig network = 3;
}
message NetworkConfig {
IPPrefix subnet = 1;
IP management_ip = 2;
repeated IPPort endpoints = 3;
bytes publicKey = 4;
}
message InitClusterRequest {
string machineName = 1;
IPPrefix network = 2;
User user = 3;
}
message InitClusterResponse {
@@ -33,3 +45,25 @@ message JoinClusterRequest {
message TokenResponse {
string token = 1;
}
message Service {
string id = 1;
string name = 2;
string mode = 3;
message Container {
string machine_id = 1;
// JSON encoded Docker types.Container.
bytes container = 2;
}
repeated Container containers = 4;
}
message InspectServiceRequest {
string id = 1;
}
message InspectServiceResponse {
Service service = 1;
}
+42 -4
View File
@@ -20,10 +20,11 @@ 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_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.
@@ -34,6 +35,7 @@ type MachineClient interface {
JoinCluster(ctx context.Context, in *JoinClusterRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
Token(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*TokenResponse, error)
Inspect(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*MachineInfo, error)
InspectService(ctx context.Context, in *InspectServiceRequest, opts ...grpc.CallOption) (*InspectServiceResponse, error)
}
type machineClient struct {
@@ -84,6 +86,16 @@ func (c *machineClient) Inspect(ctx context.Context, in *emptypb.Empty, opts ...
return out, nil
}
func (c *machineClient) InspectService(ctx context.Context, in *InspectServiceRequest, opts ...grpc.CallOption) (*InspectServiceResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(InspectServiceResponse)
err := c.cc.Invoke(ctx, Machine_InspectService_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
// MachineServer is the server API for Machine service.
// All implementations must embed UnimplementedMachineServer
// for forward compatibility.
@@ -92,6 +104,7 @@ type MachineServer interface {
JoinCluster(context.Context, *JoinClusterRequest) (*emptypb.Empty, error)
Token(context.Context, *emptypb.Empty) (*TokenResponse, error)
Inspect(context.Context, *emptypb.Empty) (*MachineInfo, error)
InspectService(context.Context, *InspectServiceRequest) (*InspectServiceResponse, error)
mustEmbedUnimplementedMachineServer()
}
@@ -114,6 +127,9 @@ func (UnimplementedMachineServer) Token(context.Context, *emptypb.Empty) (*Token
func (UnimplementedMachineServer) Inspect(context.Context, *emptypb.Empty) (*MachineInfo, error) {
return nil, status.Errorf(codes.Unimplemented, "method Inspect not implemented")
}
func (UnimplementedMachineServer) InspectService(context.Context, *InspectServiceRequest) (*InspectServiceResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method InspectService not implemented")
}
func (UnimplementedMachineServer) mustEmbedUnimplementedMachineServer() {}
func (UnimplementedMachineServer) testEmbeddedByValue() {}
@@ -207,6 +223,24 @@ func _Machine_Inspect_Handler(srv interface{}, ctx context.Context, dec func(int
return interceptor(ctx, in, info, handler)
}
func _Machine_InspectService_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(InspectServiceRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(MachineServer).InspectService(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: Machine_InspectService_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(MachineServer).InspectService(ctx, req.(*InspectServiceRequest))
}
return interceptor(ctx, in, info, handler)
}
// Machine_ServiceDesc is the grpc.ServiceDesc for Machine service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
@@ -230,6 +264,10 @@ var Machine_ServiceDesc = grpc.ServiceDesc{
MethodName: "Inspect",
Handler: _Machine_Inspect_Handler,
},
{
MethodName: "InspectService",
Handler: _Machine_InspectService_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "internal/machine/api/pb/machine.proto",
+144
View File
@@ -0,0 +1,144 @@
package proxy
import (
"fmt"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/encoding/protowire"
"google.golang.org/protobuf/proto"
"uncloud/internal/machine/api/pb"
)
// One2ManyResponder converts upstream responses into messages from upstreams, so that multiple
// successful and failure responses might be returned in One2Many mode.
type One2ManyResponder struct {
machine string
}
// AppendInfo is called to enhance response from the backend with additional data.
//
// AppendInfo enhances upstream response with machine metadata (target).
//
// This method depends on grpc protobuf response structure, each response should
// look like:
//
// message SomeResponse {
// repeated SomeReply messages = 1; // please note field ID == 1
// }
//
// message SomeReply {
// common.Metadata metadata = 1;
// <other fields go here ...>
// }
//
// As 'SomeReply' is repeated in 'SomeResponse', if we concatenate protobuf representation
// of several 'SomeResponse' messages, we still get valid 'SomeResponse' representation but with more
// entries (feature of protobuf binary representation).
//
// If we look at binary representation of any unary 'SomeResponse' message, it will always contain one
// protobuf field with field ID 1 (see above) and type 2 (embedded message SomeReply is encoded
// as string with length). So if we want to add fields to 'SomeReply', we can simply read field
// header, adjust length for new 'SomeReply' representation, and prepend new field header.
//
// At the same time, we can add 'common.Metadata' structure to 'SomeReply' by simply
// appending or prepending 'common.Metadata' as a single field. This requires 'metadata'
// field to be not defined in original response. (This is due to the fact that protobuf message
// representation is concatenation of each field representation).
//
// To build only single field (Metadata) we use helper message which contains exactly this
// field with same field ID as in every other 'SomeReply':
//
// message Empty {
// common.Metadata metadata = 1;
// }
//
// As streaming replies are not wrapped into 'SomeResponse' with 'repeated', handling is simpler: we just
// need to append Empty with details.
//
// So AppendInfo does the following: validates that response contains field ID 1 encoded as string,
// cuts field header, rest is representation of some reply. Marshal 'Empty' as protobuf,
// which builds 'common.Metadata' field, append it to original response message, build new header
// for new length of some response, and add back new field header.
func (b *One2ManyResponder) AppendInfo(streaming bool, resp []byte) ([]byte, error) {
payload, err := proto.Marshal(&pb.Empty{
Metadata: &pb.Metadata{
Machine: b.machine,
},
})
if streaming {
return append(resp, payload...), err
}
const (
metadataField = 1 // field number in proto definition for repeated response
metadataType = 2 // "string" for embedded messages
)
// decode protobuf embedded header
typ, n1 := protowire.ConsumeVarint(resp)
if n1 < 0 {
return nil, protowire.ParseError(n1)
}
_, n2 := protowire.ConsumeVarint(resp[n1:]) // length
if n2 < 0 {
return nil, protowire.ParseError(n2)
}
if typ != (metadataField<<3)|metadataType {
return nil, fmt.Errorf("unexpected message format: %d", typ)
}
if n1+n2 > len(resp) {
return nil, fmt.Errorf("unexpected message size: %d", len(resp))
}
// cut off embedded message header
resp = resp[n1+n2:]
// build new embedded message header
prefix := protowire.AppendVarint(
protowire.AppendVarint(nil, (metadataField<<3)|metadataType),
uint64(len(resp)+len(payload)),
)
resp = append(prefix, resp...)
return append(resp, payload...), err
}
// BuildError converts upstream error into message from upstream, so that multiple
// successful and failure responses might be returned.
//
// This simply relies on the fact that any response contains 'Empty' message.
// So if 'Empty' is unmarshalled into any other reply message, all the fields
// are undefined but 'Metadata':
//
// message Empty {
// common.Metadata metadata = 1;
// }
//
// message EmptyResponse {
// repeated Empty messages = 1;
// }
//
// Streaming responses are not wrapped into Empty, so we simply marshall EmptyResponse
// message.
func (b *One2ManyResponder) BuildError(streaming bool, err error) ([]byte, error) {
var resp proto.Message = &pb.Empty{
Metadata: &pb.Metadata{
Machine: b.machine,
Error: err.Error(),
Status: status.Convert(err).Proto(),
},
}
if !streaming {
resp = &pb.EmptyResponse{
Messages: []*pb.Empty{
resp.(*pb.Empty),
},
}
}
return proto.Marshal(resp)
}
+125
View File
@@ -0,0 +1,125 @@
package proxy
import (
"context"
"github.com/siderolabs/grpc-proxy/proxy"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
"sync"
)
// Director manages routing of gRPC requests between local and remote backends.
type Director struct {
localBackend *LocalBackend
remotePort uint16
remoteBackends sync.Map
// mu synchronizes access to localAddress.
mu sync.RWMutex
localAddress string
}
func NewDirector(localSockPath string, remotePort uint16) *Director {
return &Director{
localBackend: NewLocalBackend(localSockPath, ""),
remotePort: remotePort,
}
}
// UpdateLocalAddress updates the local machine address used to identify which requests should be proxied
// to the local gRPC server.
func (d *Director) UpdateLocalAddress(addr string) {
d.mu.Lock()
defer d.mu.Unlock()
d.localAddress = addr
// Replace the local backend with the one that has local address set.
d.localBackend = NewLocalBackend(d.localBackend.sockPath, addr)
}
// Director implements proxy.StreamDirector for grpc-proxy, routing requests to local or remote backends based
// on gRPC metadata in the context. Each machine metadata is injected into the response messages by the proxy
// if the request is proxied to multiple backends.
func (d *Director) Director(ctx context.Context, fullMethodName string) (proxy.Mode, []proxy.Backend, error) {
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return proxy.One2One, []proxy.Backend{d.localBackend}, nil
}
// If the request is already proxied, send it to the local backend.
if _, ok = md["proxy-authority"]; ok {
return proxy.One2One, []proxy.Backend{d.localBackend}, nil
}
// If the request metadata doesn't contain machines to proxy to, send it to the local backend.
machines, ok := md["machines"]
if !ok {
return proxy.One2One, []proxy.Backend{d.localBackend}, nil
}
if len(machines) == 0 {
return proxy.One2One, nil, status.Error(codes.InvalidArgument, "no machines specified")
}
d.mu.RLock()
localAddress := d.localAddress
localBackend := d.localBackend
d.mu.RUnlock()
backends := make([]proxy.Backend, len(machines))
for i, addr := range machines {
if addr == localAddress {
backends[i] = localBackend
continue
}
backend, err := d.remoteBackend(addr)
if err != nil {
return proxy.One2One, nil, status.Error(codes.Internal, err.Error())
}
backends[i] = backend
}
if len(backends) == 1 {
return proxy.One2One, backends, nil
}
return proxy.One2Many, backends, nil
}
// remoteBackend returns a RemoteBackend for the given address from the cache or creates a new one.
func (d *Director) remoteBackend(addr string) (*RemoteBackend, error) {
b, ok := d.remoteBackends.Load(addr)
if ok {
return b.(*RemoteBackend), nil
}
backend, err := NewRemoteBackend(addr, d.remotePort)
if err != nil {
return nil, err
}
existing, loaded := d.remoteBackends.LoadOrStore(addr, backend)
if loaded {
// A concurrent remoteBackend call built a different backend.
backend.Close()
return existing.(*RemoteBackend), nil
}
return backend, nil
}
// FlushRemoteBackends closes all remote backend connections and removes them from the cache.
func (d *Director) FlushRemoteBackends() {
d.remoteBackends.Range(func(key, value interface{}) bool {
backend, ok := value.(*RemoteBackend)
if !ok {
return true
}
backend.Close()
d.remoteBackends.Delete(key)
return true
})
}
// Close closes all backend connections.
func (d *Director) Close() {
d.localBackend.Close()
d.FlushRemoteBackends()
}
+75
View File
@@ -0,0 +1,75 @@
package proxy
import (
"context"
"github.com/siderolabs/grpc-proxy/proxy"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/metadata"
"sync"
)
// LocalBackend is a proxy.One2ManyResponder implementation that proxies to a local gRPC server listening on a Unix socket.
type LocalBackend struct {
One2ManyResponder
sockPath string
mu sync.RWMutex
conn *grpc.ClientConn
}
var _ proxy.Backend = (*LocalBackend)(nil)
// NewLocalBackend returns a new LocalBackend for the given Unix socket path. The addr parameter is the local address
// of the current machine which could be empty if it's not known. The address is used to populate response metadata
// in one2many mode.
func NewLocalBackend(sockPath, addr string) *LocalBackend {
return &LocalBackend{
One2ManyResponder: One2ManyResponder{
machine: addr,
},
sockPath: sockPath,
}
}
func (b *LocalBackend) String() string {
return b.machine
}
// GetConnection returns a gRPC connection to the local server listening on the Unix socket.
func (b *LocalBackend) GetConnection(ctx context.Context, _ string) (context.Context, *grpc.ClientConn, error) {
md, _ := metadata.FromIncomingContext(ctx)
outCtx := metadata.NewOutgoingContext(ctx, md)
b.mu.RLock()
if b.conn != nil {
defer b.mu.RUnlock()
return outCtx, b.conn, nil
}
b.mu.RUnlock()
b.mu.Lock()
defer b.mu.Unlock()
var err error
b.conn, err = grpc.NewClient(
"unix://"+b.sockPath,
grpc.WithTransportCredentials(insecure.NewCredentials()),
grpc.WithDefaultCallOptions(
grpc.ForceCodecV2(proxy.Codec()),
),
)
return outCtx, b.conn, err
}
// Close closes the upstream gRPC connection.
func (b *LocalBackend) Close() {
b.mu.Lock()
defer b.mu.Unlock()
if b.conn != nil {
b.conn.Close()
b.conn = nil
}
}
+109
View File
@@ -0,0 +1,109 @@
package proxy
import (
"context"
"fmt"
"github.com/siderolabs/grpc-proxy/proxy"
"google.golang.org/grpc"
"google.golang.org/grpc/backoff"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/metadata"
"net/netip"
"sync"
"time"
)
// RemoteBackend is a proxy.One2ManyResponder implementation that proxies to a remote gRPC server, injecting machine metadata
// into the response.
//
// Based on the Talos apid implementation:
// https://github.com/siderolabs/talos/blob/59a78da42cdea8fbccc35d0851f9b0eef928261b/internal/app/apid/pkg/backend/apid.go
type RemoteBackend struct {
One2ManyResponder
target string
mu sync.RWMutex
conn *grpc.ClientConn
}
var _ proxy.Backend = (*RemoteBackend)(nil)
// NewRemoteBackend creates a new instance of RemoteBackend for the given IPv6 address and port.
func NewRemoteBackend(addr string, port uint16) (*RemoteBackend, error) {
ip, err := netip.ParseAddr(addr)
if err != nil || !ip.Is6() {
return nil, fmt.Errorf("address must be a valid IPv6 address: %s", addr)
}
return &RemoteBackend{
One2ManyResponder: One2ManyResponder{
machine: addr,
},
target: netip.AddrPortFrom(ip, port).String(),
}, nil
}
func (b *RemoteBackend) String() string {
return b.machine
}
// GetConnection returns a gRPC connection to the remote server.
func (b *RemoteBackend) GetConnection(ctx context.Context, _ string) (context.Context, *grpc.ClientConn, error) {
md, _ := metadata.FromIncomingContext(ctx)
if authority := md[":authority"]; len(authority) > 0 {
md.Set("proxy-authority", authority...)
} else {
md.Set("proxy-authority", "unknown")
}
delete(md, ":authority")
delete(md, "machines")
outCtx := metadata.NewOutgoingContext(ctx, md)
b.mu.RLock()
if b.conn != nil {
defer b.mu.RUnlock()
return outCtx, b.conn, nil
}
b.mu.RUnlock()
b.mu.Lock()
defer b.mu.Unlock()
// Override the max delay to avoid excessive backoff when the another node is unavailable, e.g. rebooted
// or the WireGuard connection is temporary down.
//
// Default max delay is 2 minutes, which is too long for our use case.
backoffConfig := backoff.DefaultConfig
// The maximum wait time between attempts.
backoffConfig.MaxDelay = 15 * time.Second
var err error
b.conn, err = grpc.NewClient(
b.target,
grpc.WithTransportCredentials(insecure.NewCredentials()),
grpc.WithConnectParams(grpc.ConnectParams{
Backoff: backoffConfig,
// Not published as a constant in gRPC library.
// See: https://github.com/grpc/grpc-go/blob/d5dee5fdbdeb52f6ea10b37b2cc7ce37814642d7/clientconn.go#L55-L56
// Each connection attempt can take up to MinConnectTimeout.
MinConnectTimeout: 20 * time.Second,
}),
grpc.WithDefaultCallOptions(
grpc.ForceCodecV2(proxy.Codec()),
),
)
return outCtx, b.conn, err
}
// Close closes the upstream gRPC connection.
func (b *RemoteBackend) Close() {
b.mu.Lock()
defer b.mu.Unlock()
if b.conn != nil {
b.conn.Close()
b.conn = nil
}
}
+49
View File
@@ -0,0 +1,49 @@
// Copyright 2024 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
syntax = "proto3";
package google.rpc;
import "google/protobuf/any.proto";
option cc_enable_arenas = true;
option go_package = "google.golang.org/genproto/googleapis/rpc/status;status";
option java_multiple_files = true;
option java_outer_classname = "StatusProto";
option java_package = "com.google.rpc";
option objc_class_prefix = "RPC";
// The `Status` type defines a logical error model that is suitable for
// different programming environments, including REST APIs and RPC APIs. It is
// used by [gRPC](https://github.com/grpc). Each `Status` message contains
// three pieces of data: error code, error message, and error details.
//
// You can find out more about this error model and how to work with it in the
// [API Design Guide](https://cloud.google.com/apis/design/errors).
message Status {
// The status code, which should be an enum value of
// [google.rpc.Code][google.rpc.Code].
int32 code = 1;
// A developer-facing error message, which should be in English. Any
// user-facing error message should be localized and sent in the
// [google.rpc.Status.details][google.rpc.Status.details] field, or localized
// by the client.
string message = 2;
// A list of messages that carry the error details. There is a common set of
// message types for APIs to use.
repeated google.protobuf.Any details = 3;
}
+213
View File
@@ -0,0 +1,213 @@
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 that are likely unavailable from this machine. 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
}
+162 -114
View File
@@ -3,130 +3,156 @@ package cluster
import (
"bytes"
"context"
"errors"
"fmt"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/emptypb"
"log/slog"
"net/netip"
"time"
"uncloud/internal/corrosion"
"uncloud/internal/machine/api/pb"
"uncloud/internal/machine/network"
"uncloud/internal/machine/store"
"uncloud/internal/secret"
)
type Cluster struct {
pb.UnimplementedClusterServer
state *State
// TODO: temporary channel until the state is replaced with networkDB.
newMachinesCh chan *pb.MachineInfo
store *store.Store
corroAdmin *corrosion.AdminClient
// machineID is the ID of the current machine that is running the cluster service.
machineID string
}
func NewCluster(state *State) *Cluster {
func NewCluster(store *store.Store, corroAdmin *corrosion.AdminClient) *Cluster {
return &Cluster{
state: state,
newMachinesCh: make(chan *pb.MachineInfo, 1),
store: store,
corroAdmin: corroAdmin,
}
}
func (c *Cluster) SetState(state *State) {
c.state = state
// UpdateMachineID updates the current machine ID that is running the cluster service.
func (c *Cluster) UpdateMachineID(mid string) {
c.machineID = mid
}
func (c *Cluster) Network() (netip.Prefix, error) {
if c.state == nil {
return netip.Prefix{}, status.Error(codes.FailedPrecondition, "cluster is not initialized")
func (c *Cluster) Init(ctx context.Context, network netip.Prefix) error {
initialised, err := c.Initialised(ctx)
if err != nil {
return err
}
if initialised {
return fmt.Errorf("cluster is already initialised")
}
if c.state.State.Network == nil {
return netip.Prefix{}, fmt.Errorf("network not set")
if err = c.store.Put(ctx, "network", network.String()); err != nil {
return fmt.Errorf("put network to store: %w", err)
}
return c.state.State.Network.ToPrefix()
}
func (c *Cluster) SetNetwork(network *pb.IPPrefix) error {
if c.state == nil {
return status.Error(codes.FailedPrecondition, "cluster is not initialized")
if err = c.store.Put(ctx, "created_at", time.Now().UTC().Format(time.RFC3339)); err != nil {
return fmt.Errorf("put created_at to store: %w", err)
}
if c.state.State.Network != nil {
return fmt.Errorf("network already set and cannot be changed")
}
if network == nil {
return fmt.Errorf("network not set")
}
c.state.State.Network = network
return nil
}
// TODO: this is a temporary watcher for PoC until the state is state is replaced with networkDB.
func (c *Cluster) WatchNewMachines() <-chan *pb.MachineInfo {
return c.newMachinesCh
func (c *Cluster) Initialised(ctx context.Context) (bool, error) {
var createdAt string
if err := c.store.Get(ctx, "created_at", &createdAt); err != nil {
if errors.Is(err, store.ErrKeyNotFound) {
return false, nil
}
return false, status.Errorf(codes.Internal, "get created_at from store: %v", err)
}
return true, nil
}
func (c *Cluster) checkInitialised(ctx context.Context) error {
initialised, err := c.Initialised(ctx)
if err != nil {
return err
}
if !initialised {
return status.Error(codes.FailedPrecondition, "cluster is not initialised")
}
return nil
}
func (c *Cluster) Network(ctx context.Context) (netip.Prefix, error) {
if err := c.checkInitialised(ctx); err != nil {
return netip.Prefix{}, err
}
var net string
if err := c.store.Get(ctx, "network", &net); err != nil {
return netip.Prefix{}, status.Errorf(codes.Internal, "get network from store: %v", err)
}
prefix, err := netip.ParsePrefix(net)
if err != nil {
return netip.Prefix{}, status.Errorf(codes.Internal, "parse network prefix: %v", err)
}
return prefix, nil
}
// AddMachine adds a machine to the cluster.
func (c *Cluster) AddMachine(ctx context.Context, req *pb.AddMachineRequest) (*pb.AddMachineResponse, error) {
if c.state == nil {
return nil, status.Error(codes.FailedPrecondition, "cluster is not initialized")
if err := c.checkInitialised(ctx); err != nil {
return nil, err
}
if err := req.Validate(); err != nil {
if req.Network == nil {
return nil, status.Error(codes.InvalidArgument, "network not set")
}
if err := req.Network.Validate(); err != nil {
return nil, err
}
if len(req.Network.Endpoints) == 0 {
return nil, status.Error(codes.InvalidArgument, "endpoints not set")
}
machines := c.state.State.Machines
machines, err := c.store.ListMachines(ctx)
if err != nil {
return nil, status.Errorf(codes.Internal, "list machines: %v", err)
}
allocatedSubnets := make([]netip.Prefix, len(machines))
var err error
i := 0
for _, m := range machines {
for i, m := range machines {
if req.Name != "" && m.Name == req.Name {
return nil, status.Errorf(codes.AlreadyExists, "machine with name %q already exists", req.Name)
}
if m.Network.ManagementIp != nil && req.Network.ManagementIp != nil &&
m.Network.ManagementIp.Equal(req.Network.ManagementIp) {
if req.Network.ManagementIp != nil && req.Network.ManagementIp.Equal(m.Network.ManagementIp) {
manageIP, _ := req.Network.ManagementIp.ToAddr()
return nil, status.Errorf(codes.AlreadyExists, "machine with management IP %q already exists", manageIP)
return nil, status.Errorf(
codes.AlreadyExists, "machine with management IP %q already exists under the name %q",
manageIP, m.Name,
)
}
if bytes.Equal(m.Network.PublicKey, req.Network.PublicKey) {
publicKey := secret.Secret(m.Network.PublicKey)
return nil, status.Errorf(codes.AlreadyExists, "machine with public key %q already exists", publicKey)
return nil, status.Errorf(
codes.AlreadyExists, "machine with public key %q already exists under the name %q",
publicKey, m.Name,
)
}
if allocatedSubnets[i], err = m.Network.Subnet.ToPrefix(); err != nil {
return nil, err
}
i++
allocatedSubnets[i], _ = m.Network.Subnet.ToPrefix()
}
mid, err := NewMachineID()
if err != nil {
return nil, status.Errorf(codes.Internal, "generate machine ID: %v", err)
}
name := req.Name
if name == "" {
if name, err = NewRandomMachineName(); err != nil {
return nil, status.Errorf(codes.Internal, "generate machine name: %v", err)
}
}
manageIP := req.Network.ManagementIp
if manageIP == nil {
manageIP = pb.NewIP(network.ManagementIP(req.Network.PublicKey))
}
m := &pb.MachineInfo{
Id: mid,
Name: req.Name,
Network: &pb.NetworkConfig{
ManagementIp: manageIP,
PublicKey: req.Network.PublicKey,
},
}
if m.Name == "" {
m.Name, err = NewRandomMachineName()
if err != nil {
return nil, status.Errorf(codes.Internal, "generate machine name: %v", err)
}
}
clusterNetwork, err := c.Network()
// Allocate a subnet for the machine from the cluster network.
clusterNetwork, err := c.Network(ctx)
if err != nil {
return nil, status.Errorf(codes.Internal, "get cluster network: %v", err)
}
@@ -134,70 +160,92 @@ func (c *Cluster) AddMachine(ctx context.Context, req *pb.AddMachineRequest) (*p
if err != nil {
return nil, status.Errorf(codes.Internal, "create IPAM manager: %v", err)
}
subnet, err := ipam.AllocateSubnetLen(network.DefaultSubnetBits)
subnet, err := ipam.AllocateSubnetLen(DefaultSubnetBits)
if err != nil {
return nil, status.Errorf(codes.Internal, "allocate subnet for machine: %v", err)
}
m.Network.Subnet = pb.NewIPPrefix(subnet)
// TODO: announce the new machine to the cluster network using Serf and achieve consensus.
mCopy := proto.Clone(m).(*pb.MachineInfo)
c.state.State.Machines[mCopy.Id] = mCopy
// Store machine endpoints in a separate collection to allow modifying them with limited permissions.
c.state.State.Endpoints[mCopy.Id] = &pb.MachineEndpoints{
Id: mCopy.Id,
Endpoints: req.Network.Endpoints,
m := &pb.MachineInfo{
Id: mid,
Name: name,
Network: &pb.NetworkConfig{
Subnet: pb.NewIPPrefix(subnet),
ManagementIp: manageIP,
Endpoints: req.Network.Endpoints,
PublicKey: req.Network.PublicKey,
},
}
if err = c.state.Save(); err != nil {
return nil, status.Errorf(codes.Internal, "save state: %v", err)
// 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.
if err = c.store.CreateMachine(ctx, m); err != nil {
return nil, status.Errorf(codes.Internal, "create machine: %v", err)
}
slog.Info("Machine added to the cluster.", "id", m.Id, "name", m.Name)
// Include the machine endpoints in the response.
m.Network.Endpoints = req.Network.Endpoints
// TODO: notify all cluster machines about the new machine so they can update their peers config.
// In PoC we just notify the local machine.
c.newMachinesCh <- m
slog.Info("Machine added to the cluster.",
"id", m.Id, "name", m.Name, "subnet", subnet, "public_key", secret.Secret(m.Network.PublicKey))
resp := &pb.AddMachineResponse{Machine: m}
return resp, nil
}
// ListMachines lists all machines in the cluster including their membership states.
func (c *Cluster) ListMachines(ctx context.Context, _ *emptypb.Empty) (*pb.ListMachinesResponse, error) {
if c.state == nil {
return nil, status.Error(codes.FailedPrecondition, "cluster is not initialized")
}
// TODO: consider creating MachineInfo type that pb.MachineInfo is mapped to always carry valid data to reduce
// error handling caused by conversions and nil pointers.
machines := make([]*pb.MachineInfo, 0, len(c.state.State.Machines))
for _, sm := range c.state.State.Machines {
m := proto.Clone(sm).(*pb.MachineInfo)
m.Network.Endpoints = c.state.State.Endpoints[m.Id].Endpoints
machines = append(machines, m)
}
return &pb.ListMachinesResponse{Machines: machines}, nil
}
func (c *Cluster) ListMachineEndpoints(
ctx context.Context, req *pb.ListMachineEndpointsRequest,
) (*pb.ListMachineEndpointsResponse, error) {
if c.state == nil {
return nil, status.Error(codes.FailedPrecondition, "cluster is not initialized")
if err := c.checkInitialised(ctx); err != nil {
return nil, err
}
endpoints, ok := c.state.State.Endpoints[req.Id]
if !ok {
return nil, status.Errorf(codes.NotFound, "machine %q not found", req.Id)
machines, err := c.store.ListMachines(ctx)
if err != nil {
return nil, status.Error(codes.Internal, err.Error())
}
return &pb.ListMachineEndpointsResponse{Endpoints: endpoints}, nil
states, err := c.corroAdmin.ClusterMembershipStates(true)
if err != nil {
return nil, status.Errorf(codes.Internal, "get cluster membership states: %v", err)
}
members := make([]*pb.MachineMember, len(machines))
for i, m := range machines {
// If the machine is not in the cluster membership states or its state is not ALIVE or SUSPECT, it is DOWN.
// The exception is the current machine which is always UP as it is serving this request.
state := pb.MachineMember_DOWN
addr, _ := m.Network.ManagementIp.ToAddr()
for _, s := range states {
if s.Addr.Addr().Compare(addr) == 0 &&
(s.State == corrosion.MembershipStateAlive || s.State == corrosion.MembershipStateSuspect) {
state = pb.MachineMember_UP
break
}
}
// If the machine is the current machine, it is UP.
if m.Id == c.machineID {
state = pb.MachineMember_UP
}
members[i] = &pb.MachineMember{
Machine: m,
State: state,
}
}
return &pb.ListMachinesResponse{Machines: members}, nil
}
func (c *Cluster) AddUser(user *pb.User) error {
c.state.State.Users = append(c.state.State.Users, user)
return c.state.Save()
}
func (c *Cluster) ListUsers() []*pb.User {
return c.state.State.Users
}
//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
//}
+4
View File
@@ -7,6 +7,10 @@ import (
"net/netip"
)
const DefaultSubnetBits = 24
var DefaultNetwork = netip.MustParsePrefix("10.210.0.0/16")
// IPAM is an in-memory IP address manager for allocating and releasing subnets for machines from a cluster network.
type IPAM struct {
network netip.Prefix
+4 -11
View File
@@ -1,9 +1,7 @@
package cluster
import (
"crypto/rand"
"fmt"
"math/big"
"uncloud/internal/secret"
)
@@ -14,14 +12,9 @@ func NewMachineID() (string, error) {
// NewRandomMachineName generates a random machine name in the format "machine-xxxx".
func NewRandomMachineName() (string, error) {
const charset = "abcdefghijklmnopqrstuvwxyz0123456789"
suffix := make([]byte, 4)
for i := range suffix {
randIdx, err := rand.Int(rand.Reader, big.NewInt(int64(len(charset))))
if err != nil {
return "", fmt.Errorf("get random number: %w", err)
}
suffix[i] = charset[randIdx.Int64()]
suffix, err := secret.RandomAlphaNumeric(4)
if err != nil {
return "", fmt.Errorf("generate random suffix: %w", err)
}
return "machine-" + string(suffix), nil
return "machine-" + suffix, nil
}
-60
View File
@@ -1,60 +0,0 @@
package cluster
import (
"fmt"
"google.golang.org/protobuf/proto"
"os"
"path/filepath"
"uncloud/internal/machine/api/pb"
)
const StateFile = "cluster.pb"
type State struct {
State *pb.State
path string
}
func StatePath(dataDir string) string {
return filepath.Join(dataDir, StateFile)
}
func NewState(path string) *State {
return &State{
State: &pb.State{
Machines: make(map[string]*pb.MachineInfo),
Endpoints: make(map[string]*pb.MachineEndpoints),
},
path: path,
}
}
func (s *State) Load() error {
data, err := os.ReadFile(s.path)
if err != nil {
return fmt.Errorf("read state file %q: %w", s.path, err)
}
if err = proto.Unmarshal(data, s.State); err != nil {
return fmt.Errorf("parse state file %q: %w", s.path, err)
}
if s.State.Machines == nil {
s.State.Machines = make(map[string]*pb.MachineInfo)
}
if s.State.Endpoints == nil {
s.State.Endpoints = make(map[string]*pb.MachineEndpoints)
}
return nil
}
func (s *State) Save() error {
dir, _ := filepath.Split(s.path)
if err := os.MkdirAll(dir, 0700); err != nil {
return fmt.Errorf("create state directory %q: %w", dir, err)
}
data, err := proto.Marshal(s.State)
if err != nil {
return fmt.Errorf("marshal state: %w", err)
}
return os.WriteFile(s.path, data, 0600)
}
+80
View File
@@ -0,0 +1,80 @@
package corroservice
import (
"bytes"
"fmt"
"github.com/BurntSushi/toml"
"net/netip"
"os"
"path/filepath"
"uncloud/internal/fs"
)
const (
DefaultUser = "uncloud"
DefaultGossipPort = 51001
DefaultAPIPort = 51002
)
// Config represents the Corrosion config.
type Config struct {
DB DBConfig `toml:"db"`
Gossip GossipConfig `toml:"gossip"`
API APIConfig `toml:"api"`
Admin AdminConfig `toml:"admin"`
}
type DBConfig struct {
Path string `toml:"path"`
SchemaPaths []string `toml:"schema_paths"`
}
type GossipConfig struct {
Addr netip.AddrPort `toml:"addr"`
Bootstrap []string `toml:"bootstrap"`
Plaintext bool `toml:"plaintext"`
}
type APIConfig struct {
Addr netip.AddrPort `toml:"addr"`
}
type AdminConfig struct {
Path string `toml:"path"`
}
func (c *Config) Write(path, owner string) error {
var data bytes.Buffer
encoder := toml.NewEncoder(&data)
encoder.Indent = "" // Disable indentation.
if err := encoder.Encode(c); err != nil {
return fmt.Errorf("encode config: %w", err)
}
if err := os.WriteFile(path, data.Bytes(), 0600); err != nil {
return err
}
if err := fs.Chown(path, owner, owner); err != nil {
return err
}
return nil
}
func MkDataDir(dir, owner string) error {
parent, _ := filepath.Split(dir)
// Use 0711 for parent directories to allow `owner` to access its nested data directory.
if err := os.MkdirAll(parent, 0711); err != nil {
return fmt.Errorf("create directory %q: %w", parent, err)
}
if err := os.Mkdir(dir, 0700); err != nil {
if !os.IsExist(err) {
return fmt.Errorf("create directory %q: %w", dir, err)
}
}
if owner != "" {
if err := fs.Chown(dir, owner, owner); err != nil {
return err
}
}
return nil
}
+129
View File
@@ -0,0 +1,129 @@
package corroservice
import (
"context"
"fmt"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/image"
"github.com/docker/docker/api/types/mount"
"github.com/docker/docker/api/types/network"
"github.com/docker/docker/client"
"io"
"log/slog"
"path/filepath"
"time"
)
const (
LatestImage = "ghcr.io/psviderski/corrosion:latest"
)
type DockerService struct {
Client *client.Client
Image string
Name string
DataDir string
User string
}
func NewDockerService(cli *client.Client, image, name, dataDir string) *DockerService {
return &DockerService{
Client: cli,
Image: image,
Name: name,
DataDir: dataDir,
}
}
func (s *DockerService) Start(ctx context.Context) error {
_, err := s.Client.ContainerInspect(ctx, s.Name)
if err != nil {
if client.IsErrNotFound(err) {
return s.startNewContainer(ctx)
}
return fmt.Errorf("inspect container %q: %w", s.Name, err)
}
// TODO: recreate only if the container configuration has to be changed.
if err = s.Client.ContainerRemove(ctx, s.Name, container.RemoveOptions{Force: true}); err != nil {
return fmt.Errorf("remove container %q: %w", s.Name, err)
}
return s.startNewContainer(ctx)
}
func (s *DockerService) Restart(ctx context.Context) error {
if err := s.Client.ContainerRestart(ctx, s.Name, container.StopOptions{}); err != nil {
return fmt.Errorf("restart container %q: %w", s.Name, err)
}
return nil
}
func (s *DockerService) Running() bool {
c, err := s.Client.ContainerInspect(context.Background(), s.Name)
if err != nil {
return false
}
return c.State.Running
}
func (s *DockerService) containerConfig() *container.Config {
return &container.Config{
Image: s.Image,
Cmd: []string{"corrosion", "agent", "-c", filepath.Join(s.DataDir, "config.toml")},
User: s.User,
}
}
func (s *DockerService) hostConfig() *container.HostConfig {
return &container.HostConfig{
NetworkMode: network.NetworkHost,
RestartPolicy: container.RestartPolicy{
Name: container.RestartPolicyAlways,
},
Mounts: []mount.Mount{
// Bind mount the data directory at the same path inside the container to simplify path handling.
{
Type: mount.TypeBind,
Source: s.DataDir,
Target: s.DataDir,
},
},
}
}
func (s *DockerService) startNewContainer(ctx context.Context) error {
_, err := s.Client.ContainerCreate(ctx, s.containerConfig(), s.hostConfig(), nil, nil, s.Name)
if err != nil {
if !client.IsErrNotFound(err) {
return fmt.Errorf("create container: %w", err)
}
slog.Info("Pulling Docker image for corrosion service.", "image", s.Image)
start := time.Now()
respBody, err := s.Client.ImagePull(ctx, s.Image, image.PullOptions{})
if err != nil {
return fmt.Errorf("pull image: %w", err)
}
defer respBody.Close()
// Wait for pull to complete.
if _, err := io.Copy(io.Discard, respBody); err != nil {
return fmt.Errorf("read pull response: %w", err)
}
slog.Info("Docker image pulled.", "image", s.Image, "duration", time.Since(start).String())
// Create container again after image pull.
if _, err = s.Client.ContainerCreate(ctx, s.containerConfig(), s.hostConfig(), nil, nil, s.Name); err != nil {
return fmt.Errorf("create container: %w", err)
}
}
if err = s.Client.ContainerStart(ctx, s.Name, container.StartOptions{}); err != nil {
return fmt.Errorf("start container: %w", err)
}
return nil
}
+9
View File
@@ -0,0 +1,9 @@
package corroservice
import "context"
type Service interface {
Start(ctx context.Context) error
Restart(ctx context.Context) error
Running() bool
}
+157
View File
@@ -0,0 +1,157 @@
package corroservice
import (
"bufio"
"context"
"fmt"
"log/slog"
"os/exec"
"path/filepath"
"sync"
"syscall"
"time"
)
const (
DefaultCommand = "corrosion"
DefaultDataDir = "/var/lib/uncloud/corrosion"
)
// SubprocessService implements the Service interface by running the service as a subprocess.
type SubprocessService struct {
Command string
DataDir string
cmd *exec.Cmd
running bool
mu sync.Mutex
cancelWatch context.CancelFunc
}
func DefaultSubprocessService() *SubprocessService {
return &SubprocessService{
Command: DefaultCommand,
DataDir: DefaultDataDir,
}
}
// TODO: maybe stop the process if this ctx is cancelled.
func (s *SubprocessService) Start(ctx context.Context) error {
s.mu.Lock()
defer s.mu.Unlock()
if s.running {
return nil
}
return s.startProcess(ctx)
}
func (s *SubprocessService) Restart(ctx context.Context) error {
s.mu.Lock()
defer s.mu.Unlock()
if s.running {
if err := s.stopProcess(); err != nil {
return fmt.Errorf("stop process: %w", err)
}
}
return s.startProcess(ctx)
}
func (s *SubprocessService) Running() bool {
s.mu.Lock()
defer s.mu.Unlock()
return s.running
}
func (s *SubprocessService) startProcess(ctx context.Context) error {
s.cmd = exec.Command(s.Command, "agent", "-c", filepath.Join(s.DataDir, "config.toml"))
// Redirect stdout and stderr to the logger.
stdout, err := s.cmd.StdoutPipe()
if err != nil {
return fmt.Errorf("create stdout pipe: %w", err)
}
stderr, err := s.cmd.StderrPipe()
if err != nil {
return fmt.Errorf("create stderr pipe: %w", err)
}
go func() {
scanner := bufio.NewScanner(stdout)
for scanner.Scan() {
slog.Info("[corrosion]: " + scanner.Text())
}
// TODO: remove
slog.Info("######## corrosion redirect go routine end ########")
}()
go func() {
scanner := bufio.NewScanner(stderr)
for scanner.Scan() {
slog.Error("[corrosion]: " + scanner.Text())
}
}()
if err = s.cmd.Start(); err != nil {
return fmt.Errorf("start process: %w", err)
}
s.running = true
// Watch for process exit to update running status.
go func() {
if err := s.cmd.Wait(); err != nil {
slog.Error("corrosion process exited with error.", "code", s.cmd.ProcessState.ExitCode(), "err", err)
}
s.mu.Lock()
s.running = false
s.mu.Unlock()
}()
// TODO: figure out the waiting process
// Wait for initialization
//timer := time.NewTimer(2 * time.Second)
//defer timer.Stop()
//select {
////case <-timer.C:
//// s.running = true
//// return nil
//case <-watchCtx.Done():
// return fmt.Errorf("process failed to start")
//case <-ctx.Done():
// s.stopProcess()
// return ctx.Err()
//}
return nil
}
func (s *SubprocessService) stopProcess() error {
if s.cmd == nil || s.cmd.Process == nil {
return nil
}
if err := s.cmd.Process.Signal(syscall.SIGTERM); err != nil {
return fmt.Errorf("send SIGTERM: %w", err)
}
// Wait up to 5 seconds for graceful shutdown before killing the process.
done := make(chan error, 1)
go func() {
done <- s.cmd.Wait()
}()
select {
case <-time.After(5 * time.Second):
if err := s.cmd.Process.Kill(); err != nil {
return fmt.Errorf("kill process: %w", err)
}
case err := <-done:
if err != nil {
return fmt.Errorf("process exited with error: %w", err)
}
}
return nil
}
+57
View File
@@ -0,0 +1,57 @@
package corroservice
import (
"context"
"fmt"
"log/slog"
"os/exec"
"time"
)
const DefaultSystemdUnit = "uncloud-corrosion.service"
type SystemdService struct {
DataDir string
Unit string
running bool
}
func DefaultSystemdService(dataDir string) *SystemdService {
return &SystemdService{
DataDir: dataDir,
Unit: DefaultSystemdUnit,
}
}
func (s *SystemdService) Start(ctx context.Context) error {
return s.startOrRestart(ctx, "start")
}
func (s *SystemdService) Restart(ctx context.Context) error {
return s.startOrRestart(ctx, "restart")
}
func (s *SystemdService) startOrRestart(ctx context.Context, cmd string) error {
if _, err := exec.Command("systemctl", cmd, s.Unit).Output(); err != nil {
return fmt.Errorf("systemctl %s %s: %w", cmd, s.Unit, err)
}
slog.Info(fmt.Sprintf("Corrosion systemd service %sed.", cmd), "unit", s.Unit)
// Optimistically wait for the corrosion service to start and initialise the database schema before proceeding.
timer := time.NewTimer(2 * time.Second)
defer timer.Stop()
select {
case <-timer.C:
case <-ctx.Done():
return nil
}
// TODO: run a goroutine to check the status of the service and log any errors in the uncloud log.
s.running = true
return nil
}
func (s *SystemdService) Running() bool {
return s.running
}
+198
View File
@@ -0,0 +1,198 @@
package docker
import (
"context"
"encoding/json"
"fmt"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/network"
"github.com/docker/docker/errdefs"
"github.com/docker/docker/pkg/jsonmessage"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"io"
"uncloud/internal/machine/api/pb"
)
// Client is a gRPC client for the Docker service that provides a similar interface to the Docker HTTP client.
type Client struct {
conn *grpc.ClientConn
grpcClient pb.DockerClient
}
// NewClient creates a new Docker gRPC client with the provided gRPC connection.
func NewClient(conn *grpc.ClientConn) *Client {
return &Client{
conn: conn,
grpcClient: pb.NewDockerClient(conn),
}
}
// Close closes the gRPC connection.
func (c *Client) Close() error {
return c.conn.Close()
}
// CreateContainer creates a new container based on the given configuration.
func (c *Client) CreateContainer(
ctx context.Context,
config *container.Config,
hostConfig *container.HostConfig,
networkingConfig *network.NetworkingConfig,
platform *ocispec.Platform,
name string,
) (container.CreateResponse, error) {
var resp container.CreateResponse
// Serialize configs to JSON.
configBytes, err := json.Marshal(config)
if err != nil {
return resp, fmt.Errorf("marshal container config: %w", err)
}
hostConfigBytes, err := json.Marshal(hostConfig)
if err != nil {
return resp, fmt.Errorf("marshal host config: %w", err)
}
networkingConfigBytes, err := json.Marshal(networkingConfig)
if err != nil {
return resp, fmt.Errorf("marshal networking config: %w", err)
}
platformBytes, err := json.Marshal(platform)
if err != nil {
return resp, fmt.Errorf("marshal platform: %w", err)
}
grpcResp, err := c.grpcClient.CreateContainer(ctx, &pb.CreateContainerRequest{
Config: configBytes,
HostConfig: hostConfigBytes,
NetworkConfig: networkingConfigBytes,
Platform: platformBytes,
Name: name,
})
if err != nil {
if s, ok := status.FromError(err); ok {
if s.Code() == codes.NotFound {
return resp, errdefs.NotFound(err)
}
}
return resp, err
}
if err = json.Unmarshal(grpcResp.Response, &resp); err != nil {
return resp, fmt.Errorf("unmarshal gRPC response: %w", err)
}
return resp, nil
}
// StartContainer starts a container with the given ID and options.
func (c *Client) StartContainer(ctx context.Context, id string, opts container.StartOptions) error {
optsBytes, err := json.Marshal(opts)
if err != nil {
return fmt.Errorf("marshal options: %w", err)
}
_, err = c.grpcClient.StartContainer(ctx, &pb.StartContainerRequest{
Id: id,
Options: optsBytes,
})
if err != nil {
if s, ok := status.FromError(err); ok {
if s.Code() == codes.NotFound {
return errdefs.NotFound(err)
}
}
}
return err
}
type MachineContainers struct {
Metadata *pb.Metadata
Containers []types.Container
}
func (c *Client) ListContainers(ctx context.Context, opts container.ListOptions) ([]MachineContainers, error) {
optsBytes, err := json.Marshal(opts)
if err != nil {
return nil, fmt.Errorf("marshal options: %w", err)
}
resp, err := c.grpcClient.ListContainers(ctx, &pb.ListContainersRequest{Options: optsBytes})
if err != nil {
return nil, err
}
machineContainers := make([]MachineContainers, len(resp.Messages))
for i, msg := range resp.Messages {
machineContainers[i].Metadata = msg.Metadata
if msg.Metadata != nil && msg.Metadata.Error != "" {
continue
}
if err = json.Unmarshal(msg.Containers, &machineContainers[i].Containers); err != nil {
return nil, fmt.Errorf("unmarshal containers: %w", err)
}
}
return machineContainers, nil
}
// RemoveContainer stops (kills after grace period) and removes a container with the given ID.
func (c *Client) RemoveContainer(ctx context.Context, id string, opts container.RemoveOptions) error {
optsBytes, err := json.Marshal(opts)
if err != nil {
return fmt.Errorf("marshal options: %w", err)
}
_, err = c.grpcClient.RemoveContainer(ctx, &pb.RemoveContainerRequest{
Id: id,
Options: optsBytes,
})
if err != nil {
if s, ok := status.FromError(err); ok {
if s.Code() == codes.NotFound {
return errdefs.NotFound(err)
}
}
}
return err
}
type PullImageMessage struct {
Message jsonmessage.JSONMessage
Err error
}
func (c *Client) PullImage(ctx context.Context, image string) (<-chan PullImageMessage, error) {
stream, err := c.grpcClient.PullImage(ctx, &pb.PullImageRequest{Image: image})
if err != nil {
return nil, err
}
ch := make(chan PullImageMessage)
go func() {
defer close(ch)
for {
msg, err := stream.Recv()
if err == io.EOF {
return
}
if err != nil {
ch <- PullImageMessage{Err: err}
return
}
var jm jsonmessage.JSONMessage
if err = json.Unmarshal(msg.Message, &jm); err != nil {
ch <- PullImageMessage{Err: fmt.Errorf("unmarshal JSON message: %w", err)}
return
}
ch <- PullImageMessage{Message: jm}
}
}()
return ch, nil
}
+194
View File
@@ -0,0 +1,194 @@
package docker
import (
"context"
"errors"
"fmt"
dockercontainer "github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/events"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/client"
"log/slog"
"time"
"uncloud/internal/api"
"uncloud/internal/machine/store"
)
const (
NetworkName = "uncloud"
UserChain = "DOCKER-USER"
// EventsDebounceInterval defines how long to wait before processing the next Docker event. Multiple events
// occurring within this window will be processed together as a single event to prevent system overload.
EventsDebounceInterval = 100 * time.Millisecond
// SyncInterval defines a regular interval to sync containers to the cluster store.
SyncInterval = 30 * time.Second
)
type Manager struct {
client *client.Client
// machineID is the ID of the machine where the managed Docker daemon is running.
machineID string
store *store.Store
}
func NewManager(client *client.Client, machineID string, store *store.Store) *Manager {
return &Manager{
client: client,
machineID: machineID,
store: store,
}
}
// WaitDaemonReady waits for the Docker daemon to start and be ready to serve requests.
func (m *Manager) WaitDaemonReady(ctx context.Context) error {
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
ready, waitingLogged := false, false
for !ready {
select {
case <-ctx.Done():
return ctx.Err()
case <-ticker.C:
_, err := m.client.Ping(ctx)
if err == nil {
ready = true
break
}
if !client.IsErrConnectionFailed(err) {
return fmt.Errorf("connect to Docker daemon: %w", err)
}
if !waitingLogged {
slog.Info("Waiting for Docker daemon to start and be ready.")
waitingLogged = true
}
}
}
return nil
}
func (m *Manager) WatchAndSyncContainers(ctx context.Context) error {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
// Filter only local container events.
opts := events.ListOptions{
Filters: filters.NewArgs(
filters.Arg("scope", "local"),
filters.Arg("type", string(events.ContainerEventType)),
),
}
// Subscribe to Docker events before running the initial sync to avoid missing any events.
eventCh, errCh := m.client.Events(ctx, opts)
slog.Debug("Syncing containers to cluster store before processing Docker events.")
if err := m.syncContainersToStore(ctx); err != nil {
// The deferred cancel will stop the event subscription.
return fmt.Errorf("sync containers to cluster store: %w", err)
}
var (
// debouncer is used to debounce multiple Docker events into a single event sent to the debouncerCh
// to prevent system overload.
debouncer *time.Timer
debouncerCh = make(chan events.Message)
// ticker is used to trigger a regular sync of containers to the cluster store as a fallback.
ticker = time.NewTicker(SyncInterval)
)
defer ticker.Stop()
for {
select {
case e := <-eventCh:
switch e.Action {
// Actions that may trigger a container state change or creation/deletion of a container.
case events.ActionCreate,
events.ActionStart,
events.ActionStop,
events.ActionPause,
events.ActionUnPause,
events.ActionKill,
events.ActionDie,
events.ActionOOM,
events.ActionDestroy,
events.ActionHealthStatusHealthy,
events.ActionHealthStatusUnhealthy:
if debouncer == nil {
debouncer = time.AfterFunc(EventsDebounceInterval, func() {
debouncerCh <- e
})
}
}
case e := <-debouncerCh:
debouncer = nil
slog.Debug("Syncing containers to cluster store triggered by a Docker container event.",
"container_id", e.Actor.ID,
"container_name", e.Actor.Attributes["name"],
"action", e.Action)
if err := m.syncContainersToStore(ctx); err != nil {
return fmt.Errorf("sync containers to cluster store: %w", err)
}
case <-ticker.C:
slog.Debug("Syncing containers to cluster store triggered by a regular interval.",
"interval", SyncInterval)
if err := m.syncContainersToStore(ctx); err != nil {
return fmt.Errorf("sync containers to cluster store: %w", err)
}
case err := <-errCh:
if errors.Is(err, context.Canceled) {
return nil
}
return fmt.Errorf("receive Docker event: %w", err)
}
}
}
func (m *Manager) syncContainersToStore(ctx context.Context) error {
storeContainers, err := m.store.ListContainers(ctx, store.ListOptions{MachineIDs: []string{m.machineID}})
if err != nil {
return fmt.Errorf("list containers from store: %w", err)
}
// List only Uncloud service containers identified by their labels.
containers, err := m.client.ContainerList(ctx, dockercontainer.ListOptions{
Filters: filters.NewArgs(
filters.Arg("label", api.LabelServiceID),
filters.Arg("label", api.LabelServiceName),
),
})
if err != nil {
// TODO: mark all containers as outdated in the store.
return fmt.Errorf("list Docker containers: %w", err)
}
// Delete containers that are not present in the Docker daemon from the store.
var deleteIDs []string
for _, sc := range storeContainers {
found := false
for i, _ := range containers {
if containers[i].ID == sc.Container.ID {
found = true
break
}
}
if !found {
deleteIDs = append(deleteIDs, sc.Container.ID)
}
}
var storeErr error
if len(deleteIDs) > 0 {
if err = m.store.DeleteContainers(ctx, store.DeleteOptions{IDs: deleteIDs}); err != nil {
storeErr = fmt.Errorf("delete containers from store: %w", err)
}
}
// Create or update the current Docker containers in the store.
for _, dc := range containers {
c := &api.Container{Container: dc}
if err = m.store.CreateOrUpdateContainer(ctx, c, m.machineID); err != nil {
storeErr = errors.Join(storeErr, fmt.Errorf("create or update container %q: %w", c.ID, err))
}
}
return storeErr
}
+14
View File
@@ -0,0 +1,14 @@
//go:build darwin
package docker
import (
"context"
"fmt"
"net/netip"
)
// EnsureUncloudNetwork is a stub for darwin.
func (d *Manager) EnsureUncloudNetwork(ctx context.Context, subnet netip.Prefix) error {
return fmt.Errorf("not supported on darwin")
}
+77
View File
@@ -0,0 +1,77 @@
package docker
import (
"context"
"fmt"
dnetwork "github.com/docker/docker/api/types/network"
"github.com/docker/docker/client"
"github.com/docker/docker/libnetwork/iptables"
"log/slog"
"net/netip"
"uncloud/internal/machine/network"
)
// EnsureUncloudNetwork creates the Docker bridge network NetworkName with the provided machine subnet
// if it doesn't exist. If the network exists but has a different subnet, it removes and recreates the network.
// It also configures iptables to allow container access from the WireGuard network.
func (d *Manager) EnsureUncloudNetwork(ctx context.Context, subnet netip.Prefix) error {
// Ensure the Docker network 'uncloud' is created with the correct subnet.
needsCreation := false
nw, err := d.client.NetworkInspect(ctx, NetworkName, dnetwork.InspectOptions{})
if err != nil {
if !client.IsErrNotFound(err) {
return fmt.Errorf("inspect Docker network %q: %w", NetworkName, err)
}
needsCreation = true
} else if nw.IPAM.Config[0].Subnet != subnet.String() {
// Remove the Docker network if the subnet is different.
// It could be a leftover from a previous incomplete cleanup.
slog.Info(
"Removing Docker network with old subnet.", "name", NetworkName, "subnet", nw.IPAM.Config[0].Subnet,
)
if err = d.client.NetworkRemove(ctx, NetworkName); err != nil {
// It can still fail if the network is in use by a container. Leave it to the user to resolve the issue.
return fmt.Errorf("remove Docker network %q: %w", NetworkName, err)
}
needsCreation = true
}
if needsCreation {
if _, err = d.client.NetworkCreate(
ctx, NetworkName, dnetwork.CreateOptions{
Driver: "bridge",
Scope: "local",
IPAM: &dnetwork.IPAM{
Config: []dnetwork.IPAMConfig{
{
Subnet: subnet.String(),
},
},
},
},
); err != nil {
return fmt.Errorf("create Docker network %q: %w", NetworkName, err)
}
slog.Info("Docker network created.", "name", NetworkName, "subnet", subnet.String())
if nw, err = d.client.NetworkInspect(ctx, NetworkName, dnetwork.InspectOptions{}); err != nil {
return fmt.Errorf("inspect Docker network %q: %w", NetworkName, err)
}
}
// Configure iptables to allow WireGuard network to access containers. The Docker daemon should have already
// created the DOCKER-USER chain at this point.
// TODO: check if this works when firewalld used instead of raw iptables. The Docker daemon has a different
// code path for firewalld.
// Bridge name doesn't seem to be documented but this is the source code where it is generated:
// https://github.com/moby/moby/blob/v27.2.1/libnetwork/drivers/bridge/bridge_linux.go#L664
bridgeName := "br-" + nw.ID[:12]
ipt := iptables.GetIptable(iptables.IPv4)
rule := []string{"--in-interface", network.WireGuardInterfaceName, "--out-interface", bridgeName, "-j", "ACCEPT"}
if err = ipt.ProgramRule(iptables.Filter, UserChain, iptables.Insert, rule); err != nil {
return fmt.Errorf("insert iptables rule: %w", err)
}
return nil
}
+195
View File
@@ -0,0 +1,195 @@
package docker
import (
"context"
"encoding/json"
"errors"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/image"
"github.com/docker/docker/api/types/network"
"github.com/docker/docker/client"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/emptypb"
"io"
"uncloud/internal/machine/api/pb"
)
// Server implements the gRPC Docker service that proxies requests to the Docker daemon.
type Server struct {
pb.UnimplementedDockerServer
client *client.Client
}
// NewServer creates a new Docker gRPC server with the provided Docker client.
func NewServer(cli *client.Client) *Server {
return &Server{client: cli}
}
// CreateContainer creates a new container based on the given configuration.
func (s *Server) CreateContainer(ctx context.Context, req *pb.CreateContainerRequest) (*pb.CreateContainerResponse, error) {
var config container.Config
var hostConfig container.HostConfig
var networkConfig network.NetworkingConfig
var platform ocispec.Platform
// Unmarshal configurations from the request.
if err := json.Unmarshal(req.Config, &config); err != nil {
return nil, status.Errorf(codes.InvalidArgument, "unmarshal container config: %v", err)
}
if err := json.Unmarshal(req.HostConfig, &hostConfig); err != nil {
return nil, status.Errorf(codes.InvalidArgument, "unmarshal host config: %v", err)
}
if err := json.Unmarshal(req.NetworkConfig, &networkConfig); err != nil {
return nil, status.Errorf(codes.InvalidArgument, "unmarshal network config: %v", err)
}
if err := json.Unmarshal(req.Platform, &platform); err != nil {
return nil, status.Errorf(codes.InvalidArgument, "unmarshal platform: %v", err)
}
resp, err := s.client.ContainerCreate(ctx, &config, &hostConfig, &networkConfig, &platform, req.Name)
if err != nil {
if client.IsErrNotFound(err) {
return nil, status.Errorf(codes.NotFound, "create container: %v", err)
}
return nil, status.Errorf(codes.Internal, "create container: %v", err)
}
respBytes, err := json.Marshal(resp)
if err != nil {
return nil, status.Errorf(codes.Internal, "marshal response: %v", err)
}
return &pb.CreateContainerResponse{Response: respBytes}, nil
}
// StartContainer starts a container with the given ID and options.
func (s *Server) StartContainer(ctx context.Context, req *pb.StartContainerRequest) (*emptypb.Empty, error) {
var opts container.StartOptions
if len(req.Options) > 0 {
if err := json.Unmarshal(req.Options, &opts); err != nil {
return nil, status.Errorf(codes.InvalidArgument, "unmarshal options: %v", err)
}
}
if err := s.client.ContainerStart(ctx, req.Id, opts); err != nil {
if client.IsErrNotFound(err) {
return nil, status.Errorf(codes.NotFound, "start container: %v", err)
}
return nil, status.Errorf(codes.Internal, "start container: %v", err)
}
return &emptypb.Empty{}, nil
}
func (s *Server) ListContainers(ctx context.Context, req *pb.ListContainersRequest) (*pb.ListContainersResponse, error) {
var opts container.ListOptions
if len(req.Options) > 0 {
if err := json.Unmarshal(req.Options, &opts); err != nil {
return nil, status.Errorf(codes.InvalidArgument, "unmarshal options: %v", err)
}
// Handle filters separately because they implement custom JSON unmarshalling.
var raw map[string]json.RawMessage
if err := json.Unmarshal(req.Options, &raw); err != nil {
return nil, status.Errorf(codes.InvalidArgument, "unmarshal options to raw map: %v", err)
}
if filtersBytes, ok := raw["Filters"]; ok {
args, err := filters.FromJSON(string(filtersBytes))
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "unmarshal filters: %v", err)
}
opts.Filters = args
}
}
containers, err := s.client.ContainerList(ctx, opts)
if err != nil {
return nil, status.Errorf(codes.Internal, "list container: %v", err)
}
containersBytes, err := json.Marshal(containers)
if err != nil {
return nil, status.Errorf(codes.Internal, "marshal containers: %v", err)
}
return &pb.ListContainersResponse{
Messages: []*pb.MachineContainers{
{
Containers: containersBytes,
},
},
}, nil
}
// RemoveContainer stops (kills after grace period) and removes a container with the given ID.
func (s *Server) RemoveContainer(ctx context.Context, req *pb.RemoveContainerRequest) (*emptypb.Empty, error) {
var opts container.RemoveOptions
if len(req.Options) > 0 {
if err := json.Unmarshal(req.Options, &opts); err != nil {
return nil, status.Errorf(codes.InvalidArgument, "unmarshal options: %v", err)
}
}
if err := s.client.ContainerRemove(ctx, req.Id, opts); err != nil {
if client.IsErrNotFound(err) {
return nil, status.Errorf(codes.NotFound, "remove container: %v", err)
}
return nil, status.Errorf(codes.Internal, "remove container: %v", err)
}
return &emptypb.Empty{}, nil
}
func (s *Server) PullImage(req *pb.PullImageRequest, stream grpc.ServerStreamingServer[pb.JSONMessage]) error {
ctx := stream.Context()
// TODO: replace with another JSON serializable type (PullOptions.PrivilegeFunc is not serializable).
var opts image.PullOptions
if len(req.Options) > 0 {
if err := json.Unmarshal(req.Options, &opts); err != nil {
return status.Errorf(codes.InvalidArgument, "unmarshal options: %v", err)
}
}
respBody, err := s.client.ImagePull(ctx, req.Image, opts)
if err != nil {
return status.Errorf(codes.Internal, "pull image: %v", err)
}
defer respBody.Close()
decoder := json.NewDecoder(respBody)
errCh := make(chan error, 1)
go func() {
var raw json.RawMessage
for {
if err = decoder.Decode(&raw); err != nil {
if errors.Is(err, io.EOF) {
errCh <- nil
return
}
errCh <- status.Errorf(codes.Internal, "decode image pull message: %v", err)
return
}
if err = stream.Send(&pb.JSONMessage{Message: raw}); err != nil {
errCh <- status.Errorf(codes.Internal, "send image pull message to stream: %v", err)
return
}
}
}()
for {
select {
case err = <-errCh:
return err
case <-ctx.Done():
return status.Errorf(codes.Canceled, "pull image: %v", ctx.Err())
}
}
}
+383 -95
View File
@@ -2,9 +2,12 @@ package machine
import (
"context"
"encoding/json"
"errors"
"fmt"
"github.com/docker/docker/client"
"github.com/docker/go-connections/sockets"
"github.com/siderolabs/grpc-proxy/proxy"
"golang.org/x/sync/errgroup"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
@@ -15,21 +18,120 @@ import (
"net/netip"
"os"
"os/user"
"path/filepath"
"strconv"
"uncloud/internal/corrosion"
"uncloud/internal/docker"
"uncloud/internal/fs"
"uncloud/internal/machine/api/pb"
apiproxy "uncloud/internal/machine/api/proxy"
"uncloud/internal/machine/caddyfile"
"uncloud/internal/machine/cluster"
"uncloud/internal/machine/corroservice"
machinedocker "uncloud/internal/machine/docker"
"uncloud/internal/machine/network"
"uncloud/internal/machine/store"
)
const (
DefaultAPISockPath = "/run/uncloud.sock"
DefaultAPISockGroup = "uncloud"
DefaultMachineSockPath = "/run/uncloud/machine.sock"
DefaultUncloudSockPath = "/run/uncloud/uncloud.sock"
DefaultSockGroup = "uncloud"
)
type Config struct {
// DataDir is the directory where the machine stores its persistent state.
DataDir string
APISockPath string
// DataDir is the directory where the machine stores its persistent state. Default is /var/lib/uncloud.
DataDir string
MachineSockPath string
UncloudSockPath string
CorrosionDir string
CorrosionAPIListenAddr netip.AddrPort
CorrosionAPIAddr netip.AddrPort
CorrosionAdminSockPath string
CorrosionService corroservice.Service
// CorrosionUser sets the Linux user for running the corrosion service.
CorrosionUser string
// DockerClient manages system and user containers using the local Docker daemon.
DockerClient *client.Client
// CaddyfilePath specifies where the machine generates the Caddy reverse proxy configuration file for routing
// external traffic to service containers across the internal network. Default is DataDir/caddy/Caddyfile.
CaddyfilePath string
}
// SetDefaults returns a new Config with default values set where not provided.
func (c *Config) SetDefaults() (*Config, error) {
// Copy c into a new Config to avoid modifying the original.
cfg := *c
if cfg.DataDir == "" {
cfg.DataDir = "/var/lib/uncloud"
}
if cfg.MachineSockPath == "" {
cfg.MachineSockPath = DefaultMachineSockPath
}
if cfg.UncloudSockPath == "" {
cfg.UncloudSockPath = DefaultUncloudSockPath
}
if cfg.DockerClient == nil {
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
if err != nil {
return nil, fmt.Errorf("create Docker client: %w", err)
}
cfg.DockerClient = cli
}
if cfg.CorrosionDir == "" {
cfg.CorrosionDir = filepath.Join(cfg.DataDir, "corrosion")
}
if !cfg.CorrosionAPIListenAddr.IsValid() {
cfg.CorrosionAPIListenAddr = netip.AddrPortFrom(
netip.AddrFrom4([4]byte{127, 0, 0, 1}), corroservice.DefaultAPIPort)
}
if !cfg.CorrosionAPIAddr.IsValid() {
cfg.CorrosionAPIAddr = netip.AddrPortFrom(
netip.AddrFrom4([4]byte{127, 0, 0, 1}), corroservice.DefaultAPIPort)
}
if cfg.CorrosionAdminSockPath == "" {
cfg.CorrosionAdminSockPath = filepath.Join(cfg.CorrosionDir, "admin.sock")
}
if cfg.CorrosionUser == "" {
cfg.CorrosionUser = corroservice.DefaultUser
}
if cfg.CorrosionService == nil {
if isRunningInDocker() {
// Run corrosion in a nested Docker container if the machine is running in a container.
uid, gid, err := fs.LookupUIDGID(cfg.CorrosionUser)
if err != nil {
return nil, fmt.Errorf("lookup corrosion user %q: %w", cfg.CorrosionUser, err)
}
cfg.CorrosionService = &corroservice.DockerService{
Client: cfg.DockerClient,
Image: corroservice.LatestImage,
Name: "uncloud-corrosion",
DataDir: cfg.CorrosionDir,
User: fmt.Sprintf("%d:%d", uid, gid),
}
} else {
cfg.CorrosionService = corroservice.DefaultSystemdService(cfg.CorrosionDir)
}
}
if cfg.CaddyfilePath == "" {
cfg.CaddyfilePath = filepath.Join(cfg.DataDir, "caddy", "caddy.json")
}
return &cfg, nil
}
// isRunningInDocker returns true if the current process is running in a Docker container.
func isRunningInDocker() bool {
_, err := os.Stat("/.dockerenv")
return err == nil
}
type Machine struct {
@@ -42,12 +144,27 @@ type Machine struct {
// initialised is signalled when the machine is configured as a member of a cluster.
initialised chan struct{}
localServer *grpc.Server
cluster *cluster.Cluster
newMachinesCh <-chan *pb.MachineInfo
// store is the cluster store backed by a distributed Corrosion database.
store *store.Store
cluster *cluster.Cluster
docker *machinedocker.Server
// localMachineServer is the gRPC server for the machine API listening on the local Unix socket.
localMachineServer *grpc.Server
// proxyDirector manages routing of gRPC requests between local and remote machine API servers.
proxyDirector *apiproxy.Director
// localProxyServer is the gRPC proxy server for the machine API listening on the local Unix socket.
// It proxies requests to the local or remote machine API servers depending on the request targets
// and aggregates responses.
localProxyServer *grpc.Server
}
func NewMachine(config *Config) (*Machine, error) {
config, err := config.SetDefaults()
if err != nil {
return nil, fmt.Errorf("set default config values: %w", err)
}
// Load the existing machine state or create a new one.
statePath := StatePath(config.DataDir)
state, err := ParseState(statePath)
@@ -75,41 +192,58 @@ func NewMachine(config *Config) (*Machine, error) {
}
}
var c *cluster.Cluster
clusterState := cluster.NewState(cluster.StatePath(config.DataDir))
if err = clusterState.Load(); err != nil {
if errors.Is(err, os.ErrNotExist) {
// Cluster state file does not exist, initialise the cluster without a state to fail cluster requests.
c = cluster.NewCluster(nil)
} else {
return nil, fmt.Errorf("load cluster state: %w", err)
}
} else {
// Cluster state is successfully loaded, initialise the cluster with it.
c = cluster.NewCluster(clusterState)
corro, err := corrosion.NewAPIClient(config.CorrosionAPIAddr)
if err != nil {
return nil, fmt.Errorf("create corrosion API client: %w", err)
}
corroStore := store.New(corro)
corroAdmin, err := corrosion.NewAdminClient(config.CorrosionAdminSockPath)
if err != nil {
return nil, fmt.Errorf("create corrosion admin client: %w", err)
}
c := cluster.NewCluster(corroStore, corroAdmin)
// Init a gRPC Docker server that proxies requests to the local Docker daemon.
dockerCli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
if err != nil {
return nil, fmt.Errorf("create Docker client: %w", err)
}
dockerServer := machinedocker.NewServer(dockerCli)
// Init a local gRPC proxy server that proxies requests to the local or remote machine API servers.
proxyDirector := apiproxy.NewDirector(config.MachineSockPath, APIPort)
localProxyServer := grpc.NewServer(
grpc.ForceServerCodecV2(proxy.Codec()),
grpc.UnknownServiceHandler(
proxy.TransparentHandler(proxyDirector.Director),
),
)
m := &Machine{
config: *config,
state: state,
started: make(chan struct{}),
initialised: make(chan struct{}, 1),
cluster: c,
newMachinesCh: c.WatchNewMachines(),
config: *config,
state: state,
started: make(chan struct{}),
initialised: make(chan struct{}, 1),
store: corroStore,
cluster: c,
docker: dockerServer,
localProxyServer: localProxyServer,
proxyDirector: proxyDirector,
}
m.localServer = newGRPCServer(m, c)
m.localMachineServer = newGRPCServer(m, c, dockerServer)
if m.IsInitialised() {
if m.Initialised() {
m.initialised <- struct{}{}
}
return m, nil
}
func newGRPCServer(m pb.MachineServer, c pb.ClusterServer) *grpc.Server {
func newGRPCServer(m pb.MachineServer, c pb.ClusterServer, d pb.DockerServer) *grpc.Server {
s := grpc.NewServer()
pb.RegisterMachineServer(s, m)
pb.RegisterClusterServer(s, c)
pb.RegisterDockerServer(s, d)
return s
}
@@ -118,9 +252,9 @@ func (m *Machine) Started() <-chan struct{} {
return m.started
}
// IsInitialised returns true if the machine has been configured as a member of a cluster,
// Initialised returns true if the machine has been configured as a member of a cluster,
// either by initialising a new cluster on it or joining an existing one.
func (m *Machine) IsInitialised() bool {
func (m *Machine) Initialised() bool {
m.state.mu.RLock()
defer m.state.mu.RUnlock()
@@ -128,33 +262,64 @@ func (m *Machine) IsInitialised() bool {
}
func (m *Machine) Run(ctx context.Context) error {
// Docker dependency is essential for the machine to function. Block until it's ready.
if err := docker.WaitDaemonReady(ctx, m.config.DockerClient); err != nil {
return fmt.Errorf("wait for Docker daemon: %w", err)
}
// Configure and start the corrosion service on the loopback if the machine is not initialised as a cluster
// member. This provides the store required for the machine to initialise a new cluster on it. Once the machine
// is initialised, the corrosion service is managed by the networkController.
if !m.Initialised() {
if err := m.configureCorrosion(); err != nil {
return fmt.Errorf("configure corrosion service: %w", err)
}
slog.Info("Configured corrosion service.", "dir", m.config.CorrosionDir)
if err := m.config.CorrosionService.Start(ctx); err != nil {
return fmt.Errorf("start corrosion service: %w", err)
}
}
// Use an errgroup to coordinate error handling and graceful shutdown of multiple machine components.
errGroup, ctx := errgroup.WithContext(ctx)
// Start the machine local API server.
apiSockPath := DefaultAPISockPath
if m.config.APISockPath != "" {
apiSockPath = m.config.APISockPath
}
localListener, err := listenUnixSocket(apiSockPath)
// Start the local machine API server.
machineListener, err := listenUnixSocket(m.config.MachineSockPath)
if err != nil {
return fmt.Errorf("listen API unix socket %q: %w", apiSockPath, err)
return fmt.Errorf("listen machine API unix socket %q: %w", m.config.MachineSockPath, err)
}
errGroup.Go(
func() error {
slog.Info("Starting local API server.", "path", apiSockPath)
if err := m.localServer.Serve(localListener); err != nil {
return fmt.Errorf("local API server failed: %w", err)
slog.Info("Starting local machine API server.", "path", m.config.MachineSockPath)
if err := m.localMachineServer.Serve(machineListener); err != nil {
return fmt.Errorf("local machine API server failed: %w", err)
}
return nil
},
)
close(m.started)
// Control loop for managing the network controller.
// Start the local API proxy server.
proxyListener, err := listenUnixSocket(m.config.UncloudSockPath)
if err != nil {
return fmt.Errorf("listen API proxy unix socket %q: %w", m.config.UncloudSockPath, err)
}
errGroup.Go(
func() error {
if !m.IsInitialised() {
slog.Info("Starting local API proxy server.", "path", m.config.UncloudSockPath)
if err := m.localProxyServer.Serve(proxyListener); err != nil {
return fmt.Errorf("local API proxy server failed: %w", err)
}
return nil
},
)
// Signal that the machine is ready.
close(m.started)
// Control loop for managing components that depend on the machine being initialised as a cluster member.
errGroup.Go(
func() error {
if !m.Initialised() {
slog.Info(
"Waiting for the machine to be initialised as a member of a cluster " +
"to start the network controller.",
@@ -171,9 +336,40 @@ func (m *Machine) Run(ctx context.Context) error {
// It can be reset when leaving the cluster and then re-initialised again with a new configuration.
case <-m.initialised:
var err error
m.cluster.UpdateMachineID(m.state.ID)
// Ensure the corrosion config is up to date, including a new gossip address if the machine
// has just joined a cluster.
if err = m.configureCorrosion(); err != nil {
return fmt.Errorf("configure corrosion service: %w", err)
}
slog.Info("Configured corrosion service.", "dir", m.config.CorrosionDir)
slog.Info("Starting network controller.")
networkServer := newGRPCServer(m, m.cluster)
ctrl, err = newNetworkController(m.state, networkServer, m.newMachinesCh)
// Update the proxy director's local address to the machine's management IP address, allowing
// the proxy to identify which requests should be proxied to the local machine API server.
m.proxyDirector.UpdateLocalAddress(m.state.Network.ManagementIP.String())
proxyServer := grpc.NewServer(
grpc.ForceServerCodecV2(proxy.Codec()),
grpc.UnknownServiceHandler(
proxy.TransparentHandler(m.proxyDirector.Director),
),
)
caddyfileCtrl, err := caddyfile.NewController(m.store, m.config.CaddyfilePath)
if err != nil {
return fmt.Errorf("create Caddyfile controller: %w", err)
}
ctrl, err = newNetworkController(
m.state,
m.store,
proxyServer,
m.config.CorrosionService,
m.config.DockerClient,
caddyfileCtrl,
)
if err != nil {
return fmt.Errorf("initialise network controller: %w", err)
}
@@ -208,10 +404,19 @@ func (m *Machine) Run(ctx context.Context) error {
errGroup.Go(
func() error {
<-ctx.Done()
slog.Info("Stopping local API server.")
slog.Info("Stopping local machine API server.")
// TODO: implement timeout for graceful shutdown.
m.localServer.GracefulStop()
slog.Info("Local API server stopped.")
m.localMachineServer.GracefulStop()
slog.Info("Local machine API server stopped.")
slog.Info("Stopping local API proxy server.")
// TODO: implement timeout for graceful shutdown.
m.localProxyServer.GracefulStop()
// Close the proxy director to close all backend connections.
m.proxyDirector.Close()
slog.Info("Local API proxy server stopped.")
m.config.DockerClient.Close()
return nil
},
)
@@ -223,50 +428,109 @@ func (m *Machine) Run(ctx context.Context) error {
// access mode and uncloud group if the group is found, otherwise it falls back to the root group.
func listenUnixSocket(path string) (net.Listener, error) {
gid := 0 // Fall back to the root group if the uncloud group is not found.
group, err := user.LookupGroup(DefaultAPISockGroup)
group, err := user.LookupGroup(DefaultSockGroup)
if err != nil {
//goland:noinspection GoTypeAssertionOnErrors
if _, ok := err.(user.UnknownGroupError); ok {
slog.Info(
"Specified group not found, using root group for the API socket.", "group", DefaultAPISockGroup, "path",
path,
"Specified group not found, using root group for the API socket.",
"group", DefaultSockGroup, "path", path,
)
} else {
return nil, fmt.Errorf("lookup %q group ID (GID): %w", DefaultAPISockGroup, err)
return nil, fmt.Errorf("lookup %q group ID (GID): %w", DefaultSockGroup, err)
}
} else {
gid, err = strconv.Atoi(group.Gid)
if err != nil {
return nil, fmt.Errorf("parse %q group ID (GID) %q: %w", DefaultAPISockGroup, group.Gid, err)
return nil, fmt.Errorf("parse %q group ID (GID) %q: %w", DefaultSockGroup, group.Gid, err)
}
}
// Ensure the parent directory exists and has the correct group permissions.
parent, _ := filepath.Split(path)
if err = os.MkdirAll(parent, 0750); err != nil {
return nil, fmt.Errorf("create directory %q: %w", parent, err)
}
if err = os.Chown(parent, -1, gid); err != nil {
return nil, fmt.Errorf("chown directory %q: %w", parent, err)
}
return sockets.NewUnixSocket(path, gid)
}
// InitCluster resets the local machine and initialises a new cluster with it.
func (m *Machine) configureCorrosion() error {
if err := corroservice.MkDataDir(m.config.CorrosionDir, m.config.CorrosionUser); err != nil {
return fmt.Errorf("create corrosion data directory: %w", err)
}
configPath := filepath.Join(m.config.CorrosionDir, "config.toml")
schemaPath := filepath.Join(m.config.CorrosionDir, "schema.sql")
// Use a loopback address as the gossip address (required) unless the machine has joined a cluster
// and has a management IP.
gossipAddr := netip.AddrPortFrom(netip.AddrFrom4([4]byte{127, 0, 0, 1}), corroservice.DefaultGossipPort)
if m.state.Network.ManagementIP.IsValid() {
gossipAddr = netip.AddrPortFrom(m.state.Network.ManagementIP, corroservice.DefaultGossipPort)
}
// TODO: use a partial list of machine peers for bootstrapping if the cluster is large.
var bootstrap []string
for _, peer := range m.state.Network.Peers {
if peer.Subnet == nil {
// Skip non-machine peers.
continue
}
bootstrap = append(bootstrap, netip.AddrPortFrom(peer.ManagementIP, corroservice.DefaultGossipPort).String())
}
cfg := corroservice.Config{
DB: corroservice.DBConfig{
Path: filepath.Join(m.config.CorrosionDir, "store.db"),
SchemaPaths: []string{schemaPath},
},
Gossip: corroservice.GossipConfig{
Addr: gossipAddr,
Bootstrap: bootstrap,
Plaintext: true,
},
API: corroservice.APIConfig{
Addr: m.config.CorrosionAPIAddr,
},
Admin: corroservice.AdminConfig{
Path: filepath.Join(m.config.CorrosionDir, "admin.sock"),
},
}
// TODO: change file permissions to 0640 root:uncloud to emphasize the owner is the machine, not corrosion.
if err := cfg.Write(configPath, m.config.CorrosionUser); err != nil {
return fmt.Errorf("write corrosion config: %w", err)
}
if err := os.WriteFile(schemaPath, []byte(store.Schema), 0644); err != nil {
return fmt.Errorf("write corrosion schema: %w", err)
}
return nil
}
// InitCluster initialises a new cluster on the local machine with the provided network configuration.
func (m *Machine) InitCluster(ctx context.Context, req *pb.InitClusterRequest) (*pb.InitClusterResponse, error) {
var err error
if m.Initialised() {
return nil, status.Error(codes.FailedPrecondition, "machine is already configured as a cluster member")
}
clusterNetwork, err := req.Network.ToPrefix()
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "invalid network: %v", err)
}
if err = m.cluster.Init(ctx, clusterNetwork); err != nil {
return nil, status.Errorf(codes.Internal, "init cluster: %v", err)
}
slog.Info("Cluster state initialised.", "network", clusterNetwork.String())
machineName := req.MachineName
if machineName == "" {
machineName, err = cluster.NewRandomMachineName()
if err != nil {
if machineName, err = cluster.NewRandomMachineName(); err != nil {
return nil, status.Errorf(codes.Internal, "generate machine name: %v", err)
}
}
// TODO: a proper cluster leave mechanism and machine reset should be implemented later.
// For now just reset the machine state and cluster state.
clusterStatePath := cluster.StatePath(m.config.DataDir)
clusterState := cluster.NewState(clusterStatePath)
if err = clusterState.Save(); err != nil {
return nil, status.Errorf(codes.Internal, "save cluster state: %v", err)
}
m.cluster.SetState(clusterState)
slog.Info("Cluster state initialised.", "path", clusterStatePath)
if err = m.cluster.SetNetwork(req.Network); err != nil {
return nil, status.Errorf(codes.InvalidArgument, "set cluster network: %v", err)
}
// Use the public and all routable IPs as endpoints.
ips, err := network.ListRoutableIPs()
if err != nil {
@@ -314,28 +578,10 @@ func (m *Machine) InitCluster(ctx context.Context, req *pb.InitClusterRequest) (
PrivateKey: m.state.Network.PrivateKey,
PublicKey: m.state.Network.PublicKey,
}
// Add a user to the cluster and build a peers config from it if provided.
if req.User != nil {
if err = m.cluster.AddUser(req.User); err != nil {
return nil, status.Errorf(codes.Internal, "add user to cluster: %v", err)
}
userManageIP, uErr := req.User.Network.ManagementIp.ToAddr()
if uErr != nil {
return nil, status.Error(codes.Internal, uErr.Error())
}
m.state.Network.Peers = make([]network.PeerConfig, 1)
m.state.Network.Peers[0] = network.PeerConfig{
ManagementIP: userManageIP,
PublicKey: req.User.Network.PublicKey,
}
}
if err = m.state.Save(); err != nil {
return nil, status.Errorf(codes.Internal, "save machine state: %v", err)
}
slog.Info("Cluster initialised with machine.", "machine", m.state.Name)
slog.Info("Cluster initialised with machine.", "id", m.state.ID, "machine", m.state.Name)
// Signal that the machine is initialised as a member of a cluster.
m.initialised <- struct{}{}
@@ -345,10 +591,11 @@ func (m *Machine) InitCluster(ctx context.Context, req *pb.InitClusterRequest) (
return resp, nil
}
// JoinCluster resets the local machine and configures it to join an existing cluster.
func (m *Machine) JoinCluster(ctx context.Context, req *pb.JoinClusterRequest) (*emptypb.Empty, error) {
// TODO: a proper cluster leave mechanism and machine reset should be implemented later.
// For now assume the machine wasn't part of a cluster.
// JoinCluster configures the local machine to join an existing cluster.
func (m *Machine) JoinCluster(_ context.Context, req *pb.JoinClusterRequest) (*emptypb.Empty, error) {
if m.Initialised() {
return nil, status.Error(codes.FailedPrecondition, "machine is already configured as a cluster member")
}
if req.Machine.Id == "" {
return nil, status.Error(codes.InvalidArgument, "machine ID not set")
@@ -455,3 +702,44 @@ func (m *Machine) Inspect(_ context.Context, _ *emptypb.Empty) (*pb.MachineInfo,
},
}, nil
}
// InspectService returns detailed information about a service and its containers.
func (m *Machine) InspectService(
ctx context.Context, req *pb.InspectServiceRequest,
) (*pb.InspectServiceResponse, error) {
opts := store.ListOptions{ServiceIDOrName: store.ServiceIDOrNameOptions{
ID: req.Id,
Name: req.Id,
}}
records, err := m.store.ListContainers(ctx, opts)
if err != nil {
return nil, status.Errorf(codes.Internal, "list containers: %v", err)
}
if len(records) == 0 {
return nil, status.Error(codes.NotFound, "service not found")
}
// TODO: handle SyncStatus to return only trusted container statuses.
// TODO: handle multiple services with the same name but different IDs. This can happen when two services
// with the same name are created concurrently on different machines.
containers := make([]*pb.Service_Container, len(records))
for i, r := range records {
containerJSON, err := json.Marshal(r.Container)
if err != nil {
return nil, status.Errorf(codes.Internal, "marshal container: %v", err)
}
containers[i] = &pb.Service_Container{
MachineId: r.MachineID,
Container: containerJSON,
}
}
svc := &pb.Service{
Id: records[0].Container.ServiceID(),
Name: records[0].Container.ServiceName(),
Mode: records[0].Container.ServiceMode(),
Containers: containers,
}
return &pb.InspectServiceResponse{Service: svc}, nil
}
+254 -70
View File
@@ -2,33 +2,54 @@ package machine
import (
"context"
"errors"
"fmt"
"github.com/cenkalti/backoff/v4"
"github.com/docker/docker/client"
"golang.org/x/sync/errgroup"
"google.golang.org/grpc"
"log/slog"
"net"
"net/netip"
"slices"
"strconv"
"time"
"uncloud/internal/machine/api/pb"
"uncloud/internal/machine/caddyfile"
"uncloud/internal/machine/corroservice"
"uncloud/internal/machine/docker"
"uncloud/internal/machine/network"
"uncloud/internal/machine/store"
)
const (
APIPort = 51000
DockerNetworkName = "uncloud"
DockerUserChain = "DOCKER-USER"
APIPort = 51000
)
type networkController struct {
state *State
wgnet *network.WireGuardNetwork
state *State
store *store.Store
wgnet *network.WireGuardNetwork
endpointChanges <-chan network.EndpointChangeEvent
server *grpc.Server
newMachinesCh <-chan *pb.MachineInfo
corroService corroservice.Service
dockerCli *client.Client
caddyfileCtrl *caddyfile.Controller
// TODO: DNS server/resolver listening on the machine IP, e.g. 10.210.0.1:53. It can't listen on 127.0.X.X
// like resolved does because it needs to be reachable from both the host and the containers.
}
func newNetworkController(state *State, server *grpc.Server, newMachCh <-chan *pb.MachineInfo) (
func newNetworkController(
state *State,
store *store.Store,
server *grpc.Server,
corroService corroservice.Service,
dockerCli *client.Client,
caddyfileCtrl *caddyfile.Controller,
) (
*networkController, error,
) {
slog.Info("Starting WireGuard network.")
@@ -36,12 +57,17 @@ func newNetworkController(state *State, server *grpc.Server, newMachCh <-chan *p
if err != nil {
return nil, fmt.Errorf("create WireGuard network: %w", err)
}
endpointChanges := wgnet.WatchEndpoints()
return &networkController{
state: state,
wgnet: wgnet,
server: server,
newMachinesCh: newMachCh,
state: state,
store: store,
wgnet: wgnet,
endpointChanges: endpointChanges,
server: server,
corroService: corroService,
dockerCli: dockerCli,
caddyfileCtrl: caddyfileCtrl,
}, nil
}
@@ -51,6 +77,21 @@ func (nc *networkController) Run(ctx context.Context) error {
}
slog.Info("WireGuard network configured.")
if nc.corroService.Running() {
// Corrosion service was running before the WireGuard network was configured so we need to restart it.
slog.Info("Restarting corrosion service to apply new configuration with WireGuard network.")
if err := nc.corroService.Restart(ctx); err != nil {
return fmt.Errorf("restart corrosion service: %w", err)
}
} else {
slog.Info("Starting corrosion service.")
if err := nc.corroService.Start(ctx); err != nil {
return fmt.Errorf("start corrosion service: %w", err)
}
}
// TODO: Figure out if we need to manually stop the corrosion service when the context is done or just
// rely on systemd to handle service dependencies on its own.
errGroup, ctx := errgroup.WithContext(ctx)
// Start the network API server. Assume the management IP can't be changed when the network is running.
@@ -69,40 +110,75 @@ func (nc *networkController) Run(ctx context.Context) error {
},
)
// Setup Docker network and iptables rules in a goroutine because it may block until the Docker daemon is ready.
// Setup Docker network and synchronise containers to the cluster store.
errGroup.Go(
func() error {
if err := nc.setupDockerNetwork(ctx); err != nil {
return fmt.Errorf("setup Docker network: %w", err)
}
slog.Info("Docker network configured.")
return nil
return nc.prepareAndWatchDocker(ctx)
},
)
// Handle new machines added to the cluster. Handling new machines and endpoint changes should be done
// Handle machine changes in the cluster. Handling machine and endpoint changes should be done
// in separate goroutines to avoid a deadlock when reconfiguring the network.
errGroup.Go(
func() error {
if err := nc.handleNewMachines(ctx); err != nil {
if err := nc.handleMachineChanges(ctx); err != nil {
return fmt.Errorf("handle new machines: %w", err)
}
return nil
},
)
// TODO: run another goroutine to watch WG network endpoint changes and update the state accordingly.
// Network updates in the state should not occur outside of this controller.
// Watch for endpoint changes and update the machine state accordingly.
errGroup.Go(
func() error {
for {
select {
case e, ok := <-nc.endpointChanges:
if !ok {
// The channel was closed, stop watching for changes.
nc.endpointChanges = nil
return nil
}
nc.state.mu.Lock()
for i := range nc.state.Network.Peers {
if nc.state.Network.Peers[i].PublicKey.Equal(e.PublicKey) {
nc.state.Network.Peers[i].Endpoint = &e.Endpoint
break
}
}
if err := nc.state.Save(); err != nil {
slog.Error("Failed to save machine state.", "err", err)
}
nc.state.mu.Unlock()
slog.Debug("Preserved endpoint change in the machine state.",
"public_key", e.PublicKey, "endpoint", e.Endpoint)
case <-ctx.Done():
return nil
}
}
},
)
errGroup.Go(
func() error {
if err = nc.wgnet.Run(ctx); err != nil {
if err := nc.wgnet.Run(ctx); err != nil {
return fmt.Errorf("WireGuard network failed: %w", err)
}
return nil
},
)
errGroup.Go(func() error {
slog.Info("Starting Caddyfile controller.")
if err := nc.caddyfileCtrl.Run(ctx); err != nil {
//goland:noinspection GoErrorStringFormat
return fmt.Errorf("Caddyfile controller failed: %w", err)
}
return nil
})
// Wait for the context to be done and stop the network API server.
errGroup.Go(
func() error {
@@ -118,59 +194,167 @@ func (nc *networkController) Run(ctx context.Context) error {
return errGroup.Wait()
}
func (nc *networkController) handleNewMachines(ctx context.Context) error {
for {
select {
case minfo := <-nc.newMachinesCh:
slog.Info("Handling new machine added to the cluster.", "name", minfo.Name)
// prepareAndWatchDocker configures the Docker network and watches local Docker containers to sync them
// to the cluster store.
func (nc *networkController) prepareAndWatchDocker(ctx context.Context) error {
manager := docker.NewManager(nc.dockerCli, nc.state.ID, nc.store)
if err := manager.WaitDaemonReady(ctx); err != nil {
return fmt.Errorf("wait for Docker daemon: %w", err)
}
// Skip the current machine.
nc.state.mu.RLock()
currentMachID := nc.state.ID
nc.state.mu.RUnlock()
if minfo.Id == currentMachID {
continue
}
if err := manager.EnsureUncloudNetwork(ctx, nc.state.Network.Subnet); err != nil {
return fmt.Errorf("ensure Docker network: %w", err)
}
slog.Info("Docker network configured.")
if err := minfo.Network.Validate(); err != nil {
slog.Error("Invalid machine network configuration.", "err", err)
continue
}
// Ignore errors as they are already validated.
subnet, _ := minfo.Network.Subnet.ToPrefix()
manageIP, _ := minfo.Network.ManagementIp.ToAddr()
endpoints := make([]netip.AddrPort, len(minfo.Network.Endpoints))
for i, ep := range minfo.Network.Endpoints {
addrPort, _ := ep.ToAddrPort()
endpoints[i] = addrPort
}
peer := network.PeerConfig{
Subnet: &subnet,
ManagementIP: manageIP,
AllEndpoints: endpoints,
PublicKey: minfo.Network.PublicKey,
}
if len(endpoints) > 0 {
peer.Endpoint = &endpoints[0]
}
nc.state.mu.Lock()
// TODO: deduplicate peers by public key, maybe implement addNetworkPeer method in the state.
nc.state.Network.Peers = append(nc.state.Network.Peers, peer)
err := nc.state.Save()
nc.state.mu.Unlock()
if err != nil {
return fmt.Errorf("save machine state: %w", err)
}
if err := nc.wgnet.Configure(*nc.state.Network); err != nil {
return fmt.Errorf("configure network with new peer: %w", err)
}
case <-ctx.Done():
slog.Info("Watching Docker containers and syncing them to cluster store.")
// Retry to watch and sync containers until the context is done.
boff := backoff.WithContext(backoff.NewExponentialBackOff(
backoff.WithInitialInterval(100*time.Millisecond),
backoff.WithMaxInterval(5*time.Second),
backoff.WithMaxElapsedTime(0),
), ctx)
watchAndSync := func() error {
if wErr := manager.WatchAndSyncContainers(ctx); wErr != nil {
slog.Error("Failed to watch and sync containers to cluster store, retrying.", "err", wErr)
return wErr
}
return nil
}
if err := backoff.Retry(watchAndSync, boff); err != nil {
if errors.Is(err, context.Canceled) {
return nil
}
return fmt.Errorf("watch and sync containers to cluster store: %w", err)
}
return nil
}
// handleMachineChanges subscribes to machine changes in the cluster and reconfigures the network peers accordingly
// when changes occur.
func (nc *networkController) handleMachineChanges(ctx context.Context) error {
for {
// Retry to subscribe to machine changes indefinitely until the context is done.
boff := backoff.WithContext(backoff.NewExponentialBackOff(
backoff.WithInitialInterval(1*time.Second),
backoff.WithMaxInterval(60*time.Second),
backoff.WithMaxElapsedTime(0),
), ctx)
var (
machines []*pb.MachineInfo
changes <-chan struct{}
err error
)
subscribe := func() error {
if machines, changes, err = nc.store.SubscribeMachines(ctx); err != nil {
slog.Info("Failed to subscribe to machine changes, retrying.", "err", err)
}
return err
}
if err = backoff.Retry(subscribe, boff); err != nil {
if errors.Is(err, context.Canceled) {
return nil
}
slog.Error("Unexpected error while retrying to subscribe to machine changes.", "err", err)
continue
}
slog.Info("Subscribed to machine changes in the cluster to reconfigure network peers.")
// The machine store may be empty when a machine first joins the cluster, before store synchronization
// completes. Skip configuration now and apply it when the store changes are received.
if len(machines) > 0 {
slog.Info("Reconfiguring network peers with the current machines.", "machines", len(machines))
if err = nc.configurePeers(machines); err != nil {
slog.Error("Failed to configure peers.", "err", err)
}
}
// For simplicity, reconfigure all peers on any change.
for {
select {
// TODO: test when Corrosion fails and the subscription fails to resubscribe (after 1 minute). It seems
// the changes channel will be closed and this will become a busy loop. Perhaps, the outer for loop should
// be reworked as well.
case <-changes:
slog.Info("Cluster machines changed, reconfiguring network peers.")
if machines, err = nc.store.ListMachines(ctx); err != nil {
slog.Error("Failed to list machines.", "err", err)
continue
}
if err = nc.configurePeers(machines); err != nil {
slog.Error("Failed to configure peers.", "err", err)
}
case <-ctx.Done():
return nil
}
}
}
}
func (nc *networkController) configurePeers(machines []*pb.MachineInfo) error {
if len(machines) == 0 {
return fmt.Errorf("no machines to configure peers")
}
nc.state.mu.RLock()
currentPeerEndpoints := make(map[string]*netip.AddrPort, len(nc.state.Network.Peers))
for _, p := range nc.state.Network.Peers {
currentPeerEndpoints[p.PublicKey.String()] = p.Endpoint
}
nc.state.mu.RUnlock()
// Construct the list of peers from the machine configurations ensuring that the current endpoint is preserved.
peers := make([]network.PeerConfig, 0, len(machines)-1)
for _, m := range machines {
// Skip the current machine.
if m.Id == nc.state.ID {
continue
}
if err := m.Network.Validate(); err != nil {
slog.Error("Invalid machine network configuration.", "machine", m.Name, "err", err)
continue
}
// Ignore errors as they are already validated.
subnet, _ := m.Network.Subnet.ToPrefix()
manageIP, _ := m.Network.ManagementIp.ToAddr()
endpoints := make([]netip.AddrPort, len(m.Network.Endpoints))
for i, ep := range m.Network.Endpoints {
addrPort, _ := ep.ToAddrPort()
endpoints[i] = addrPort
}
peer := network.PeerConfig{
Subnet: &subnet,
ManagementIP: manageIP,
AllEndpoints: endpoints,
PublicKey: m.Network.PublicKey,
}
currentEndpoint := currentPeerEndpoints[peer.PublicKey.String()]
if currentEndpoint != nil && slices.Contains(endpoints, *currentEndpoint) {
peer.Endpoint = currentEndpoint
} else if len(endpoints) > 0 {
peer.Endpoint = &endpoints[0]
}
peers = append(peers, peer)
}
// Preserve the new list of peers in the machine state.
nc.state.mu.Lock()
nc.state.Network.Peers = peers
err := nc.state.Save()
nc.state.mu.Unlock()
if err != nil {
return fmt.Errorf("save machine state: %w", err)
}
nc.state.mu.RLock()
defer nc.state.mu.RUnlock()
if err = nc.wgnet.Configure(*nc.state.Network); err != nil {
return fmt.Errorf("configure network peers: %w", err)
}
return nil
}
// TODO: method to shutdown network when leaving a cluster. Regular context cancellation shouldn't bring it down.
+8
View File
@@ -7,6 +7,7 @@ import (
"net"
"net/http"
"net/netip"
"regexp"
"strings"
"time"
)
@@ -24,6 +25,13 @@ func ListRoutableIPs() ([]netip.Addr, error) {
// Skip the Uncloud WireGuard and Docker interfaces.
continue
}
// Docker bridge name doesn't seem to be documented but this is the source code where it is generated:
// https://github.com/moby/moby/blob/v27.2.1/libnetwork/drivers/bridge/bridge_linux.go#L664
if match, _ := regexp.MatchString(`br-[0-9a-f]{12}`, iface.Name); match {
// Skip Docker bridge interfaces.
continue
}
if iface.Flags&net.FlagUp == 0 || iface.Flags&net.FlagRunning == 0 || iface.Flags&net.FlagLoopback != 0 {
// Skip interfaces:
// * Not administratively UP.
+30 -7
View File
@@ -8,11 +8,6 @@ import (
"uncloud/internal/secret"
)
var (
DefaultNetwork = netip.MustParsePrefix("10.210.0.0/16")
DefaultSubnetBits = 24
)
type Config struct {
// Subnet is the IPv4 address range allocated to the machine. The machine's IP address is the first address
// in the subnet. Other IP addresses are allocated to containers running on the machine.
@@ -41,7 +36,9 @@ func (c Config) IsConfigured() bool {
c.PrivateKey != nil && c.PublicKey != nil
}
func (c Config) toDeviceConfig() (wgtypes.Config, error) {
// toDeviceConfig converts the configuration to a WireGuard device configuration. It updates the existing peers
// without replacing them to not disrupt existing connections and to not lose track of the last handshake time.
func (c Config) toDeviceConfig(currentPeers []wgtypes.Peer) (wgtypes.Config, error) {
privateKey, err := wgtypes.NewKey(c.PrivateKey)
if err != nil {
return wgtypes.Config{}, fmt.Errorf("parse private key: %w", err)
@@ -50,6 +47,8 @@ func (c Config) toDeviceConfig() (wgtypes.Config, error) {
persistentKeepalive := WireGuardKeepaliveInterval
wgPeerConfigs := make([]wgtypes.PeerConfig, len(c.Peers))
// A set of new peer public keys for checking which current peers should be removed.
newPeersSet := make(map[string]struct{}, len(c.Peers))
for i, peerConfig := range c.Peers {
peerPublicKey, kErr := wgtypes.NewKey(peerConfig.PublicKey)
if kErr != nil {
@@ -75,12 +74,36 @@ func (c Config) toDeviceConfig() (wgtypes.Config, error) {
Port: int(peerConfig.Endpoint.Port()),
}
}
newPeersSet[wgPeerConfigs[i].PublicKey.String()] = struct{}{}
}
// Remove peers that are not in the configuration.
for _, p := range currentPeers {
if _, ok := newPeersSet[p.PublicKey.String()]; !ok {
wgPeerConfigs = append(wgPeerConfigs, wgtypes.PeerConfig{
PublicKey: p.PublicKey,
Remove: true,
})
}
}
return wgtypes.Config{
PrivateKey: &privateKey,
ListenPort: &listenPort,
ReplacePeers: true,
ReplacePeers: false,
Peers: wgPeerConfigs,
}, nil
}
func (p *PeerConfig) prefixes() ([]netip.Prefix, error) {
managePrefix, err := addrToSingleIPPrefix(p.ManagementIP)
if err != nil {
return nil, fmt.Errorf("parse management IP: %w", err)
}
prefixes := []netip.Prefix{managePrefix}
if p.Subnet != nil {
prefixes = append(prefixes, *p.Subnet)
}
return prefixes, nil
}
+172
View File
@@ -0,0 +1,172 @@
package network
import (
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
"log/slog"
"net/netip"
"slices"
"time"
)
const (
PeerStatusUnknown = "unknown"
PeerStatusUp = "up"
PeerStatusDown = "down"
)
type peer struct {
config PeerConfig
lastEndpointChangeTime time.Time
lastHandshakeTime time.Time
receiveBytes int64
transmitBytes int64
status string
}
func newPeer(config PeerConfig, wgPeer *wgtypes.Peer) *peer {
p := &peer{
config: config,
status: PeerStatusUnknown,
}
if p.config.Endpoint != nil {
p.lastEndpointChangeTime = time.Now()
// Reset the endpoint change time if the endpoint is the same as the one in the current WireGuard peer.
// This is to avoid unnecessary endpoint rotation for the already connected peer.
if wgPeer != nil && wgPeer.Endpoint != nil {
wgEndpoint := wgPeer.Endpoint.AddrPort()
if *p.config.Endpoint == wgEndpoint {
p.lastEndpointChangeTime = time.Time{}
}
}
}
return p
}
func (p *peer) updateConfig(config PeerConfig) {
if p.config.Endpoint != config.Endpoint {
p.lastEndpointChangeTime = time.Now()
p.status = PeerStatusUnknown
}
p.config = config
}
func (p *peer) updateFromDevice(wgPeer wgtypes.Peer) (endpointChanged bool) {
if wgPeer.Endpoint != nil {
wgEndpoint := wgPeer.Endpoint.AddrPort()
if p.config.Endpoint == nil || *p.config.Endpoint != wgEndpoint {
// The peer endpoint has been automatically updated on the WireGuard device which normally happens
// when the peer establishes a reverse connection to this machine.
p.config.Endpoint = &wgEndpoint
// Reset the endpoint change time to not attempt to rotate the endpoint and correctly calculate the status.
p.lastEndpointChangeTime = time.Time{}
endpointChanged = true
slog.Info("Peer endpoint automatically updated on WireGuard interface by establishing a reverse "+
"connection to this machine.", "public_key", p.config.PublicKey, "endpoint", wgEndpoint)
}
}
p.lastHandshakeTime = wgPeer.LastHandshakeTime
p.receiveBytes = wgPeer.ReceiveBytes
p.transmitBytes = wgPeer.TransmitBytes
p.calculateStatus()
return
}
// Peer status calculation is based on Talos Kubespan implementation:
// https://github.com/siderolabs/talos/blob/v1.8.0/internal/app/machined/pkg/adapters/kubespan/peer_status.go
// endpointConnectionTimeout is time to wait for initial handshake when the endpoint is just set.
const endpointConnectionTimeout = 15 * time.Second
// peerDownInterval is the time since last handshake when established peer is considered to be down.
//
// WG whitepaper defines a downed peer as being:
// Handshake Timeout (180s) + Rekey Timeout (5s) + Rekey Attempt Timeout (90s)
//
// This interval is applied when the link is already established.
const peerDownInterval = (180 + 5 + 90) * time.Second
// calculateStatus updates the peer's connection status based on other field values.
//
// Goal: endpoint is ultimately down if we haven't seen handshake for more than peerDownInterval,
// but as the endpoints get updated we want faster feedback, so we start checking more aggressively
// that the handshake happened within endpointConnectionTimeout since last endpoint change.
//
// Timeline:
//
// ---------------------------------------------------------------------->
// ^ ^ ^
// | | |
// T0 T0+endpointConnectionTimeout T0+peerDownInterval
//
// Where T0 = lastEndpointChangeTime
//
// The question is where is LastHandshakeTimeout vs. those points above:
//
// - if we're past (T0+peerDownInterval), simply check that time since last handshake < peerDownInterval
// - if we're between (T0+endpointConnectionTimeout) and (T0+peerDownInterval), and there's no handshake
// after the endpoint change, assume that the endpoint is down
// - if we're between (T0) and (T0+endpointConnectionTimeout), and there's no handshake since the endpoint change,
// consider the state to be unknown
func (p *peer) calculateStatus() {
lastStatus := p.status
sinceLastHandshake := time.Since(p.lastHandshakeTime)
sinceEndpointChange := time.Since(p.lastEndpointChangeTime)
switch {
case sinceEndpointChange > peerDownInterval: // past T0+peerDownInterval
// If we got handshake in the last peerDownInterval, endpoint is up.
if sinceLastHandshake < peerDownInterval {
p.status = PeerStatusUp
} else {
p.status = PeerStatusDown
}
case sinceEndpointChange < endpointConnectionTimeout: // between (T0) and (T0+endpointConnectionTimeout)
// Endpoint got recently updated, consider no handshake as 'unknown'.
if p.lastHandshakeTime.After(p.lastEndpointChangeTime) {
p.status = PeerStatusUp
} else {
p.status = PeerStatusUnknown
}
default: // otherwise, we're between (T0+endpointConnectionTimeout) and (T0+peerDownInterval)
// If we haven't had the handshake yet, consider the endpoint to be down.
if p.lastHandshakeTime.After(p.lastEndpointChangeTime) {
p.status = PeerStatusUp
} else {
p.status = PeerStatusDown
}
}
if p.status == PeerStatusDown && p.config.Endpoint == nil {
// No endpoint, so unknown.
p.status = PeerStatusUnknown
}
if p.status != lastStatus {
slog.Info("Peer status changed.", "public_key", p.config.PublicKey,
"status", p.status, "previous_status", lastStatus)
}
}
// shouldChangeEndpoint returns the next endpoint to use and a boolean indicating if the endpoint should be changed.
func (p *peer) shouldChangeEndpoint() (netip.AddrPort, bool) {
if p.config.Endpoint != nil && p.status != PeerStatusDown {
// Shouldn't change the endpoint if it's set and the status is 'up' or 'unknown'.
return netip.AddrPort{}, false
}
if len(p.config.AllEndpoints) == 0 {
// No endpoints to choose from.
return netip.AddrPort{}, false
}
if p.config.Endpoint == nil {
// No endpoint set, so choose the first one.
return p.config.AllEndpoints[0], true
}
if len(p.config.AllEndpoints) == 1 && p.config.Endpoint == &p.config.AllEndpoints[0] {
// Only one endpoint and it's the current one, can't rotate.
return netip.AddrPort{}, false
}
// The endpoint is set and the status is 'down', so rotate to the next one.
idx := slices.Index(p.config.AllEndpoints, *p.config.Endpoint)
endpoint := p.config.AllEndpoints[(idx+1)%len(p.config.AllEndpoints)]
return endpoint, true
}
+7
View File
@@ -3,6 +3,7 @@ package network
import (
"fmt"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
"net/netip"
"time"
"uncloud/internal/secret"
)
@@ -14,6 +15,12 @@ const (
WireGuardKeepaliveInterval = 25 * time.Second
)
type EndpointChangeEvent struct {
PublicKey secret.Secret
// Endpoint is the new endpoint of the peer.
Endpoint netip.AddrPort
}
// NewMachineKeys generates a new WireGuard private and public key pair.
func NewMachineKeys() (privKey, pubKey secret.Secret, err error) {
wgPrivKey, err := wgtypes.GeneratePrivateKey()
@@ -20,3 +20,7 @@ func (n *WireGuardNetwork) Configure(config Config) error {
func (n *WireGuardNetwork) Run(ctx context.Context) error {
return errors.New("not implemented on darwin")
}
func (n *WireGuardNetwork) WatchEndpoints() <-chan EndpointChangeEvent {
return nil
}
+233 -56
View File
@@ -10,33 +10,32 @@ import (
"go4.org/netipx"
"golang.org/x/sys/unix"
"golang.zx2c4.com/wireguard/wgctrl"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
"log/slog"
"net"
"net/netip"
"slices"
"sync"
"time"
"uncloud/internal/secret"
)
type WireGuardNetwork struct {
link netlink.Link
peers []peer
link netlink.Link
// peers is a map of peers indexed by their public key.
peers map[string]*peer
// watchers is a list of channels that are notified when the endpoints of the peers change.
watchers []chan EndpointChangeEvent
// mu synchronises concurrent network configuration changes.
mu sync.Mutex
}
type peer struct {
config PeerConfig
lastEndpointChangeTime time.Time
}
func NewWireGuardNetwork() (*WireGuardNetwork, error) {
link, err := createOrGetLink(WireGuardInterfaceName)
if err != nil {
return nil, fmt.Errorf("create or get WireGuard link %q: %v", WireGuardInterfaceName, err)
}
return &WireGuardNetwork{
link: link,
}, nil
return &WireGuardNetwork{link: link}, nil
}
// createOrGetLink creates a new WireGuard link with the given name if it doesn't already exist, otherwise it returns the existing link.
@@ -74,39 +73,9 @@ func (n *WireGuardNetwork) Configure(config Config) error {
n.mu.Lock()
defer n.mu.Unlock()
// Reconstruct the list of peers, ensuring that the last endpoint change time is preserved for any existing peers.
existingPeersByPublicKey := map[string]peer{}
for _, p := range n.peers {
existingPeersByPublicKey[p.config.PublicKey.String()] = p
}
n.peers = make([]peer, len(config.Peers))
for i, peerConfig := range config.Peers {
n.peers[i] = peer{
config: peerConfig,
}
existingPeer, ok := existingPeersByPublicKey[peerConfig.PublicKey.String()]
if ok && existingPeer.config.Endpoint == peerConfig.Endpoint {
n.peers[i].lastEndpointChangeTime = existingPeer.lastEndpointChangeTime
} else {
n.peers[i].lastEndpointChangeTime = time.Now()
}
}
wg, err := wgctrl.New()
if err != nil {
return fmt.Errorf("create WireGuard client: %w", err)
}
//goland:noinspection GoUnhandledErrorResult
defer wg.Close()
wgConfig, err := config.toDeviceConfig()
if err != nil {
if err := n.configureDevice(config); err != nil {
return err
}
// Apply the new configuration to the WireGuard device.
if err = wg.ConfigureDevice(n.link.Attrs().Name, wgConfig); err != nil {
return fmt.Errorf("configure WireGuard device %q: %w", n.link.Attrs().Name, err)
}
slog.Info("Configured WireGuard interface.", "name", n.link.Attrs().Name)
machinePrefix := netip.PrefixFrom(MachineIP(config.Subnet), config.Subnet.Bits())
@@ -141,6 +110,65 @@ func (n *WireGuardNetwork) Configure(config Config) error {
return nil
}
func (n *WireGuardNetwork) configureDevice(config Config) error {
wg, err := wgctrl.New()
if err != nil {
return fmt.Errorf("create WireGuard client: %w", err)
}
defer wg.Close()
// Get the current WireGuard peers from the device which are required to reconstruct the local peers structs
// and build a new config for the device.
dev, err := wg.Device(n.link.Attrs().Name)
if err != nil {
return fmt.Errorf("get WireGuard device %q: %w", n.link.Attrs().Name, err)
}
if n.peers == nil {
// This is the first time we configure this instance of the network. If the device has been configured earlier
// and the daemon restarted, attempt to reconstruct the peers from the device to avoid unnecessary endpoint
// rotations and connection disruptions.
n.peers = make(map[string]*peer, len(config.Peers))
wgPeers := make(map[string]*wgtypes.Peer, len(dev.Peers))
for i, _ := range dev.Peers {
wgPeers[secret.Secret(dev.Peers[i].PublicKey[:]).String()] = &dev.Peers[i]
}
for _, pc := range config.Peers {
wgPeer := wgPeers[pc.PublicKey.String()]
n.peers[pc.PublicKey.String()] = newPeer(pc, wgPeer)
}
}
// Create or update peer structs based on the config.
newPeersSet := make(map[string]struct{}, len(config.Peers))
for _, pc := range config.Peers {
if p, ok := n.peers[pc.PublicKey.String()]; ok {
p.updateConfig(pc)
} else {
// WireGuard peers should not be configured out of band so the current WG peer is provided only
// for the first time configuration above.
n.peers[pc.PublicKey.String()] = newPeer(pc, nil)
}
newPeersSet[pc.PublicKey.String()] = struct{}{}
}
// Delete peers that are no longer in the config.
for k := range n.peers {
if _, ok := newPeersSet[k]; !ok {
delete(n.peers, k)
}
}
wgConfig, err := config.toDeviceConfig(dev.Peers)
if err != nil {
return err
}
// Apply the new configuration to the WireGuard device.
if err = wg.ConfigureDevice(n.link.Attrs().Name, wgConfig); err != nil {
return fmt.Errorf("configure WireGuard device %q: %w", n.link.Attrs().Name, err)
}
return nil
}
// updateAddresses assigns addresses to the WireGuard interface and removes old ones.
// It also removes any other addresses that have been added out of band.
func (n *WireGuardNetwork) updateAddresses(addrs []netip.Prefix) error {
@@ -178,7 +206,7 @@ func (n *WireGuardNetwork) updatePeerRoutes() error {
// Build a set of compacted IP ranges for all peers.
var ipsetBuilder netipx.IPSetBuilder
for _, p := range n.peers {
prefixes, err := p.prefixes()
prefixes, err := p.config.prefixes()
if err != nil {
return fmt.Errorf("get peer addresses: %w", err)
}
@@ -200,13 +228,16 @@ func (n *WireGuardNetwork) updatePeerRoutes() error {
Scope: netlink.SCOPE_LINK,
Dst: &dst,
},
); err != nil && !errors.Is(err, unix.EEXIST) {
return fmt.Errorf("add route to WireGuard link %q: %w", n.link.Attrs().Name, err)
); err != nil {
if !errors.Is(err, unix.EEXIST) {
return fmt.Errorf("add route to WireGuard link %q: %w", n.link.Attrs().Name, err)
}
} else {
slog.Debug(
"Added route to peer(s) via WireGuard interface.",
"name", n.link.Attrs().Name, "dst", prefix,
)
}
slog.Debug(
"Added route to peer(s) via WireGuard interface.",
"name", n.link.Attrs().Name, "dst", prefix,
)
}
// Remove old routes to IP ranges that are no longer in the configuration.
@@ -235,18 +266,164 @@ func (n *WireGuardNetwork) updatePeerRoutes() error {
}
func (n *WireGuardNetwork) Run(ctx context.Context) error {
<-ctx.Done()
wg, err := wgctrl.New()
if err != nil {
return fmt.Errorf("create WireGuard client: %w", err)
}
defer wg.Close()
ticker := time.NewTicker(1 * time.Second)
for {
select {
case <-ticker.C:
n.mu.Lock()
if err = n.updatePeersFromDevice(ctx); err != nil {
slog.Error("Failed to update peers status from WireGuard interface.",
"name", n.link.Attrs().Name, "err", err)
}
if err = n.changeWireGuardEndpoints(ctx); err != nil {
slog.Error("Failed to update peer endpoints on WireGuard interface.",
"name", n.link.Attrs().Name, "err", err)
}
n.mu.Unlock()
case <-ctx.Done():
for _, ch := range n.watchers {
close(ch)
}
return nil
}
}
}
// WatchEndpoints returns a channel that receives endpoint change events for the WireGuard peers.
func (n *WireGuardNetwork) WatchEndpoints() <-chan EndpointChangeEvent {
ch := make(chan EndpointChangeEvent)
n.watchers = append(n.watchers, ch)
return ch
}
// updatePeersFromDevice updates the peers status from the WireGuard device peers.
// mu lock must be held before calling this method.
func (n *WireGuardNetwork) updatePeersFromDevice(ctx context.Context) error {
wg, err := wgctrl.New()
if err != nil {
return fmt.Errorf("create WireGuard client: %w", err)
}
defer wg.Close()
dev, err := wg.Device(n.link.Attrs().Name)
if err != nil {
return fmt.Errorf("get WireGuard device %q: %w", n.link.Attrs().Name, err)
}
var events []EndpointChangeEvent
for _, wgPeer := range dev.Peers {
publicKey := secret.Secret(wgPeer.PublicKey[:])
if p, ok := n.peers[publicKey.String()]; ok {
endpointChanged := p.updateFromDevice(wgPeer)
if endpointChanged {
events = append(events, EndpointChangeEvent{
PublicKey: publicKey,
Endpoint: *p.config.Endpoint,
})
}
} else {
// Assume that WG peers are not updated out of band so they should always be in sync with the config.
slog.Warn("Found WireGuard peer that is not in the configuration.", "public_key", publicKey)
}
}
if len(events) > 0 {
if err = n.notifyWatchers(ctx, events); err != nil {
// As of October 2024, the machine is the only watcher to persist changed endpoints in the machine
// state which can tolerate missed events. Therefore, we can ignore the error here.
slog.Error("Failed to notify watchers about a peer endpoint change.", "err", err)
}
}
return nil
}
func (p peer) prefixes() ([]netip.Prefix, error) {
managePrefix, err := addrToSingleIPPrefix(p.config.ManagementIP)
// changeWireGuardEndpoints rotates the endpoints of the WireGuard peers with 'down' status
// in an attempt to find a working one.
// mu lock must be held before calling this method.
func (n *WireGuardNetwork) changeWireGuardEndpoints(ctx context.Context) error {
var wgPeerConfigs []wgtypes.PeerConfig
var events []EndpointChangeEvent
for _, p := range n.peers {
newEndpoint, ok := p.shouldChangeEndpoint()
if !ok {
continue
}
newConfig := p.config
newConfig.Endpoint = &newEndpoint
p.updateConfig(newConfig)
publicKey, err := wgtypes.NewKey(p.config.PublicKey)
if err != nil {
return fmt.Errorf("parse peer public key: %w", err)
}
wgPeerConfigs = append(wgPeerConfigs, wgtypes.PeerConfig{
PublicKey: publicKey,
UpdateOnly: true,
Endpoint: &net.UDPAddr{
IP: p.config.Endpoint.Addr().AsSlice(),
Port: int(p.config.Endpoint.Port()),
},
})
events = append(events, EndpointChangeEvent{
PublicKey: p.config.PublicKey,
Endpoint: *p.config.Endpoint,
})
}
if len(wgPeerConfigs) == 0 {
// No changes to the endpoints.
return nil
}
wg, err := wgctrl.New()
if err != nil {
return nil, fmt.Errorf("parse management IP: %w", err)
return fmt.Errorf("create WireGuard client: %w", err)
}
prefixes := []netip.Prefix{managePrefix}
if p.config.Subnet != nil {
prefixes = append(prefixes, *p.config.Subnet)
defer wg.Close()
wgConfigPatch := wgtypes.Config{
ReplacePeers: false,
Peers: wgPeerConfigs,
}
return prefixes, nil
// Apply the configuration patch to the WireGuard device.
if err = wg.ConfigureDevice(n.link.Attrs().Name, wgConfigPatch); err != nil {
return fmt.Errorf("configure WireGuard device %q with endpoint changes: %w", n.link.Attrs().Name, err)
}
for _, pc := range wgPeerConfigs {
publicKey := secret.Secret(pc.PublicKey[:])
slog.Info("Changed peer endpoint on WireGuard interface.",
"name", n.link.Attrs().Name, "public_key", publicKey, "endpoint", pc.Endpoint,
"status", n.peers[publicKey.String()].status)
}
if err = n.notifyWatchers(ctx, events); err != nil {
// As of October 2024, the machine is the only watcher to persist changed endpoints in the machine
// state which can tolerate missed events. Therefore, we can ignore the error here.
slog.Error("Failed to notify watchers about a peer endpoint change.", "err", err)
}
return nil
}
// notifyWatchers notifies the watchers about peer endpoint changes.
func (n *WireGuardNetwork) notifyWatchers(ctx context.Context, events []EndpointChangeEvent) error {
for _, ch := range n.watchers {
for _, e := range events {
select {
case ch <- e:
// Use a timeout to avoid blocking the network control loop.
case <-time.After(1 * time.Second):
return errors.New("timeout 1 second")
case <-ctx.Done():
return nil
}
}
}
return nil
}
-13
View File
@@ -1,13 +0,0 @@
//go:build darwin
package machine
import (
"context"
"fmt"
)
// setupDockerNetwork is a stub for darwin.
func (nc *networkController) setupDockerNetwork(ctx context.Context) error {
return fmt.Errorf("not supported on darwin")
}
-108
View File
@@ -1,108 +0,0 @@
//go:build linux
package machine
import (
"context"
"fmt"
dnetwork "github.com/docker/docker/api/types/network"
"github.com/docker/docker/client"
"github.com/docker/docker/libnetwork/iptables"
"log/slog"
"time"
"uncloud/internal/machine/network"
)
// setupDockerNetwork creates the Docker bridge network DockerNetworkName with the machine subnet and configures
// iptables to allow WireGuard network to access containers.
func (nc *networkController) setupDockerNetwork(ctx context.Context) error {
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
if err != nil {
return fmt.Errorf("init Docker client: %w", err)
}
defer cli.Close()
// Wait for the Docker daemon to start and be ready by sending a ping request in a loop.
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
ready, waitingLogged := false, false
for !ready {
select {
case <-ctx.Done():
return ctx.Err()
case <-ticker.C:
_, err = cli.Ping(ctx)
if err == nil {
ready = true
break
}
if !client.IsErrConnectionFailed(err) {
return fmt.Errorf("connect to Docker daemon: %w", err)
}
if !waitingLogged {
slog.Info("Waiting for Docker daemon to start and be ready to setup Docker network.")
waitingLogged = true
}
}
}
// Ensure the Docker network 'uncloud' is created with the correct subnet.
needsCreation := false
nw, err := cli.NetworkInspect(ctx, DockerNetworkName, dnetwork.InspectOptions{})
if err != nil {
if !client.IsErrNotFound(err) {
return fmt.Errorf("inspect Docker network %q: %w", DockerNetworkName, err)
}
needsCreation = true
} else if nw.IPAM.Config[0].Subnet != nc.state.Network.Subnet.String() {
// Remove the Docker network if the subnet is different.
// It could be a leftover from a previous incomplete cleanup.
slog.Info(
"Removing Docker network with old subnet.", "name", DockerNetworkName, "subnet", nw.IPAM.Config[0].Subnet,
)
if err = cli.NetworkRemove(ctx, DockerNetworkName); err != nil {
// It can still fail if the network is in use by a container. Leave it to the user to resolve the issue.
return fmt.Errorf("remove Docker network %q: %w", DockerNetworkName, err)
}
needsCreation = true
}
if needsCreation {
if _, err = cli.NetworkCreate(
ctx, DockerNetworkName, dnetwork.CreateOptions{
Driver: "bridge",
Scope: "local",
IPAM: &dnetwork.IPAM{
Config: []dnetwork.IPAMConfig{
{
Subnet: nc.state.Network.Subnet.String(),
},
},
},
},
); err != nil {
return fmt.Errorf("create Docker network %q: %w", DockerNetworkName, err)
}
slog.Info("Docker network created.", "name", DockerNetworkName, "subnet", nc.state.Network.Subnet.String())
if nw, err = cli.NetworkInspect(ctx, DockerNetworkName, dnetwork.InspectOptions{}); err != nil {
return fmt.Errorf("inspect Docker network %q: %w", DockerNetworkName, err)
}
}
// Configure iptables to allow WireGuard network to access containers. The Docker daemon should have already
// created the DOCKER-USER chain at this point.
// TODO: check if this works when firewalld used instead of raw iptables. The Docker daemon has a different
// code path for firewalld.
// Bridge name doesn't seem to be documented but this is the source code where it is generated:
// https://github.com/moby/moby/blob/v27.2.1/libnetwork/drivers/bridge/bridge_linux.go#L664
bridgeName := "br-" + nw.ID[:12]
ipt := iptables.GetIptable(iptables.IPv4)
rule := []string{"--in-interface", network.WireGuardInterfaceName, "--out-interface", bridgeName, "-j", "ACCEPT"}
if err = ipt.ProgramRule(iptables.Filter, DockerUserChain, iptables.Insert, rule); err != nil {
return fmt.Errorf("insert iptables rule: %w", err)
}
return nil
}
+1 -37
View File
@@ -3,11 +3,9 @@ package machine
import (
"encoding/json"
"fmt"
"net/netip"
"os"
"path/filepath"
"sync"
"uncloud/internal/machine/cluster"
"uncloud/internal/machine/network"
)
@@ -73,7 +71,7 @@ func (c *State) Save() error {
return fmt.Errorf("state path not set")
}
dir, _ := filepath.Split(c.path)
if err := os.MkdirAll(dir, 0700); err != nil {
if err := os.MkdirAll(dir, 0711); err != nil {
return fmt.Errorf("create state directory %q: %w", dir, err)
}
@@ -83,37 +81,3 @@ func (c *State) Save() error {
}
return os.WriteFile(c.path, data, 0600)
}
// NewBootstrapConfig returns a new machine configuration that should be applied to the first machine in a cluster.
func NewBootstrapConfig(name string, subnet netip.Prefix, peers ...network.PeerConfig) (*State, error) {
mid, err := cluster.NewMachineID()
if err != nil {
return nil, fmt.Errorf("generate machine ID: %w", err)
}
if name == "" {
name, err = cluster.NewRandomMachineName()
if err != nil {
return nil, fmt.Errorf("generate machine name: %w", err)
}
}
privKey, pubKey, err := network.NewMachineKeys()
if err != nil {
return nil, fmt.Errorf("generate machine keys: %w", err)
}
if subnet == (netip.Prefix{}) {
// Use the first /24 subnet in the default network.
subnet = netip.PrefixFrom(network.DefaultNetwork.Addr(), network.DefaultSubnetBits)
}
return &State{
ID: mid,
Name: name,
Network: &network.Config{
Subnet: subnet,
ManagementIP: network.ManagementIP(pubKey),
PrivateKey: privKey,
PublicKey: pubKey,
Peers: peers,
},
}, nil
}
+220
View File
@@ -0,0 +1,220 @@
package store
import (
"context"
"encoding/json"
"fmt"
sq "github.com/Masterminds/squirrel"
"log/slog"
"strings"
"time"
"uncloud/internal/api"
)
const (
// SyncStatusSynced indicates that a container record is synchronised with the Docker daemon. The record may
// become outdated even when the status is "synced" if the machine crashes or a network partition occurs.
// The cluster membership state of the machine should also be checked to determine if the record can be trusted.
SyncStatusSynced = "synced"
// SyncStatusOutdated indicates that a container record may be outdated, for example, due to being unable
// to retrieve the container's state from the Docker daemon or when the machine is being stopped or restarted.
SyncStatusOutdated = "outdated"
)
type ContainerRecord struct {
Container *api.Container
MachineID string
SyncStatus string
UpdatedAt time.Time
}
type ListOptions struct {
// MachineIDs filters containers by the machine IDs they are running on.
MachineIDs []string
ServiceIDOrName ServiceIDOrNameOptions
}
// ServiceIDOrNameOptions filters containers by the service ID or name they are part of. If both ID and Name are
// provided, they are combined with an OR operator.
type ServiceIDOrNameOptions struct {
ID string
Name string
}
type DeleteOptions struct {
IDs []string
}
// CreateOrUpdateContainer creates a new container record or updates an existing one in the store database.
// The container is associated with the given machine ID that indicates which machine the container is running on.
func (s *Store) CreateOrUpdateContainer(ctx context.Context, c *api.Container, machineID string) error {
cJSON, err := json.Marshal(c)
if err != nil {
return fmt.Errorf("marshal container: %w", err)
}
// Insert or update the container record if the container or machine ID has changed.
res, err := s.corro.ExecContext(ctx, `
INSERT INTO containers (id, container, machine_id, sync_status, updated_at)
VALUES (?, ?, ?, ?, datetime('now'))
ON CONFLICT (id) DO UPDATE SET container = excluded.container,
machine_id = excluded.machine_id,
sync_status = excluded.sync_status,
updated_at = excluded.updated_at
WHERE containers.container != excluded.container
OR containers.machine_id != excluded.machine_id`,
c.ID, string(cJSON), machineID, SyncStatusSynced)
if err != nil {
return fmt.Errorf("upsert query: %w", err)
}
if res.RowsAffected > 0 {
slog.Debug("Container record updated in store DB.", "id", c.ID, "machine_id", machineID)
}
return nil
}
// ListContainers returns a list of container records from the store database that match the given options.
func (s *Store) ListContainers(ctx context.Context, opts ListOptions) ([]*ContainerRecord, error) {
q := sq.Select("container", "machine_id", "sync_status", "updated_at").From("containers").
Where(sq.Eq{"sync_status": SyncStatusSynced})
if len(opts.MachineIDs) > 0 {
q = q.Where(sq.Eq{"machine_id": opts.MachineIDs})
}
if opts.ServiceIDOrName.ID != "" || opts.ServiceIDOrName.Name != "" {
var conditions []sq.Sqlizer
if opts.ServiceIDOrName.ID != "" {
conditions = append(conditions, sq.Eq{"service_id": opts.ServiceIDOrName.ID})
}
if opts.ServiceIDOrName.Name != "" {
conditions = append(conditions, sq.Eq{"service_name": opts.ServiceIDOrName.Name})
}
q = q.Where(sq.Or(conditions))
}
query, args, err := q.ToSql()
if err != nil {
return nil, fmt.Errorf("build query: %w", err)
}
rows, err := s.corro.QueryContext(ctx, query, args...)
if err != nil {
return nil, fmt.Errorf("select query: %w", err)
}
defer rows.Close()
var containers []*ContainerRecord
var cJSON, machineID, syncStatus, updatedAtStr string
var updatedAt time.Time
for rows.Next() {
if err = rows.Scan(&cJSON, &machineID, &syncStatus, &updatedAtStr); err != nil {
return nil, fmt.Errorf("scan container record: %w", err)
}
var c api.Container
if err = json.Unmarshal([]byte(cJSON), &c); err != nil {
return nil, fmt.Errorf("unmarshal container: %w", err)
}
if updatedAt, err = time.Parse(time.DateTime, updatedAtStr); err != nil {
return nil, fmt.Errorf("parse updated_at: %w", err)
}
containers = append(containers, &ContainerRecord{
Container: &c,
MachineID: machineID,
SyncStatus: syncStatus,
UpdatedAt: updatedAt,
})
}
return containers, nil
}
// DeleteContainers deletes container records from the store database that match the given options.
func (s *Store) DeleteContainers(ctx context.Context, opts DeleteOptions) error {
query := "DELETE FROM containers"
var args []any
if len(opts.IDs) > 0 {
query += " WHERE id IN (?" + strings.Repeat(", ?", len(opts.IDs)-1) + ")"
args = make([]any, len(opts.IDs))
for i, id := range opts.IDs {
args[i] = id
}
}
res, err := s.corro.ExecContext(ctx, query, args...)
if err != nil {
return fmt.Errorf("delete query: %w", err)
}
if res.RowsAffected > 0 {
slog.Debug("Container records deleted from store DB.", "ids", opts.IDs, "count", res.RowsAffected)
}
return nil
}
// SubscribeContainers returns a list of containers and a channel that signals changes to the list. The channel doesn't
// receive any values, it just signals when a container(s) has been added, updated, or deleted in the database.
func (s *Store) SubscribeContainers(ctx context.Context) ([]*ContainerRecord, <-chan struct{}, error) {
// TODO: figure out whether we need sync_status at all.
q := sq.Select("container", "machine_id", "sync_status", "updated_at").From("containers").
Where(sq.Eq{"sync_status": SyncStatusSynced})
query, args, err := q.ToSql()
if err != nil {
return nil, nil, fmt.Errorf("build query: %w", err)
}
sub, err := s.corro.SubscribeContext(ctx, query, args, false)
if err != nil {
return nil, nil, err
}
var containers []*ContainerRecord
var cJSON, updatedAtStr string
rows := sub.Rows()
for rows.Next() {
var cr ContainerRecord
if err = rows.Scan(&cJSON, &cr.MachineID, &cr.SyncStatus, &updatedAtStr); err != nil {
return nil, nil, err
}
if err = json.Unmarshal([]byte(cJSON), &cr.Container); err != nil {
return nil, nil, fmt.Errorf("unmarshal container: %w", err)
}
if cr.UpdatedAt, err = time.Parse(time.DateTime, updatedAtStr); err != nil {
return nil, nil, fmt.Errorf("parse updated_at: %w", err)
}
containers = append(containers, &cr)
}
events, err := sub.Changes()
if err != nil {
return nil, nil, fmt.Errorf("get subscription changes: %w", err)
}
changes := make(chan struct{})
go func() {
defer close(changes)
for {
select {
case <-ctx.Done():
return
case _, ok := <-events:
if !ok {
// events channel has been closed.
if sub.Err() != nil {
slog.Error("Containers subscription failed.", "id", sub.ID(), "err", sub.Err())
}
return
}
// Just signal that there is a change in the containers list.
changes <- struct{}{}
}
}
}()
return containers, changes, nil
}
Binary file not shown.
+37
View File
@@ -0,0 +1,37 @@
-- cluster table stores the key-value pairs of the cluster configuration.
CREATE TABLE cluster
(
key TEXT NOT NULL PRIMARY KEY,
value ANY
);
-- machines table stores the basic information of the machines in the cluster.
CREATE TABLE machines
(
id TEXT NOT NULL PRIMARY KEY,
name TEXT AS (json_extract(info, '$.name')),
-- info is a JSON-serialized MachineInfo protobuf message.
info TEXT NOT NULL DEFAULT '{}' CHECK (json_valid(info))
-- TODO: add created_at and updated_at fields to track machine age and last update time.
);
-- containers table stores the Uncloud-managed Docker containers created in the cluster.
CREATE TABLE containers
(
id TEXT NOT NULL PRIMARY KEY,
-- container is a JSON-serialized api.Container struct.
container TEXT NOT NULL DEFAULT '{}' CHECK (json_valid(container)),
machine_id TEXT NOT NULL DEFAULT '',
service_id TEXT AS (json_extract(container, '$.Labels."uncloud.service.id"')),
service_name TEXT AS (json_extract(container, '$.Labels."uncloud.service.name"')),
-- sync_status indicates if the record reflects the actual Docker state of the container.
sync_status TEXT NOT NULL DEFAULT '',
-- updated_at is the last time the record was updated.
updated_at TIMESTAMP NOT NULL DEFAULT '1970-01-01 00:00:00'
);
CREATE INDEX idx_machines_name ON machines (name);
CREATE INDEX idx_containers_machine_id ON containers (machine_id);
CREATE INDEX idx_containers_service_id ON containers (service_id);
CREATE INDEX idx_containers_service_name ON containers (service_name);
+138
View File
@@ -0,0 +1,138 @@
package store
import (
"context"
_ "embed"
"errors"
"fmt"
"google.golang.org/protobuf/encoding/protojson"
"log/slog"
"uncloud/internal/corrosion"
"uncloud/internal/machine/api/pb"
)
var (
//go:embed schema.sql
Schema string
ErrKeyNotFound = errors.New("key not found")
)
// Store is a cluster store backed by a distributed Corrosion database.
type Store struct {
corro *corrosion.APIClient
}
func New(corro *corrosion.APIClient) *Store {
return &Store{corro: corro}
}
func (s *Store) Get(ctx context.Context, key string, value any) error {
rows, err := s.corro.QueryContext(ctx, "SELECT value FROM cluster WHERE key = ?", key)
if err != nil {
return err
}
if !rows.Next() {
if rows.Err() != nil {
return rows.Err()
}
return ErrKeyNotFound
}
if err = rows.Scan(value); err != nil {
return err
}
return nil
}
func (s *Store) Put(ctx context.Context, key string, value any) error {
_, err := s.corro.ExecContext(ctx, "INSERT OR REPLACE INTO cluster (key, value) VALUES (?, ?)", key, value)
return err
}
func (s *Store) CreateMachine(ctx context.Context, m *pb.MachineInfo) error {
mJSON, err := protojson.Marshal(m)
if err != nil {
return fmt.Errorf("marshal machine info: %w", err)
}
_, err = s.corro.ExecContext(ctx, "INSERT INTO machines (id, info) VALUES (?, ?)", m.Id, string(mJSON))
if err != nil {
return fmt.Errorf("insert query: %w", err)
}
return nil
}
func (s *Store) ListMachines(ctx context.Context) ([]*pb.MachineInfo, error) {
rows, err := s.corro.QueryContext(ctx, "SELECT info FROM machines ORDER BY name")
if err != nil {
return nil, err
}
defer rows.Close()
var machines []*pb.MachineInfo
for rows.Next() {
var mJSON string
if err = rows.Scan(&mJSON); err != nil {
return nil, err
}
var m pb.MachineInfo
if err = protojson.Unmarshal([]byte(mJSON), &m); err != nil {
return nil, fmt.Errorf("unmarshal machine info: %w", err)
}
if err = m.Network.Validate(); err != nil {
slog.Error("Invalid network configuration for machine in store", "id", m.Id, "err", err)
continue
}
machines = append(machines, &m)
}
return machines, nil
}
// SubscribeMachines returns a list of machines and a channel that signals changes to the list. The channel doesn't
// receive any values, it just signals when a machine has been added, updated, or deleted in the database.
func (s *Store) SubscribeMachines(ctx context.Context) ([]*pb.MachineInfo, <-chan struct{}, error) {
sub, err := s.corro.SubscribeContext(ctx, "SELECT info FROM machines ORDER BY name", nil, false)
if err != nil {
return nil, nil, err
}
rows := sub.Rows()
var machines []*pb.MachineInfo
for rows.Next() {
var mJSON string
if err = rows.Scan(&mJSON); err != nil {
return nil, nil, err
}
var m pb.MachineInfo
if err = protojson.Unmarshal([]byte(mJSON), &m); err != nil {
return nil, nil, fmt.Errorf("unmarshal machine info: %w", err)
}
machines = append(machines, &m)
}
events, err := sub.Changes()
if err != nil {
return nil, nil, fmt.Errorf("get subscription changes: %w", err)
}
changes := make(chan struct{})
go func() {
defer close(changes)
for {
select {
case <-ctx.Done():
return
case _, ok := <-events:
if !ok {
// events channel has been closed.
if sub.Err() != nil {
slog.Error("Machines subscription failed.", "id", sub.ID(), "err", sub.Err())
}
return
}
// Just signal that there is a change in the machines list.
changes <- struct{}{}
}
}
}()
return machines, changes, nil
}
+30
View File
@@ -0,0 +1,30 @@
package secret
import (
"crypto/rand"
"fmt"
"math/big"
)
// NewID generates a unique 128-bit random ID as a hex-encoded string.
func NewID() (string, error) {
if s, err := New(16); err != nil {
return "", err
} else {
return s.String(), nil
}
}
// RandomAlphaNumeric generates a random string of the specified length using the characters [a-z0-9].
func RandomAlphaNumeric(length int) (string, error) {
const charSet = "abcdefghijklmnopqrstuvwxyz0123456789"
s := make([]byte, length)
for i := range s {
randIdx, err := rand.Int(rand.Reader, big.NewInt(int64(len(charSet))))
if err != nil {
return "", fmt.Errorf("get random number: %w", err)
}
s[i] = charSet[randIdx.Int64()]
}
return string(s), nil
}
-8
View File
@@ -54,11 +54,3 @@ func New(len int) (Secret, error) {
}
return s, nil
}
func NewID() (string, error) {
if s, err := New(16); err != nil {
return "", err
} else {
return s.String(), nil
}
}
+298
View File
@@ -0,0 +1,298 @@
package ucind
import (
"context"
"errors"
"fmt"
"github.com/cenkalti/backoff/v4"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/network"
dockerclient "github.com/docker/docker/client"
"google.golang.org/protobuf/types/known/emptypb"
"net/netip"
"time"
"uncloud/internal/machine"
"uncloud/internal/machine/api/pb"
"uncloud/internal/machine/cluster"
)
const (
ClusterNameLabel = "ucind.cluster.name"
ManagedLabel = "ucind.managed"
)
type Cluster struct {
Name string
Machines []Machine
}
type CreateClusterOptions struct {
Machines int
}
func (p *Provisioner) CreateCluster(ctx context.Context, name string, opts CreateClusterOptions) (Cluster, error) {
var c Cluster
_, err := p.InspectCluster(ctx, name)
if err == nil {
return c, fmt.Errorf("cluster with name '%s' already exists", name)
}
if !errors.Is(err, ErrNotFound) {
return c, fmt.Errorf("inspect cluster '%s': %w", name, err)
}
netOpts := network.CreateOptions{
Labels: map[string]string{
ClusterNameLabel: name,
ManagedLabel: "",
},
}
// Create a Docker network with the same as the cluster name.
if _, err = p.dockerCli.NetworkCreate(ctx, name, netOpts); err != nil {
return c, fmt.Errorf("create Docker network '%s': %w", name, err)
}
c.Name = name
// Create machines (containers) in the created cluster network.
for i := 1; i < opts.Machines+1; i++ {
mopts := CreateMachineOptions{
Name: fmt.Sprintf("machine-%d", i),
}
m, err := p.CreateMachine(ctx, name, mopts)
if err != nil {
return c, fmt.Errorf("create machine '%s': %w", mopts.Name, err)
}
c.Machines = append(c.Machines, m)
}
if len(c.Machines) == 0 {
return c, nil
}
if err = p.initCluster(ctx, c.Machines); err != nil {
return c, err
}
if p.configUpdater != nil {
if err = p.configUpdater.AddCluster(c); err != nil {
return c, fmt.Errorf("add cluster to Uncloud config: %w", err)
}
fmt.Printf("Cluster '%s' added to Uncloud config.\n", c.Name)
}
return c, nil
}
func (p *Provisioner) initCluster(ctx context.Context, machines []Machine) error {
// Init a new cluster on the first machine.
initMachine := machines[0]
if err := p.WaitMachineReady(ctx, initMachine, 5*time.Second); err != nil {
return fmt.Errorf("wait for machine %q to be ready: %w", initMachine.Name, err)
}
initClient, err := initMachine.Connect(ctx)
if err != nil {
return fmt.Errorf("create machine client over TCP '%s': %w", initMachine.APIAddress, err)
}
defer initClient.Close()
req := &pb.InitClusterRequest{
MachineName: initMachine.Name,
Network: pb.NewIPPrefix(cluster.DefaultNetwork),
}
initResp, err := initClient.InitCluster(ctx, req)
if err != nil {
return fmt.Errorf("init cluster: %w", err)
}
fmt.Printf("Cluster %q initialised with machine %q\n", initMachine.ClusterName, initResp.Machine.Name)
// Join the rest of the machines to the cluster.
for _, m := range machines[1:] {
if err = p.WaitMachineReady(ctx, m, 5*time.Second); err != nil {
return fmt.Errorf("wait for machine %q to be ready: %w", m.Name, err)
}
cli, err := m.Connect(ctx)
if err != nil {
return fmt.Errorf("create machine client over TCP '%s': %w", m.APIAddress, err)
}
//goland:noinspection GoDeferInLoop
defer cli.Close()
tokenResp, err := cli.Token(ctx, &emptypb.Empty{})
if err != nil {
return fmt.Errorf("get machine token: %w", err)
}
token, err := machine.ParseToken(tokenResp.Token)
if err != nil {
return fmt.Errorf("parse machine token: %w", err)
}
// Register the machine in the cluster using its public key and endpoints from the token.
endpoints := make([]*pb.IPPort, len(token.Endpoints))
for i, addrPort := range token.Endpoints {
endpoints[i] = pb.NewIPPort(addrPort)
}
addReq := &pb.AddMachineRequest{
Name: m.Name,
Network: &pb.NetworkConfig{
Endpoints: endpoints,
PublicKey: token.PublicKey,
},
}
addResp, err := initClient.AddMachine(ctx, addReq)
if err != nil {
return fmt.Errorf("add machine to cluster: %w", err)
}
// Configure the machine to join the cluster.
joinReq := &pb.JoinClusterRequest{
Machine: addResp.Machine,
OtherMachines: []*pb.MachineInfo{initResp.Machine},
}
if _, err = cli.JoinCluster(ctx, joinReq); err != nil {
return fmt.Errorf("join cluster: %w", err)
}
fmt.Printf("Machine %q added to cluster\n", addResp.Machine.Name)
}
return nil
}
func (p *Provisioner) InspectCluster(ctx context.Context, name string) (Cluster, error) {
var c Cluster
// Docker network name is the same as the cluster name.
net, err := p.dockerCli.NetworkInspect(ctx, name, network.InspectOptions{})
if err != nil {
if dockerclient.IsErrNotFound(err) {
return c, ErrNotFound
}
return c, fmt.Errorf("inspect Docker network '%s': %w", name, err)
}
if _, ok := net.Labels[ManagedLabel]; !ok {
// The network with the cluster name exists, but it's not managed by ucind.
return c, ErrNotFound
}
c.Name = name
// Include all containers (machines) with the cluster name label.
opts := container.ListOptions{
All: true,
Filters: filters.NewArgs(
filters.Arg("label", ClusterNameLabel+"="+name),
filters.Arg("label", ManagedLabel),
),
}
containers, err := p.dockerCli.ContainerList(ctx, opts)
if err != nil {
return c, fmt.Errorf("list Docker containers with cluster name '%s': %w", name, err)
}
for _, ctr := range containers {
m := Machine{
ClusterName: name,
ContainerName: ctr.Names[0],
Name: ctr.Labels[MachineNameLabel],
}
for _, port := range ctr.Ports {
if port.PrivatePort == UncloudAPIPort {
if ip, err := netip.ParseAddr(port.IP); err == nil {
m.APIAddress = netip.AddrPortFrom(ip, port.PublicPort)
break
}
}
}
if !m.APIAddress.IsValid() {
return c, fmt.Errorf("API binding not found for container '%s'", m.ContainerName)
}
c.Machines = append(c.Machines, m)
}
return c, nil
}
// WaitClusterReady waits for all machines in the cluster to be ready and UP.
func (p *Provisioner) WaitClusterReady(ctx context.Context, c Cluster, timeout time.Duration) error {
firstMachine := c.Machines[0]
if err := p.WaitMachineReady(ctx, firstMachine, timeout); err != nil {
return fmt.Errorf("wait for machine '%s' to be ready: %w", firstMachine.Name, err)
}
cli, err := firstMachine.Connect(ctx)
if err != nil {
return fmt.Errorf("connect to machine over TCP '%s': %w", firstMachine.APIAddress, err)
}
defer cli.Close()
boff := backoff.WithContext(backoff.NewExponentialBackOff(
backoff.WithInitialInterval(100*time.Millisecond),
backoff.WithMaxInterval(1*time.Second),
backoff.WithMaxElapsedTime(timeout),
), ctx)
checkMachinesUp := func() error {
machines, err := cli.ListMachines(ctx)
if err != nil {
return fmt.Errorf("list machines: %w", err)
}
if len(machines) != len(c.Machines) {
return fmt.Errorf("expected %d machines, got %d", len(c.Machines), len(machines))
}
for _, m := range machines {
if m.State != pb.MachineMember_UP {
return fmt.Errorf("machine '%s' state is not UP: %s", m.Machine.Name, m.State)
}
}
return nil
}
return backoff.Retry(checkMachinesUp, boff)
}
func (p *Provisioner) RemoveCluster(ctx context.Context, name string) error {
if _, err := p.InspectCluster(ctx, name); err != nil {
if errors.Is(err, ErrNotFound) {
return nil
}
return err
}
// Remove all containers (machines) with the cluster name label.
opts := container.ListOptions{
All: true,
Filters: filters.NewArgs(
filters.Arg("label", ClusterNameLabel+"="+name),
filters.Arg("label", ManagedLabel),
),
}
containers, err := p.dockerCli.ContainerList(ctx, opts)
if err != nil {
return fmt.Errorf("list Docker containers with cluster name '%s': %w", name, err)
}
for _, c := range containers {
if err = p.dockerCli.ContainerRemove(ctx, c.ID, container.RemoveOptions{Force: true}); err != nil {
return fmt.Errorf("remove Docker container '%s': %w", c.ID, err)
}
}
if err = p.dockerCli.NetworkRemove(ctx, name); err != nil {
return fmt.Errorf("remove Docker network '%s': %w", name, err)
}
if p.configUpdater != nil {
if err = p.configUpdater.RemoveCluster(name); err != nil {
return fmt.Errorf("remove cluster from Uncloud config: %w", err)
}
fmt.Printf("Cluster '%s' removed from Uncloud config.\n", name)
}
return nil
}
+68
View File
@@ -0,0 +1,68 @@
package ucind
import (
"fmt"
"uncloud/internal/cli/config"
)
type ConfigUpdater struct {
path string
}
func NewConfigUpdater(path string) *ConfigUpdater {
return &ConfigUpdater{path: path}
}
func (u *ConfigUpdater) AddCluster(c Cluster) error {
cfg, err := config.NewFromFile(u.path)
if err != nil {
return fmt.Errorf("read Uncloud config: %w", err)
}
if _, ok := cfg.Clusters[c.Name]; ok {
return fmt.Errorf("cluster '%s' already exists", c.Name)
}
clusterCfg := &config.Cluster{
Name: c.Name,
Connections: make([]config.MachineConnection, len(c.Machines)),
}
for i, m := range c.Machines {
clusterCfg.Connections[i] = config.MachineConnection{
TCP: m.APIAddress,
}
}
cfg.Clusters[c.Name] = clusterCfg
cfg.CurrentCluster = c.Name
if err = cfg.Save(); err != nil {
return fmt.Errorf("save config: %w", err)
}
return nil
}
func (u *ConfigUpdater) RemoveCluster(name string) error {
cfg, err := config.NewFromFile(u.path)
if err != nil {
return fmt.Errorf("read Uncloud config: %w", err)
}
if _, ok := cfg.Clusters[name]; !ok {
return nil
}
delete(cfg.Clusters, name)
if cfg.CurrentCluster == name {
cfg.CurrentCluster = ""
}
if _, ok := cfg.Clusters["default"]; ok {
cfg.CurrentCluster = "default"
}
if err = cfg.Save(); err != nil {
return fmt.Errorf("save config: %w", err)
}
return nil
}
+208
View File
@@ -0,0 +1,208 @@
package ucind
import (
"context"
"errors"
"fmt"
"github.com/cenkalti/backoff/v4"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/image"
dockerclient "github.com/docker/docker/client"
"github.com/docker/go-connections/nat"
"google.golang.org/protobuf/types/known/emptypb"
"io"
"net"
"net/netip"
"time"
"uncloud/internal/cli/client"
"uncloud/internal/cli/client/connector"
"uncloud/internal/secret"
)
const (
DefaultImage = "ghcr.io/psviderski/ucind:latest"
UncloudAPIPort = 51000
MachineNameLabel = "ucind.machine.name"
)
type Machine struct {
ClusterName string
ContainerName string
Name string
APIAddress netip.AddrPort
}
func (m *Machine) Connect(ctx context.Context) (*client.Client, error) {
return client.New(ctx, connector.NewTCPConnector(m.APIAddress))
}
type CreateMachineOptions struct {
Name string
Image string
}
func (p *Provisioner) CreateMachine(ctx context.Context, clusterName string, opts CreateMachineOptions) (Machine, error) {
var m Machine
machineName := opts.Name
if machineName == "" {
var err error
if machineName, err = randomMachineName(); err != nil {
return m, fmt.Errorf("generate random machine name: %w", err)
}
}
containerName := clusterName + "-" + machineName
img := DefaultImage
if opts.Image != "" {
img = opts.Image
}
apiPort := nat.Port(fmt.Sprintf("%d/tcp", UncloudAPIPort))
config := &container.Config{
Image: img,
Labels: map[string]string{
ClusterNameLabel: clusterName,
MachineNameLabel: machineName,
ManagedLabel: "",
},
ExposedPorts: nat.PortSet{
apiPort: struct{}{},
},
}
hostConfig := &container.HostConfig{
NetworkMode: container.NetworkMode(clusterName),
PortBindings: nat.PortMap{
apiPort: []nat.PortBinding{
{
HostIP: "127.0.0.1",
// Host port is a random available port.
},
},
},
Privileged: true,
RestartPolicy: container.RestartPolicy{
Name: container.RestartPolicyAlways,
},
}
if _, err := p.createContainerWithImagePull(ctx, containerName, config, hostConfig); err != nil {
return m, err
}
if err := p.dockerCli.ContainerStart(ctx, containerName, container.StartOptions{}); err != nil {
return m, fmt.Errorf("start Docker container: %w", err)
}
apiPortBindings, err := p.waitPortPublished(ctx, containerName, apiPort)
if err != nil {
return m, fmt.Errorf("wait for machine API port '%s' to be published: %w", apiPort, err)
}
apiAddr, err := netip.ParseAddrPort(net.JoinHostPort(apiPortBindings[0].HostIP, apiPortBindings[0].HostPort))
if err != nil {
return m, fmt.Errorf("parse machine API port binding: %w", err)
}
m = Machine{
ClusterName: clusterName,
ContainerName: containerName,
Name: machineName,
APIAddress: apiAddr,
}
return m, nil
}
// createContainerWithImagePull creates a Docker container. If the image is missing, it pulls the image first.
func (p *Provisioner) createContainerWithImagePull(
ctx context.Context, name string, config *container.Config, hostConfig *container.HostConfig,
) (container.CreateResponse, error) {
var resp container.CreateResponse
_, err := p.dockerCli.ContainerCreate(ctx, config, hostConfig, nil, nil, name)
if err == nil {
return resp, nil
}
if !dockerclient.IsErrNotFound(err) {
return resp, fmt.Errorf("create Docker container: %w", err)
}
respBody, err := p.dockerCli.ImagePull(ctx, config.Image, image.PullOptions{})
if err != nil {
return resp, fmt.Errorf("pull Docker image: %w", err)
}
defer respBody.Close()
// Wait for pull to complete.
if _, err = io.Copy(io.Discard, respBody); err != nil {
return resp, fmt.Errorf("read Docker pull response: %w", err)
}
// Create container again after image pull.
if resp, err = p.dockerCli.ContainerCreate(ctx, config, hostConfig, nil, nil, name); err != nil {
return resp, fmt.Errorf("create Docker container: %w", err)
}
return resp, nil
}
// waitPortPublished waits for a Docker container port to be published on the host which happens asynchronously.
func (p *Provisioner) waitPortPublished(ctx context.Context, containerID string, port nat.Port) ([]nat.PortBinding, error) {
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
for {
c, err := p.dockerCli.ContainerInspect(ctx, containerID)
if err != nil {
return nil, fmt.Errorf("inspect container: %w", err)
}
binding, ok := c.NetworkSettings.Ports[port]
if !ok {
return nil, fmt.Errorf("port '%s' not published", port)
}
if len(binding) > 0 {
return binding, nil
}
select {
case <-time.After(10 * time.Millisecond):
case <-ctx.Done():
if errors.Is(ctx.Err(), context.DeadlineExceeded) {
return nil, errors.New("timeout")
}
return nil, ctx.Err()
}
}
}
func randomMachineName() (string, error) {
suffix, err := secret.RandomAlphaNumeric(4)
if err != nil {
return "", fmt.Errorf("generate random suffix: %w", err)
}
return "machine-" + suffix, nil
}
// WaitMachineReady waits for the machine API to respond.
func (p *Provisioner) WaitMachineReady(ctx context.Context, m Machine, timeout time.Duration) error {
cli, err := m.Connect(ctx)
if err != nil {
return fmt.Errorf("connect to machine over TCP '%s': %w", m.APIAddress, err)
}
defer cli.Close()
boff := backoff.WithContext(backoff.NewExponentialBackOff(
backoff.WithInitialInterval(100*time.Millisecond),
backoff.WithMaxInterval(1*time.Second),
backoff.WithMaxElapsedTime(timeout),
), ctx)
inspect := func() error {
if _, err := cli.Inspect(ctx, &emptypb.Empty{}); err != nil {
return fmt.Errorf("inspect machine: %w", err)
}
return nil
}
return backoff.Retry(inspect, boff)
}
+20
View File
@@ -0,0 +1,20 @@
package ucind
import (
"errors"
"github.com/docker/docker/client"
)
var ErrNotFound = errors.New("not found")
type Provisioner struct {
dockerCli *client.Client
configUpdater *ConfigUpdater
}
func NewProvisioner(cli *client.Client, configUpdater *ConfigUpdater) *Provisioner {
return &Provisioner{
dockerCli: cli,
configUpdater: configUpdater,
}
}
+83
View File
@@ -0,0 +1,83 @@
#!/bin/sh
# This is a fork of https://github.com/moby/moby/blob/65cfcc28ab37cb75e1560e4b4738719c07c6618e/hack/dind
# with a fix for cgroup v2 initialization races.
set -e
# DinD: a wrapper script which allows docker to be run inside a docker container.
# Original version by Jerome Petazzoni <jerome@docker.com>
# See the blog post: https://www.docker.com/blog/docker-can-now-run-within-docker/
#
# This script should be executed inside a docker container in privileged mode
# ('docker run --privileged', introduced in docker 0.6).
# Usage: dind CMD [ARG...]
# apparmor sucks and Docker needs to know that it's in a container (c) @tianon
#
# Set the container env-var, so that AppArmor is enabled in the daemon and
# containerd when running docker-in-docker.
#
# see: https://github.com/containerd/containerd/blob/787943dc1027a67f3b52631e084db0d4a6be2ccc/pkg/apparmor/apparmor_linux.go#L29-L45
# see: https://github.com/moby/moby/commit/de191e86321f7d3136ff42ff75826b8107399497
export container=docker
# Allow AppArmor to work inside the container;
#
# aa-status
# apparmor filesystem is not mounted.
# apparmor module is loaded.
#
# mount -t securityfs none /sys/kernel/security
#
# aa-status
# apparmor module is loaded.
# 30 profiles are loaded.
# 30 profiles are in enforce mode.
# /snap/snapd/18357/usr/lib/snapd/snap-confine
# ...
#
# Note: https://0xn3va.gitbook.io/cheat-sheets/container/escaping/sensitive-mounts#sys-kernel-security
#
# ## /sys/kernel/security
#
# In /sys/kernel/security mounted the securityfs interface, which allows
# configuration of Linux Security Modules. This allows configuration of
# AppArmor policies, and so access to this may allow a container to disable
# its MAC system.
#
# Given that we're running privileged already, this should not be an issue.
if [ -d /sys/kernel/security ] && ! mountpoint -q /sys/kernel/security; then
mount -t securityfs none /sys/kernel/security || {
echo >&2 'Could not mount /sys/kernel/security.'
echo >&2 'AppArmor detection and --privileged mode might break.'
}
fi
# Mount /tmp (conditionally)
if ! mountpoint -q /tmp; then
mount -t tmpfs none /tmp
fi
# cgroup v2: enable nesting
if [ -f /sys/fs/cgroup/cgroup.controllers ]; then
# move the processes from the root group to the /init group,
# otherwise writing subtree_control fails with EBUSY.
# An error during moving non-existent process (i.e., "cat") is ignored.
mkdir -p /sys/fs/cgroup/init
xargs -rn1 < /sys/fs/cgroup/cgroup.procs > /sys/fs/cgroup/init/cgroup.procs || :
# enable controllers with retry on cgroup initialization races
timeout 5s sh -c "until sed -e 's/ / +/g' -e 's/^/+/' < /sys/fs/cgroup/cgroup.controllers \
> /sys/fs/cgroup/cgroup.subtree_control 2>/dev/null; do sleep 0.1; done"
fi
# Change mount propagation to shared to make the environment more similar to a
# modern Linux system, e.g. with SystemD as PID 1.
mount --make-rshared /
if [ $# -gt 0 ]; then
exec "$@"
fi
echo >&2 'ERROR: No command specified.'
echo >&2 'You probably want to run hack/make.sh, or maybe a shell?'
+41
View File
@@ -0,0 +1,41 @@
#!/bin/sh
set -eu
# Cleanup function to properly terminate background processes.
cleanup() {
echo "Terminating container processes..."
# Terminate the main process if it has been started.
if [ -n "$MAIN_PID" ]; then
kill "$MAIN_PID" 2>/dev/null || true
fi
# Terminate Docker daemon if PID file exists.
if [ -f /run/docker.pid ]; then
kill "$(cat /run/docker.pid)" 2>/dev/null || true
fi
# Terminate socat proxy if running.
pkill socat 2>/dev/null || true
# Wait for processes to terminate.
wait
}
trap cleanup INT TERM EXIT
dind dockerd &
echo "Waiting for Docker in Docker to be ready..."
timeout 5s sh -c "until docker info &> /dev/null; do sleep 0.1; done"
echo "Docker in Docker is ready."
echo "Loading corrosion image from /images/corrosion.tar..."
docker load < /images/corrosion.tar
# Make machine API accessible from the host via port publishing.
echo "Proxying Uncloud API port 51000/tcp to Unix socket /run/uncloud/uncloud.sock..."
socat TCP-LISTEN:51000,reuseaddr,fork,bind="$(hostname -i)" UNIX-CONNECT:/run/uncloud/uncloud.sock &
# Execute the passed command and wait for it while maintaining signal handling.
"$@" &
MAIN_PID=$!
wait $MAIN_PID

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