Compare commits

..
158 Commits
Author SHA1 Message Date
Pavel Sviderski 0e80f2e5b2 chore: replace immutable hash with container spec comparison 2025-03-31 15:58:02 +10:00
Pavel Sviderski 64f6a4f3d3 chore: use original service spec instead of deriving from container, fixes diff for scale 2025-03-30 12:54:36 +10:00
Pavel Sviderski 0975cbab66 chore: handle RemoveContainer server-side that deletes container from machine db 2025-03-29 20:06:49 +10:00
Pavel Sviderski 615022c9ac feat(scale): set file permissions for machine.db database to 600 (may container secrets as env vars) 2025-03-29 15:19:37 +10:00
Pavel Sviderski ecbd4378e4 feat(scale): store container spec in local machine.db sqlite 2025-03-29 15:14:43 +10:00
Pavel Sviderski 1becd033e6 chore: rename CaddyConfigPath field 2025-03-29 11:39:45 +10:00
Pavel Sviderski 1cba1eef0a refactor: handle CreateContainer server-side 2025-03-28 19:01:19 +10:00
Pavel Sviderski 3893005866 fix(compose): remove ServiceSpec validation for 0 replicas, it's handled when resolving defaults 2025-03-27 20:40:11 +10:00
Pavel Sviderski 1911e03c82 feat(compose): handle image pull policy 2025-03-27 15:15:57 +10:00
Pavel Sviderski 641b11a92d feat: implement --pull policy for run command: always, missing, never 2025-03-27 15:06:32 +10:00
Pavel Sviderski 7c7530f977 chore: add comments and TODOs about the disabled image digest resolver 2025-03-27 14:12:20 +10:00
Pavel Sviderski 6e7fe405ef feat(compose): partially implement image resolver (pinner) but disable for now 2025-03-26 20:42:41 +10:00
Pavel Sviderski 03c5c054e1 feat(compose): client methods for inspecting images 2025-03-26 20:38:41 +10:00
Pavel Sviderski c42968c812 feat(compose): add InspectRemoteImage api endpoint to inspect images in registry 2025-03-26 20:37:53 +10:00
Pavel Sviderski ffef41081b feat(compose): add ContainerSpec.PullPolicy field to define when to pull image from registry 2025-03-25 11:03:19 +10:00
Pavel Sviderski 58b51b974c refactor: postpone spec resolver initialisation when creating Deployment 2025-03-24 21:56:30 +10:00
Pavel Sviderski 2d923380a3 chore: compare container ports to spec in assertContainerMatchesSpec 2025-03-24 20:24:40 +10:00
Pavel Sviderski 3910101a88 refactor: embed ServiceSpecResolver into Deployment to handle spec only there 2025-03-24 20:10:28 +10:00
Pavel Sviderski 7a9d0cf9db refactor: move compose and deploy packages under client 2025-03-24 14:41:50 +10:00
Pavel Sviderski 042dd594e0 refactor: decouple deploy and compose packages from client dependency 2025-03-24 14:38:16 +10:00
Pavel Sviderski 1471a94162 refactor(compose): move compose deployment from client to compose pkg 2025-03-24 11:11:36 +10:00
Pavel Sviderski b1abd07dde refactor: move api, client, compose packages to pkg 2025-03-22 18:43:00 +10:00
Pavel Sviderski 3757813b20 refactor: rename main module to github.com/psviderski/uncloud 2025-03-22 18:25:08 +10:00
Pavel Sviderski f1a150821c fix(compose): client.ImageInspect to handle broadcasted responses to multiple machines 2025-03-22 08:44:17 +10:00
Pavel Sviderski 874af1ba10 feat(compose): implement InspectImage rpc with broadcast support 2025-03-21 19:43:58 +10:00
Pavel Sviderski 2ad098c612 feat(deploy): introduce proto for InspectImage and InspectRemoteImage methods 2025-03-20 21:36:41 +10:00
Pavel Sviderski 4172cb826f refactor: compose deployment plan to not use PrepareDeploymentSpec 2025-03-19 16:54:00 +10:00
Pavel Sviderski 041d51b74f refactor: compose deployment with pluggable spec resolver, basic test 2025-03-19 16:28:17 +10:00
Pavel Sviderski fb01604d21 fix: recreate containers only if spec changed by comparing spec hash 2025-03-19 13:11:04 +10:00
Pavel SviderskiandGitHub bd8bcbd2f9 Merge pull request #40 from cedws/userguide 2025-03-19 09:24:50 +10:00
Connor Edwards 07501813c8 docs: s/uncloud/uc 2025-03-18 21:04:43 +09:00
Pavel Sviderski 1bbcfa8519 feat(entrypoint): allow to override service ENTRYPOINT for run command 2025-03-18 16:44:15 +10:00
Pavel Sviderski e8715fce38 fix: list containers if some of them concurrently removed, fixes #33 2025-03-18 15:39:15 +10:00
Pavel Sviderski a0a66f34c4 feat(connect): add --connect flag to connect to remote cluster machine without using config 2025-03-18 14:01:38 +10:00
Pavel Sviderski b799b7518e feat(compose): add TODO to address deployment idempotency 2025-03-17 20:32:14 +10:00
Connor Edwards 2571498ed2 docs: add basic user guide 2025-03-16 14:51:42 +09:00
Pavel Sviderski 5bcbfc332d feat(compose): deploy compose services without waiting dependencies 2025-03-15 20:45:02 +10:00
Pavel Sviderski 34574e41fc feat(compose): plan compose deployment 2025-03-15 20:43:30 +10:00
Pavel Sviderski c7355afa3d refactor: move Confirm logic to common place from caddy deployment 2025-03-15 20:42:44 +10:00
Pavel Sviderski a907fdb1af chore: update todo 2025-03-15 20:41:57 +10:00
Pavel Sviderski c9ba4adccb chore: add todo for ServiceSpec that it may return a spec not equal to original 2025-03-15 20:41:02 +10:00
Pavel Sviderski a0551d2d64 fix: print service endpoints for run service only when non-empty 2025-03-15 19:35:21 +10:00
Pavel Sviderski 76129ea056 refactor: include service name in deployment plan 2025-03-15 19:34:37 +10:00
Pavel Sviderski 6ffd35f596 feat(compose): parse Compose project and transform ports extension 2025-03-15 14:18:06 +10:00
Pavel Sviderski 0ef776e7f1 chore: add link to uncloud-dns repo in README 2025-03-14 22:01:19 +10:00
Pavel Sviderski 0b22b2136a chore: add deployments and managed DNS features to README 2025-03-14 21:59:40 +10:00
Pavel Sviderski e1530bb6f3 feat(deploy): load compose project 2025-03-14 21:41:06 +10:00
Pavel Sviderski 7fc07ce4bd feat(deploy): init a new deploy command to deploy services from Compose file 2025-03-12 20:37:01 +10:00
Pavel Sviderski eb42082955 fix(dns): update DNS records when reserving a new cluster domain, fixes #36 2025-03-11 21:12:00 +10:00
Pavel Sviderski 8719e37856 fix(dns): retry HTTP requests to verify Caddy reachability for up to 5 seconds 2025-03-10 21:26:02 +10:00
Pavel Sviderski e1cc39b0d5 lint: variable collides with imported package name 2025-03-09 19:26:06 +10:00
Pavel Sviderski efe646f54c chore: validate unsupported tcp/udp protocols for ingress port 2025-03-08 19:07:51 +10:00
Pavel Sviderski e130d803e4 fix(website): demo min height to not shrink before video is loaded 2025-03-06 17:30:16 +10:00
Pavel Sviderski 56e7e8baa2 chore(readme): cleanup coming soon features 2025-03-06 17:13:29 +10:00
Pavel Sviderski b963ef39d1 chore(makefile): demo-reset target to reset machines before recording a VHS demo 2025-03-06 16:55:16 +10:00
Pavel Sviderski 1df0fa290f chore(readme): add uc machine ls command 2025-03-06 16:54:35 +10:00
Pavel Sviderski c3bb0c9f49 chore(readme): add Quick demo section with video, update how it works outputs 2025-03-06 16:49:58 +10:00
Pavel Sviderski cf65cecd9e chore: replace terminal example with a demo video on landing 2025-03-06 15:49:06 +10:00
Pavel Sviderski fff17dbb60 add VHS tape script for the terminal demo 2025-03-05 22:17:54 +10:00
Pavel Sviderski 586a4fa129 feat(scale): basic service scale command 2025-03-05 20:24:54 +10:00
Pavel Sviderski 2a82f75cad fix: deduplicate discovered machine IPs when adding a new machine 2025-03-05 09:14:11 +10:00
Pavel Sviderski 805afd0d28 fix(dns): update cluster domain DNS records when adding a new machine to cluster 2025-03-04 20:49:41 +10:00
Pavel Sviderski 11098ea7d1 fix: url for uninstall.sh script in install script 2025-03-04 12:54:48 +10:00
Pavel Sviderski 90e357a1dc feat: install uncloud-uninstall script when provisioning machine 2025-03-04 12:50:29 +10:00
Pavel Sviderski 91d58bc701 chore: add uninstall.sh script to remove uncloud and managed containers and network from machine 2025-03-04 12:14:49 +10:00
Pavel Sviderski 69d3a511d6 feat(dns): reserve cluster domain when initialising cluster by default + update dns records 2025-03-03 19:12:21 +10:00
Pavel Sviderski c3123820b2 fix: install script tar flag 2025-03-03 18:51:41 +10:00
Pavel Sviderski 2e49d94fad fix: deduplicate discovered machine IPs 2025-03-03 18:42:00 +10:00
Pavel Sviderski bb51e01c5c fix: install script unpack uncloudd binary to tmp dir 2025-03-03 18:38:51 +10:00
Pavel Sviderski cdb2884e4e fix: check for dockerd command to determine if docker engine is installed 2025-03-03 18:05:52 +10:00
Pavel Sviderski 137abde808 feat: set container hostname equal to container name 2025-03-03 16:58:42 +10:00
Pavel Sviderski 342969b7f4 fix(caddy): remove debug prints 2025-03-03 15:05:57 +10:00
Pavel Sviderski d9e350c4dd fix(caddy): config generation for multiple https containers 2025-03-03 15:04:04 +10:00
Pavel Sviderski 3f4f263c05 test: increase test cluster wait timeout 5->30 seconds 2025-03-03 12:21:22 +10:00
Pavel Sviderski 0760ba3b18 test: replicated deployment with machine filter 2025-03-03 11:26:08 +10:00
Pavel Sviderski 0896bd27fb test: basic replicated deployment, prioritise host with the most up-to-date containers 2025-03-02 10:23:49 +10:00
Pavel Sviderski a16b2d1c74 feat: service ls command include ID column if there are multiple services with same name 2025-03-01 22:31:58 +10:00
Pavel Sviderski 52117954a8 fix: specs equals ignores replicas 2025-03-01 22:31:18 +10:00
Pavel Sviderski 435a794fa5 fix: e2e tests 2025-02-28 18:00:02 +10:00
Pavel Sviderski 9642863d7b feat(replicas): support --replicas and --machines for run command 2025-02-28 17:53:58 +10:00
Pavel Sviderski 6bb9bdeb5a feat(replicas): implement rolling strategy for replicated service with basic spread strategy 2025-02-28 17:51:38 +10:00
Pavel Sviderski 30f1e135f1 feat: print ENDPOINTS column when listing services 2025-02-27 20:37:02 +10:00
Pavel Sviderski 58ecc17ac1 feat: print service endpoints on service creation 2025-02-27 20:37:02 +10:00
Anton Ovchinnikov 4c239ed79a fix(smm): Update site description 2025-02-27 00:08:03 +01:00
Anton Ovchinnikov 968dd50705 fix(smm): Use PNG logo, add Twitter tags 2025-02-26 23:55:26 +01:00
Pavel Sviderski d7f437107d fix(dns): error message for ingress port without hostname and no cluster domain 2025-02-26 21:13:18 +10:00
Pavel Sviderski 72548fbd1b feat(dns): allow to specify http(s) ports without hostname to default to cluster domain 2025-02-26 21:10:55 +10:00
Pavel Sviderski a5f6ec4a68 feat(dns): expose service as service-name.cluster-domain if cluster domain reserved 2025-02-26 21:10:21 +10:00
Pavel Sviderski 4cc68e3133 include cloud vm in dev set in Makefile 2025-02-26 15:52:33 +10:00
Pavel Sviderski 2bacebe900 refactor(dns): remove leading dot from domain when interecting with Uncloud DNS 2025-02-26 15:45:28 +10:00
Pavel Sviderski 09bea1d12d feat(dns): update ingress records in Uncloud DNS when deploying Caddy 2025-02-26 14:23:00 +10:00
Pavel Sviderski 5e9b883ad5 fix: increase wait time for machine to 5 min, discard unknown when decoding json to proto 2025-02-26 12:19:04 +10:00
Pavel Sviderski 1046396edc feat(dns): configure and store public IP when adding new machine 2025-02-26 11:16:13 +10:00
Pavel SviderskiandGitHub 35804458b0 Merge pull request #28 from psviderski/tonyo/27-feat-social-tags
feat(smm): Add OpenGraph tags
2025-02-26 10:10:49 +10:00
Anton Ovchinnikov 6bb64ac5dc feat(smm): Add OpenGraph tags 2025-02-26 00:35:33 +01:00
Pavel Sviderski e5f68947f9 feat(dns): missed CLI changes 2025-02-25 21:22:32 +10:00
Pavel Sviderski 289d964b2a feat(dns): configure and store public IP when initialising machine 2025-02-25 21:21:37 +10:00
Pavel Sviderski d0fd9db258 feat(dns): serve machine IP at /.uncloud-verify to verify Caddy reachability 2025-02-25 17:36:44 +10:00
Pavel Sviderski 0ea997e826 feat(dns): update caddy deploy command to create ingress records (stubs for now) 2025-02-24 20:33:54 +10:00
Pavel Sviderski 2d84059d7f feat(dns): client for creating ingress records 2025-02-24 20:33:08 +10:00
Pavel Sviderski ecec42ea60 feat(dns): cluster API for creating DNS records for cluster domain 2025-02-24 20:32:30 +10:00
Pavel Sviderski af4629b679 feat(dns): add CLI commands to reserve, show, delete cluster domain 2025-02-24 17:37:43 +10:00
Pavel Sviderski f6cf5cc250 feat(dns): cluster API for reserving/releasing cluster domain in Uncloud DNS 2025-02-24 17:35:19 +10:00
Pavel Sviderski f37bfd01fe feat: init cluster API for updating domain records in Uncloud DNS 2025-02-22 21:20:16 +10:00
Pavel Sviderski d9897e5d1e feat: set restart policy for all services to always 2025-02-19 16:53:55 +10:00
Pavel Sviderski bcb67a4073 refactor: e2e tests for container config validation 2025-02-19 15:19:00 +10:00
Pavel Sviderski bcb5a2a173 feat: deploy caddy service when adding new machine to cluster 2025-02-18 21:35:36 +10:00
Pavel Sviderski c3b88077ac feat: deploy caddy service when initialising new cluster on remote machine 2025-02-18 18:38:38 +10:00
Pavel Sviderski 98e1bdc9b5 fix: fail global deployment if no matchine machines found, improve caddy deploy output in particular 2025-02-17 20:31:37 +10:00
Pavel Sviderski e13408d79f feat: allow to filter machines for global service deployments 2025-02-17 20:26:40 +10:00
Pavel Sviderski 8610322682 fix: panic when pulling non-existent image 2025-02-17 16:12:37 +10:00
Pavel Sviderski 0be42f1f59 feat: print deployment plan for caddy deployment 2025-02-17 14:29:56 +10:00
Pavel Sviderski dcbfcbd0fb fix: show user-friendly image name when inspecting service 2025-02-16 21:12:56 +10:00
Pavel Sviderski d0e46186ed feat: look up latest caddy version tag when deploying caddy service 2025-02-16 21:10:06 +10:00
Pavel Sviderski 625a8baed3 feat: implement 'caddy deploy' command to deploy global service using caddy:latest image 2025-02-15 10:51:02 +10:00
Pavel Sviderski 98db5ed440 feat: caddy deploy command and stub implementation 2025-02-15 08:52:14 +10:00
Pavel Sviderski c4398f9f06 refactor: deploy global service using Deployment with RollingStrategy 2025-02-14 20:45:23 +10:00
Pavel Sviderski a53fa2c1d8 feat: do not redeploy global service if spec hasn't changed 2025-02-14 17:49:46 +10:00
Pavel Sviderski 5e2b2ac836 refactor: api.Container to use the detailed ContainerJSON struct instead of Container (Summary) 2025-02-14 16:37:40 +10:00
Pavel Sviderski 43f28284f1 feat: rolling deploy strategy for global service, always recreate 2025-02-13 21:32:08 +10:00
Pavel Sviderski b17fd7531f feat: detect conflicing service port on a container 2025-02-13 11:50:34 +10:00
Pavel Sviderski 9859be50a6 feat: add Deployment, Strategy, Operation primitives to compose service deployments 2025-02-12 21:17:30 +10:00
Pavel Sviderski f209851986 feat: progress for removing service(s) 2025-02-11 21:21:50 +10:00
Pavel Sviderski 4da51636fd refactor: remove service with StopContainer and RemoveContainer 2025-02-11 20:43:31 +10:00
Pavel Sviderski 76a9f8e8b6 refactor: client StartContainer pass serviceID 2025-02-11 16:47:52 +10:00
Pavel Sviderski a477db6b16 refactor: client CreateContainer and StartContainer methods 2025-02-11 15:59:36 +10:00
Pavel Sviderski 04fd1b1425 feat: introduce client.Service entity for managing service containers 2025-02-10 20:21:42 +10:00
Pavel Sviderski c0bf1f2930 add contributors section to README, feature @cedws 2025-02-10 11:38:35 +10:00
Pavel Sviderski 0bf759333a fix: populate ssh_key_file in the config when adding machine to cluster 2025-02-10 09:14:54 +10:00
Pavel SviderskiandGitHub b482d836aa Merge pull request #21 from cedws/identityfile
feat: add ssh_key_file field to connections config
2025-02-10 09:03:13 +10:00
Connor Edwards 3d2d9b78e8 fix: rename IdentityFile to SSHKeyFile 2025-02-09 18:39:44 +00:00
Connor Edwards 99219dc376 fix: set identity_file in config after init 2025-02-08 15:59:05 +00:00
Connor Edwards 408703360c fix: use os.UserHomeDir() instead of $HOME 2025-02-08 15:56:18 +00:00
Connor Edwards ac48b8bee6 feat: add identity_file field to config connections config 2025-02-08 15:56:18 +00:00
Pavel SviderskiandGitHub e3a310397b Merge pull request #20 from cedws/init-19
fix: execute valid bash cmd when running init as root
2025-02-08 16:42:14 +10:00
Connor Edwards b2f7c6fb33 fix: execute valid bash cmd when running init as root 2025-02-07 21:14:27 +00:00
Pavel Sviderski 653f47f507 add clean up step to quick start 2025-02-07 14:53:30 +10:00
Pavel Sviderski 5b66cec627 verify checksum in install_cli.sh 2025-02-06 15:17:33 +10:00
Pavel Sviderski 0d9a2b6e07 add brew install command to README 2025-02-06 14:54:55 +10:00
Pavel Sviderski fb68303825 fix: cli install script, add manual instruction 2025-02-06 14:38:55 +10:00
Pavel Sviderski cfe5b018f3 fix: install.sh install command 2025-02-06 14:01:55 +10:00
Pavel Sviderski b451f2cf7c fix: install_cli.sh install command 2025-02-06 13:56:56 +10:00
Pavel Sviderski 7d25bbafdf fix: goreleaser format for homebrew 2025-02-06 13:51:15 +10:00
Pavel Sviderski 58e1c2eea3 goreleaser: specify id for brew 2025-02-06 13:21:52 +10:00
Pavel Sviderski 6c791840bd update token for brew release 2025-02-06 13:15:27 +10:00
Pavel Sviderski 3af97306e1 add homebrew release to goreleaser 2025-02-06 13:09:34 +10:00
Pavel Sviderski d645958631 make DNS A record creation a separate step in quick start 2025-02-06 08:42:39 +10:00
Pavel Sviderski 48bc94828e link design doc 2025-02-05 21:35:16 +10:00
Pavel Sviderski a43f4bf5df minor README 2025-02-05 21:01:55 +10:00
Pavel Sviderski 22f705d6c9 update image name on landing 2025-02-05 20:48:02 +10:00
Pavel Sviderski efcb9ee947 add How it works section to README 2025-02-05 20:47:53 +10:00
Pavel Sviderski a347b32b65 fix typo 2025-02-03 11:55:03 +10:00
Pavel Sviderski 7e865b61c2 update get started links on landing 2025-02-03 11:48:11 +10:00
Pavel Sviderski b959f5d9f3 remove duplicate feature 2025-02-02 22:17:51 +10:00
Pavel Sviderski df64a00813 header padding 2025-02-02 22:15:48 +10:00
Pavel Sviderski 07dc5c373d update README: header, features, why uncloud 2025-02-02 22:12:42 +10:00
Pavel SviderskiandGitHub a6e8616b36 Update curl url in README.md 2025-01-30 15:14:07 +10:00
Pavel Sviderski 7041244728 update curl | sh url 2025-01-24 22:49:57 +10:00
128 changed files with 10810 additions and 3243 deletions
+1
View File
@@ -4,6 +4,7 @@
# Allow files and directories. # Allow files and directories.
!cmd/ !cmd/
!internal/ !internal/
!pkg/
!scripts/docker/ !scripts/docker/
!go.* !go.*
+3
View File
@@ -32,3 +32,6 @@ dist/
# Web # Web
node_modules/ node_modules/
# VS Code
.vscode
+22 -3
View File
@@ -40,7 +40,9 @@ archives:
- id: uncloud - id: uncloud
builds: builds:
- uncloud - uncloud
format: gz formats:
# gz is not compatible with homebrew releases, so we use tar.gz
- tar.gz
name_template: >- name_template: >-
{{ .Binary }}_ {{ .Binary }}_
{{- if eq .Os "darwin"}}macos {{- if eq .Os "darwin"}}macos
@@ -49,14 +51,16 @@ archives:
# use zip for windows archives # use zip for windows archives
format_overrides: format_overrides:
- goos: windows - goos: windows
format: zip formats:
- zip
files: files:
- none* - none*
- id: uncloudd - id: uncloudd
builds: builds:
- uncloudd - uncloudd
format: gz formats:
- tar.gz
name_template: "{{ .Binary }}_{{ .Os }}_{{ .Arch }}" name_template: "{{ .Binary }}_{{ .Os }}_{{ .Arch }}"
files: files:
- none* - none*
@@ -70,3 +74,18 @@ changelog:
exclude: exclude:
- "^docs:" - "^docs:"
- "^test:" - "^test:"
brews:
- name: uncloud
description: "Uncloud CLI"
homepage: "https://uncloud.run"
ids:
- uncloud
install: |
bin.install "uncloud"
bin.install_symlink "uncloud" => "uc"
skip_upload: false
repository:
owner: psviderski
name: homebrew-tap
token: "{{ .Env.GITHUB_TOKEN_HOMEBREW_TAP }}"
+14
View File
@@ -8,6 +8,10 @@ update-dev:
scp uncloudd-linux-amd64 spy@192.168.40.176:~/ && \ scp uncloudd-linux-amd64 spy@192.168.40.176:~/ && \
ssh spy@192.168.40.176 sudo install ./uncloudd-linux-amd64 /usr/local/bin/uncloudd && \ ssh spy@192.168.40.176 sudo install ./uncloudd-linux-amd64 /usr/local/bin/uncloudd && \
rm uncloudd-linux-amd64 rm uncloudd-linux-amd64
# GOOS=linux GOARCH=arm64 go build -o uncloudd-linux-arm64 ./cmd/uncloudd && \
# scp uncloudd-linux-arm64 ubuntu@152.67.101.197:~/ && \
# ssh ubuntu@152.67.101.197 sudo install ./uncloudd-linux-arm64 /usr/local/bin/uncloudd && \
# rm uncloudd-linux-arm64
update-restart-dev: update-restart-dev:
GOOS=linux GOARCH=amd64 go build -o uncloudd-linux-amd64 ./cmd/uncloudd && \ GOOS=linux GOARCH=amd64 go build -o uncloudd-linux-amd64 ./cmd/uncloudd && \
@@ -16,12 +20,22 @@ update-restart-dev:
scp uncloudd-linux-amd64 spy@192.168.40.176:~/ && \ 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" && \ ssh spy@192.168.40.176 "sudo install ./uncloudd-linux-amd64 /usr/local/bin/uncloudd && sudo systemctl restart uncloud" && \
rm uncloudd-linux-amd64 rm uncloudd-linux-amd64
# GOOS=linux GOARCH=arm64 go build -o uncloudd-linux-arm64 ./cmd/uncloudd && \
# scp uncloudd-linux-arm64 ubuntu@152.67.101.197:~/ && \
# ssh ubuntu@152.67.101.197 "sudo install ./uncloudd-linux-arm64 /usr/local/bin/uncloudd && sudo systemctl restart uncloud" && \
# rm uncloudd-linux-arm64
reset-dev: reset-dev:
ssh spy@192.168.40.243 "sudo systemctl stop uncloud && sudo rm -rf /var/lib/uncloud" 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 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" ssh ubuntu@152.67.101.197 "sudo systemctl stop uncloud && sudo rm -rf /var/lib/uncloud"
demo-reset:
rm -fv ~/.config/uncloud/config.toml
ssh ubuntu@152.67.101.197 "AUTO_CONFIRM=true sudo -E uncloud-uninstall && docker rmi caddy:2.9.1"
ssh root@5.223.45.199 "AUTO_CONFIRM=true sudo -E uncloud-uninstall"
ssh spy@192.168.40.243 "AUTO_CONFIRM=true sudo -E uncloud-uninstall"
.PHONY: proto .PHONY: proto
proto: proto:
protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative \ protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative \
+260 -23
View File
@@ -1,44 +1,281 @@
<div align="center"> <div align="center">
<img src="./website/images/logo.svg" height="100" alt="Uncloud logo"/> <img src="./website/images/logo.svg" height="100" alt="Uncloud logo"/>
<h1>Uncloud</h2> <h1>Uncloud</h1>
A lightweight Docker clustering tool for running web apps on your own servers — from cloud VMs to bare metal.<br> <p><strong>Docker simplicity. Multi-machine power.</strong></p>
Replace Heroku and Render, no Kubernetes required.
</div> </div>
## Quick start Uncloud is a lightweight clustering and container orchestration tool that lets you deploy and manage web apps across
cloud VMs and bare metal with minimized cluster management overhead. It creates a secure WireGuard mesh network between
your Docker hosts and provides automatic service discovery, load balancing, ingress with HTTPS, and simple CLI commands
to manage your apps.
Unlike traditional orchestrators, there's no central control plane and quorum to maintain. Each machine maintains a
synchronized copy of the cluster state through peer-to-peer communication, keeping cluster operations functional even if
some machines go offline.
Uncloud aims to be the solution for developers who want the flexibility of self-hosted infrastructure without the
operational complexity of Kubernetes.
## 🎬 Quick demo
Deploy a highly available web app with automatic HTTPS across multiple regions and on-premises in just a couple minutes.
<a href="https://uncloud.wistia.com/medias/k47uwt9uau?wvideo=k47uwt9uau">
<img src="https://embed-ssl.wistia.com/deliveries/3cf7014a48b93afc556444bed3e39a8c.jpg?image_crop_resized=900x526&image_play_button_rounded=true&image_play_button_size=2x&image_play_button_color=18181Be0" alt="Uncloud demo" width="450" height="263" />
</a>
## ✨ Features
* **Deploy anywhere**: Combine cloud VMs, dedicated servers, and bare metal into a unified computing environment —
regardless of location or provider.
* **Zero-config private network**: Automatic WireGuard mesh with peer discovery and NAT traversal. Containers get unique
IPs for direct cross-machine communication.
* **No control plane**: Fully decentralized design eliminates single points of failure and reduces operational overhead.
* **Imperative over declarative**: Favoring imperative operations over state reconciliation simplifies both the mental
model and troubleshooting.
* **Zero-downtime deployments**: Rolling updates with health checks and automatic rollback on failure.
* **Service discovery**: Built-in DNS server resolves service names to container IPs.
* **Managed DNS**: Automatic DNS records `*.<id>.cluster.uncloud.run` for services with public access via managed
[Uncloud DNS](https://github.com/psviderski/uncloud-dns) service.
* **Automatic HTTPS**: Built-in Caddy reverse proxy handles TLS certificate provisioning and renewal using Let's
Encrypt.
* **Docker-like CLI**: Familiar commands for managing both infrastructure and applications.
* **Remote management**: Control your entire infrastructure through SSH access to any single machine in the cluster.
Coming soon:
* Infrastructure as Code using Docker Compose format
* Project isolation through environments/namespaces
* Persistent volumes and secrets management
* Monitoring and log aggregation
* Database deployment and management
Here is a diagram of an Uncloud multi-provider cluster of 3 machines:
![Diagram: multi-provider cluster of 3 machines](website/images/diagram.webp)
## 💫 Why Uncloud?
Modern cloud platforms like Heroku and Render offer amazing developer experiences but at a premium price. Traditional
container orchestrators like Kubernetes provide power and flexibility but require significant operational expertise. I
believe there's a sweet spot in between — a pragmatic solution for the majority of us who aren't running at Google
scale. You should be able to:
* **Own your infrastructure and data**: Whether driven by costs, compliance, or flexibility, run applications on any
combination of cloud VMs and personal hardware while maintaining the cloud-like experience you love.
* **Stay simple**: Don't worry about control planes, highly-available clusters, or complex YAML configurations for
common use cases.
* **Build with proven primitives**: Get production-grade networking, deployment primitives, service discovery, load
balancing, and ingress with HTTPS out of the box without becoming a distributed systems expert.
* **Support sustainable computing** 🌿: Minimize system overhead to maximize resources available for your applications.
Uncloud's goal is to make deployment and management of containerized applications feel as seamless as using a cloud
platform, whether you're running on a $5 VPS, a spare Mac mini, or a rack of bare metal servers.
## 🚀 Quick start
1. Install Uncloud CLI: 1. Install Uncloud CLI:
```bash ```bash
curl -fsSL https://raw.githubusercontent.com/psviderski/uncloud/main/scripts/install_cli.sh | sh brew install psviderski/tap/uncloud
```
# or using curl (macOS/Linux)
curl -fsS https://get.uncloud.run/install.sh | sh
```
2. Initialize your first machine: 2. Initialize your first machine:
```bash ```bash
uc machine init root@your-server-ip 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 3. Deploy your app from a Docker image and publish its container port 8000 as HTTPS using `app.example.com` domain:
automatic HTTPS:
```bash
uc run -p app.example.com:8000/https image/my-app
```
4. Create a DNS A record in your DNS provider (Cloudflare, Namecheap, etc.) that points `app.example.com` to your
server's IP address. Allow a few minutes for DNS propagation.
That's it! Your app is now running and accessible at https://app.example.com ✨
5. Clean up when you're done:
```bash
uc ls
# Copy the service name from the output and run the rm command:
uc rm my-app-name
```
If you want to fully uninstall Uncloud on a machine, run:
```bash
uncloud-uninstall
```
View the [user guide](docs/user_guide.md) for more information.
## ⚙️ How it works
Check out the [design document](docs/design.md) to understand Uncloud's design philosophy and goals. Here, let's peek
under the hood to see what happens when you run certain commands.
**When you initialize a new cluster on a machine:**
```bash ```bash
uc run --name my-app -p app.example.com:8000/https registry/app $ uc machine init --name oracle-vm ubuntu@152.67.101.197
Downloading Uncloud install script: https://raw.githubusercontent.com/psviderski/uncloud/refs/heads/main/scripts/install.sh
⏳ Running Uncloud install script...
✓ Docker is already installed.
⏳ Installing Docker...
...
✓ Docker installed successfully.
✓ Linux user and group 'uncloud' created.
✓ Linux user 'ubuntu' added to group 'uncloud'.
⏳ Installing Uncloud binaries...
⏳ Downloading uncloudd binary: https://github.com/psviderski/uncloud/releases/latest/download/uncloudd_linux_arm64.tar.gz
✓ uncloudd binary installed: /usr/local/bin/uncloudd
⏳ Downloading uninstall script: https://raw.githubusercontent.com/psviderski/uncloud/refs/heads/main/scripts/uninstall.sh
✓ uncloud-uninstall script installed: /usr/local/bin/uncloud-uninstall
✓ Systemd unit file created: /etc/systemd/system/uncloud.service
Created symlink /etc/systemd/system/multi-user.target.wants/uncloud.service → /etc/systemd/system/uncloud.service.
⏳ Downloading uncloud-corrosion binary: https://github.com/psviderski/corrosion/releases/latest/download/corrosion-aarch64-unknown-linux-gnu.tar.gz
✓ uncloud-corrosion binary installed: /usr/local/bin/uncloud-corrosion
✓ Systemd unit file created: /etc/systemd/system/uncloud-corrosion.service
⏳ Starting Uncloud machine daemon (uncloud.service)...
✓ Uncloud machine daemon started.
✓ Uncloud installed on the machine successfully! 🎉
Cluster "default" initialised with machine "oracle-vm"
Waiting for the machine to be ready...
Reserved cluster domain: xuw3xd.cluster.uncloud.run
[+] Deploying service caddy 1/1
✔ Container caddy-c47x on oracle-vm Started 0.9s
Updating cluster domain records in Uncloud DNS to point to machines running caddy service...
[+] Verifying internet access to caddy service 1/1
✔ Machine oracle-vm (152.67.101.197) Reachable 0.1s
DNS records updated to use only the internet-reachable machines running caddy service:
*.xuw3xd.cluster.uncloud.run A → 152.67.101.197
``` ```
That's it! Your app is now running and accessible at https://app.example.com ✨ 1. The CLI SSHs into the machine and installs Docker, the `uncloudd` machine daemon and
[corrosion](https://github.com/superfly/corrosion) service, managed by systemd.
2. Generates a unique WireGuard key pair, allocates a dedicated subnet `10.210.0.0/24` for the machine and its
containers, and configures `uncloudd` accordingly. All subsequent communication happens with `uncloudd`
through its gRPC API over SSH.
3. Configures and starts `corrosion`, a CRDT-based distributed SQLite database to share cluster state between machines.
4. Creates a Docker bridge network connected to the WireGuard interface.
5. This machine becomes an entry point for the newly created cluster which is stored in the cluster config under
`~/.config/uncloud` on your local machine.
## Project status **When you add another machine:**
Uncloud is an open source project I'm ([@psviderski](https://github.com/psviderski)) actively developing. I'd love to ```bash
share this journey with you. [Subscribe](https://uncloud.run/) to follow the progress, get early insights into new $ uc machine add --name hetzner-server root@5.223.45.199
features, and be the first to know when it's ready for production use. Downloading Uncloud install script: https://raw.githubusercontent.com/psviderski/uncloud/refs/heads/main/scripts/install.sh
⏳ Running Uncloud install script...
✓ Docker is already installed.
✓ Linux user and group 'uncloud' created.
⏳ Installing Uncloud binaries...
⏳ Downloading uncloudd binary: https://github.com/psviderski/uncloud/releases/latest/download/uncloudd_linux_amd64.tar.gz
✓ uncloudd binary installed: /usr/local/bin/uncloudd
⏳ Downloading uninstall script: https://raw.githubusercontent.com/psviderski/uncloud/refs/heads/main/scripts/uninstall.sh
✓ uncloud-uninstall script installed: /usr/local/bin/uncloud-uninstall
✓ Systemd unit file created: /etc/systemd/system/uncloud.service
Created symlink /etc/systemd/system/multi-user.target.wants/uncloud.service → /etc/systemd/system/uncloud.service.
⏳ Downloading uncloud-corrosion binary: https://github.com/psviderski/corrosion/releases/latest/download/corrosion-x86_64-unknown-linux-gnu.tar.gz
✓ uncloud-corrosion binary installed: /usr/local/bin/uncloud-corrosion
✓ Systemd unit file created: /etc/systemd/system/uncloud-corrosion.service
⏳ Starting Uncloud machine daemon (uncloud.service)...
✓ Uncloud machine daemon started.
✓ Uncloud installed on the machine successfully! 🎉
Machine "hetzner-server" added to cluster
Waiting for the machine to be ready...
I'd also love your input: [+] Deploying service caddy 1/1
✔ Container caddy-d36c on hetzner-server Started 1.0s
- 🐛 Found a bug? [Open an issue](https://github.com/psviderski/uncloud/issues) Updating cluster domain records in Uncloud DNS to point to machines running caddy service...
- 💡 Have ideas? [Join the discussion](https://github.com/psviderski/uncloud/discussions) [+] Verifying internet access to caddy service 2/2
✔ Machine hetzner-server (5.223.45.199) Reachable 0.2s
✔ Machine oracle-vm (152.67.101.197) Reachable 0.1s
## Motivation DNS records updated to use only the internet-reachable machines running caddy service:
*.xuw3xd.cluster.uncloud.run A → 152.67.101.197, 5.223.45.199
TBD $ uc machine ls
NAME STATE ADDRESS PUBLIC IP WIREGUARD ENDPOINTS
oracle-vm Up 10.210.0.1/24 152.67.101.197 10.0.0.95:51820, 152.67.101.197:51820
hetzner-server Up 10.210.1.1/24 5.223.45.199 5.223.45.199:51820, [2a01:4ff:2f0:128b::1]:51820
```
1. The second machine gets provisioned just like the first. A non-root SSH user will need `sudo` access.
2. Allocates a new subnet `10.210.1.0/24` for the second machine and its containers.
3. Registers the second machine in the cluster state and exchanges WireGuard keys with the first machine.
4. Both machines establish a WireGuard tunnel between each other, allowing Docker containers connected to the bridge
network to communicate directly across machines.
5. Configures and starts `corrosion` on the second machine to sync the cluster state.
6. The second machine is added as an alternative entry point in the cluster config.
7. If one of the machines goes offline, the other machine can still serve cluster operations.
If one more machine is added, the process repeats with a new subnet. The new machine needs to establish a WireGuard
connection with only one of the existing machines. Other machines will learn about it through the shared cluster state
and automatically establish a WireGuard tunnel with it.
**When you run a service:**
```bash
$ uc run -p app.example.com:8000/https image/my-app
[+] Running service my-app-1b3b (replicated mode) 1/1
✔ Container my-app-1b3b-tcex on oracle-vm Started
my-app-1b3b endpoints:
• https://app.example.com → :8000
• https://my-app-1b3b.xuw3xd.cluster.uncloud.run → :8000
```
1. CLI picks a machine to run your container.
2. `uncloudd` that the CLI communicates with uses [`grpc-proxy`](https://github.com/siderolabs/grpc-proxy) to forward
the request to the target machine to launch a container there.
3. `uncloudd` on the target machine starts the Docker container in the bridge network and stores its info in the
cluster's distributed state.
4. The container gets a cluster-unique IP address from the bridge network (in the `10.210.X.2-254` range) and becomes
accessible from other machines in the cluster.
5. Caddy reverse proxy which runs in [`global`](https://github.com/compose-spec/compose-spec/blob/main/deploy.md#mode)
mode on each machine watches the cluster state for new services and updates its configuration to route traffic to the
new container.
Look ma, no control plane or master nodes to maintain! Just a simple overlay network and eventually consistent state
sync that lets machines work together. Want to check on things or make changes? Connect to any machine either implicitly
using the CLI or directly over SSH. They all have the complete cluster state and can control everything. It's like each
machine is a full backup of your control plane.
## 🏗 Project status
Uncloud is currently in active development and is **not ready for production use**. Features may change significantly
and there may be breaking changes between releases.
I'd love your input! Here's how you can contribute:
* 🐛 Found a bug? [Open an issue](https://github.com/psviderski/uncloud/issues)
* 💡 Have ideas? [Join the discussion](https://github.com/psviderski/uncloud/discussions)
## 📫 Stay updated
* [Subscribe](https://uncloud.run/#subscribe) to my newsletter to follow the progress, get early insights into new
features, and be the first to know when it's ready for production use.
* Watch this repository for releases.
* Follow [@psviderski](https://github.com/psviderski) on GitHub.
## ❤️ Contributors
Thank you [@cedws](https://github.com/cedws) for being the first contributor to Uncloud! 🎉
<a href="https://github.com/psviderski/uncloud/graphs/contributors">
<img src="https://contrib.rocks/image?repo=psviderski/uncloud" />
</a>
+1 -1
View File
@@ -3,7 +3,7 @@ package cluster
import ( import (
"fmt" "fmt"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"uncloud/internal/ucind" "github.com/psviderski/uncloud/internal/ucind"
) )
func NewCreateCommand() *cobra.Command { func NewCreateCommand() *cobra.Command {
+1 -1
View File
@@ -3,7 +3,7 @@ package cluster
import ( import (
"fmt" "fmt"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"uncloud/internal/ucind" "github.com/psviderski/uncloud/internal/ucind"
) )
func NewRemoveCommand() *cobra.Command { func NewRemoveCommand() *cobra.Command {
+2 -2
View File
@@ -7,8 +7,8 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
"os" "os"
"strings" "strings"
"uncloud/cmd/ucind/cluster" "github.com/psviderski/uncloud/cmd/ucind/cluster"
"uncloud/internal/ucind" "github.com/psviderski/uncloud/internal/ucind"
) )
func main() { func main() {
+225
View File
@@ -0,0 +1,225 @@
package caddy
import (
"context"
"errors"
"fmt"
"github.com/docker/cli/cli/streams"
"github.com/docker/compose/v2/pkg/progress"
"github.com/psviderski/uncloud/internal/cli"
"github.com/psviderski/uncloud/internal/machine/api/pb"
"github.com/psviderski/uncloud/pkg/api"
"github.com/psviderski/uncloud/pkg/client"
"github.com/psviderski/uncloud/pkg/client/deploy"
"github.com/spf13/cobra"
"maps"
"slices"
"strings"
)
type deployOptions struct {
image string
machine string
cluster string
}
func NewDeployCommand() *cobra.Command {
opts := deployOptions{}
cmd := &cobra.Command{
Use: "deploy",
Short: "Deploy or upgrade Caddy reverse proxy across all machines in the cluster.",
Long: "Deploy or upgrade Caddy reverse proxy across all machines in the cluster.\n" +
"A rolling update is performed when updating existing containers to minimise disruption.",
RunE: func(cmd *cobra.Command, args []string) error {
uncli := cmd.Context().Value("cli").(*cli.CLI)
return runDeploy(cmd.Context(), uncli, opts)
},
}
cmd.Flags().StringVar(&opts.image, "image", "",
"Caddy Docker image to deploy. (default caddy:LATEST_VERSION)")
cmd.Flags().StringVarP(&opts.machine, "machine", "m", "",
"Machine names to deploy to (comma-separated). (default is all machines)")
cmd.Flags().StringVarP(
&opts.cluster, "cluster", "c", "",
"Name of the cluster to deploy to. (default is the current cluster)",
)
return cmd
}
func runDeploy(ctx context.Context, uncli *cli.CLI, opts deployOptions) error {
clusterClient, err := uncli.ConnectCluster(ctx, opts.cluster)
if err != nil {
return fmt.Errorf("connect to cluster: %w", err)
}
defer clusterClient.Close()
svc, err := clusterClient.InspectService(ctx, client.CaddyServiceName)
if err != nil {
if !errors.Is(err, api.ErrNotFound) {
return fmt.Errorf("inspect caddy service: %w", err)
}
fmt.Printf("Service: %s (not running)\n", client.CaddyServiceName)
} else {
fmt.Printf("Service: %s (%s mode)\n", svc.Name, svc.Mode)
// Collect unique images of all containers in the running caddy service.
images := make(map[string]struct{}, len(svc.Containers))
for _, c := range svc.Containers {
images[c.Container.Config.Image] = struct{}{}
}
currentImages := slices.Collect(maps.Keys(images))
if len(currentImages) > 1 {
commaSeparatedImages := strings.Join(currentImages, ", ")
fmt.Printf("Current images (multiple versions detected): %s\n", commaSeparatedImages)
} else {
fmt.Printf("Current image: %s\n", currentImages[0])
}
}
if opts.image != "" {
fmt.Printf("Target image: %s\n", opts.image)
}
fmt.Println()
fmt.Println("Preparing a deployment plan...")
var filter deploy.MachineFilter
if opts.machine != "" {
machines := strings.Split(opts.machine, ",")
for i, m := range machines {
machines[i] = strings.TrimSpace(m)
}
filter = machineFilter(machines)
}
d, err := clusterClient.NewCaddyDeployment(opts.image, filter)
if err != nil {
return fmt.Errorf("create caddy deployment: %w", err)
}
if opts.image == "" {
fmt.Printf("Target image: %s (latest stable)\n", d.Spec.Container.Image)
}
plan, err := d.Plan(ctx)
if err != nil {
if errors.Is(err, deploy.ErrNoMatchingMachines) {
return fmt.Errorf("no machines found matching: %s", opts.machine)
}
return fmt.Errorf("plan caddy deployment: %w", err)
}
if len(plan.Operations) == 0 {
if opts.machine != "" {
fmt.Printf("%s service is up to date on selected machines.\n", client.CaddyServiceName)
} else {
fmt.Printf("%s service is up to date.\n", client.CaddyServiceName)
}
} else {
if svc.ID == "" {
if opts.machine != "" {
fmt.Println("This will run a Caddy container on selected machines.")
} else {
fmt.Println("This will run a Caddy container on each machine.")
}
} else {
if opts.machine != "" {
fmt.Println("This will perform a rolling update of Caddy containers on selected machines.")
} else {
fmt.Println("This will perform a rolling update of Caddy containers on each machine.")
}
}
// Initialise a machine and container name resolver to properly format the plan output.
resolver, err := clusterClient.ServiceOperationNameResolver(ctx, svc)
if err != nil {
return fmt.Errorf("create machine and container name resolver for service operations: %w", err)
}
fmt.Println()
fmt.Println("Deployment plan:")
fmt.Println(plan.Format(resolver))
fmt.Println()
confirmed, err := cli.Confirm()
if err != nil {
return fmt.Errorf("confirm deployment: %w", err)
}
if !confirmed {
fmt.Println("Cancelled. No changes were made.")
return nil
}
err = progress.RunWithTitle(ctx, func(ctx context.Context) error {
if _, err = d.Run(ctx); err != nil {
return fmt.Errorf("deploy caddy: %w", err)
}
return nil
}, uncli.ProgressOut(), fmt.Sprintf("Deploying service %s (%s mode)", d.Spec.Name, d.Spec.Mode))
if err != nil {
return err
}
}
fmt.Println()
return UpdateDomainRecords(ctx, clusterClient, uncli.ProgressOut())
}
func UpdateDomainRecords(ctx context.Context, clusterClient *client.Client, progressOut *streams.Out) error {
if _, err := clusterClient.GetDomain(ctx); err != nil {
if errors.Is(err, api.ErrNotFound) {
fmt.Println("Skipping DNS records update as no cluster domain is reserved (see 'uc dns').")
return nil
}
return fmt.Errorf("get cluster domain: %w", err)
}
fmt.Println("Updating cluster domain records in Uncloud DNS to point to machines running caddy service...")
// TODO: split the method into two: one to get the records and one to update them to ask for update confirmation.
var records []*pb.DNSRecord
err := progress.RunWithTitle(ctx, func(ctx context.Context) error {
var err error
records, err = clusterClient.CreateIngressRecords(ctx, client.CaddyServiceName)
return err
}, progressOut, "Verifying internet access to caddy service")
if err != nil {
if errors.Is(err, client.ErrNoReachableMachines) {
fmt.Println()
fmt.Println("DNS records could not be updated as there are no internet-reachable machines running " +
"caddy containers.")
fmt.Println()
fmt.Println("Possible solutions:")
fmt.Println("- Ensure your machines have public IP addresses")
fmt.Println("- Use --public-ip flag when adding machines to override the automatically detected IPs")
fmt.Println("- Check firewall settings on your machines")
fmt.Println("- Configure port forwarding if behind NAT")
fmt.Println("- Retry Caddy deployment after resolving connectivity issues with 'uc caddy deploy'")
fmt.Println()
fmt.Println("Your services will not be accessible from the internet until at least one machine " +
"becomes reachable.")
}
return fmt.Errorf("failed to update DNS records pointing to caddy service: %w", err)
}
fmt.Println()
fmt.Println("DNS records updated to use only the internet-reachable machines running caddy service:")
for _, r := range records {
fmt.Printf(" %s %s → %s\n", r.Name, r.Type, strings.Join(r.Values, ", "))
}
return nil
}
func machineFilter(machines []string) deploy.MachineFilter {
if len(machines) == 0 {
return nil
}
return func(m *pb.MachineInfo) bool {
return slices.Contains(machines, m.Name)
}
}
+16
View File
@@ -0,0 +1,16 @@
package caddy
import (
"github.com/spf13/cobra"
)
func NewRootCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "caddy",
Short: "Manage Caddy reverse proxy service.",
}
cmd.AddCommand(
NewDeployCommand(),
)
return cmd
}
+137
View File
@@ -0,0 +1,137 @@
package main
import (
"context"
"errors"
"fmt"
"github.com/compose-spec/compose-go/v2/types"
"github.com/docker/compose/v2/pkg/progress"
"github.com/psviderski/uncloud/internal/cli"
"github.com/psviderski/uncloud/pkg/api"
"github.com/psviderski/uncloud/pkg/client/compose"
"github.com/psviderski/uncloud/pkg/client/deploy"
"github.com/spf13/cobra"
"strings"
)
type deployOptions struct {
files []string
services []string
cluster string
}
// NewDeployCommand creates a new command to deploy services from a Compose file.
func NewDeployCommand() *cobra.Command {
opts := deployOptions{}
cmd := &cobra.Command{
Use: "deploy [FLAGS] [SERVICE...]",
// TODO: remove WIP when the command is fully implemented.
Short: "WIP: Deploy services from a Compose file.",
RunE: func(cmd *cobra.Command, args []string) error {
uncli := cmd.Context().Value("cli").(*cli.CLI)
if len(args) > 0 {
opts.services = args
}
return runDeploy(cmd.Context(), uncli, opts)
},
}
cmd.Flags().StringSliceVarP(&opts.files, "file", "f", nil,
"One or more Compose files to deploy services from. (default compose-ports-long.yaml)")
cmd.Flags().StringVarP(&opts.cluster, "cluster", "c", "",
"Name of the cluster to deploy to (default is the current cluster)")
return cmd
}
// runDeploy parses the Compose file(s) and deploys the services.
func runDeploy(ctx context.Context, uncli *cli.CLI, opts deployOptions) error {
project, err := compose.LoadProject(ctx, opts.files)
if err != nil {
return fmt.Errorf("load compose file(s): %w", err)
}
if len(opts.services) > 0 {
// TODO: handle dependencies properly.
project, err = project.WithSelectedServices(opts.services, types.IgnoreDependencies)
if err != nil {
return fmt.Errorf("select services: %w", err)
}
}
clusterClient, err := uncli.ConnectCluster(ctx, opts.cluster)
if err != nil {
return fmt.Errorf("connect to cluster: %w", err)
}
defer clusterClient.Close()
composeDeploy, err := compose.NewDeployment(ctx, clusterClient, project)
if err != nil {
return fmt.Errorf("create compose deployment: %w", err)
}
plan, err := composeDeploy.Plan(ctx)
if err != nil {
return fmt.Errorf("plan deployment: %w", err)
}
if len(plan.Operations) == 0 {
fmt.Println("Services are up to date.")
return nil
}
fmt.Println("Deployment plan:")
for _, op := range plan.Operations {
svcPlan, ok := op.(*deploy.Plan)
if !ok {
return fmt.Errorf("expected service Plan, got: %T", op)
}
svc, err := clusterClient.InspectService(ctx, svcPlan.ServiceID)
if err != nil {
if !errors.Is(err, api.ErrNotFound) {
return fmt.Errorf("inspect service: %w", err)
}
fmt.Printf("- Run service [name=%s]\n", svcPlan.ServiceName)
} else {
fmt.Printf("- Update service [name=%s]\n", svc.Name)
}
// Initialise a machine and container name resolver to properly format the service plan output.
resolver, err := clusterClient.ServiceOperationNameResolver(ctx, svc)
if err != nil {
return fmt.Errorf("create machine and container name resolver for service operations: %w", err)
}
fmt.Println(indent(svcPlan.Format(resolver), " "))
}
fmt.Println()
confirmed, err := cli.Confirm()
if err != nil {
return fmt.Errorf("confirm deployment: %w", err)
}
if !confirmed {
fmt.Println("Cancelled. No changes were made.")
return nil
}
return progress.RunWithTitle(ctx, func(ctx context.Context) error {
if err := plan.Execute(ctx, clusterClient); err != nil {
return fmt.Errorf("deploy services: %w", err)
}
return nil
}, uncli.ProgressOut(), "Deploying services")
}
func indent(text, prefix string) string {
lines := strings.Split(text, "\n")
for i, line := range lines {
lines[i] = prefix + line
}
return strings.Join(lines, "\n")
}
+54
View File
@@ -0,0 +1,54 @@
package dns
import (
"context"
"errors"
"fmt"
"github.com/spf13/cobra"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/emptypb"
"github.com/psviderski/uncloud/internal/cli"
)
type releaseOptions struct {
cluster string
}
func NewReleaseCommand() *cobra.Command {
opts := releaseOptions{}
cmd := &cobra.Command{
Use: "release",
Short: "Release the reserved cluster domain.",
RunE: func(cmd *cobra.Command, args []string) error {
uncli := cmd.Context().Value("cli").(*cli.CLI)
return release(cmd.Context(), uncli, opts)
},
}
cmd.Flags().StringVarP(
&opts.cluster, "cluster", "c", "",
"Name of the cluster. (default is the current cluster)",
)
return cmd
}
func release(ctx context.Context, uncli *cli.CLI, opts releaseOptions) error {
client, err := uncli.ConnectCluster(ctx, opts.cluster)
if err != nil {
return fmt.Errorf("connect to cluster: %w", err)
}
defer client.Close()
domain, err := client.ReleaseDomain(ctx, &emptypb.Empty{})
if err != nil {
if status.Convert(err).Code() == codes.NotFound {
return errors.New("no domain reserved")
}
}
fmt.Printf("Released cluster domain: %s\n", domain.Name)
return nil
}
+74
View File
@@ -0,0 +1,74 @@
package dns
import (
"context"
"errors"
"fmt"
"github.com/psviderski/uncloud/cmd/uncloud/caddy"
"github.com/psviderski/uncloud/internal/cli"
"github.com/psviderski/uncloud/internal/machine/api/pb"
"github.com/psviderski/uncloud/pkg/api"
"github.com/psviderski/uncloud/pkg/client"
"github.com/spf13/cobra"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
const DefaultUncloudDNSAPIEndpoint = "https://dns.uncloud.run/v1"
type reserveOptions struct {
endpoint string
cluster string
}
func NewReserveCommand() *cobra.Command {
opts := reserveOptions{}
cmd := &cobra.Command{
Use: "reserve",
Short: "Reserve a cluster domain in Uncloud DNS.",
RunE: func(cmd *cobra.Command, args []string) error {
uncli := cmd.Context().Value("cli").(*cli.CLI)
return reserve(cmd.Context(), uncli, opts)
},
}
cmd.Flags().StringVar(&opts.endpoint, "endpoint", DefaultUncloudDNSAPIEndpoint,
"API endpoint for the Uncloud DNS service.")
cmd.Flags().StringVarP(
&opts.cluster, "cluster", "c", "",
"Name of the cluster. (default is the current cluster)",
)
return cmd
}
func reserve(ctx context.Context, uncli *cli.CLI, opts reserveOptions) error {
clusterClient, err := uncli.ConnectCluster(ctx, opts.cluster)
if err != nil {
return fmt.Errorf("connect to cluster: %w", err)
}
defer clusterClient.Close()
domain, err := clusterClient.ReserveDomain(ctx, &pb.ReserveDomainRequest{Endpoint: opts.endpoint})
if err != nil {
if status.Convert(err).Code() == codes.AlreadyExists {
return errors.New("domain already reserved")
}
return err
}
fmt.Printf("Reserved cluster domain: %s\n", domain.Name)
// Update cluster domain records in Uncloud DNS to point to machines running caddy service if it has been deployed.
if _, err = clusterClient.InspectService(ctx, client.CaddyServiceName); err != nil {
if errors.Is(err, api.ErrNotFound) {
fmt.Println("Deploy the Caddy reverse proxy service ('uc caddy deploy') to enable internet access " +
"to your services via the reserved or your custom domain.")
return nil
}
return fmt.Errorf("inspect caddy service: %w", err)
}
return caddy.UpdateDomainRecords(ctx, clusterClient, uncli.ProgressOut())
}
+22
View File
@@ -0,0 +1,22 @@
package dns
import (
"github.com/spf13/cobra"
)
func NewRootCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "dns",
Short: "Manage cluster domain in Uncloud DNS.",
Long: "Manage cluster domain in Uncloud DNS.\n" +
"DNS commands allow you to reserve or release a unique '<id>.cluster.uncloud.run' domain for your " +
"cluster. When reserved, Caddy service deployments will automatically update DNS records to route " +
"traffic to the services in the cluster.",
}
cmd.AddCommand(
NewReleaseCommand(),
NewReserveCommand(),
NewShowCommand(),
)
return cmd
}
+53
View File
@@ -0,0 +1,53 @@
package dns
import (
"context"
"errors"
"fmt"
"github.com/psviderski/uncloud/internal/cli"
"github.com/psviderski/uncloud/pkg/api"
"github.com/spf13/cobra"
)
type showOptions struct {
cluster string
}
func NewShowCommand() *cobra.Command {
opts := showOptions{}
cmd := &cobra.Command{
Use: "show",
Short: "Print the cluster domain name.",
RunE: func(cmd *cobra.Command, args []string) error {
uncli := cmd.Context().Value("cli").(*cli.CLI)
return show(cmd.Context(), uncli, opts)
},
}
cmd.Flags().StringVarP(
&opts.cluster, "cluster", "c", "",
"Name of the cluster. (default is the current cluster)",
)
return cmd
}
func show(ctx context.Context, uncli *cli.CLI, opts showOptions) error {
clusterClient, err := uncli.ConnectCluster(ctx, opts.cluster)
if err != nil {
return fmt.Errorf("connect to cluster: %w", err)
}
defer clusterClient.Close()
domain, err := clusterClient.GetDomain(ctx)
if err != nil {
if errors.Is(err, api.ErrNotFound) {
return errors.New("no domain reserved")
}
return err
}
fmt.Println(domain)
return nil
}
+135 -6
View File
@@ -1,16 +1,31 @@
package machine package machine
import ( import (
"context"
"errors"
"fmt" "fmt"
"github.com/cenkalti/backoff/v4"
"github.com/docker/compose/v2/pkg/progress"
"github.com/psviderski/uncloud/cmd/uncloud/caddy"
"github.com/psviderski/uncloud/internal/cli"
"github.com/psviderski/uncloud/internal/cli/config"
"github.com/psviderski/uncloud/internal/machine/api/pb"
"github.com/psviderski/uncloud/pkg/api"
"github.com/psviderski/uncloud/pkg/client"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"uncloud/internal/cli" "google.golang.org/grpc/codes"
"uncloud/internal/cli/config" "google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/emptypb"
"net/netip"
"time"
) )
type addOptions struct { type addOptions struct {
name string name string
sshKey string noCaddy bool
cluster string publicIP string
sshKey string
cluster string
} }
func NewAddCommand() *cobra.Command { func NewAddCommand() *cobra.Command {
@@ -33,10 +48,19 @@ func NewAddCommand() *cobra.Command {
KeyPath: opts.sshKey, KeyPath: opts.sshKey,
} }
return uncli.AddMachine(cmd.Context(), remoteMachine, opts.cluster, opts.name) return add(cmd.Context(), uncli, remoteMachine, opts)
}, },
} }
cmd.Flags().StringVarP(&opts.name, "name", "n", "", "Assign a name to the machine.") cmd.Flags().StringVarP(&opts.name, "name", "n", "", "Assign a name to the machine.")
cmd.Flags().BoolVar(
&opts.noCaddy, "no-caddy", false,
"Don't deploy Caddy reverse proxy service to the machine.",
)
cmd.Flags().StringVar(
&opts.publicIP, "public-ip", "auto",
"Public IP address of the machine for ingress configuration. Use 'auto' for automatic detection, "+
"blank '' or 'none' to disable ingress on this machine, or specify an IP address.",
)
cmd.Flags().StringVarP( cmd.Flags().StringVarP(
&opts.sshKey, "ssh-key", "i", "", &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_*)",
@@ -47,3 +71,108 @@ func NewAddCommand() *cobra.Command {
) )
return cmd return cmd
} }
func add(ctx context.Context, uncli *cli.CLI, remoteMachine cli.RemoteMachine, opts addOptions) error {
var publicIP *netip.Addr
switch opts.publicIP {
case "auto":
publicIP = &netip.Addr{}
case "", "none":
publicIP = nil
default:
ip, err := netip.ParseAddr(opts.publicIP)
if err != nil {
return fmt.Errorf("parse public IP: %w", err)
}
publicIP = &ip
}
machineClient, err := uncli.AddMachine(ctx, remoteMachine, opts.cluster, opts.name, publicIP)
if err != nil {
return err
}
defer machineClient.Close()
if opts.noCaddy {
return nil
}
// Wait for the cluster to be initialised to be able to deploy the Caddy service.
fmt.Println("Waiting for the machine to be ready...")
fmt.Println()
if err = waitClusterInitialised(ctx, machineClient); err != nil {
return fmt.Errorf("wait for cluster to be initialised on machine: %w", err)
}
// Inspect the added machine to get its ID to create a filter for the Caddy deployment.
minfo, err := machineClient.Inspect(ctx, &emptypb.Empty{})
if err != nil {
return fmt.Errorf("inspect machine: %w", err)
}
filter := func(m *pb.MachineInfo) bool {
return m.Id == minfo.Id
}
// Deploy a Caddy service container to the added machine. If caddy service is already deployed on other machines,
// use the deployed image version. Otherwise, use the latest version.
caddyImage := ""
caddySvc, err := machineClient.InspectService(ctx, client.CaddyServiceName)
if err != nil {
if !errors.Is(err, api.ErrNotFound) {
return fmt.Errorf("inspect caddy service: %w", err)
}
} else {
caddyImage = caddySvc.Containers[0].Container.Config.Image
// Find the latest created container and use its image.
var latestCreated time.Time
for _, c := range caddySvc.Containers[1:] {
created, err := time.Parse(time.RFC3339Nano, c.Container.Created)
if err != nil {
continue
}
if created.After(latestCreated) {
latestCreated = created
caddyImage = c.Container.Config.Image
}
}
}
d, err := machineClient.NewCaddyDeployment(caddyImage, filter)
if err != nil {
return fmt.Errorf("create caddy deployment: %w", err)
}
err = progress.RunWithTitle(ctx, func(ctx context.Context) error {
if _, err = d.Run(ctx); err != nil {
return fmt.Errorf("deploy caddy: %w", err)
}
return nil
}, uncli.ProgressOut(), fmt.Sprintf("Deploying service %s", d.Spec.Name))
if err != nil {
return err
}
fmt.Println()
return caddy.UpdateDomainRecords(ctx, machineClient, uncli.ProgressOut())
}
func waitClusterInitialised(ctx context.Context, client *client.Client) error {
boff := backoff.WithContext(backoff.NewExponentialBackOff(
backoff.WithMaxInterval(1*time.Second),
backoff.WithMaxElapsedTime(5*time.Minute),
), ctx)
check := func() error {
_, err := client.ListMachines(ctx)
if err == nil {
return nil
}
statusErr := status.Convert(err)
if statusErr.Code() == codes.FailedPrecondition {
return err
}
return backoff.Permanent(err)
}
return backoff.Retry(check, boff)
}
+104 -14
View File
@@ -1,19 +1,28 @@
package machine package machine
import ( import (
"context"
"fmt" "fmt"
"github.com/docker/compose/v2/pkg/progress"
"github.com/psviderski/uncloud/cmd/uncloud/caddy"
"github.com/psviderski/uncloud/cmd/uncloud/dns"
"github.com/psviderski/uncloud/internal/cli"
"github.com/psviderski/uncloud/internal/cli/config"
"github.com/psviderski/uncloud/internal/machine/api/pb"
"github.com/psviderski/uncloud/internal/machine/cluster"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"net/netip" "net/netip"
"uncloud/internal/cli"
"uncloud/internal/cli/config"
"uncloud/internal/machine/cluster"
) )
type initOptions struct { type initOptions struct {
name string dnsEndpoint string
network string name string
sshKey string network string
cluster string noCaddy bool
noDNS bool
publicIP string
sshKey string
cluster string
} }
func NewInitCommand() *cobra.Command { func NewInitCommand() *cobra.Command {
@@ -39,22 +48,36 @@ func NewInitCommand() *cobra.Command {
KeyPath: opts.sshKey, KeyPath: opts.sshKey,
} }
} }
netPrefix, err := netip.ParsePrefix(opts.network)
if err != nil {
return fmt.Errorf("parse network CIDR: %w", err)
}
return uncli.InitCluster(cmd.Context(), remoteMachine, opts.cluster, opts.name, netPrefix) return initCluster(cmd.Context(), uncli, remoteMachine, opts)
}, },
} }
cmd.Flags().StringVarP(&opts.name, "name", "n", "", "Assign a name to the machine.") cmd.Flags().StringVar(&opts.dnsEndpoint, "dns-endpoint", dns.DefaultUncloudDNSAPIEndpoint,
"API endpoint for the Uncloud DNS service.")
cmd.Flags().StringVarP(
&opts.name, "name", "n", "",
"Assign a name to the machine.",
)
cmd.Flags().StringVar( cmd.Flags().StringVar(
&opts.network, "network", cluster.DefaultNetwork.String(), &opts.network, "network", cluster.DefaultNetwork.String(),
"IPv4 network CIDR to use for machines and services.", "IPv4 network CIDR to use for machines and services.",
) )
cmd.Flags().BoolVar(
&opts.noCaddy, "no-caddy", false,
"Don't deploy Caddy reverse proxy service to the machine.",
)
cmd.Flags().BoolVar(
&opts.noDNS, "no-dns", false,
"Don't reserve a cluster domain in Uncloud DNS.",
)
cmd.Flags().StringVar(
&opts.publicIP, "public-ip", "auto",
"Public IP address of the machine for ingress configuration. Use 'auto' for automatic detection, "+
"blank '' or 'none' to disable ingress on this machine, or specify an IP address.",
)
cmd.Flags().StringVarP( cmd.Flags().StringVarP(
&opts.sshKey, "ssh-key", "i", "", &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( cmd.Flags().StringVarP(
&opts.cluster, "cluster", "c", "", &opts.cluster, "cluster", "c", "",
@@ -63,3 +86,70 @@ func NewInitCommand() *cobra.Command {
return cmd return cmd
} }
func initCluster(ctx context.Context, uncli *cli.CLI, remoteMachine *cli.RemoteMachine, opts initOptions) error {
netPrefix, err := netip.ParsePrefix(opts.network)
if err != nil {
return fmt.Errorf("parse network CIDR: %w", err)
}
var publicIP *netip.Addr
switch opts.publicIP {
case "auto":
publicIP = &netip.Addr{}
case "", "none":
publicIP = nil
default:
ip, err := netip.ParseAddr(opts.publicIP)
if err != nil {
return fmt.Errorf("parse public IP: %w", err)
}
publicIP = &ip
}
client, err := uncli.InitCluster(ctx, remoteMachine, opts.cluster, opts.name, netPrefix, publicIP)
if err != nil {
return err
}
defer client.Close()
if opts.noCaddy && opts.noDNS {
return nil
}
// Deploy the Caddy service to the initialised machine.
// The creation of a deployment plan talks to cluster API. Since the API needs a few moments to become available
// after cluster initialisation, we keep the user informed during this wait.
fmt.Println("Waiting for the machine to be ready...")
fmt.Println()
if !opts.noDNS {
domain, err := client.ReserveDomain(ctx, &pb.ReserveDomainRequest{Endpoint: opts.dnsEndpoint})
if err != nil {
return fmt.Errorf("reserve cluster domain in Uncloud DNS: %w", err)
}
fmt.Printf("Reserved cluster domain: %s\n", domain.Name)
}
if !opts.noCaddy {
d, err := client.NewCaddyDeployment("", nil)
if err != nil {
return fmt.Errorf("create caddy deployment: %w", err)
}
err = progress.RunWithTitle(ctx, func(ctx context.Context) error {
if _, err = d.Run(ctx); err != nil {
return fmt.Errorf("deploy caddy: %w", err)
}
return nil
}, uncli.ProgressOut(), fmt.Sprintf("Deploying service %s", d.Spec.Name))
if err != nil {
return err
}
fmt.Println()
return caddy.UpdateDomainRecords(ctx, client, uncli.ProgressOut())
}
return nil
}
+12 -6
View File
@@ -8,9 +8,8 @@ import (
"os" "os"
"strings" "strings"
"text/tabwriter" "text/tabwriter"
"uncloud/internal/cli" "github.com/psviderski/uncloud/internal/cli"
"uncloud/internal/machine/network" "github.com/psviderski/uncloud/internal/machine/network"
"uncloud/internal/secret"
) )
func NewListCommand() *cobra.Command { func NewListCommand() *cobra.Command {
@@ -46,7 +45,7 @@ func list(ctx context.Context, uncli *cli.CLI, clusterName string) error {
// Print the list of machines in a table format. // Print the list of machines in a table format.
tw := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', 0) tw := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', 0)
// Print header. // Print header.
if _, err = fmt.Fprintln(tw, "NAME\tSTATE\tADDRESS\tPUBLIC KEY\tENDPOINTS"); err != nil { if _, err = fmt.Fprintln(tw, "NAME\tSTATE\tADDRESS\tPUBLIC IP\tWIREGUARD ENDPOINTS"); err != nil {
return fmt.Errorf("write header: %w", err) return fmt.Errorf("write header: %w", err)
} }
// Print rows. // Print rows.
@@ -54,14 +53,21 @@ func list(ctx context.Context, uncli *cli.CLI, clusterName string) error {
m := member.Machine m := member.Machine
subnet, _ := m.Network.Subnet.ToPrefix() subnet, _ := m.Network.Subnet.ToPrefix()
subnet = netip.PrefixFrom(network.MachineIP(subnet), subnet.Bits()) subnet = netip.PrefixFrom(network.MachineIP(subnet), subnet.Bits())
publicIP := "-"
if m.PublicIp != nil {
ip, _ := m.PublicIp.ToAddr()
publicIP = ip.String()
}
endpoints := make([]string, len(m.Network.Endpoints)) endpoints := make([]string, len(m.Network.Endpoints))
for i, ep := range m.Network.Endpoints { for i, ep := range m.Network.Endpoints {
addrPort, _ := ep.ToAddrPort() addrPort, _ := ep.ToAddrPort()
endpoints[i] = addrPort.String() endpoints[i] = addrPort.String()
} }
publicKey := secret.Secret(m.Network.PublicKey)
if _, err = fmt.Fprintf( if _, err = fmt.Fprintf(
tw, "%s\t%s\t%s\t%s\t%s\n", m.Name, capitalise(member.State.String()), subnet, publicKey, strings.Join(endpoints, ", "), tw, "%s\t%s\t%s\t%s\t%s\n", m.Name, capitalise(member.State.String()), subnet, publicIP, strings.Join(endpoints, ", "),
); err != nil { ); err != nil {
return fmt.Errorf("write row: %w", err) return fmt.Errorf("write row: %w", err)
} }
+2 -2
View File
@@ -3,8 +3,8 @@ package machine
import ( import (
"fmt" "fmt"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"uncloud/internal/daemon" "github.com/psviderski/uncloud/internal/daemon"
"uncloud/internal/machine" "github.com/psviderski/uncloud/internal/machine"
) )
type tokenOptions struct { type tokenOptions struct {
+46 -14
View File
@@ -3,31 +3,55 @@ package main
import ( import (
"context" "context"
"fmt" "fmt"
"github.com/spf13/cobra" "net/netip"
"os"
"strings" "strings"
"uncloud/cmd/uncloud/machine"
"uncloud/cmd/uncloud/service" "github.com/psviderski/uncloud/cmd/uncloud/caddy"
"uncloud/internal/cli" "github.com/psviderski/uncloud/cmd/uncloud/dns"
"github.com/psviderski/uncloud/cmd/uncloud/machine"
"github.com/psviderski/uncloud/cmd/uncloud/service"
"github.com/psviderski/uncloud/internal/cli"
"github.com/psviderski/uncloud/internal/cli/config"
"github.com/psviderski/uncloud/internal/fs"
"github.com/spf13/cobra"
) )
type globalOptions struct {
configPath string
connect string
}
func main() { func main() {
var configPath string opts := globalOptions{}
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "uncloud", Use: "uncloud",
Short: "A CLI tool for managing Uncloud resources such as clusters, machines, and services.", Short: "A CLI tool for managing Uncloud resources such as clusters, machines, and services.",
SilenceUsage: true, SilenceUsage: true,
SilenceErrors: true, SilenceErrors: true,
PersistentPreRunE: func(cmd *cobra.Command, args []string) error { PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
if strings.HasPrefix(configPath, "~/") { var conn *config.MachineConnection
home, err := os.UserHomeDir() if opts.connect != "" {
if err != nil { if strings.HasPrefix(opts.connect, "tcp://") {
return fmt.Errorf("get user home directory to resolve %q: %w", configPath, err) addrPort, err := netip.ParseAddrPort(opts.connect[len("tcp://"):])
if err != nil {
return fmt.Errorf("parse TCP address: %w", err)
}
conn = &config.MachineConnection{
TCP: addrPort,
}
} else {
dest := opts.connect
if strings.HasPrefix(dest, "ssh://") {
dest = dest[len("ssh://"):]
}
conn = &config.MachineConnection{
SSH: config.SSHDestination(dest),
}
} }
configPath = strings.Replace(configPath, "~", home, 1)
} }
uncli, err := cli.New(configPath) configPath := fs.ExpandHomeDir(opts.configPath)
uncli, err := cli.New(configPath, conn)
if err != nil { if err != nil {
return fmt.Errorf("initialize CLI: %w", err) return fmt.Errorf("initialize CLI: %w", err)
} }
@@ -35,18 +59,26 @@ func main() {
return nil return nil
}, },
} }
cmd.PersistentFlags().StringVar(&opts.connect, "connect", "",
"Connect to a remote cluster machine without using the Uncloud configuration file.\n"+
"Format: [ssh://]user@host[:port] or tcp://host:port")
// TODO: allow to override using UNCLOUD_CONFIG env var. // TODO: allow to override using UNCLOUD_CONFIG env var.
cmd.PersistentFlags().StringVar(&configPath, "uncloud-config", "~/.config/uncloud/config.toml", cmd.PersistentFlags().StringVar(&opts.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.MarkPersistentFlagFilename("uncloud-config", "toml")
cmd.AddCommand( cmd.AddCommand(
NewDeployCommand(),
caddy.NewRootCommand(),
dns.NewRootCommand(),
machine.NewRootCommand(), machine.NewRootCommand(),
service.NewRootCommand(), service.NewRootCommand(),
service.NewInspectCommand(), service.NewInspectCommand(),
service.NewListCommand(), service.NewListCommand(),
service.NewRmCommand(), service.NewRmCommand(),
service.NewRunCommand(), service.NewRunCommand(),
service.NewScaleCommand(),
) )
cobra.CheckErr(cmd.Execute()) cobra.CheckErr(cmd.Execute())
} }
+11 -4
View File
@@ -10,7 +10,7 @@ import (
"time" "time"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"uncloud/internal/cli" "github.com/psviderski/uncloud/internal/cli"
) )
type inspectOptions struct { type inspectOptions struct {
@@ -70,21 +70,28 @@ func inspect(ctx context.Context, uncli *cli.CLI, opts inspectOptions) error {
} }
for _, ctr := range svc.Containers { for _, ctr := range svc.Containers {
createdAt := time.Unix(ctr.Container.Created, 0) createdAt, err := time.Parse(time.RFC3339Nano, ctr.Container.Created)
if err != nil {
return fmt.Errorf("parse created time: %w", err)
}
created := units.HumanDuration(time.Now().UTC().Sub(createdAt)) + " ago" created := units.HumanDuration(time.Now().UTC().Sub(createdAt)) + " ago"
machine := machinesNamesByID[ctr.MachineID] machine := machinesNamesByID[ctr.MachineID]
if machine == "" { if machine == "" {
machine = ctr.MachineID machine = ctr.MachineID
} }
state, err := ctr.Container.HumanState()
if err != nil {
return fmt.Errorf("get human state: %w", err)
}
_, err = fmt.Fprintf( _, err = fmt.Fprintf(
tw, tw,
"%s\t%s\t%s\t%s\t%s\n", "%s\t%s\t%s\t%s\t%s\n",
stringid.TruncateID(ctr.Container.ID), stringid.TruncateID(ctr.Container.ID),
ctr.Container.Image, ctr.Container.Config.Image,
created, created,
ctr.Container.Status, state,
machine, machine,
) )
if err != nil { if err != nil {
+29 -3
View File
@@ -3,10 +3,11 @@ package service
import ( import (
"context" "context"
"fmt" "fmt"
"github.com/psviderski/uncloud/internal/cli"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"os" "os"
"strings"
"text/tabwriter" "text/tabwriter"
"uncloud/internal/cli"
) )
func NewListCommand() *cobra.Command { func NewListCommand() *cobra.Command {
@@ -39,13 +40,38 @@ func list(ctx context.Context, uncli *cli.CLI, clusterName string) error {
return fmt.Errorf("list services: %w", err) return fmt.Errorf("list services: %w", err)
} }
serviceNames := make(map[string]struct{}, len(services))
haveDuplicateNames := false
for _, svc := range services {
if _, exists := serviceNames[svc.Name]; exists {
haveDuplicateNames = true
break
}
serviceNames[svc.Name] = struct{}{}
}
// Print the list of services in a table format. // Print the list of services in a table format.
tw := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', 0) tw := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', 0)
if _, err = fmt.Fprintln(tw, "SERVICE ID\tNAME\tMODE\tREPLICAS"); err != nil {
// Include the ID column if there are duplicate service names to differentiate them.
if haveDuplicateNames {
if _, err = fmt.Fprintf(tw, "ID\t"); err != nil {
return fmt.Errorf("write header: %w", err)
}
}
if _, err = fmt.Fprintln(tw, "NAME\tMODE\tREPLICAS\tENDPOINTS"); err != nil {
return fmt.Errorf("write header: %w", err) return fmt.Errorf("write header: %w", err)
} }
for _, s := range services { 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 { endpointsSlice := s.Endpoints()
endpoints := strings.Join(endpointsSlice, ", ")
if haveDuplicateNames {
if _, err = fmt.Fprintf(tw, "%s\t", s.ID); err != nil {
return fmt.Errorf("write row: %w", err)
}
}
if _, err = fmt.Fprintf(tw, "%s\t%s\t%d\t%s\n", s.Name, s.Mode, len(s.Containers), endpoints); err != nil {
return fmt.Errorf("write row: %w", err) return fmt.Errorf("write row: %w", err)
} }
} }
+10 -5
View File
@@ -3,8 +3,11 @@ package service
import ( import (
"context" "context"
"fmt" "fmt"
"github.com/docker/cli/cli/streams"
"github.com/docker/compose/v2/pkg/progress"
"github.com/psviderski/uncloud/internal/cli"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"uncloud/internal/cli" "os"
) )
type rmOptions struct { type rmOptions struct {
@@ -40,10 +43,12 @@ func rm(ctx context.Context, uncli *cli.CLI, opts rmOptions) error {
defer client.Close() defer client.Close()
for _, s := range opts.services { for _, s := range opts.services {
if err = client.RemoveService(ctx, s); err != nil { err = progress.RunWithTitle(ctx, func(ctx context.Context) error {
return fmt.Errorf("remove service %q: %w", s, err) if err = client.RemoveService(ctx, s); err != nil {
} return fmt.Errorf("remove service '%s': %w", s, err)
fmt.Printf("Service %q removed.\n", s) }
return nil
}, streams.NewOut(os.Stdout), "Removing service "+s)
} }
return nil return nil
+2
View File
@@ -10,9 +10,11 @@ func NewRootCommand() *cobra.Command {
Short: "Manage services in an Uncloud cluster.", Short: "Manage services in an Uncloud cluster.",
} }
cmd.AddCommand( cmd.AddCommand(
NewInspectCommand(),
NewListCommand(), NewListCommand(),
NewRmCommand(), NewRmCommand(),
NewRunCommand(), NewRunCommand(),
NewScaleCommand(),
) )
return cmd return cmd
} }
+98 -28
View File
@@ -3,19 +3,27 @@ package service
import ( import (
"context" "context"
"fmt" "fmt"
"github.com/psviderski/uncloud/internal/cli"
"github.com/psviderski/uncloud/internal/machine/api/pb"
"github.com/psviderski/uncloud/pkg/api"
"github.com/psviderski/uncloud/pkg/client/deploy"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"uncloud/internal/api" "slices"
"uncloud/internal/cli" "strings"
) )
type runOptions struct { type runOptions struct {
command []string command []string
image string entrypoint string
machine string entrypointChanged bool
mode string image string
name string machines []string
publish []string mode string
volumes []string name string
publish []string
pull string
replicas uint
volumes []string
cluster string cluster string
} }
@@ -30,6 +38,7 @@ func NewRunCommand() *cobra.Command {
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
uncli := cmd.Context().Value("cli").(*cli.CLI) uncli := cmd.Context().Value("cli").(*cli.CLI)
opts.entrypointChanged = cmd.Flag("entrypoint").Changed
opts.image = args[0] opts.image = args[0]
if len(args) > 1 { if len(args) > 1 {
opts.command = args[1:] opts.command = args[1:]
@@ -39,25 +48,32 @@ func NewRunCommand() *cobra.Command {
}, },
} }
// TODO: implement placement constraints and translate --machine to a constraint. cmd.Flags().StringVar(&opts.entrypoint, "entrypoint", "",
//cmd.Flags().StringVarP( "Overwrite the default ENTRYPOINT of the image. Pass an empty string \"\" to reset it.")
// &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, cmd.Flags().StringVar(&opts.mode, "mode", api.ServiceModeReplicated,
fmt.Sprintf("Replication mode of the service: either %q (a specified number of containers across "+ fmt.Sprintf("Replication mode of the service: either '%s' (a specified number of containers across "+
"the machines) or %q (one container on every machine).", "the machines) or '%s' (one container on every machine).",
api.ServiceModeReplicated, api.ServiceModeGlobal)) api.ServiceModeReplicated, api.ServiceModeGlobal))
cmd.Flags().StringSliceVarP(&opts.machines, "machine", "m", nil,
"Placement constraint by machine name, limiting which machines the service can run on. Can be specified "+
"multiple times or as a comma-separated list of machine names. (default is any suitable machine)")
cmd.Flags().StringVarP(&opts.name, "name", "n", "", cmd.Flags().StringVarP(&opts.name, "name", "n", "",
"Assign a name to the service. A random name is generated if not specified.") "Assign a name to the service. A random name is generated if not specified.")
cmd.Flags().StringSliceVarP(&opts.publish, "publish", "p", nil, cmd.Flags().StringSliceVarP(&opts.publish, "publish", "p", nil,
"Publish a service port to make it accessible outside the cluster. Can be specified multiple times.\n"+ "Publish a service port to make it accessible outside the cluster. Can be specified multiple times.\n"+
"Format: [hostname:][load_balancer_port:]container_port[/protocol] or [host_ip:]:host_port:container_port[/protocol]@host\n"+ "Format: [hostname:][load_balancer_port:]container_port[/protocol] or [host_ip:]:host_port:container_port[/protocol]@host\n"+
"Supported protocols: tcp, udp, http, https (default is tcp). If a hostname for http(s) port is not specified, a random hostname is generated.\n"+ "Supported protocols: tcp, udp, http, https (default is tcp). If a hostname for http(s) port is not specified,\n"+
"service-name.cluster-domain will be used as the hostname.\n"+
"Examples:\n"+ "Examples:\n"+
" -p 8080/https Publish port 8080 as HTTPS via load balancer with default service-name.cluster-domain hostname\n"+
" -p app.example.com:8080/https Publish port 8080 as HTTPS via load balancer with custom hostname\n"+ " -p app.example.com:8080/https Publish port 8080 as HTTPS via load balancer with custom hostname\n"+
" -p 9000:8080 Publish port 8080 as TCP port 9000 via load balancer\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") " -p 53:5353/udp@host Bind UDP port 5353 to host port 53")
cmd.Flags().StringVar(&opts.pull, "pull", api.PullPolicyMissing,
fmt.Sprintf("Pull image from the registry before running service containers ('%s', '%s', '%s').",
api.PullPolicyAlways, api.PullPolicyMissing, api.PullPolicyNever))
cmd.Flags().UintVar(&opts.replicas, "replicas", 1,
"Number of containers to run for the service. Only valid for a replicated service.")
cmd.Flags().StringSliceVarP(&opts.volumes, "volume", "v", nil, cmd.Flags().StringSliceVarP(&opts.volumes, "volume", "v", nil,
"Bind mount a host file or directory into a service container using the format "+ "Bind mount a host file or directory into a service container using the format "+
"/host/path:/container/path[:ro]. Can be specified multiple times.") "/host/path:/container/path[:ro]. Can be specified multiple times.")
@@ -72,9 +88,38 @@ func NewRunCommand() *cobra.Command {
func run(ctx context.Context, uncli *cli.CLI, opts runOptions) error { func run(ctx context.Context, uncli *cli.CLI, opts runOptions) error {
switch opts.mode { switch opts.mode {
case "", api.ServiceModeReplicated, api.ServiceModeGlobal: case api.ServiceModeReplicated, api.ServiceModeGlobal:
default: default:
return fmt.Errorf("invalid replication mode: %q", opts.mode) return fmt.Errorf("invalid replication mode: '%s'", opts.mode)
}
switch opts.pull {
case api.PullPolicyAlways, api.PullPolicyMissing, api.PullPolicyNever:
default:
return fmt.Errorf("invalid pull policy: '%s'", opts.pull)
}
var machineFilter deploy.MachineFilter
if len(opts.machines) > 0 {
var machines []string
for _, value := range opts.machines {
if value == "" {
continue
}
mlist := strings.Split(value, ",")
for _, m := range mlist {
if m = strings.TrimSpace(m); m != "" {
machines = append(machines, m)
}
}
}
if len(machines) > 0 {
machineFilter = func(m *pb.MachineInfo) bool {
return slices.Contains(machines, m.Name)
}
}
} }
ports := make([]api.PortSpec, len(opts.publish)) ports := make([]api.PortSpec, len(opts.publish))
@@ -89,27 +134,52 @@ func run(ctx context.Context, uncli *cli.CLI, opts runOptions) error {
spec := api.ServiceSpec{ spec := api.ServiceSpec{
Container: api.ContainerSpec{ Container: api.ContainerSpec{
Command: opts.command, Command: opts.command,
Image: opts.image, Image: opts.image,
Volumes: opts.volumes, PullPolicy: opts.pull,
Volumes: opts.volumes,
}, },
Mode: opts.mode, Mode: opts.mode,
Name: opts.name, Name: opts.name,
Ports: ports, Ports: ports,
Replicas: opts.replicas,
} }
// Overwrite the default ENTRYPOINT of the image or reset it if an empty string is passed.
if opts.entrypoint != "" {
spec.Container.Entrypoint = []string{opts.entrypoint}
} else if opts.entrypointChanged {
spec.Container.Entrypoint = []string{""}
}
if err := spec.Validate(); err != nil { if err := spec.Validate(); err != nil {
return fmt.Errorf("invalid service configuration: %w", err) return fmt.Errorf("invalid service configuration: %w", err)
} }
client, err := uncli.ConnectCluster(ctx, opts.cluster) clusterClient, err := uncli.ConnectCluster(ctx, opts.cluster)
if err != nil { if err != nil {
return fmt.Errorf("connect to cluster: %w", err) return fmt.Errorf("connect to cluster: %w", err)
} }
defer client.Close() defer clusterClient.Close()
if _, err = client.RunService(ctx, spec); err != nil { resp, err := clusterClient.RunService(ctx, spec, machineFilter)
if err != nil {
return fmt.Errorf("run service: %w", err) return fmt.Errorf("run service: %w", err)
} }
svc, err := clusterClient.InspectService(ctx, resp.ID)
if err != nil {
return fmt.Errorf("inspect service: %w", err)
}
endpoints := svc.Endpoints()
if len(endpoints) > 0 {
fmt.Println()
fmt.Printf("%s endpoints:\n", svc.Name)
for _, endpoint := range endpoints {
fmt.Printf(" • %s\n", endpoint)
}
}
return nil return nil
} }
+152
View File
@@ -0,0 +1,152 @@
package service
import (
"context"
"fmt"
"strconv"
"github.com/charmbracelet/huh"
"github.com/docker/compose/v2/pkg/progress"
"github.com/psviderski/uncloud/internal/cli"
"github.com/psviderski/uncloud/pkg/api"
"github.com/spf13/cobra"
)
type scaleOptions struct {
service string
replicas uint
cluster string
}
func NewScaleCommand() *cobra.Command {
opts := scaleOptions{}
cmd := &cobra.Command{
Use: "scale SERVICE REPLICAS",
Short: "Scale a replicated service by changing the number of replicas.",
Long: "Scale a replicated service by changing the number of replicas. Scaling down requires confirmation.",
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
uncli := cmd.Context().Value("cli").(*cli.CLI)
opts.service = args[0]
replicas, err := strconv.ParseUint(args[1], 10, 0)
if err != nil {
return fmt.Errorf("invalid number of replicas: %w", err)
}
opts.replicas = uint(replicas)
return scale(cmd.Context(), uncli, opts)
},
}
cmd.Flags().StringVarP(
&opts.cluster, "cluster", "c", "",
"Name of the cluster. (default is the current cluster)",
)
return cmd
}
func scale(ctx context.Context, uncli *cli.CLI, opts scaleOptions) error {
if opts.replicas == 0 {
return fmt.Errorf(
"scaling to zero replicas is not supported. This would effectively remove the service without preserving "+
"its configuration, making it impossible to scale back up. Uncloud derives the service configuration "+
"from existing containers. Use 'uc rm %s' instead if you want to remove the service",
opts.service,
)
}
clusterClient, err := uncli.ConnectCluster(ctx, opts.cluster)
if err != nil {
return fmt.Errorf("connect to cluster: %w", err)
}
defer clusterClient.Close()
svc, err := clusterClient.InspectService(ctx, opts.service)
if err != nil {
return fmt.Errorf("inspect service '%s': %w", opts.service, err)
}
if svc.Mode != api.ServiceModeReplicated {
return fmt.Errorf("scaling is only supported for services in %s mode, service '%s' is in %s mode",
api.ServiceModeReplicated, svc.Name, svc.Mode)
}
currentReplicas := uint(len(svc.Containers))
if currentReplicas == opts.replicas {
fmt.Printf("Service '%s' already has %d replicas. No changes required.\n", svc.Name, currentReplicas)
return nil
}
// TODO: Check if all containers have the same spec. If not, prompt user to choose which one to scale.
// This can happen if a service deployment failed midway and some containers were not updated.
spec := svc.Containers[0].Container.ServiceSpec
spec.Replicas = opts.replicas
deployment := clusterClient.NewDeployment(spec, nil)
plan, err := deployment.Plan(ctx)
if err != nil {
return fmt.Errorf("plan deployment: %w", err)
}
if len(plan.Operations) == 0 {
fmt.Printf("Service '%s' is already scaled to %d replicas.\n", svc.Name, opts.replicas)
return nil
}
if opts.replicas < currentReplicas {
// Initialise a machine and container name resolver to properly format the plan output.
resolver, err := clusterClient.ServiceOperationNameResolver(ctx, svc)
if err != nil {
return fmt.Errorf("create machine and container name resolver for service operations: %w", err)
}
fmt.Printf("Scaling plan for service %s (%d → %d replicas):\n", svc.Name, currentReplicas, opts.replicas)
fmt.Println(plan.Format(resolver))
fmt.Println()
// Ask for confirmation before scaling down as it may cause data loss.
confirmed, err := confirm()
if err != nil {
return fmt.Errorf("confirm scaling: %w", err)
}
if !confirmed {
fmt.Println("Cancelled. No changes were made.")
return nil
}
}
title := fmt.Sprintf("Scaling service %s (%d → %d replicas)", svc.Name, currentReplicas, opts.replicas)
err = progress.RunWithTitle(ctx, func(ctx context.Context) error {
if _, err = deployment.Run(ctx); err != nil {
return fmt.Errorf("deploy service: %w", err)
}
return nil
}, uncli.ProgressOut(), title)
if err != nil {
return err
}
return nil
}
func confirm() (bool, error) {
var confirmed bool
form := huh.NewForm(
huh.NewGroup(
huh.NewConfirm().
Title(
"Do you want to continue?",
).
Affirmative("Yes!").
Negative("No").
Value(&confirmed),
),
)
if err := form.Run(); err != nil {
return false, err
}
return confirmed, nil
}
+3 -3
View File
@@ -7,9 +7,9 @@ import (
"os" "os"
"os/signal" "os/signal"
"syscall" "syscall"
"uncloud/internal/daemon" "github.com/psviderski/uncloud/internal/daemon"
"uncloud/internal/log" "github.com/psviderski/uncloud/internal/log"
"uncloud/internal/machine" "github.com/psviderski/uncloud/internal/machine"
) )
func main() { func main() {
+59
View File
@@ -0,0 +1,59 @@
# User Guide
## Initialising a cluster
To begin setting up a new Uncloud cluster, create the desired nodes. Ensure that their firewall allows the required ports.
### Ports
Nodes running Uncloud with a standard configuration must have the following inbound ports allowed in the firewall:
* 51820/udp (WireGuard meshnet)
* 22/tcp (default SSH management port)
* 80/tcp (required for challenge if uncloud.run DNS is enabled, pass --no-dns during init to disable)
In addition, any ports for any workloads you want to expose will need to be open.
### Configuration
Uncloud stores its configuration in `~/.config/uncloud/config.toml`. If you wish to reinitialise a cluster, simply remove it from this config.
### Initialisation
Begin by initialising the first node in your cluster with `uc machine init [USER@HOST:PORT]`. If you do not have a need for Caddy reverse proxy, you may disable this feature with `--no-caddy`.
This command will idempotently install Docker, uncloudd, uncloud-corrosion. If Caddy is enabled, it will set up a reverse proxy. If Uncloud DNS is enabled, it will create a DNS A record for the machine's public IP address under `*.[CLUSTER ID].cluster.uncloud.run`.
If you wish to uninstall Uncloud and its components, run `uncloud-uninstall`.
### Adding a node
Just like the initialisation of the first node, a node can be added to the cluster with `uc machine add`.
### DNS
Uncloud (uncloud.run) DNS can be managed with the `uc dns` subcommand.
* To reserve a domain name, run `uc dns reserve`
* To release a domain name, run `uc dns release`.
* To see the domain name, run `uc dns show`
### Running a service
Services on an Uncloud cluster can be managed with `uc service`.
You can run a service with two replicas that expose port 80 like the following:
```
uc service run -p 80/http --replicas 2 nginxdemos/hello
```
This requires the Caddy reverse proxy to be deployed. If it wasn't previously, it can be deployed with `uc caddy deploy`.
Since this doesn't specify a service name, a random one will be generated.
The service can be deleted by its service name:
```
uc service rm hello-gsdo
```
+1 -1
View File
@@ -11,7 +11,7 @@ import (
"go.uber.org/zap" "go.uber.org/zap"
"net/netip" "net/netip"
"time" "time"
"uncloud/internal/machine/network" "github.com/psviderski/uncloud/internal/machine/network"
) )
const ( const (
+44 -25
View File
@@ -1,23 +1,27 @@
module uncloud module github.com/psviderski/uncloud
go 1.23 go 1.23.0
toolchain go1.23.2 toolchain go1.23.2
require ( require (
github.com/BurntSushi/toml v1.4.0 github.com/BurntSushi/toml v1.4.0
github.com/Masterminds/semver v1.5.0
github.com/Masterminds/squirrel v1.5.4 github.com/Masterminds/squirrel v1.5.4
github.com/caddyserver/caddy/v2 v2.8.4 github.com/caddyserver/caddy/v2 v2.8.4
github.com/cenkalti/backoff/v4 v4.3.0 github.com/cenkalti/backoff/v4 v4.3.0
github.com/charmbracelet/huh v0.6.0 github.com/charmbracelet/huh v0.6.0
github.com/compose-spec/compose-go/v2 v2.4.5
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf
github.com/deckarep/golang-set/v2 v2.8.0
github.com/dgraph-io/badger/v3 v3.2103.5 github.com/dgraph-io/badger/v3 v3.2103.5
github.com/distribution/reference v0.6.0 github.com/distribution/reference v0.6.0
github.com/docker/cli v27.4.0-rc.4+incompatible github.com/docker/cli v27.5.0+incompatible
github.com/docker/compose/v2 v2.31.0 github.com/docker/compose/v2 v2.31.0
github.com/docker/docker v27.4.0-rc.2+incompatible github.com/docker/docker v27.4.0-rc.2+incompatible
github.com/docker/go-connections v0.5.0 github.com/docker/go-connections v0.5.0
github.com/docker/go-units v0.5.0 github.com/docker/go-units v0.5.0
github.com/google/go-containerregistry v0.20.2
github.com/hashicorp/memberlist v0.5.1 github.com/hashicorp/memberlist v0.5.1
github.com/hashicorp/serf v0.10.1 github.com/hashicorp/serf v0.10.1
github.com/ipfs/boxo v0.21.0 github.com/ipfs/boxo v0.21.0
@@ -27,7 +31,9 @@ require (
github.com/ipfs/go-ds-crdt v0.5.2 github.com/ipfs/go-ds-crdt v0.5.2
github.com/ipfs/go-ipld-format v0.6.0 github.com/ipfs/go-ipld-format v0.6.0
github.com/ipfs/go-log/v2 v2.5.1 github.com/ipfs/go-log/v2 v2.5.1
github.com/jmoiron/sqlx v1.4.0
github.com/lmittmann/tint v1.0.5 github.com/lmittmann/tint v1.0.5
github.com/opencontainers/go-digest v1.0.0
github.com/opencontainers/image-spec v1.1.0 github.com/opencontainers/image-spec v1.1.0
github.com/siderolabs/discovery-api v0.1.4 github.com/siderolabs/discovery-api v0.1.4
github.com/siderolabs/discovery-client v0.1.9 github.com/siderolabs/discovery-client v0.1.9
@@ -37,15 +43,16 @@ require (
github.com/vishvananda/netlink v1.3.0 github.com/vishvananda/netlink v1.3.0
go.uber.org/zap v1.27.0 go.uber.org/zap v1.27.0
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba go4.org/netipx v0.0.0-20231129151722-fdeea329fbba
golang.org/x/crypto v0.31.0 golang.org/x/crypto v0.32.0
golang.org/x/net v0.32.0 golang.org/x/net v0.34.0
golang.org/x/sync v0.10.0 golang.org/x/sync v0.10.0
golang.org/x/sys v0.28.0 golang.org/x/sys v0.31.0
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6 golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6
google.golang.org/genproto/googleapis/rpc v0.0.0-20241007155032-5fefd90f89a9 google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576
google.golang.org/grpc v1.68.0 google.golang.org/grpc v1.68.1
google.golang.org/protobuf v1.36.0 google.golang.org/protobuf v1.36.3
modernc.org/sqlite v1.36.3
) )
require ( require (
@@ -57,6 +64,7 @@ require (
github.com/Masterminds/semver/v3 v3.2.1 // indirect github.com/Masterminds/semver/v3 v3.2.1 // indirect
github.com/Masterminds/sprig/v3 v3.2.3 // indirect github.com/Masterminds/sprig/v3 v3.2.3 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/OneOfOne/xxhash v1.2.8 // indirect
github.com/antlr4-go/antlr/v4 v4.13.0 // indirect github.com/antlr4-go/antlr/v4 v4.13.0 // indirect
github.com/armon/circbuf v0.0.0-20190214190532-5111143e8da2 // indirect github.com/armon/circbuf v0.0.0-20190214190532-5111143e8da2 // indirect
github.com/armon/go-metrics v0.4.1 // indirect github.com/armon/go-metrics v0.4.1 // indirect
@@ -79,7 +87,7 @@ require (
github.com/charmbracelet/x/exp/strings v0.0.0-20240919170804-a4978c8e603a // indirect github.com/charmbracelet/x/exp/strings v0.0.0-20240919170804-a4978c8e603a // indirect
github.com/charmbracelet/x/term v0.2.0 // indirect github.com/charmbracelet/x/term v0.2.0 // indirect
github.com/chzyer/readline v1.5.1 // indirect github.com/chzyer/readline v1.5.1 // indirect
github.com/compose-spec/compose-go/v2 v2.4.5 // indirect github.com/cloudflare/cfssl v1.6.4 // indirect
github.com/containerd/console v1.0.4 // indirect github.com/containerd/console v1.0.4 // indirect
github.com/containerd/containerd v1.7.24 // indirect github.com/containerd/containerd v1.7.24 // indirect
github.com/containerd/containerd/api v1.7.19 // indirect github.com/containerd/containerd/api v1.7.19 // indirect
@@ -87,9 +95,10 @@ require (
github.com/containerd/errdefs v0.3.0 // indirect github.com/containerd/errdefs v0.3.0 // indirect
github.com/containerd/log v0.1.0 // indirect github.com/containerd/log v0.1.0 // indirect
github.com/containerd/platforms v0.2.1 // indirect github.com/containerd/platforms v0.2.1 // indirect
github.com/containerd/stargz-snapshotter/estargz v0.16.3 // indirect
github.com/containerd/ttrpc v1.2.5 // indirect github.com/containerd/ttrpc v1.2.5 // indirect
github.com/containerd/typeurl/v2 v2.2.0 // indirect github.com/containerd/typeurl/v2 v2.2.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // 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 v1.6.2 // indirect
@@ -113,7 +122,7 @@ require (
github.com/go-logfmt/logfmt v0.6.0 // indirect github.com/go-logfmt/logfmt v0.6.0 // indirect
github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-sql-driver/mysql v1.7.1 // indirect github.com/go-sql-driver/mysql v1.8.1 // indirect
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
github.com/go-viper/mapstructure/v2 v2.0.0 // indirect github.com/go-viper/mapstructure/v2 v2.0.0 // indirect
github.com/godbus/dbus/v5 v5.1.0 // indirect github.com/godbus/dbus/v5 v5.1.0 // indirect
@@ -127,11 +136,11 @@ require (
github.com/google/cel-go v0.20.1 // indirect github.com/google/cel-go v0.20.1 // indirect
github.com/google/flatbuffers v24.3.25+incompatible // indirect github.com/google/flatbuffers v24.3.25+incompatible // indirect
github.com/google/go-cmp v0.6.0 // indirect github.com/google/go-cmp v0.6.0 // indirect
github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad // indirect github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/google/uuid v1.6.0 // indirect github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/mux v1.8.1 // indirect github.com/gorilla/mux v1.8.1 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-immutable-radix v1.3.1 // indirect github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
github.com/hashicorp/go-msgpack v0.5.5 // indirect github.com/hashicorp/go-msgpack v0.5.5 // indirect
@@ -192,6 +201,7 @@ require (
github.com/minio/sha256-simd v1.0.1 // indirect github.com/minio/sha256-simd v1.0.1 // indirect
github.com/mitchellh/cli v1.1.5 // indirect github.com/mitchellh/cli v1.1.5 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/go-ps v1.0.0 // indirect github.com/mitchellh/go-ps v1.0.0 // indirect
github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect
@@ -218,8 +228,8 @@ require (
github.com/multiformats/go-multistream v0.5.0 // indirect github.com/multiformats/go-multistream v0.5.0 // indirect
github.com/multiformats/go-varint v0.0.7 // indirect github.com/multiformats/go-varint v0.0.7 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/onsi/ginkgo/v2 v2.22.0 // indirect github.com/onsi/ginkgo/v2 v2.22.0 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/pelletier/go-toml v1.9.5 // indirect github.com/pelletier/go-toml v1.9.5 // indirect
github.com/pires/go-proxyproto v0.7.0 // indirect github.com/pires/go-proxyproto v0.7.0 // indirect
github.com/pkg/errors v0.9.1 // indirect github.com/pkg/errors v0.9.1 // indirect
@@ -233,6 +243,7 @@ require (
github.com/prometheus/procfs v0.15.1 // indirect github.com/prometheus/procfs v0.15.1 // indirect
github.com/quic-go/qpack v0.5.1 // indirect github.com/quic-go/qpack v0.5.1 // indirect
github.com/quic-go/quic-go v0.48.2 // indirect github.com/quic-go/quic-go v0.48.2 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rivo/uniseg v0.4.7 // indirect github.com/rivo/uniseg v0.4.7 // indirect
github.com/rs/xid v1.5.0 // indirect github.com/rs/xid v1.5.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect
@@ -261,22 +272,27 @@ require (
github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea // indirect github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea // indirect
github.com/tonistiigi/vt100 v0.0.0-20240514184818-90bafcd6abab // indirect github.com/tonistiigi/vt100 v0.0.0-20240514184818-90bafcd6abab // indirect
github.com/urfave/cli v1.22.16 // indirect github.com/urfave/cli v1.22.16 // indirect
github.com/vbatts/tar-split v0.11.6 // indirect
github.com/vishvananda/netns v0.0.4 // indirect github.com/vishvananda/netns v0.0.4 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
github.com/zeebo/blake3 v0.2.4 // indirect github.com/zeebo/blake3 v0.2.4 // indirect
go.etcd.io/bbolt v1.3.11 // indirect go.etcd.io/bbolt v1.3.11 // indirect
go.opencensus.io v0.24.0 // indirect go.opencensus.io v0.24.0 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.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/httptrace/otelhttptrace v0.46.1 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.52.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 // indirect
go.opentelemetry.io/otel v1.32.0 // indirect go.opentelemetry.io/otel v1.33.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.44.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.44.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.27.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0 // indirect
go.opentelemetry.io/otel/metric v1.32.0 // indirect go.opentelemetry.io/otel/metric v1.33.0 // indirect
go.opentelemetry.io/otel/sdk v1.32.0 // indirect go.opentelemetry.io/otel/sdk v1.33.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.32.0 // indirect go.opentelemetry.io/otel/sdk/metric v1.32.0 // indirect
go.opentelemetry.io/otel/trace v1.32.0 // indirect go.opentelemetry.io/otel/trace v1.33.0 // indirect
go.opentelemetry.io/proto/otlp v1.3.1 // indirect go.opentelemetry.io/proto/otlp v1.4.0 // indirect
go.step.sm/cli-utils v0.9.0 // indirect go.step.sm/cli-utils v0.9.0 // indirect
go.step.sm/crypto v0.45.0 // indirect go.step.sm/crypto v0.45.0 // indirect
go.step.sm/linkedca v0.20.1 // indirect go.step.sm/linkedca v0.20.1 // indirect
@@ -287,15 +303,18 @@ require (
golang.org/x/crypto/x509roots/fallback v0.0.0-20240507223354-67b13616a595 // indirect golang.org/x/crypto/x509roots/fallback v0.0.0-20240507223354-67b13616a595 // indirect
golang.org/x/exp v0.0.0-20241215155358-4a5509556b9e // indirect golang.org/x/exp v0.0.0-20241215155358-4a5509556b9e // indirect
golang.org/x/mod v0.22.0 // indirect golang.org/x/mod v0.22.0 // indirect
golang.org/x/term v0.27.0 // indirect golang.org/x/term v0.28.0 // indirect
golang.org/x/text v0.21.0 // indirect golang.org/x/text v0.21.0 // indirect
golang.org/x/time v0.8.0 // indirect golang.org/x/time v0.8.0 // indirect
golang.org/x/tools v0.28.0 // indirect golang.org/x/tools v0.29.0 // indirect
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
google.golang.org/genproto v0.0.0-20240401170217-c3f982113cda // indirect google.golang.org/genproto v0.0.0-20240401170217-c3f982113cda // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259 // indirect gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259 // indirect
howett.net/plist v1.0.0 // indirect howett.net/plist v1.0.0 // indirect
lukechampine.com/blake3 v1.3.0 // indirect lukechampine.com/blake3 v1.3.0 // indirect
modernc.org/libc v1.61.13 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.8.2 // indirect
) )
+108 -51
View File
@@ -31,6 +31,8 @@ github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ
github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww=
github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0= github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0=
@@ -45,8 +47,9 @@ github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERo
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/Microsoft/hcsshim v0.12.5 h1:bpTInLlDy/nDRWFVcefDZZ1+U8tS+rz3MxjKgu9boo0= github.com/Microsoft/hcsshim v0.12.5 h1:bpTInLlDy/nDRWFVcefDZZ1+U8tS+rz3MxjKgu9boo0=
github.com/Microsoft/hcsshim v0.12.5/go.mod h1:tIUGego4G1EN5Hb6KC90aDYiUI2dqLSTTOCjVNpOgZ8= github.com/Microsoft/hcsshim v0.12.5/go.mod h1:tIUGego4G1EN5Hb6KC90aDYiUI2dqLSTTOCjVNpOgZ8=
github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/OneOfOne/xxhash v1.2.8 h1:31czK/TI9sNkxIKfaUfGlU47BAxQ0ztGgd9vPyqimf8=
github.com/OneOfOne/xxhash v1.2.8/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q=
github.com/Shopify/logrus-bugsnag v0.0.0-20170309145241-6dbc35f2c30d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ= github.com/Shopify/logrus-bugsnag v0.0.0-20170309145241-6dbc35f2c30d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ=
github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d h1:UrqY+r/OJnIp5u0s1SbQ8dVfLCZJsnvazdBP5hS4iRs= github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d h1:UrqY+r/OJnIp5u0s1SbQ8dVfLCZJsnvazdBP5hS4iRs=
github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ= github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ=
@@ -166,8 +169,9 @@ github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38
github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag=
github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cloudflare/cfssl v0.0.0-20180223231731-4e2dcbde5004 h1:lkAMpLVBDaj17e85keuznYcH5rqI438v41pKcBl4ZxQ=
github.com/cloudflare/cfssl v0.0.0-20180223231731-4e2dcbde5004/go.mod h1:yMWuSON2oQp+43nFtAV/uvKQIFpSPerB57DCt9t8sSA= github.com/cloudflare/cfssl v0.0.0-20180223231731-4e2dcbde5004/go.mod h1:yMWuSON2oQp+43nFtAV/uvKQIFpSPerB57DCt9t8sSA=
github.com/cloudflare/cfssl v1.6.4 h1:NMOvfrEjFfC63K3SGXgAnFdsgkmiq4kATme5BfcqrO8=
github.com/cloudflare/cfssl v1.6.4/go.mod h1:8b3CQMxfWPAeom3zBnGJ6sd+G1NkL5TXqmDXacb+1J0=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I=
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
@@ -197,9 +201,8 @@ github.com/containerd/nydus-snapshotter v0.14.0 h1:6/eAi6d7MjaeLLuMO8Udfe5GVsDud
github.com/containerd/nydus-snapshotter v0.14.0/go.mod h1:TT4jv2SnIDxEBu4H2YOvWQHPOap031ydTaHTuvc5VQk= github.com/containerd/nydus-snapshotter v0.14.0/go.mod h1:TT4jv2SnIDxEBu4H2YOvWQHPOap031ydTaHTuvc5VQk=
github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A=
github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw=
github.com/containerd/stargz-snapshotter v0.15.1 h1:fpsP4kf/Z4n2EYnU0WT8ZCE3eiKDwikDhL6VwxIlgeA= github.com/containerd/stargz-snapshotter/estargz v0.16.3 h1:7evrXtoh1mSbGj/pfRccTampEyKpjpOnS3CyiV1Ebr8=
github.com/containerd/stargz-snapshotter/estargz v0.15.1 h1:eXJjw9RbkLFgioVaTG+G/ZW/0kEe2oEKCdS/ZxIyoCU= github.com/containerd/stargz-snapshotter/estargz v0.16.3/go.mod h1:uyr4BfYfOj3G9WBVE8cOlQmXAbPN9VEQpBBeJIuOipU=
github.com/containerd/stargz-snapshotter/estargz v0.15.1/go.mod h1:gr2RNwukQ/S9Nv33Lt6UC7xEx58C+LHRdoqbEKjz1Kk=
github.com/containerd/ttrpc v1.2.5 h1:IFckT1EFQoFBMG4c3sMdT8EP3/aKfumK1msY+Ze4oLU= github.com/containerd/ttrpc v1.2.5 h1:IFckT1EFQoFBMG4c3sMdT8EP3/aKfumK1msY+Ze4oLU=
github.com/containerd/ttrpc v1.2.5/go.mod h1:YCXHsb32f+Sq5/72xHubdiJRQY9inL4a4ZQrAbN1q9o= github.com/containerd/ttrpc v1.2.5/go.mod h1:YCXHsb32f+Sq5/72xHubdiJRQY9inL4a4ZQrAbN1q9o=
github.com/containerd/typeurl/v2 v2.2.0 h1:6NBDbQzr7I5LHgp34xAXYF5DOTQDn05X58lsPEmzLso= github.com/containerd/typeurl/v2 v2.2.0 h1:6NBDbQzr7I5LHgp34xAXYF5DOTQDn05X58lsPEmzLso=
@@ -216,8 +219,9 @@ github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSV
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc=
github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/creack/pty v1.1.21 h1:1/QdRyBaHHJP61QkWMXlOIBfsgdDeeKfK8SYVUWJKf0= github.com/creack/pty v1.1.21 h1:1/QdRyBaHHJP61QkWMXlOIBfsgdDeeKfK8SYVUWJKf0=
@@ -229,6 +233,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c h1:pFUpOrbxDR6AkioZ1ySsx5yxlDQZ8stG2b88gTPxgJU= github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c h1:pFUpOrbxDR6AkioZ1ySsx5yxlDQZ8stG2b88gTPxgJU=
github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c/go.mod h1:6UhI8N9EjYm1c2odKpFpAYeR8dsBeM7PtzQhRgxRr9U= github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c/go.mod h1:6UhI8N9EjYm1c2odKpFpAYeR8dsBeM7PtzQhRgxRr9U=
github.com/deckarep/golang-set/v2 v2.8.0 h1:swm0rlPCmdWn9mESxKOjWk8hXSqoxOp+ZlfuyaAdFlQ=
github.com/deckarep/golang-set/v2 v2.8.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4=
github.com/decred/dcrd/crypto/blake256 v1.0.1 h1:7PltbUIQB7u/FfZ39+DGa/ShuMyJ5ilcvdfma9wOH6Y= github.com/decred/dcrd/crypto/blake256 v1.0.1 h1:7PltbUIQB7u/FfZ39+DGa/ShuMyJ5ilcvdfma9wOH6Y=
github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnNEcHYvcCuK6dPZSg= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnNEcHYvcCuK6dPZSg=
@@ -251,8 +257,8 @@ github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5Qvfr
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/docker/buildx v0.18.0 h1:rSauXHeJt90NvtXrLK5J992Eb0UPJZs2vV3u1zTf1nE= github.com/docker/buildx v0.18.0 h1:rSauXHeJt90NvtXrLK5J992Eb0UPJZs2vV3u1zTf1nE=
github.com/docker/buildx v0.18.0/go.mod h1:JGNSshOhHs5FhG3u51jXUf4lLOeD2QBIlJ2vaRB67p4= github.com/docker/buildx v0.18.0/go.mod h1:JGNSshOhHs5FhG3u51jXUf4lLOeD2QBIlJ2vaRB67p4=
github.com/docker/cli v27.4.0-rc.4+incompatible h1:nnrHCfd5eODviSGzCpIyYFcJovrq+/dVW4K4oqQu7WA= github.com/docker/cli v27.5.0+incompatible h1:aMphQkcGtpHixwwhAXJT1rrK/detk2JIvDaFkLctbGM=
github.com/docker/cli v27.4.0-rc.4+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/cli v27.5.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/compose/v2 v2.31.0 h1:8Sm0c4MjIhksguxIA5koYMXoTJDAp/CaZ1cdZrMvMdw= github.com/docker/compose/v2 v2.31.0 h1:8Sm0c4MjIhksguxIA5koYMXoTJDAp/CaZ1cdZrMvMdw=
github.com/docker/compose/v2 v2.31.0/go.mod h1:oQq3UDEdsnB3AUO72AxaoeLbkCgmUu1+8tLzvmphmXA= github.com/docker/compose/v2 v2.31.0/go.mod h1:oQq3UDEdsnB3AUO72AxaoeLbkCgmUu1+8tLzvmphmXA=
github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
@@ -328,8 +334,8 @@ github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ4
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-sql-driver/mysql v1.3.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-sql-driver/mysql v1.3.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI= github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/go-stack/stack v1.6.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-stack/stack v1.6.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
@@ -393,6 +399,8 @@ github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-containerregistry v0.20.2 h1:B1wPJ1SN/S7pB+ZAimcciVD+r+yV/l/DSArMxlbwseo=
github.com/google/go-containerregistry v0.20.2/go.mod h1:z38EKdKh4h7IP2gSfUUqEvalZBqs6AoLeWfUy34nQC8=
github.com/google/go-tpm v0.9.0 h1:sQF6YqWMi+SCXpsmS3fd21oPy/vSddwZry4JnmltHVk= github.com/google/go-tpm v0.9.0 h1:sQF6YqWMi+SCXpsmS3fd21oPy/vSddwZry4JnmltHVk=
github.com/google/go-tpm v0.9.0/go.mod h1:FkNVkc6C+IsvDI9Jw1OveJmxGZUUaKxtrpOS47QWKfU= github.com/google/go-tpm v0.9.0/go.mod h1:FkNVkc6C+IsvDI9Jw1OveJmxGZUUaKxtrpOS47QWKfU=
github.com/google/go-tpm-tools v0.4.4 h1:oiQfAIkc6xTy9Fl5NKTeTJkBTlXdHsxAofmQyxBKY98= github.com/google/go-tpm-tools v0.4.4 h1:oiQfAIkc6xTy9Fl5NKTeTJkBTlXdHsxAofmQyxBKY98=
@@ -402,8 +410,8 @@ github.com/google/go-tspi v0.3.0/go.mod h1:xfMGI3G0PhxCdNVcYr1C4C+EizojDg/TXuX5b
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8=
github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo=
github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad h1:a6HEuzUHeKH6hwfN/ZoQgRgVIWFJljSWa/zetS2WTvg= github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o= github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o=
github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw=
@@ -425,8 +433,8 @@ github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0= github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0 h1:TmHmbvxPmaegwhDubVz0lICL0J5Ka2vwTzhoePEXsGE=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k= github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0/go.mod h1:qztMSjm835F2bXf+5HKAPIS5qsmQDqZna/PgVt4rWtI=
github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed h1:5upAirOpQc1Q53c0bnx2ufif5kANL7bfZWcc6VJWJd8= github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed h1:5upAirOpQc1Q53c0bnx2ufif5kANL7bfZWcc6VJWJd8=
github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4= github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
@@ -591,6 +599,8 @@ github.com/jinzhu/gorm v0.0.0-20170222002820-5409931a1bb8/go.mod h1:Vla75njaFJ8c
github.com/jinzhu/inflection v0.0.0-20170102125226-1c35d901db3d h1:jRQLvyVGL+iVtDElaEIDdKwpPqUIZJfzkNLV34htpEc= github.com/jinzhu/inflection v0.0.0-20170102125226-1c35d901db3d h1:jRQLvyVGL+iVtDElaEIDdKwpPqUIZJfzkNLV34htpEc=
github.com/jinzhu/inflection v0.0.0-20170102125226-1c35d901db3d/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/inflection v0.0.0-20170102125226-1c35d901db3d/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA= github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=
github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
@@ -631,8 +641,9 @@ github.com/lib/pq v0.0.0-20150723085316-0dad96c0b94f/go.mod h1:5WUZQaWbwv1U+lTRe
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8=
github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/libdns/libdns v0.2.2 h1:O6ws7bAfRPaBsgAYt8MDe2HcNBGC29hkZ9MX2eUSX3s= github.com/libdns/libdns v0.2.2 h1:O6ws7bAfRPaBsgAYt8MDe2HcNBGC29hkZ9MX2eUSX3s=
github.com/libdns/libdns v0.2.2/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ= github.com/libdns/libdns v0.2.2/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ=
github.com/libp2p/go-buffer-pool v0.1.0 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6cdF0Y8= github.com/libp2p/go-buffer-pool v0.1.0 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6cdF0Y8=
@@ -693,6 +704,8 @@ github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh
github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk= github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk=
github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
github.com/mattn/go-sqlite3 v1.6.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/mattn/go-sqlite3 v1.6.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mdlayher/genetlink v1.3.2 h1:KdrNKe+CTu+IbZnm/GVUMXSqBBLqcGpRDa0xkQy56gw= github.com/mdlayher/genetlink v1.3.2 h1:KdrNKe+CTu+IbZnm/GVUMXSqBBLqcGpRDa0xkQy56gw=
github.com/mdlayher/genetlink v1.3.2/go.mod h1:tcC3pkCrPUGIKKsCsp0B3AdaaKuHtaxoJRz3cc+528o= github.com/mdlayher/genetlink v1.3.2/go.mod h1:tcC3pkCrPUGIKKsCsp0B3AdaaKuHtaxoJRz3cc+528o=
@@ -727,6 +740,7 @@ github.com/mitchellh/cli v1.1.5/go.mod h1:v8+iFts2sPIKUV1ltktPXMCC8fumSKFItNcD2c
github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc= github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc=
github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg= github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg=
@@ -800,6 +814,8 @@ github.com/multiformats/go-varint v0.0.7/go.mod h1:r8PUYw/fD/SjBCiKOoDlGF6QawOEL
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.0/go.mod h1:oUhWkIvk5aDxtKvDDuw8gItl8pKl42LzjC9KZE0HfGg= github.com/onsi/ginkgo v1.12.0/go.mod h1:oUhWkIvk5aDxtKvDDuw8gItl8pKl42LzjC9KZE0HfGg=
@@ -916,12 +932,14 @@ github.com/quic-go/webtransport-go v0.8.0 h1:HxSrwun11U+LlmwpgM1kEqIqH90IT4N8auv
github.com/quic-go/webtransport-go v0.8.0/go.mod h1:N99tjprW432Ut5ONql/aUhSLT0YVSlwHohQsuac9WaM= github.com/quic-go/webtransport-go v0.8.0/go.mod h1:N99tjprW432Ut5ONql/aUhSLT0YVSlwHohQsuac9WaM=
github.com/raulk/go-watchdog v1.3.0 h1:oUmdlHxdkXRJlwfG0O9omj8ukerm8MEQavSiDTEtBsk= github.com/raulk/go-watchdog v1.3.0 h1:oUmdlHxdkXRJlwfG0O9omj8ukerm8MEQavSiDTEtBsk=
github.com/raulk/go-watchdog v1.3.0/go.mod h1:fIvOnLbF0b0ZwkB9YU4mOW9Did//4vPZtDqv66NfsMU= github.com/raulk/go-watchdog v1.3.0/go.mod h1:fIvOnLbF0b0ZwkB9YU4mOW9Did//4vPZtDqv66NfsMU=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc= github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc=
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
@@ -1048,8 +1066,8 @@ github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljT
github.com/urfave/cli v1.22.10/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/urfave/cli v1.22.10/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/urfave/cli v1.22.16 h1:MH0k6uJxdwdeWQTwhSO42Pwr4YLrNLwBtg1MRgTqPdQ= github.com/urfave/cli v1.22.16 h1:MH0k6uJxdwdeWQTwhSO42Pwr4YLrNLwBtg1MRgTqPdQ=
github.com/urfave/cli v1.22.16/go.mod h1:EeJR6BKodywf4zciqrdw6hpCPk68JO9z5LazXZMn5Po= github.com/urfave/cli v1.22.16/go.mod h1:EeJR6BKodywf4zciqrdw6hpCPk68JO9z5LazXZMn5Po=
github.com/vbatts/tar-split v0.11.5 h1:3bHCTIheBm1qFTcgh9oPu+nNBtX+XJIupG/vacinCts= github.com/vbatts/tar-split v0.11.6 h1:4SjTW5+PU11n6fZenf2IPoV8/tz3AaYHMWjf23envGs=
github.com/vbatts/tar-split v0.11.5/go.mod h1:yZbwRsSeGjusneWgA781EKej9HF8vme8okylkAeNKLk= github.com/vbatts/tar-split v0.11.6/go.mod h1:dqKNtesIOr2j2Qv3W/cHjnvk9I8+G7oAkFDFN6TCBEI=
github.com/vishvananda/netlink v1.3.0 h1:X7l42GfcV4S6E4vHTsw48qbrV+9PVojNfIhZcwQdrZk= github.com/vishvananda/netlink v1.3.0 h1:X7l42GfcV4S6E4vHTsw48qbrV+9PVojNfIhZcwQdrZk=
github.com/vishvananda/netlink v1.3.0/go.mod h1:i6NetklAujEcC6fK0JPjT8qSwWyO0HLn4UKG+hGqeJs= github.com/vishvananda/netlink v1.3.0/go.mod h1:i6NetklAujEcC6fK0JPjT8qSwWyO0HLn4UKG+hGqeJs=
github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8= github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8=
@@ -1058,6 +1076,15 @@ github.com/warpfork/go-testmark v0.12.1 h1:rMgCpJfwy1sJ50x0M0NgyphxYYPMOODIJHhsX
github.com/warpfork/go-testmark v0.12.1/go.mod h1:kHwy7wfvGSPh1rQJYKayD4AbtNaeyZdcGi9tNJTaa5Y= github.com/warpfork/go-testmark v0.12.1/go.mod h1:kHwy7wfvGSPh1rQJYKayD4AbtNaeyZdcGi9tNJTaa5Y=
github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0 h1:GDDkbFiaK8jsSDJfjId/PEGEShv6ugrt4kYsC5UIDaQ= github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0 h1:GDDkbFiaK8jsSDJfjId/PEGEShv6ugrt4kYsC5UIDaQ=
github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0/go.mod h1:x6AKhvSSexNrVSrViXSHUEbICjmGXhtgABaHIySUSGw= github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0/go.mod h1:x6AKhvSSexNrVSrViXSHUEbICjmGXhtgABaHIySUSGw=
github.com/weppos/publicsuffix-go v0.15.1-0.20210511084619-b1f36a2d6c0b h1:FsyNrX12e5BkplJq7wKOLk0+C6LZ+KGXvuEcKUYm5ss=
github.com/weppos/publicsuffix-go v0.15.1-0.20210511084619-b1f36a2d6c0b/go.mod h1:HYux0V0Zi04bHNwOHy4cXJVz/TQjYonnF6aoYhj+3QE=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo=
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
@@ -1070,37 +1097,43 @@ github.com/zeebo/blake3 v0.2.4/go.mod h1:7eeQ6d2iXWRGF6npfaxl2CU+xy2Fjo2gxeyZGCR
github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo= github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo=
github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4= github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4=
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
github.com/zmap/zcrypto v0.0.0-20210511125630-18f1e0152cfc h1:zkGwegkOW709y0oiAraH/3D8njopUR/pARHv4tZZ6pw=
github.com/zmap/zcrypto v0.0.0-20210511125630-18f1e0152cfc/go.mod h1:FM4U1E3NzlNMRnSUTU3P1UdukWhYGifqEsjk9fn7BCk=
github.com/zmap/zlint/v3 v3.1.0 h1:WjVytZo79m/L1+/Mlphl09WBob6YTGljN5IGWZFpAv0=
github.com/zmap/zlint/v3 v3.1.0/go.mod h1:L7t8s3sEKkb0A2BxGy1IWrxt1ZATa1R4QfJZaQOD3zU=
go.etcd.io/bbolt v1.3.11 h1:yGEzV1wPz2yVCLsD8ZAiGHhHVlczyC9d1rP43/VCRJ0= go.etcd.io/bbolt v1.3.11 h1:yGEzV1wPz2yVCLsD8ZAiGHhHVlczyC9d1rP43/VCRJ0=
go.etcd.io/bbolt v1.3.11/go.mod h1:dksAq7YMXoljX0xu6VF5DMZGbhYYoLUalEiSySYAS4I= go.etcd.io/bbolt v1.3.11/go.mod h1:dksAq7YMXoljX0xu6VF5DMZGbhYYoLUalEiSySYAS4I=
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 h1:4Pp6oUg3+e/6M4C0A/3kJ2VYa++dsWVTtGgLVj5xtHg= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 h1:4Pp6oUg3+e/6M4C0A/3kJ2VYa++dsWVTtGgLVj5xtHg=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0/go.mod h1:Mjt1i1INqiaoZOMGR1RIUJN+i3ChKoFRqzrRQhlkbs0= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0/go.mod h1:Mjt1i1INqiaoZOMGR1RIUJN+i3ChKoFRqzrRQhlkbs0=
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.46.1 h1:gbhw/u49SS3gkPWiYweQNJGm/uJN5GkI/FrosxSHT7A= go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.46.1 h1:gbhw/u49SS3gkPWiYweQNJGm/uJN5GkI/FrosxSHT7A=
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.46.1/go.mod h1:GnOaBaFQ2we3b9AGWJpsBa7v1S5RlQzlC3O7dRMxZhM= go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.46.1/go.mod h1:GnOaBaFQ2we3b9AGWJpsBa7v1S5RlQzlC3O7dRMxZhM=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.52.0 h1:9l89oX4ba9kHbBol3Xin3leYJ+252h0zszDtBwyKe2A= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 h1:yd02MEjBdJkG3uabWP9apV+OuWRIXGDuJEUJbOHmCFU=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.52.0/go.mod h1:XLZfZboOJWHNKUv7eH0inh0E9VV6eWDFB/9yJyTLPp0= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0/go.mod h1:umTcuxiv1n/s/S6/c2AT/g2CQ7u5C59sHDNmfSwgz7Q=
go.opentelemetry.io/otel v1.32.0 h1:WnBN+Xjcteh0zdk01SVqV55d/m62NJLJdIyb4y/WO5U= go.opentelemetry.io/otel v1.33.0 h1:/FerN9bax5LoK51X/sI0SVYrjSE0/yUL7DpxW4K3FWw=
go.opentelemetry.io/otel v1.32.0/go.mod h1:00DCVSB0RQcnzlwyTfqtxSm+DRr9hpYrHjNGiBHVQIg= go.opentelemetry.io/otel v1.33.0/go.mod h1:SUUkR6csvUQl+yjReHu5uM3EtVV7MBm5FHKRlNx4I8I=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.44.0 h1:jd0+5t/YynESZqsSyPz+7PAFdEop0dlN0+PkyHYo8oI= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.44.0 h1:jd0+5t/YynESZqsSyPz+7PAFdEop0dlN0+PkyHYo8oI=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.44.0/go.mod h1:U707O40ee1FpQGyhvqnzmCJm1Wh6OX6GGBVn0E6Uyyk= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.44.0/go.mod h1:U707O40ee1FpQGyhvqnzmCJm1Wh6OX6GGBVn0E6Uyyk=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.27.0 h1:R9DE4kQ4k+YtfLI2ULwX82VtNQ2J8yZmA7ZIF/D+7Mc= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0 h1:Vh5HayB/0HHfOQA7Ctx69E/Y/DcQSMPpKANYVMQ7fBA=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.27.0/go.mod h1:OQFyQVrDlbe+R7xrEyDr/2Wr67Ol0hRUgsfA+V5A95s= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0/go.mod h1:cpgtDBaqD/6ok/UG0jT15/uKjAY8mRA53diogHBg3UI=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0 h1:qFffATk0X+HD+f1Z8lswGiOQYKHRlzfmdJm0wEaVrFA= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0 h1:qFffATk0X+HD+f1Z8lswGiOQYKHRlzfmdJm0wEaVrFA=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0/go.mod h1:MOiCmryaYtc+V0Ei+Tx9o5S1ZjA7kzLucuVuyzBZloQ= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0/go.mod h1:MOiCmryaYtc+V0Ei+Tx9o5S1ZjA7kzLucuVuyzBZloQ=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.27.0 h1:QY7/0NeRPKlzusf40ZE4t1VlMKbqSNT7cJRYzWuja0s= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.27.0 h1:QY7/0NeRPKlzusf40ZE4t1VlMKbqSNT7cJRYzWuja0s=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.27.0/go.mod h1:HVkSiDhTM9BoUJU8qE6j2eSWLLXvi1USXjyd2BXT8PY= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.27.0/go.mod h1:HVkSiDhTM9BoUJU8qE6j2eSWLLXvi1USXjyd2BXT8PY=
go.opentelemetry.io/otel/metric v1.32.0 h1:xV2umtmNcThh2/a/aCP+h64Xx5wsj8qqnkYZktzNa0M= go.opentelemetry.io/otel/metric v1.33.0 h1:r+JOocAyeRVXD8lZpjdQjzMadVZp2M4WmQ+5WtEnklQ=
go.opentelemetry.io/otel/metric v1.32.0/go.mod h1:jH7CIbbK6SH2V2wE16W05BHCtIDzauciCRLoc/SyMv8= go.opentelemetry.io/otel/metric v1.33.0/go.mod h1:L9+Fyctbp6HFTddIxClbQkjtubW6O9QS3Ann/M82u6M=
go.opentelemetry.io/otel/sdk v1.32.0 h1:RNxepc9vK59A8XsgZQouW8ue8Gkb4jpWtJm9ge5lEG4= go.opentelemetry.io/otel/sdk v1.33.0 h1:iax7M131HuAm9QkZotNHEfstof92xM+N8sr3uHXc2IM=
go.opentelemetry.io/otel/sdk v1.32.0/go.mod h1:LqgegDBjKMmb2GC6/PrTnteJG39I8/vJCAP9LlJXEjU= go.opentelemetry.io/otel/sdk v1.33.0/go.mod h1:A1Q5oi7/9XaMlIWzPSxLRWOI8nG3FnzHJNbiENQuihM=
go.opentelemetry.io/otel/sdk/metric v1.32.0 h1:rZvFnvmvawYb0alrYkjraqJq0Z4ZUJAiyYCU9snn1CU= go.opentelemetry.io/otel/sdk/metric v1.32.0 h1:rZvFnvmvawYb0alrYkjraqJq0Z4ZUJAiyYCU9snn1CU=
go.opentelemetry.io/otel/sdk/metric v1.32.0/go.mod h1:PWeZlq0zt9YkYAp3gjKZ0eicRYvOh1Gd+X99x6GHpCQ= go.opentelemetry.io/otel/sdk/metric v1.32.0/go.mod h1:PWeZlq0zt9YkYAp3gjKZ0eicRYvOh1Gd+X99x6GHpCQ=
go.opentelemetry.io/otel/trace v1.32.0 h1:WIC9mYrXf8TmY/EXuULKc8hR17vE+Hjv2cssQDe03fM= go.opentelemetry.io/otel/trace v1.33.0 h1:cCJuF7LRjUFso9LPnEAHJDB2pqzp+hbO8eu1qqW2d/s=
go.opentelemetry.io/otel/trace v1.32.0/go.mod h1:+i4rkvCraA+tG6AzwloGaCtkx53Fa+L+V8e9a7YvhT8= go.opentelemetry.io/otel/trace v1.33.0/go.mod h1:uIcdVUZMpTAmz0tI1z04GoVSezK37CbGV4fr1f2nBck=
go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= go.opentelemetry.io/proto/otlp v1.4.0 h1:TA9WRvW6zMwP+Ssb6fLoUIuirti1gGbP28GcKG1jgeg=
go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= go.opentelemetry.io/proto/otlp v1.4.0/go.mod h1:PPBWZIP98o2ElSqI35IHfu7hIhSwvc5N38Jw8pXuGFY=
go.step.sm/cli-utils v0.9.0 h1:55jYcsQbnArNqepZyAwcato6Zy2MoZDRkWW+jF+aPfQ= go.step.sm/cli-utils v0.9.0 h1:55jYcsQbnArNqepZyAwcato6Zy2MoZDRkWW+jF+aPfQ=
go.step.sm/cli-utils v0.9.0/go.mod h1:Y/CRoWl1FVR9j+7PnAewufAwKmBOTzR6l9+7EYGAnp8= go.step.sm/cli-utils v0.9.0/go.mod h1:Y/CRoWl1FVR9j+7PnAewufAwKmBOTzR6l9+7EYGAnp8=
go.step.sm/crypto v0.45.0 h1:Z0WYAaaOYrJmKP9sJkPW+6wy3pgN3Ija8ek/D4serjc= go.step.sm/crypto v0.45.0 h1:Z0WYAaaOYrJmKP9sJkPW+6wy3pgN3Ija8ek/D4serjc=
@@ -1161,8 +1194,8 @@ golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5y
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
golang.org/x/crypto/x509roots/fallback v0.0.0-20240507223354-67b13616a595 h1:TgSqweA595vD0Zt86JzLv3Pb/syKg8gd5KMGGbJPYFw= golang.org/x/crypto/x509roots/fallback v0.0.0-20240507223354-67b13616a595 h1:TgSqweA595vD0Zt86JzLv3Pb/syKg8gd5KMGGbJPYFw=
golang.org/x/crypto/x509roots/fallback v0.0.0-20240507223354-67b13616a595/go.mod h1:kNa9WdvYnzFwC79zRpLRMJbdEFlhyM5RPFBBZp/wWH8= golang.org/x/crypto/x509roots/fallback v0.0.0-20240507223354-67b13616a595/go.mod h1:kNa9WdvYnzFwC79zRpLRMJbdEFlhyM5RPFBBZp/wWH8=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@@ -1202,8 +1235,8 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.32.0 h1:ZqPmj8Kzc+Y6e0+skZsuACbx+wzMgo5MQsJh9Qd6aYI= golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
golang.org/x/net v0.32.0/go.mod h1:CwU0IoeOlnQQWJ6ioyFrfRuomB8GKF6KbYXZVyeXNfs= golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE= golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE=
golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
@@ -1266,8 +1299,8 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@@ -1275,8 +1308,8 @@ golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q= golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
@@ -1310,8 +1343,8 @@ golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4f
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.28.0 h1:WuB6qZ4RPCQo5aP3WdKZS7i595EdWqWR8vqJTlwTVK8= golang.org/x/tools v0.29.0 h1:Xx0h3TtM9rzQpQuR4dKLrdglAmCEN5Oi+P74JdhdzXE=
golang.org/x/tools v0.28.0/go.mod h1:dcIOrVd3mfQKTgrDVQHqCPMWy6lnhfhtX3hLXYVLfRw= golang.org/x/tools v0.29.0/go.mod h1:KMQVMRsVxU6nHCFXrBPhDB8XncLNLM0lIy/F14RP588=
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -1334,10 +1367,10 @@ google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20240401170217-c3f982113cda h1:wu/KJm9KJwpfHWhkkZGohVC6KRrc1oJNr4jwtQMOQXw= google.golang.org/genproto v0.0.0-20240401170217-c3f982113cda h1:wu/KJm9KJwpfHWhkkZGohVC6KRrc1oJNr4jwtQMOQXw=
google.golang.org/genproto v0.0.0-20240401170217-c3f982113cda/go.mod h1:g2LLCvCeCSir/JJSWosk19BR4NVxGqHUC6rxIRsd7Aw= google.golang.org/genproto v0.0.0-20240401170217-c3f982113cda/go.mod h1:g2LLCvCeCSir/JJSWosk19BR4NVxGqHUC6rxIRsd7Aw=
google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 h1:hjSy6tcFQZ171igDaN5QHOw2n6vx40juYbC/x67CEhc= google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 h1:CkkIfIt50+lT6NHAVoRYEyAvQGFM7xEwXUUywFvEb3Q=
google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:qpvKtACPCQhAdu3PyQgV4l3LMXZEtft7y8QcarRsp9I= google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576/go.mod h1:1R3kvZ1dtP3+4p4d3G8uJ8rFk/fWlScl38vanWACI08=
google.golang.org/genproto/googleapis/rpc v0.0.0-20241007155032-5fefd90f89a9 h1:QCqS/PdaHTSWGvupk2F/ehwHtGc0/GYkT+3GAcR1CCc= google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576 h1:8ZmaLZE4XWrtU3MyClkYqqtl6Oegr3235h7jxsDyqCY=
google.golang.org/genproto/googleapis/rpc v0.0.0-20241007155032-5fefd90f89a9/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI= google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU=
google.golang.org/grpc v1.0.5/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.0.5/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
@@ -1345,8 +1378,8 @@ google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyac
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.68.0 h1:aHQeeJbo8zAkAa3pRzrVjZlbz6uSfeOXlJNQM0RAbz0= google.golang.org/grpc v1.68.1 h1:oI5oTa11+ng8r8XMMN7jAOmWfPZWbYpCFaMUTACxkM0=
google.golang.org/grpc v1.68.0/go.mod h1:fmSPC5AsjSBCK54MyHRx48kpOti1/jRfOlwEWywNjWA= google.golang.org/grpc v1.68.1/go.mod h1:+q1XYFJjShcqn0QZHvCyeR4CXPA+llXIeUIfIe00waw=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
@@ -1356,8 +1389,8 @@ google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.36.0 h1:mjIs9gYtt56AzC4ZaffQuh88TZurBGhIJMBZGSxNerQ= google.golang.org/protobuf v1.36.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU=
google.golang.org/protobuf v1.36.0/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U= gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/cenkalti/backoff.v2 v2.2.1 h1:eJ9UAg01/HIHG987TwxvnzK2MgxXq97YY6rYDpY9aII= gopkg.in/cenkalti/backoff.v2 v2.2.1 h1:eJ9UAg01/HIHG987TwxvnzK2MgxXq97YY6rYDpY9aII=
@@ -1403,3 +1436,27 @@ k8s.io/klog/v2 v2.110.1 h1:U/Af64HJf7FcwMcXyKm2RPM22WZzyR7OSpYj5tg3cL0=
k8s.io/klog/v2 v2.110.1/go.mod h1:YGtd1984u+GgbuZ7e08/yBuAfKLSO0+uR1Fhi6ExXjo= k8s.io/klog/v2 v2.110.1/go.mod h1:YGtd1984u+GgbuZ7e08/yBuAfKLSO0+uR1Fhi6ExXjo=
lukechampine.com/blake3 v1.3.0 h1:sJ3XhFINmHSrYCgl958hscfIa3bw8x4DqMP3u1YvoYE= lukechampine.com/blake3 v1.3.0 h1:sJ3XhFINmHSrYCgl958hscfIa3bw8x4DqMP3u1YvoYE=
lukechampine.com/blake3 v1.3.0/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k= lukechampine.com/blake3 v1.3.0/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k=
modernc.org/cc/v4 v4.24.4 h1:TFkx1s6dCkQpd6dKurBNmpo+G8Zl4Sq/ztJ+2+DEsh0=
modernc.org/cc/v4 v4.24.4/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
modernc.org/ccgo/v4 v4.23.16 h1:Z2N+kk38b7SfySC1ZkpGLN2vthNJP1+ZzGZIlH7uBxo=
modernc.org/ccgo/v4 v4.23.16/go.mod h1:nNma8goMTY7aQZQNTyN9AIoJfxav4nvTnvKThAeMDdo=
modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=
modernc.org/gc/v2 v2.6.3 h1:aJVhcqAte49LF+mGveZ5KPlsp4tdGdAOT4sipJXADjw=
modernc.org/gc/v2 v2.6.3/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
modernc.org/libc v1.61.13 h1:3LRd6ZO1ezsFiX1y+bHd1ipyEHIJKvuprv0sLTBwLW8=
modernc.org/libc v1.61.13/go.mod h1:8F/uJWL/3nNil0Lgt1Dpz+GgkApWh04N3el3hxJcA6E=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.8.2 h1:cL9L4bcoAObu4NkxOlKWBWtNHIsnnACGF/TbqQ6sbcI=
modernc.org/memory v1.8.2/go.mod h1:ZbjSvMO5NQ1A2i3bWeDiVMxIorXwdClKE/0SZ+BMotU=
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
modernc.org/sqlite v1.36.3 h1:qYMYlFR+rtLDUzuXoST1SDIdEPbX8xzuhdF90WsX1ss=
modernc.org/sqlite v1.36.3/go.mod h1:ADySlx7K4FdY5MaJcEv86hTJ0PjedAloTUuif0YS3ws=
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
-86
View File
@@ -1,86 +0,0 @@
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
@@ -1,92 +0,0 @@
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())
})
}
-98
View File
@@ -1,98 +0,0 @@
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
}
+131 -47
View File
@@ -4,28 +4,42 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"github.com/docker/cli/cli/streams"
"github.com/psviderski/uncloud/internal/cli/config"
"github.com/psviderski/uncloud/internal/fs"
"github.com/psviderski/uncloud/internal/machine"
"github.com/psviderski/uncloud/internal/machine/api/pb"
"github.com/psviderski/uncloud/internal/sshexec"
"github.com/psviderski/uncloud/pkg/api"
"github.com/psviderski/uncloud/pkg/client"
"github.com/psviderski/uncloud/pkg/client/connector"
"net/netip"
"os"
"github.com/charmbracelet/huh" "github.com/charmbracelet/huh"
"google.golang.org/protobuf/types/known/emptypb" "google.golang.org/protobuf/types/known/emptypb"
"net/netip"
"uncloud/internal/cli/client"
"uncloud/internal/cli/client/connector"
"uncloud/internal/cli/config"
"uncloud/internal/machine"
"uncloud/internal/machine/api/pb"
"uncloud/internal/sshexec"
) )
const defaultClusterName = "default" const defaultClusterName = "default"
type CLI struct { type CLI struct {
config *config.Config config *config.Config
conn *config.MachineConnection
} }
func New(configPath string) (*CLI, error) { // New creates a new CLI instance with the given config path or remote machine connection.
// If the connection is provided, the config is ignored for all operations which is useful for interacting with
// a cluster without creating a config.
func New(configPath string, conn *config.MachineConnection) (*CLI, error) {
if conn != nil {
return &CLI{conn: conn}, nil
}
cfg, err := config.NewFromFile(configPath) cfg, err := config.NewFromFile(configPath)
if err != nil { if err != nil {
return nil, fmt.Errorf("read Uncloud config: %w", err) return nil, fmt.Errorf("read Uncloud config: %w", err)
} }
return &CLI{ return &CLI{
config: cfg, config: cfg,
}, nil }, nil
@@ -43,13 +57,19 @@ func (cli *CLI) CreateCluster(name string) error {
func (cli *CLI) SetCurrentCluster(name string) error { func (cli *CLI) SetCurrentCluster(name string) error {
if _, ok := cli.config.Clusters[name]; !ok { if _, ok := cli.config.Clusters[name]; !ok {
return client.ErrNotFound return api.ErrNotFound
} }
cli.config.CurrentCluster = name cli.config.CurrentCluster = name
return cli.config.Save() return cli.config.Save()
} }
// ConnectCluster connects to a cluster using the given cluster name or the current cluster if not specified.
// If the CLI was initialised with a machine connection, the config is ignored and the connection is used instead.
func (cli *CLI) ConnectCluster(ctx context.Context, clusterName string) (*client.Client, error) { func (cli *CLI) ConnectCluster(ctx context.Context, clusterName string) (*client.Client, error) {
if cli.conn != nil {
return connectCluster(ctx, *cli.conn)
}
if len(cli.config.Clusters) == 0 { if len(cli.config.Clusters) == 0 {
return nil, errors.New( return nil, errors.New(
"no clusters found in the Uncloud config. " + "no clusters found in the Uncloud config. " +
@@ -84,57 +104,89 @@ 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. // TODO: iterate over all connections and try to connect to the cluster using the first successful connection.
conn := cfg.Connections[0] conn := cfg.Connections[0]
c, err := connectCluster(ctx, conn)
if err != nil {
return nil, errors.New("no valid connection configuration found for the cluster")
}
return c, nil
}
func connectCluster(ctx context.Context, conn config.MachineConnection) (*client.Client, error) {
if conn.SSH != "" { if conn.SSH != "" {
user, host, port, err := conn.SSH.Parse() user, host, port, err := conn.SSH.Parse()
if err != nil { if err != nil {
return nil, fmt.Errorf("parse SSH connection %q: %w", conn.SSH, err) return nil, fmt.Errorf("parse SSH connection %q: %w", conn.SSH, err)
} }
keyPath := fs.ExpandHomeDir(conn.SSHKeyFile)
sshConfig := &connector.SSHConnectorConfig{ sshConfig := &connector.SSHConnectorConfig{
User: user, User: user,
Host: host, Host: host,
Port: port, Port: port,
KeyPath: keyPath,
} }
return client.New(ctx, connector.NewSSHConnector(sshConfig)) return client.New(ctx, connector.NewSSHConnector(sshConfig))
} else if conn.TCP.IsValid() { } else if conn.TCP.IsValid() {
return client.New(ctx, connector.NewTCPConnector(conn.TCP)) return client.New(ctx, connector.NewTCPConnector(conn.TCP))
} }
return nil, errors.New("no valid connection configuration found for the cluster")
return nil, errors.New("connection configuration is invalid")
} }
// InitCluster initialises a new cluster on a remote machine and returns a client to interact with the cluster.
// The client should be closed after use by the caller.
func (cli *CLI) InitCluster( func (cli *CLI) InitCluster(
ctx context.Context, remoteMachine *RemoteMachine, clusterName, machineName string, netPrefix netip.Prefix, ctx context.Context,
) error { remoteMachine *RemoteMachine,
clusterName,
machineName string,
netPrefix netip.Prefix,
publicIP *netip.Addr,
) (*client.Client, error) {
if remoteMachine != nil { if remoteMachine != nil {
return cli.initRemoteMachine(ctx, *remoteMachine, clusterName, machineName, netPrefix) return cli.initRemoteMachine(ctx, *remoteMachine, clusterName, machineName, netPrefix, publicIP)
} }
// TODO: implement local machine initialisation // TODO: implement local machine initialisation
return fmt.Errorf("local machine initialisation is not implemented yet") return nil, fmt.Errorf("local machine initialisation is not implemented yet")
} }
func (cli *CLI) initRemoteMachine( func (cli *CLI) initRemoteMachine(
ctx context.Context, remoteMachine RemoteMachine, clusterName, machineName string, netPrefix netip.Prefix, ctx context.Context,
) error { remoteMachine RemoteMachine,
clusterName,
machineName string,
netPrefix netip.Prefix,
publicIP *netip.Addr,
) (*client.Client, error) {
if clusterName == "" { if clusterName == "" {
clusterName = defaultClusterName clusterName = defaultClusterName
} }
if _, ok := cli.config.Clusters[clusterName]; ok { if _, ok := cli.config.Clusters[clusterName]; ok {
return fmt.Errorf("cluster %q already exists", clusterName) return nil, fmt.Errorf("cluster %q already exists", clusterName)
} }
machineClient, err := cli.provisionRemoteMachine(ctx, remoteMachine) machineClient, err := cli.provisionRemoteMachine(ctx, remoteMachine)
if err != nil { if err != nil {
return err return nil, err
} }
defer machineClient.Close() // Ensure machineClient is closed on error.
defer func() {
if err != nil {
machineClient.Close()
}
}()
// Check if the machine is already initialised as a cluster member and prompt the user to reset it first. // Check if the machine is already initialised as a cluster member and prompt the user to reset it first.
minfo, err := machineClient.Inspect(ctx, &emptypb.Empty{}) minfo, err := machineClient.Inspect(ctx, &emptypb.Empty{})
if err != nil { if err != nil {
return fmt.Errorf("inspect machine: %w", err) return nil, fmt.Errorf("inspect machine: %w", err)
} }
if minfo.Id != "" { if minfo.Id != "" {
if err = cli.promptResetMachine(); err != nil { if err = cli.promptResetMachine(); err != nil {
return err return nil, err
} }
} }
@@ -142,65 +194,82 @@ func (cli *CLI) initRemoteMachine(
MachineName: machineName, MachineName: machineName,
Network: pb.NewIPPrefix(netPrefix), Network: pb.NewIPPrefix(netPrefix),
} }
if publicIP != nil {
if publicIP.IsValid() {
req.PublicIpConfig = &pb.InitClusterRequest_PublicIp{PublicIp: pb.NewIP(*publicIP)}
} else {
// Invalid or in other words zero IP means to automatically detect the public IP.
req.PublicIpConfig = &pb.InitClusterRequest_PublicIpAuto{PublicIpAuto: true}
}
}
resp, err := machineClient.InitCluster(ctx, req) resp, err := machineClient.InitCluster(ctx, req)
if err != nil { if err != nil {
return fmt.Errorf("init cluster: %w", err) return nil, fmt.Errorf("init cluster: %w", err)
} }
fmt.Printf("Cluster %q initialised with machine %q\n", clusterName, resp.Machine.Name) fmt.Printf("Cluster %q initialised with machine %q\n", clusterName, resp.Machine.Name)
if err = cli.CreateCluster(clusterName); err != nil { if err = cli.CreateCluster(clusterName); err != nil {
return fmt.Errorf("save cluster to config: %w", err) return nil, 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. // Set the current cluster to the just created one if it is the only cluster in the config.
if len(cli.config.Clusters) == 1 { if len(cli.config.Clusters) == 1 {
if err = cli.SetCurrentCluster(clusterName); err != nil { if err = cli.SetCurrentCluster(clusterName); err != nil {
return fmt.Errorf("set current cluster: %w", err) return nil, fmt.Errorf("set current cluster: %w", err)
} }
} }
// Save the machine's SSH connection details in the cluster config. // Save the machine's SSH connection details in the cluster config.
connCfg := config.MachineConnection{ connCfg := config.MachineConnection{
SSH: config.NewSSHDestination(remoteMachine.User, remoteMachine.Host, remoteMachine.Port), SSH: config.NewSSHDestination(remoteMachine.User, remoteMachine.Host, remoteMachine.Port),
SSHKeyFile: remoteMachine.KeyPath,
} }
cli.config.Clusters[clusterName].Connections = append(cli.config.Clusters[clusterName].Connections, connCfg) cli.config.Clusters[clusterName].Connections = append(cli.config.Clusters[clusterName].Connections, connCfg)
if err = cli.config.Save(); err != nil { if err = cli.config.Save(); err != nil {
return fmt.Errorf("save config: %w", err) return nil, fmt.Errorf("save config: %w", err)
} }
return nil return machineClient, nil
} }
func (cli *CLI) AddMachine(ctx context.Context, remoteMachine RemoteMachine, clusterName, machineName string) error { // AddMachine provisions a remote machine and adds it to the cluster. It returns a client to interact with the machine
// which should be closed after use by the caller.
func (cli *CLI) AddMachine(
ctx context.Context, remoteMachine RemoteMachine, clusterName, machineName string, publicIP *netip.Addr,
) (*client.Client, error) {
c, err := cli.ConnectCluster(ctx, clusterName) c, err := cli.ConnectCluster(ctx, clusterName)
if err != nil { if err != nil {
return fmt.Errorf("connect to cluster: %w", err) return nil, fmt.Errorf("connect to cluster: %w", err)
} }
defer func() { defer c.Close()
_ = c.Close()
}()
machineClient, err := cli.provisionRemoteMachine(ctx, remoteMachine) machineClient, err := cli.provisionRemoteMachine(ctx, remoteMachine)
if err != nil { if err != nil {
return err return nil, err
} }
defer machineClient.Close() defer func() {
if err != nil {
machineClient.Close()
}
}()
// Check if the machine is already initialised as a cluster member and prompt the user to reset it first. // Check if the machine is already initialised as a cluster member and prompt the user to reset it first.
minfo, err := machineClient.Inspect(ctx, &emptypb.Empty{}) minfo, err := machineClient.Inspect(ctx, &emptypb.Empty{})
if err != nil { if err != nil {
return fmt.Errorf("inspect machine: %w", err) return nil, fmt.Errorf("inspect machine: %w", err)
} }
if minfo.Id != "" { if minfo.Id != "" {
if err = cli.promptResetMachine(); err != nil { if err = cli.promptResetMachine(); err != nil {
return err return nil, err
} }
} }
tokenResp, err := machineClient.Token(ctx, &emptypb.Empty{}) tokenResp, err := machineClient.Token(ctx, &emptypb.Empty{})
if err != nil { if err != nil {
return fmt.Errorf("get remote machine token: %w", err) return nil, fmt.Errorf("get remote machine token: %w", err)
} }
token, err := machine.ParseToken(tokenResp.Token) token, err := machine.ParseToken(tokenResp.Token)
if err != nil { if err != nil {
return fmt.Errorf("parse remote machine token: %w", err) return nil, fmt.Errorf("parse remote machine token: %w", err)
} }
// Register the machine in the cluster using its public key and endpoints from the token. // Register the machine in the cluster using its public key and endpoints from the token.
@@ -215,15 +284,24 @@ func (cli *CLI) AddMachine(ctx context.Context, remoteMachine RemoteMachine, clu
PublicKey: token.PublicKey, PublicKey: token.PublicKey,
}, },
} }
if publicIP != nil {
if publicIP.IsValid() {
addReq.PublicIp = pb.NewIP(*publicIP)
} else if token.PublicIP.IsValid() {
// Invalid or in other words zero IP means to use an automatically detected public IP from the token.
addReq.PublicIp = pb.NewIP(token.PublicIP)
}
}
addResp, err := c.AddMachine(ctx, addReq) addResp, err := c.AddMachine(ctx, addReq)
if err != nil { if err != nil {
return fmt.Errorf("add machine to cluster: %w", err) return nil, fmt.Errorf("add machine to cluster: %w", err)
} }
// List other machines in the cluster to include them in the join request. // List other machines in the cluster to include them in the join request.
machines, err := c.ListMachines(ctx) machines, err := c.ListMachines(ctx)
if err != nil { if err != nil {
return fmt.Errorf("list cluster machines: %w", err) return nil, fmt.Errorf("list cluster machines: %w", err)
} }
otherMachines := make([]*pb.MachineInfo, 0, len(machines)-1) otherMachines := make([]*pb.MachineInfo, 0, len(machines)-1)
for _, m := range machines { for _, m := range machines {
@@ -238,24 +316,25 @@ func (cli *CLI) AddMachine(ctx context.Context, remoteMachine RemoteMachine, clu
OtherMachines: otherMachines, OtherMachines: otherMachines,
} }
if _, err = machineClient.JoinCluster(ctx, joinReq); err != nil { if _, err = machineClient.JoinCluster(ctx, joinReq); err != nil {
return fmt.Errorf("join cluster: %w", err) return nil, fmt.Errorf("join cluster: %w", err)
} }
fmt.Printf("Machine %q added to cluster\n", addResp.Machine.Name) fmt.Printf("Machine %q added to cluster\n", addResp.Machine.Name)
// Save the machine's SSH connection details in the cluster config. // Save the machine's SSH connection details in the cluster config.
connCfg := config.MachineConnection{ connCfg := config.MachineConnection{
SSH: config.NewSSHDestination(remoteMachine.User, remoteMachine.Host, remoteMachine.Port), SSH: config.NewSSHDestination(remoteMachine.User, remoteMachine.Host, remoteMachine.Port),
SSHKeyFile: remoteMachine.KeyPath,
} }
if clusterName == "" { if clusterName == "" {
clusterName = cli.config.CurrentCluster clusterName = cli.config.CurrentCluster
} }
cli.config.Clusters[clusterName].Connections = append(cli.config.Clusters[clusterName].Connections, connCfg) cli.config.Clusters[clusterName].Connections = append(cli.config.Clusters[clusterName].Connections, connCfg)
if err = cli.config.Save(); err != nil { if err = cli.config.Save(); err != nil {
return fmt.Errorf("save config: %w", err) return nil, fmt.Errorf("save config: %w", err)
} }
return nil return machineClient, nil
} }
// provisionRemoteMachine installs the Uncloud daemon and dependencies on the remote machine over SSH and returns // provisionRemoteMachine installs the Uncloud daemon and dependencies on the remote machine over SSH and returns
@@ -319,3 +398,8 @@ func (cli *CLI) promptResetMachine() error {
// TODO: implement resetting the remote machine. // TODO: implement resetting the remote machine.
return fmt.Errorf("resetting the remote machine is not implemented yet") return fmt.Errorf("resetting the remote machine is not implemented yet")
} }
// ProgressOut returns an output stream for progress writer.
func (cli *CLI) ProgressOut() *streams.Out {
return streams.NewOut(os.Stdout)
}
-15
View File
@@ -1,15 +0,0 @@
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
}
-678
View File
@@ -1,678 +0,0 @@
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
}
+6 -5
View File
@@ -5,7 +5,7 @@ import (
"net/netip" "net/netip"
"strconv" "strconv"
"strings" "strings"
"uncloud/internal/secret" "github.com/psviderski/uncloud/internal/secret"
) )
const ( const (
@@ -14,10 +14,11 @@ const (
) )
type MachineConnection struct { type MachineConnection struct {
SSH SSHDestination `toml:"ssh,omitempty"` SSH SSHDestination `toml:"ssh,omitempty"`
TCP netip.AddrPort `toml:"tcp,omitempty"` SSHKeyFile string `toml:"ssh_key_file,omitempty"`
Host string `toml:"host,omitempty"` TCP netip.AddrPort `toml:"tcp,omitempty"`
PublicKey secret.Secret `toml:"public_key,omitempty"` Host string `toml:"host,omitempty"`
PublicKey secret.Secret `toml:"public_key,omitempty"`
} }
// SSHDestination represents an SSH destination string in the canonical form of "user@host:port". // SSHDestination represents an SSH destination string in the canonical form of "user@host:port".
+23 -11
View File
@@ -4,7 +4,7 @@ import (
"context" "context"
"fmt" "fmt"
"os" "os"
"uncloud/internal/sshexec" "github.com/psviderski/uncloud/internal/sshexec"
) )
// TODO: support pinning the script version to the CLI version. // TODO: support pinning the script version to the CLI version.
@@ -17,24 +17,36 @@ type RemoteMachine struct {
KeyPath string KeyPath string
} }
func installCmd(user string) string {
sudoPrefix := "sudo"
// Add the SSH user (non-root) to the uncloud group to allow access to the Uncloud daemon unix socket.
env := "UNCLOUD_GROUP_ADD_USER=" + user
curlBashCmd := fmt.Sprintf(
"curl -fsSL %s | %s %s bash", sshexec.Quote(installScriptURL), sudoPrefix, sshexec.Quote(env),
)
if user == "root" {
curlBashCmd = fmt.Sprintf(
"curl -fsSL %s | bash", sshexec.Quote(installScriptURL),
)
}
return curlBashCmd
}
// provisionMachine provisions the remote machine by downloading the Uncloud install script from GitHub and running it. // provisionMachine provisions the remote machine by downloading the Uncloud install script from GitHub and running it.
func provisionMachine(ctx context.Context, exec sshexec.Executor) error { func provisionMachine(ctx context.Context, exec sshexec.Executor) error {
user, err := exec.Run(ctx, "whoami") user, err := exec.Run(ctx, "whoami")
if err != nil { if err != nil {
return fmt.Errorf("run whoami: %w", err) return fmt.Errorf("run whoami: %w", err)
} }
sudoPrefix, env := "", ""
if user != "root" { installCmd := installCmd(user)
sudoPrefix = "sudo"
// Add the SSH user (non-root) to the uncloud group to allow access to the Uncloud daemon unix socket.
env = "UNCLOUD_GROUP_ADD_USER=" + user
}
fmt.Println("Downloading Uncloud install script:", installScriptURL) fmt.Println("Downloading Uncloud install script:", installScriptURL)
curlBashCmd := fmt.Sprintf(
"curl -fsSL %s | %s %s bash", sshexec.Quote(installScriptURL), sudoPrefix, sshexec.Quote(env), cmd := sshexec.QuoteCommand("bash", "-c", "set -o pipefail; "+installCmd)
)
cmd := sshexec.QuoteCommand("bash", "-c", "set -o pipefail; "+curlBashCmd)
if err = exec.Stream(ctx, cmd, os.Stdout, os.Stderr); err != nil { if err = exec.Stream(ctx, cmd, os.Stdout, os.Stderr); err != nil {
return fmt.Errorf("download and run install script: %w", err) return fmt.Errorf("download and run install script: %w", err)
} }
+19
View File
@@ -0,0 +1,19 @@
package cli
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestInstallCmd(t *testing.T) {
t.Run("root", func(t *testing.T) {
cmd := installCmd("root")
assert.NotContains(t, cmd, "sudo")
})
t.Run("nonroot", func(t *testing.T) {
cmd := installCmd("nonroot")
assert.Contains(t, cmd, "sudo")
})
}
+23
View File
@@ -0,0 +1,23 @@
package cli
import "github.com/charmbracelet/huh"
func Confirm() (bool, error) {
var confirmed bool
form := huh.NewForm(
huh.NewGroup(
huh.NewConfirm().
Title(
"Do you want to continue?",
).
Affirmative("Yes!").
Negative("No").
Value(&confirmed),
),
)
if err := form.Run(); err != nil {
return false, err
}
return confirmed, nil
}
+1 -1
View File
@@ -5,7 +5,7 @@ import (
"fmt" "fmt"
systemd "github.com/coreos/go-systemd/daemon" systemd "github.com/coreos/go-systemd/daemon"
"log/slog" "log/slog"
"uncloud/internal/machine" "github.com/psviderski/uncloud/internal/machine"
) )
type Daemon struct { type Daemon struct {
+3 -3
View File
@@ -3,10 +3,10 @@ package daemon
import ( import (
"errors" "errors"
"fmt" "fmt"
"github.com/psviderski/uncloud/internal/machine"
"github.com/psviderski/uncloud/internal/machine/network"
"net/netip" "net/netip"
"os" "os"
"uncloud/internal/machine"
"uncloud/internal/machine/network"
) )
// MachineToken returns the local machine's token that can be used for adding the machine to a cluster. // MachineToken returns the local machine's token that can be used for adding the machine to a cluster.
@@ -37,5 +37,5 @@ func MachineToken(dataDir string) (machine.Token, error) {
for i, ip := range ips { for i, ip := range ips {
endpoints[i] = netip.AddrPortFrom(ip, network.WireGuardPort) endpoints[i] = netip.AddrPortFrom(ip, network.WireGuardPort)
} }
return machine.NewToken(state.Network.PublicKey, endpoints), nil return machine.NewToken(state.Network.PublicKey, publicIP, endpoints), nil
} }
+34
View File
@@ -0,0 +1,34 @@
package dns
const (
RecordTypeA RecordType = "A"
RecordTypeAAAA RecordType = "AAAA"
)
type RecordType string
type DomainResponse struct {
Name string `json:"name,omitempty"`
Token string `json:"token,omitempty"`
}
type RecordRequest struct {
Name string `json:"name,omitempty"`
Type RecordType `json:"type,omitempty"`
Values []string `json:"values,omitempty"`
}
type RecordResponse struct {
RecordRequest
FQDN string `json:"fqdn,omitempty"`
}
type AuthErrorResponse struct {
Status int `json:"status,omitempty"`
Message string `json:"msg,omitempty"`
Data authErrorData `json:"data,omitempty"`
}
type authErrorData struct {
NoDomain bool `json:"noDomain,omitempty"`
}
+152
View File
@@ -0,0 +1,152 @@
package dns
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"log/slog"
"net/http"
)
// The dns package code is based on https://github.com/acorn-io/runtime/blob/main/pkg/dns.
// Client handles interactions with the Uncloud DNS API service.
type Client interface {
// ReserveDomain calls Uncloud DNS to reserve a new domain. It returns the domain, a token for authentication,
// and an error.
ReserveDomain(endpoint string) (string, string, error)
// CreateRecords calls Uncloud DNS to create or update DNS records based on the supplied RecordRequests
// for the specified domain.
CreateRecords(endpoint, domain, token string, records []RecordRequest) ([]RecordResponse, error)
}
// ErrAuthNoDomain indicates that a request failed authentication because the domain was not found.
// If encountered, a new domain needs to be reserved.
var ErrAuthNoDomain = errors.New("the supplied domain failed authentication")
// NewClient creates a new AcornDNS client
func NewClient() Client {
return &client{
c: http.DefaultClient,
}
}
type client struct {
c *http.Client
}
func (c *client) ReserveDomain(endpoint string) (string, string, error) {
url := fmt.Sprintf("%s/%s", endpoint, "domains")
req, err := c.request(http.MethodPost, url, nil, "")
if err != nil {
return "", "", err
}
resp := &DomainResponse{}
err = c.do(req, resp)
if err != nil {
return "", "", err
}
return resp.Name, resp.Token, nil
}
func (c *client) CreateRecords(endpoint, domain, token string, records []RecordRequest) ([]RecordResponse, error) {
url := fmt.Sprintf("%s/domains/%s/records", endpoint, domain)
var resp []RecordResponse
for _, recordRequest := range records {
body, err := jsonBody(recordRequest)
if err != nil {
return resp, err
}
req, err := c.request(http.MethodPost, url, body, token)
if err != nil {
return resp, err
}
var recordResp RecordResponse
if err = c.do(req, &recordResp); err != nil {
return resp, err
}
resp = append(resp, recordResp)
}
return resp, nil
}
func (c *client) request(method string, url string, body io.Reader, token string) (*http.Request, error) {
req, err := http.NewRequest(method, url, body)
if err != nil {
return nil, err
}
req.Header.Add("Content-Type", "application/json")
if token != "" {
bearer := "Bearer " + token
req.Header.Add("Authorization", bearer)
}
return req, nil
}
func (c *client) do(req *http.Request, responseBody any) error {
slog.Debug("Making request to DNS service.", "method", req.Method, "url", req.URL)
resp, err := c.c.Do(req)
if err != nil {
return err
}
slog.Debug("Response code for request to DNS service.",
"method", req.Method, "url", req.URL, "code", resp.StatusCode)
// When err is nil, resp contains a non-nil resp.Body which must be closed.
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("read response body: %w", err)
}
if resp.StatusCode == http.StatusUnauthorized {
var authError AuthErrorResponse
err = json.Unmarshal(body, &authError)
if err != nil {
return fmt.Errorf("unmarshal auth error response: %w", err)
}
if authError.Data.NoDomain {
return ErrAuthNoDomain
}
return errors.New("authentication failed")
}
if code := resp.StatusCode; code < 200 || code > 300 {
return fmt.Errorf("unexpected response status code: %d", code)
}
if responseBody != nil {
err = json.Unmarshal(body, responseBody)
if err != nil {
return fmt.Errorf("unmarshal response body (%s): %w", string(body), err)
}
}
return nil
}
func jsonBody(payload any) (io.Reader, error) {
buf := &bytes.Buffer{}
err := json.NewEncoder(buf).Encode(payload)
if err != nil {
return nil, err
}
return buf, nil
}
+15
View File
@@ -5,8 +5,23 @@ import (
"os" "os"
"os/user" "os/user"
"strconv" "strconv"
"strings"
) )
func ExpandHomeDir(path string) string {
if len(path) == 0 {
return path
}
if path[0] == '~' {
home, err := os.UserHomeDir()
if err != nil {
return path
}
return strings.Replace(path, "~", home, 1)
}
return path
}
// LookupUIDGID returns the user and group IDs for the given username. // LookupUIDGID returns the user and group IDs for the given username.
func LookupUIDGID(username string) (uid, gid int, err error) { func LookupUIDGID(username string) (uid, gid int, err error) {
usr, err := user.Lookup(username) usr, err := user.Lookup(username)
+22
View File
@@ -0,0 +1,22 @@
package fs
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestExpandHomeDir(t *testing.T) {
t.Run("empty", func(t *testing.T) {
assert.Equal(t, "", ExpandHomeDir(""))
})
t.Run("no home", func(t *testing.T) {
assert.Equal(t, "/path", ExpandHomeDir("/path"))
})
t.Run("home", func(t *testing.T) {
t.Setenv("HOME", "/home/user")
assert.Equal(t, "/home/user/path", ExpandHomeDir("~/path"))
})
}
+501 -69
View File
@@ -78,13 +78,63 @@ func (MachineMember_MembershipState) EnumDescriptor() ([]byte, []int) {
return file_internal_machine_api_pb_cluster_proto_rawDescGZIP(), []int{2, 0} return file_internal_machine_api_pb_cluster_proto_rawDescGZIP(), []int{2, 0}
} }
type DNSRecord_RecordType int32
const (
DNSRecord_UNSPECIFIED DNSRecord_RecordType = 0
DNSRecord_A DNSRecord_RecordType = 1
DNSRecord_AAAA DNSRecord_RecordType = 2
)
// Enum value maps for DNSRecord_RecordType.
var (
DNSRecord_RecordType_name = map[int32]string{
0: "UNSPECIFIED",
1: "A",
2: "AAAA",
}
DNSRecord_RecordType_value = map[string]int32{
"UNSPECIFIED": 0,
"A": 1,
"AAAA": 2,
}
)
func (x DNSRecord_RecordType) Enum() *DNSRecord_RecordType {
p := new(DNSRecord_RecordType)
*p = x
return p
}
func (x DNSRecord_RecordType) String() string {
return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
}
func (DNSRecord_RecordType) Descriptor() protoreflect.EnumDescriptor {
return file_internal_machine_api_pb_cluster_proto_enumTypes[1].Descriptor()
}
func (DNSRecord_RecordType) Type() protoreflect.EnumType {
return &file_internal_machine_api_pb_cluster_proto_enumTypes[1]
}
func (x DNSRecord_RecordType) Number() protoreflect.EnumNumber {
return protoreflect.EnumNumber(x)
}
// Deprecated: Use DNSRecord_RecordType.Descriptor instead.
func (DNSRecord_RecordType) EnumDescriptor() ([]byte, []int) {
return file_internal_machine_api_pb_cluster_proto_rawDescGZIP(), []int{8, 0}
}
type AddMachineRequest struct { type AddMachineRequest struct {
state protoimpl.MessageState state protoimpl.MessageState
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields unknownFields protoimpl.UnknownFields
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
Network *NetworkConfig `protobuf:"bytes,2,opt,name=network,proto3" json:"network,omitempty"` Network *NetworkConfig `protobuf:"bytes,2,opt,name=network,proto3" json:"network,omitempty"`
PublicIp *IP `protobuf:"bytes,3,opt,name=public_ip,json=publicIp,proto3" json:"public_ip,omitempty"`
} }
func (x *AddMachineRequest) Reset() { func (x *AddMachineRequest) Reset() {
@@ -133,6 +183,13 @@ func (x *AddMachineRequest) GetNetwork() *NetworkConfig {
return nil return nil
} }
func (x *AddMachineRequest) GetPublicIp() *IP {
if x != nil {
return x.PublicIp
}
return nil
}
type AddMachineResponse struct { type AddMachineResponse struct {
state protoimpl.MessageState state protoimpl.MessageState
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
@@ -282,6 +339,257 @@ func (x *ListMachinesResponse) GetMachines() []*MachineMember {
return nil return nil
} }
type Domain struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
}
func (x *Domain) Reset() {
*x = Domain{}
if protoimpl.UnsafeEnabled {
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *Domain) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Domain) ProtoMessage() {}
func (x *Domain) ProtoReflect() protoreflect.Message {
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[4]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Domain.ProtoReflect.Descriptor instead.
func (*Domain) Descriptor() ([]byte, []int) {
return file_internal_machine_api_pb_cluster_proto_rawDescGZIP(), []int{4}
}
func (x *Domain) GetName() string {
if x != nil {
return x.Name
}
return ""
}
type ReserveDomainRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Endpoint string `protobuf:"bytes,1,opt,name=endpoint,proto3" json:"endpoint,omitempty"`
}
func (x *ReserveDomainRequest) Reset() {
*x = ReserveDomainRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[5]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *ReserveDomainRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ReserveDomainRequest) ProtoMessage() {}
func (x *ReserveDomainRequest) ProtoReflect() protoreflect.Message {
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[5]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ReserveDomainRequest.ProtoReflect.Descriptor instead.
func (*ReserveDomainRequest) Descriptor() ([]byte, []int) {
return file_internal_machine_api_pb_cluster_proto_rawDescGZIP(), []int{5}
}
func (x *ReserveDomainRequest) GetEndpoint() string {
if x != nil {
return x.Endpoint
}
return ""
}
type CreateDomainRecordsRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Records []*DNSRecord `protobuf:"bytes,1,rep,name=records,proto3" json:"records,omitempty"`
}
func (x *CreateDomainRecordsRequest) Reset() {
*x = CreateDomainRecordsRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[6]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *CreateDomainRecordsRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*CreateDomainRecordsRequest) ProtoMessage() {}
func (x *CreateDomainRecordsRequest) ProtoReflect() protoreflect.Message {
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[6]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use CreateDomainRecordsRequest.ProtoReflect.Descriptor instead.
func (*CreateDomainRecordsRequest) Descriptor() ([]byte, []int) {
return file_internal_machine_api_pb_cluster_proto_rawDescGZIP(), []int{6}
}
func (x *CreateDomainRecordsRequest) GetRecords() []*DNSRecord {
if x != nil {
return x.Records
}
return nil
}
type CreateDomainRecordsResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Records []*DNSRecord `protobuf:"bytes,1,rep,name=records,proto3" json:"records,omitempty"`
}
func (x *CreateDomainRecordsResponse) Reset() {
*x = CreateDomainRecordsResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[7]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *CreateDomainRecordsResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*CreateDomainRecordsResponse) ProtoMessage() {}
func (x *CreateDomainRecordsResponse) ProtoReflect() protoreflect.Message {
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[7]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use CreateDomainRecordsResponse.ProtoReflect.Descriptor instead.
func (*CreateDomainRecordsResponse) Descriptor() ([]byte, []int) {
return file_internal_machine_api_pb_cluster_proto_rawDescGZIP(), []int{7}
}
func (x *CreateDomainRecordsResponse) GetRecords() []*DNSRecord {
if x != nil {
return x.Records
}
return nil
}
type DNSRecord struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
Type DNSRecord_RecordType `protobuf:"varint,2,opt,name=type,proto3,enum=api.DNSRecord_RecordType" json:"type,omitempty"`
Values []string `protobuf:"bytes,3,rep,name=values,proto3" json:"values,omitempty"`
}
func (x *DNSRecord) Reset() {
*x = DNSRecord{}
if protoimpl.UnsafeEnabled {
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[8]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *DNSRecord) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*DNSRecord) ProtoMessage() {}
func (x *DNSRecord) ProtoReflect() protoreflect.Message {
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[8]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use DNSRecord.ProtoReflect.Descriptor instead.
func (*DNSRecord) Descriptor() ([]byte, []int) {
return file_internal_machine_api_pb_cluster_proto_rawDescGZIP(), []int{8}
}
func (x *DNSRecord) GetName() string {
if x != nil {
return x.Name
}
return ""
}
func (x *DNSRecord) GetType() DNSRecord_RecordType {
if x != nil {
return x.Type
}
return DNSRecord_UNSPECIFIED
}
func (x *DNSRecord) GetValues() []string {
if x != nil {
return x.Values
}
return nil
}
var File_internal_machine_api_pb_cluster_proto protoreflect.FileDescriptor var File_internal_machine_api_pb_cluster_proto protoreflect.FileDescriptor
var file_internal_machine_api_pb_cluster_proto_rawDesc = []byte{ var file_internal_machine_api_pb_cluster_proto_rawDesc = []byte{
@@ -289,48 +597,92 @@ var file_internal_machine_api_pb_cluster_proto_rawDesc = []byte{
0x6e, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x62, 0x2f, 0x63, 0x6c, 0x75, 0x73, 0x74, 0x65, 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, 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, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x65, 0x6d,
0x70, 0x74, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x25, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x70, 0x74, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x24, 0x69, 0x6e, 0x74, 0x65, 0x72,
0x6e, 0x61, 0x6c, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x6e, 0x61, 0x6c, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f,
0x70, 0x62, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x70, 0x62, 0x2f, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a,
0x22, 0x55, 0x0a, 0x11, 0x41, 0x64, 0x64, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x25, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e,
0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x62, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65,
0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x2c, 0x0a, 0x07, 0x6e, 0x65, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x7b, 0x0a, 0x11, 0x41, 0x64, 0x64, 0x4d, 0x61, 0x63,
0x77, 0x6f, 0x72, 0x6b, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x61, 0x70, 0x69, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x6e,
0x2e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x07, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12,
0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x22, 0x40, 0x0a, 0x12, 0x41, 0x64, 0x64, 0x4d, 0x61, 0x2c, 0x0a, 0x07, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b,
0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2a, 0x0a, 0x32, 0x12, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x43, 0x6f,
0x07, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x07, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x12, 0x24, 0x0a,
0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x69, 0x70, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b,
0x52, 0x07, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x22, 0xb4, 0x01, 0x0a, 0x0d, 0x4d, 0x61, 0x32, 0x07, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x50, 0x52, 0x08, 0x70, 0x75, 0x62, 0x6c, 0x69,
0x63, 0x68, 0x69, 0x6e, 0x65, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x12, 0x2a, 0x0a, 0x07, 0x6d, 0x63, 0x49, 0x70, 0x22, 0x40, 0x0a, 0x12, 0x41, 0x64, 0x64, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e,
0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x61, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2a, 0x0a, 0x07, 0x6d, 0x61, 0x63,
0x70, 0x69, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x07, 0x68, 0x69, 0x6e, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x61, 0x70, 0x69,
0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x12, 0x38, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x07, 0x6d, 0x61,
0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x22, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d, 0x61, 0x63, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x22, 0xb4, 0x01, 0x0a, 0x0d, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e,
0x68, 0x69, 0x6e, 0x65, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x2e, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x65, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x12, 0x2a, 0x0a, 0x07, 0x6d, 0x61, 0x63, 0x68, 0x69,
0x72, 0x73, 0x68, 0x69, 0x70, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x6e, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d,
0x65, 0x22, 0x3d, 0x0a, 0x0f, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, 0x68, 0x69, 0x70, 0x53, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x07, 0x6d, 0x61, 0x63, 0x68,
0x74, 0x61, 0x74, 0x65, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x69, 0x6e, 0x65, 0x12, 0x38, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01,
0x00, 0x12, 0x06, 0x0a, 0x02, 0x55, 0x50, 0x10, 0x01, 0x12, 0x0b, 0x0a, 0x07, 0x53, 0x55, 0x53, 0x28, 0x0e, 0x32, 0x22, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65,
0x50, 0x45, 0x43, 0x54, 0x10, 0x02, 0x12, 0x08, 0x0a, 0x04, 0x44, 0x4f, 0x57, 0x4e, 0x10, 0x03, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x2e, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, 0x68, 0x69,
0x22, 0x46, 0x0a, 0x14, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x70, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x22, 0x3d, 0x0a,
0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2e, 0x0a, 0x08, 0x6d, 0x61, 0x63, 0x68, 0x0f, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, 0x68, 0x69, 0x70, 0x53, 0x74, 0x61, 0x74, 0x65,
0x69, 0x6e, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x61, 0x70, 0x69, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x06, 0x0a,
0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x52, 0x08, 0x02, 0x55, 0x50, 0x10, 0x01, 0x12, 0x0b, 0x0a, 0x07, 0x53, 0x55, 0x53, 0x50, 0x45, 0x43, 0x54,
0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x32, 0x8b, 0x01, 0x0a, 0x07, 0x43, 0x6c, 0x75, 0x10, 0x02, 0x12, 0x08, 0x0a, 0x04, 0x44, 0x4f, 0x57, 0x4e, 0x10, 0x03, 0x22, 0x46, 0x0a, 0x14,
0x73, 0x74, 0x65, 0x72, 0x12, 0x3d, 0x0a, 0x0a, 0x41, 0x64, 0x64, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70,
0x6e, 0x65, 0x12, 0x16, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x41, 0x64, 0x64, 0x4d, 0x61, 0x63, 0x68, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2e, 0x0a, 0x08, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73,
0x69, 0x6e, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x17, 0x2e, 0x61, 0x70, 0x69, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d, 0x61, 0x63,
0x2e, 0x41, 0x64, 0x64, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x68, 0x69, 0x6e, 0x65, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x52, 0x08, 0x6d, 0x61, 0x63, 0x68,
0x6e, 0x73, 0x65, 0x12, 0x41, 0x0a, 0x0c, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x69, 0x6e, 0x65, 0x73, 0x22, 0x1c, 0x0a, 0x06, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x12,
0x6e, 0x65, 0x73, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61,
0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x19, 0x2e, 0x61, 0x70, 0x6d, 0x65, 0x22, 0x32, 0x0a, 0x14, 0x52, 0x65, 0x73, 0x65, 0x72, 0x76, 0x65, 0x44, 0x6f, 0x6d,
0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x52, 0x65, 0x61, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x65, 0x6e,
0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x37, 0x5a, 0x35, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x65, 0x6e,
0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x70, 0x73, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x6b, 0x69, 0x2f, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x22, 0x46, 0x0a, 0x1a, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65,
0x75, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x52, 0x65, 0x71,
0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x62, 0x62, 0x75, 0x65, 0x73, 0x74, 0x12, 0x28, 0x0a, 0x07, 0x72, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x18,
0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x44, 0x4e, 0x53, 0x52,
0x65, 0x63, 0x6f, 0x72, 0x64, 0x52, 0x07, 0x72, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x22, 0x47,
0x0a, 0x1b, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x52, 0x65,
0x63, 0x6f, 0x72, 0x64, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x28, 0x0a,
0x07, 0x72, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0e,
0x2e, 0x61, 0x70, 0x69, 0x2e, 0x44, 0x4e, 0x53, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x52, 0x07,
0x72, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x22, 0x96, 0x01, 0x0a, 0x09, 0x44, 0x4e, 0x53, 0x52,
0x65, 0x63, 0x6f, 0x72, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20,
0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x2d, 0x0a, 0x04, 0x74, 0x79, 0x70,
0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x19, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x44, 0x4e,
0x53, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x54, 0x79,
0x70, 0x65, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x76, 0x61, 0x6c, 0x75,
0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73,
0x22, 0x2e, 0x0a, 0x0a, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x54, 0x79, 0x70, 0x65, 0x12, 0x0f,
0x0a, 0x0b, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12,
0x05, 0x0a, 0x01, 0x41, 0x10, 0x01, 0x12, 0x08, 0x0a, 0x04, 0x41, 0x41, 0x41, 0x41, 0x10, 0x02,
0x32, 0x86, 0x03, 0x0a, 0x07, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x12, 0x3d, 0x0a, 0x0a,
0x41, 0x64, 0x64, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x12, 0x16, 0x2e, 0x61, 0x70, 0x69,
0x2e, 0x41, 0x64, 0x64, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65,
0x73, 0x74, 0x1a, 0x17, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x41, 0x64, 0x64, 0x4d, 0x61, 0x63, 0x68,
0x69, 0x6e, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x41, 0x0a, 0x0c, 0x4c,
0x69, 0x73, 0x74, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x12, 0x16, 0x2e, 0x67, 0x6f,
0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d,
0x70, 0x74, 0x79, 0x1a, 0x19, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x61,
0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x37,
0x0a, 0x0d, 0x52, 0x65, 0x73, 0x65, 0x72, 0x76, 0x65, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12,
0x19, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x52, 0x65, 0x73, 0x65, 0x72, 0x76, 0x65, 0x44, 0x6f, 0x6d,
0x61, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0b, 0x2e, 0x61, 0x70, 0x69,
0x2e, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x30, 0x0a, 0x09, 0x47, 0x65, 0x74, 0x44, 0x6f,
0x6d, 0x61, 0x69, 0x6e, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72,
0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x0b, 0x2e, 0x61,
0x70, 0x69, 0x2e, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x34, 0x0a, 0x0d, 0x52, 0x65, 0x6c,
0x65, 0x61, 0x73, 0x65, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f,
0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70,
0x74, 0x79, 0x1a, 0x0b, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12,
0x58, 0x0a, 0x13, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x52,
0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x12, 0x1f, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x72, 0x65,
0x61, 0x74, 0x65, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73,
0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x72,
0x65, 0x61, 0x74, 0x65, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64,
0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x37, 0x5a, 0x35, 0x67, 0x69, 0x74,
0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x70, 0x73, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73,
0x6b, 0x69, 0x2f, 0x75, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72,
0x6e, 0x61, 0x6c, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f,
0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
} }
var ( var (
@@ -345,33 +697,52 @@ func file_internal_machine_api_pb_cluster_proto_rawDescGZIP() []byte {
return file_internal_machine_api_pb_cluster_proto_rawDescData return file_internal_machine_api_pb_cluster_proto_rawDescData
} }
var file_internal_machine_api_pb_cluster_proto_enumTypes = make([]protoimpl.EnumInfo, 1) var file_internal_machine_api_pb_cluster_proto_enumTypes = make([]protoimpl.EnumInfo, 2)
var file_internal_machine_api_pb_cluster_proto_msgTypes = make([]protoimpl.MessageInfo, 4) var file_internal_machine_api_pb_cluster_proto_msgTypes = make([]protoimpl.MessageInfo, 9)
var file_internal_machine_api_pb_cluster_proto_goTypes = []any{ var file_internal_machine_api_pb_cluster_proto_goTypes = []any{
(MachineMember_MembershipState)(0), // 0: api.MachineMember.MembershipState (MachineMember_MembershipState)(0), // 0: api.MachineMember.MembershipState
(*AddMachineRequest)(nil), // 1: api.AddMachineRequest (DNSRecord_RecordType)(0), // 1: api.DNSRecord.RecordType
(*AddMachineResponse)(nil), // 2: api.AddMachineResponse (*AddMachineRequest)(nil), // 2: api.AddMachineRequest
(*MachineMember)(nil), // 3: api.MachineMember (*AddMachineResponse)(nil), // 3: api.AddMachineResponse
(*ListMachinesResponse)(nil), // 4: api.ListMachinesResponse (*MachineMember)(nil), // 4: api.MachineMember
(*NetworkConfig)(nil), // 5: api.NetworkConfig (*ListMachinesResponse)(nil), // 5: api.ListMachinesResponse
(*MachineInfo)(nil), // 6: api.MachineInfo (*Domain)(nil), // 6: api.Domain
(*emptypb.Empty)(nil), // 7: google.protobuf.Empty (*ReserveDomainRequest)(nil), // 7: api.ReserveDomainRequest
(*CreateDomainRecordsRequest)(nil), // 8: api.CreateDomainRecordsRequest
(*CreateDomainRecordsResponse)(nil), // 9: api.CreateDomainRecordsResponse
(*DNSRecord)(nil), // 10: api.DNSRecord
(*NetworkConfig)(nil), // 11: api.NetworkConfig
(*IP)(nil), // 12: api.IP
(*MachineInfo)(nil), // 13: api.MachineInfo
(*emptypb.Empty)(nil), // 14: google.protobuf.Empty
} }
var file_internal_machine_api_pb_cluster_proto_depIdxs = []int32{ var file_internal_machine_api_pb_cluster_proto_depIdxs = []int32{
5, // 0: api.AddMachineRequest.network:type_name -> api.NetworkConfig 11, // 0: api.AddMachineRequest.network:type_name -> api.NetworkConfig
6, // 1: api.AddMachineResponse.machine:type_name -> api.MachineInfo 12, // 1: api.AddMachineRequest.public_ip:type_name -> api.IP
6, // 2: api.MachineMember.machine:type_name -> api.MachineInfo 13, // 2: api.AddMachineResponse.machine:type_name -> api.MachineInfo
0, // 3: api.MachineMember.state:type_name -> api.MachineMember.MembershipState 13, // 3: api.MachineMember.machine:type_name -> api.MachineInfo
3, // 4: api.ListMachinesResponse.machines:type_name -> api.MachineMember 0, // 4: api.MachineMember.state:type_name -> api.MachineMember.MembershipState
1, // 5: api.Cluster.AddMachine:input_type -> api.AddMachineRequest 4, // 5: api.ListMachinesResponse.machines:type_name -> api.MachineMember
7, // 6: api.Cluster.ListMachines:input_type -> google.protobuf.Empty 10, // 6: api.CreateDomainRecordsRequest.records:type_name -> api.DNSRecord
2, // 7: api.Cluster.AddMachine:output_type -> api.AddMachineResponse 10, // 7: api.CreateDomainRecordsResponse.records:type_name -> api.DNSRecord
4, // 8: api.Cluster.ListMachines:output_type -> api.ListMachinesResponse 1, // 8: api.DNSRecord.type:type_name -> api.DNSRecord.RecordType
7, // [7:9] is the sub-list for method output_type 2, // 9: api.Cluster.AddMachine:input_type -> api.AddMachineRequest
5, // [5:7] is the sub-list for method input_type 14, // 10: api.Cluster.ListMachines:input_type -> google.protobuf.Empty
5, // [5:5] is the sub-list for extension type_name 7, // 11: api.Cluster.ReserveDomain:input_type -> api.ReserveDomainRequest
5, // [5:5] is the sub-list for extension extendee 14, // 12: api.Cluster.GetDomain:input_type -> google.protobuf.Empty
0, // [0:5] is the sub-list for field type_name 14, // 13: api.Cluster.ReleaseDomain:input_type -> google.protobuf.Empty
8, // 14: api.Cluster.CreateDomainRecords:input_type -> api.CreateDomainRecordsRequest
3, // 15: api.Cluster.AddMachine:output_type -> api.AddMachineResponse
5, // 16: api.Cluster.ListMachines:output_type -> api.ListMachinesResponse
6, // 17: api.Cluster.ReserveDomain:output_type -> api.Domain
6, // 18: api.Cluster.GetDomain:output_type -> api.Domain
6, // 19: api.Cluster.ReleaseDomain:output_type -> api.Domain
9, // 20: api.Cluster.CreateDomainRecords:output_type -> api.CreateDomainRecordsResponse
15, // [15:21] is the sub-list for method output_type
9, // [9:15] is the sub-list for method input_type
9, // [9:9] is the sub-list for extension type_name
9, // [9:9] is the sub-list for extension extendee
0, // [0:9] is the sub-list for field type_name
} }
func init() { file_internal_machine_api_pb_cluster_proto_init() } func init() { file_internal_machine_api_pb_cluster_proto_init() }
@@ -379,6 +750,7 @@ func file_internal_machine_api_pb_cluster_proto_init() {
if File_internal_machine_api_pb_cluster_proto != nil { if File_internal_machine_api_pb_cluster_proto != nil {
return return
} }
file_internal_machine_api_pb_common_proto_init()
file_internal_machine_api_pb_machine_proto_init() file_internal_machine_api_pb_machine_proto_init()
if !protoimpl.UnsafeEnabled { if !protoimpl.UnsafeEnabled {
file_internal_machine_api_pb_cluster_proto_msgTypes[0].Exporter = func(v any, i int) any { file_internal_machine_api_pb_cluster_proto_msgTypes[0].Exporter = func(v any, i int) any {
@@ -429,14 +801,74 @@ func file_internal_machine_api_pb_cluster_proto_init() {
return nil return nil
} }
} }
file_internal_machine_api_pb_cluster_proto_msgTypes[4].Exporter = func(v any, i int) any {
switch v := v.(*Domain); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_internal_machine_api_pb_cluster_proto_msgTypes[5].Exporter = func(v any, i int) any {
switch v := v.(*ReserveDomainRequest); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_internal_machine_api_pb_cluster_proto_msgTypes[6].Exporter = func(v any, i int) any {
switch v := v.(*CreateDomainRecordsRequest); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_internal_machine_api_pb_cluster_proto_msgTypes[7].Exporter = func(v any, i int) any {
switch v := v.(*CreateDomainRecordsResponse); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_internal_machine_api_pb_cluster_proto_msgTypes[8].Exporter = func(v any, i int) any {
switch v := v.(*DNSRecord); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
} }
type x struct{} type x struct{}
out := protoimpl.TypeBuilder{ out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{ File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(), GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_internal_machine_api_pb_cluster_proto_rawDesc, RawDescriptor: file_internal_machine_api_pb_cluster_proto_rawDesc,
NumEnums: 1, NumEnums: 2,
NumMessages: 4, NumMessages: 9,
NumExtensions: 0, NumExtensions: 0,
NumServices: 1, NumServices: 1,
}, },
+35
View File
@@ -5,16 +5,23 @@ package api;
option go_package = "github.com/psviderski/uncloud/internal/machine/api/pb"; option go_package = "github.com/psviderski/uncloud/internal/machine/api/pb";
import "google/protobuf/empty.proto"; import "google/protobuf/empty.proto";
import "internal/machine/api/pb/common.proto";
import "internal/machine/api/pb/machine.proto"; import "internal/machine/api/pb/machine.proto";
service Cluster { service Cluster {
rpc AddMachine(AddMachineRequest) returns (AddMachineResponse); rpc AddMachine(AddMachineRequest) returns (AddMachineResponse);
rpc ListMachines(google.protobuf.Empty) returns (ListMachinesResponse); rpc ListMachines(google.protobuf.Empty) returns (ListMachinesResponse);
rpc ReserveDomain(ReserveDomainRequest) returns (Domain);
rpc GetDomain(google.protobuf.Empty) returns (Domain);
rpc ReleaseDomain(google.protobuf.Empty) returns (Domain);
rpc CreateDomainRecords(CreateDomainRecordsRequest) returns (CreateDomainRecordsResponse);
} }
message AddMachineRequest { message AddMachineRequest {
string name = 1; string name = 1;
NetworkConfig network = 2; NetworkConfig network = 2;
IP public_ip = 3;
} }
message AddMachineResponse { message AddMachineResponse {
@@ -41,3 +48,31 @@ message MachineMember {
message ListMachinesResponse { message ListMachinesResponse {
repeated MachineMember machines = 1; repeated MachineMember machines = 1;
} }
message Domain {
string name = 1;
}
message ReserveDomainRequest {
string endpoint = 1;
}
message CreateDomainRecordsRequest {
repeated DNSRecord records = 1;
}
message CreateDomainRecordsResponse {
repeated DNSRecord records = 1;
}
message DNSRecord {
string name = 1;
enum RecordType {
UNSPECIFIED = 0;
A = 1;
AAAA = 2;
}
RecordType type = 2;
repeated string values = 3;
}
+154 -2
View File
@@ -20,8 +20,12 @@ import (
const _ = grpc.SupportPackageIsVersion9 const _ = grpc.SupportPackageIsVersion9
const ( const (
Cluster_AddMachine_FullMethodName = "/api.Cluster/AddMachine" Cluster_AddMachine_FullMethodName = "/api.Cluster/AddMachine"
Cluster_ListMachines_FullMethodName = "/api.Cluster/ListMachines" Cluster_ListMachines_FullMethodName = "/api.Cluster/ListMachines"
Cluster_ReserveDomain_FullMethodName = "/api.Cluster/ReserveDomain"
Cluster_GetDomain_FullMethodName = "/api.Cluster/GetDomain"
Cluster_ReleaseDomain_FullMethodName = "/api.Cluster/ReleaseDomain"
Cluster_CreateDomainRecords_FullMethodName = "/api.Cluster/CreateDomainRecords"
) )
// ClusterClient is the client API for Cluster service. // ClusterClient is the client API for Cluster service.
@@ -30,6 +34,10 @@ const (
type ClusterClient interface { type ClusterClient interface {
AddMachine(ctx context.Context, in *AddMachineRequest, opts ...grpc.CallOption) (*AddMachineResponse, error) AddMachine(ctx context.Context, in *AddMachineRequest, opts ...grpc.CallOption) (*AddMachineResponse, error)
ListMachines(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*ListMachinesResponse, error) ListMachines(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*ListMachinesResponse, error)
ReserveDomain(ctx context.Context, in *ReserveDomainRequest, opts ...grpc.CallOption) (*Domain, error)
GetDomain(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*Domain, error)
ReleaseDomain(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*Domain, error)
CreateDomainRecords(ctx context.Context, in *CreateDomainRecordsRequest, opts ...grpc.CallOption) (*CreateDomainRecordsResponse, error)
} }
type clusterClient struct { type clusterClient struct {
@@ -60,12 +68,56 @@ func (c *clusterClient) ListMachines(ctx context.Context, in *emptypb.Empty, opt
return out, nil return out, nil
} }
func (c *clusterClient) ReserveDomain(ctx context.Context, in *ReserveDomainRequest, opts ...grpc.CallOption) (*Domain, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(Domain)
err := c.cc.Invoke(ctx, Cluster_ReserveDomain_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *clusterClient) GetDomain(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*Domain, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(Domain)
err := c.cc.Invoke(ctx, Cluster_GetDomain_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *clusterClient) ReleaseDomain(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*Domain, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(Domain)
err := c.cc.Invoke(ctx, Cluster_ReleaseDomain_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *clusterClient) CreateDomainRecords(ctx context.Context, in *CreateDomainRecordsRequest, opts ...grpc.CallOption) (*CreateDomainRecordsResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(CreateDomainRecordsResponse)
err := c.cc.Invoke(ctx, Cluster_CreateDomainRecords_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
// ClusterServer is the server API for Cluster service. // ClusterServer is the server API for Cluster service.
// All implementations must embed UnimplementedClusterServer // All implementations must embed UnimplementedClusterServer
// for forward compatibility. // for forward compatibility.
type ClusterServer interface { type ClusterServer interface {
AddMachine(context.Context, *AddMachineRequest) (*AddMachineResponse, error) AddMachine(context.Context, *AddMachineRequest) (*AddMachineResponse, error)
ListMachines(context.Context, *emptypb.Empty) (*ListMachinesResponse, error) ListMachines(context.Context, *emptypb.Empty) (*ListMachinesResponse, error)
ReserveDomain(context.Context, *ReserveDomainRequest) (*Domain, error)
GetDomain(context.Context, *emptypb.Empty) (*Domain, error)
ReleaseDomain(context.Context, *emptypb.Empty) (*Domain, error)
CreateDomainRecords(context.Context, *CreateDomainRecordsRequest) (*CreateDomainRecordsResponse, error)
mustEmbedUnimplementedClusterServer() mustEmbedUnimplementedClusterServer()
} }
@@ -82,6 +134,18 @@ func (UnimplementedClusterServer) AddMachine(context.Context, *AddMachineRequest
func (UnimplementedClusterServer) ListMachines(context.Context, *emptypb.Empty) (*ListMachinesResponse, error) { func (UnimplementedClusterServer) ListMachines(context.Context, *emptypb.Empty) (*ListMachinesResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ListMachines not implemented") return nil, status.Errorf(codes.Unimplemented, "method ListMachines not implemented")
} }
func (UnimplementedClusterServer) ReserveDomain(context.Context, *ReserveDomainRequest) (*Domain, error) {
return nil, status.Errorf(codes.Unimplemented, "method ReserveDomain not implemented")
}
func (UnimplementedClusterServer) GetDomain(context.Context, *emptypb.Empty) (*Domain, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetDomain not implemented")
}
func (UnimplementedClusterServer) ReleaseDomain(context.Context, *emptypb.Empty) (*Domain, error) {
return nil, status.Errorf(codes.Unimplemented, "method ReleaseDomain not implemented")
}
func (UnimplementedClusterServer) CreateDomainRecords(context.Context, *CreateDomainRecordsRequest) (*CreateDomainRecordsResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method CreateDomainRecords not implemented")
}
func (UnimplementedClusterServer) mustEmbedUnimplementedClusterServer() {} func (UnimplementedClusterServer) mustEmbedUnimplementedClusterServer() {}
func (UnimplementedClusterServer) testEmbeddedByValue() {} func (UnimplementedClusterServer) testEmbeddedByValue() {}
@@ -139,6 +203,78 @@ func _Cluster_ListMachines_Handler(srv interface{}, ctx context.Context, dec fun
return interceptor(ctx, in, info, handler) return interceptor(ctx, in, info, handler)
} }
func _Cluster_ReserveDomain_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ReserveDomainRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(ClusterServer).ReserveDomain(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: Cluster_ReserveDomain_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ClusterServer).ReserveDomain(ctx, req.(*ReserveDomainRequest))
}
return interceptor(ctx, in, info, handler)
}
func _Cluster_GetDomain_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(emptypb.Empty)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(ClusterServer).GetDomain(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: Cluster_GetDomain_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ClusterServer).GetDomain(ctx, req.(*emptypb.Empty))
}
return interceptor(ctx, in, info, handler)
}
func _Cluster_ReleaseDomain_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(emptypb.Empty)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(ClusterServer).ReleaseDomain(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: Cluster_ReleaseDomain_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ClusterServer).ReleaseDomain(ctx, req.(*emptypb.Empty))
}
return interceptor(ctx, in, info, handler)
}
func _Cluster_CreateDomainRecords_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(CreateDomainRecordsRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(ClusterServer).CreateDomainRecords(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: Cluster_CreateDomainRecords_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ClusterServer).CreateDomainRecords(ctx, req.(*CreateDomainRecordsRequest))
}
return interceptor(ctx, in, info, handler)
}
// Cluster_ServiceDesc is the grpc.ServiceDesc for Cluster service. // Cluster_ServiceDesc is the grpc.ServiceDesc for Cluster service.
// It's only intended for direct use with grpc.RegisterService, // It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy) // and not to be introspected or modified (even as a copy)
@@ -154,6 +290,22 @@ var Cluster_ServiceDesc = grpc.ServiceDesc{
MethodName: "ListMachines", MethodName: "ListMachines",
Handler: _Cluster_ListMachines_Handler, Handler: _Cluster_ListMachines_Handler,
}, },
{
MethodName: "ReserveDomain",
Handler: _Cluster_ReserveDomain_Handler,
},
{
MethodName: "GetDomain",
Handler: _Cluster_GetDomain_Handler,
},
{
MethodName: "ReleaseDomain",
Handler: _Cluster_ReleaseDomain_Handler,
},
{
MethodName: "CreateDomainRecords",
Handler: _Cluster_CreateDomainRecords_Handler,
},
}, },
Streams: []grpc.StreamDesc{}, Streams: []grpc.StreamDesc{},
Metadata: "internal/machine/api/pb/cluster.proto", Metadata: "internal/machine/api/pb/cluster.proto",
File diff suppressed because it is too large Load Diff
+101 -12
View File
@@ -9,64 +9,153 @@ import "internal/machine/api/pb/common.proto";
service Docker { service Docker {
rpc CreateContainer(CreateContainerRequest) returns (CreateContainerResponse); rpc CreateContainer(CreateContainerRequest) returns (CreateContainerResponse);
rpc InspectContainer(InspectContainerRequest) returns (InspectContainerResponse);
rpc StartContainer(StartContainerRequest) returns (google.protobuf.Empty); rpc StartContainer(StartContainerRequest) returns (google.protobuf.Empty);
rpc StopContainer(StopContainerRequest) returns (google.protobuf.Empty);
rpc ListContainers(ListContainersRequest) returns (ListContainersResponse); rpc ListContainers(ListContainersRequest) returns (ListContainersResponse);
rpc RemoveContainer(RemoveContainerRequest) returns (google.protobuf.Empty); rpc RemoveContainer(RemoveContainerRequest) returns (google.protobuf.Empty);
rpc PullImage(PullImageRequest) returns (stream JSONMessage); rpc PullImage(PullImageRequest) returns (stream JSONMessage);
rpc InspectImage(InspectImageRequest) returns (InspectImageResponse);
// InspectRemoteImage returns the image metadata for an image in a remote registry using the machine's
// Docker auth credentials if necessary.
rpc InspectRemoteImage(InspectRemoteImageRequest) returns (InspectRemoteImageResponse);
rpc CreateServiceContainer(CreateServiceContainerRequest) returns (CreateContainerResponse);
rpc InspectServiceContainer(InspectContainerRequest) returns (ServiceContainer);
rpc ListServiceContainers(ListServiceContainersRequest) returns (ListServiceContainersResponse);
rpc RemoveServiceContainer(RemoveContainerRequest) returns (google.protobuf.Empty);
} }
message CreateContainerRequest { message CreateContainerRequest {
// JSON serialized container.Config. // JSON serialised container.Config.
bytes config = 1; bytes config = 1;
// JSON serialized container.HostConfig. // JSON serialised container.HostConfig.
bytes host_config = 2; bytes host_config = 2;
// JSON serialized network.NetworkingConfig. // JSON serialised network.NetworkingConfig.
bytes network_config = 3; bytes network_config = 3;
// JSON serialized ocispec.Platform. // JSON serialised ocispec.Platform.
bytes platform = 4; bytes platform = 4;
string name = 5; string name = 5;
} }
message CreateContainerResponse { message CreateContainerResponse {
// JSON serialized container.CreateResponse. // JSON serialised container.CreateResponse.
bytes response = 1;
}
message InspectContainerRequest {
string id = 1;
}
message InspectContainerResponse {
// JSON serialised container.InspectResponse.
bytes response = 1; bytes response = 1;
} }
message StartContainerRequest { message StartContainerRequest {
string id = 1; string id = 1;
// JSON serialized container.StartOptions. // JSON serialised container.StartOptions.
bytes options = 2;
}
message StopContainerRequest {
string id = 1;
// JSON serialised container.StopOptions.
bytes options = 2; bytes options = 2;
} }
message ListContainersRequest { message ListContainersRequest {
// JSON serialized container.ListOptions. // JSON serialised container.ListOptions.
bytes options = 1; bytes options = 1;
} }
// ListContainersResponse structure allows broadcasting ListContainers requests to multiple machines.
message ListContainersResponse { message ListContainersResponse {
// Must contain only one repeated messages field to allow broadcasting ListContainers requests to multiple machines.
repeated MachineContainers messages = 1; repeated MachineContainers messages = 1;
} }
message MachineContainers { message MachineContainers {
Metadata metadata = 1; Metadata metadata = 1;
// JSON serialized []container.Summary. // JSON serialised []container.InspectResponse.
bytes containers = 2; bytes containers = 2;
} }
message RemoveContainerRequest { message RemoveContainerRequest {
string id = 1; string id = 1;
// JSON serialized container.RemoveOptions. // JSON serialised container.RemoveOptions.
bytes options = 2; bytes options = 2;
} }
message PullImageRequest { message PullImageRequest {
string image = 1; string image = 1;
// JSON serialized image.PullOptions. // JSON serialised image.PullOptions.
bytes options = 2; bytes options = 2;
} }
message JSONMessage { message JSONMessage {
// JSON serialized jsonmessage.JSONMessage. // JSON serialised jsonmessage.JSONMessage.
bytes message = 1; bytes message = 1;
} }
message InspectImageRequest {
string id = 1;
}
message InspectImageResponse {
// Must contain only one repeated messages field to allow broadcasting InspectImage requests to multiple machines.
repeated Image messages = 1;
}
message Image {
Metadata metadata = 1;
// JSON serialised types.ImageInspect.
bytes image = 2;
}
message InspectRemoteImageRequest {
string id = 1;
}
message InspectRemoteImageResponse {
// Must contain only one repeated messages field to allow broadcasting InspectRemoteImage requests to multiple machines.
repeated RemoteImage messages = 1;
}
message RemoteImage {
Metadata metadata = 1;
// Image reference in the canonical form with the digest.
string reference = 2;
// Raw JSON manifest from the registry.
bytes manifest = 3;
}
message CreateServiceContainerRequest {
string service_id = 1;
// JSON serialised api.ServiceSpec.
bytes service_spec = 2;
string container_name = 3;
}
message ServiceContainer {
// JSON serialised container.InspectResponse.
bytes container = 1;
// JSON serialised api.ServiceSpec.
bytes service_spec = 2;
}
message ListServiceContainersRequest {
string service_id = 1;
// JSON serialised container.ListOptions.
bytes options = 2;
}
message ListServiceContainersResponse {
// Must contain only one repeated messages field to allow broadcasting ListServiceContainers requests
// to multiple machines.
repeated MachineServiceContainers messages = 1;
}
message MachineServiceContainers {
Metadata metadata = 1;
repeated ServiceContainer containers = 2;
}
+313 -5
View File
@@ -20,11 +20,19 @@ import (
const _ = grpc.SupportPackageIsVersion9 const _ = grpc.SupportPackageIsVersion9
const ( const (
Docker_CreateContainer_FullMethodName = "/api.Docker/CreateContainer" Docker_CreateContainer_FullMethodName = "/api.Docker/CreateContainer"
Docker_StartContainer_FullMethodName = "/api.Docker/StartContainer" Docker_InspectContainer_FullMethodName = "/api.Docker/InspectContainer"
Docker_ListContainers_FullMethodName = "/api.Docker/ListContainers" Docker_StartContainer_FullMethodName = "/api.Docker/StartContainer"
Docker_RemoveContainer_FullMethodName = "/api.Docker/RemoveContainer" Docker_StopContainer_FullMethodName = "/api.Docker/StopContainer"
Docker_PullImage_FullMethodName = "/api.Docker/PullImage" Docker_ListContainers_FullMethodName = "/api.Docker/ListContainers"
Docker_RemoveContainer_FullMethodName = "/api.Docker/RemoveContainer"
Docker_PullImage_FullMethodName = "/api.Docker/PullImage"
Docker_InspectImage_FullMethodName = "/api.Docker/InspectImage"
Docker_InspectRemoteImage_FullMethodName = "/api.Docker/InspectRemoteImage"
Docker_CreateServiceContainer_FullMethodName = "/api.Docker/CreateServiceContainer"
Docker_InspectServiceContainer_FullMethodName = "/api.Docker/InspectServiceContainer"
Docker_ListServiceContainers_FullMethodName = "/api.Docker/ListServiceContainers"
Docker_RemoveServiceContainer_FullMethodName = "/api.Docker/RemoveServiceContainer"
) )
// DockerClient is the client API for Docker service. // DockerClient is the client API for Docker service.
@@ -32,10 +40,20 @@ const (
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
type DockerClient interface { type DockerClient interface {
CreateContainer(ctx context.Context, in *CreateContainerRequest, opts ...grpc.CallOption) (*CreateContainerResponse, error) CreateContainer(ctx context.Context, in *CreateContainerRequest, opts ...grpc.CallOption) (*CreateContainerResponse, error)
InspectContainer(ctx context.Context, in *InspectContainerRequest, opts ...grpc.CallOption) (*InspectContainerResponse, error)
StartContainer(ctx context.Context, in *StartContainerRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) StartContainer(ctx context.Context, in *StartContainerRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
StopContainer(ctx context.Context, in *StopContainerRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
ListContainers(ctx context.Context, in *ListContainersRequest, opts ...grpc.CallOption) (*ListContainersResponse, error) ListContainers(ctx context.Context, in *ListContainersRequest, opts ...grpc.CallOption) (*ListContainersResponse, error)
RemoveContainer(ctx context.Context, in *RemoveContainerRequest, opts ...grpc.CallOption) (*emptypb.Empty, 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) PullImage(ctx context.Context, in *PullImageRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[JSONMessage], error)
InspectImage(ctx context.Context, in *InspectImageRequest, opts ...grpc.CallOption) (*InspectImageResponse, error)
// InspectRemoteImage returns the image metadata for an image in a remote registry using the machine's
// Docker auth credentials if necessary.
InspectRemoteImage(ctx context.Context, in *InspectRemoteImageRequest, opts ...grpc.CallOption) (*InspectRemoteImageResponse, error)
CreateServiceContainer(ctx context.Context, in *CreateServiceContainerRequest, opts ...grpc.CallOption) (*CreateContainerResponse, error)
InspectServiceContainer(ctx context.Context, in *InspectContainerRequest, opts ...grpc.CallOption) (*ServiceContainer, error)
ListServiceContainers(ctx context.Context, in *ListServiceContainersRequest, opts ...grpc.CallOption) (*ListServiceContainersResponse, error)
RemoveServiceContainer(ctx context.Context, in *RemoveContainerRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
} }
type dockerClient struct { type dockerClient struct {
@@ -56,6 +74,16 @@ func (c *dockerClient) CreateContainer(ctx context.Context, in *CreateContainerR
return out, nil return out, nil
} }
func (c *dockerClient) InspectContainer(ctx context.Context, in *InspectContainerRequest, opts ...grpc.CallOption) (*InspectContainerResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(InspectContainerResponse)
err := c.cc.Invoke(ctx, Docker_InspectContainer_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) { func (c *dockerClient) StartContainer(ctx context.Context, in *StartContainerRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(emptypb.Empty) out := new(emptypb.Empty)
@@ -66,6 +94,16 @@ func (c *dockerClient) StartContainer(ctx context.Context, in *StartContainerReq
return out, nil return out, nil
} }
func (c *dockerClient) StopContainer(ctx context.Context, in *StopContainerRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(emptypb.Empty)
err := c.cc.Invoke(ctx, Docker_StopContainer_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) { func (c *dockerClient) ListContainers(ctx context.Context, in *ListContainersRequest, opts ...grpc.CallOption) (*ListContainersResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ListContainersResponse) out := new(ListContainersResponse)
@@ -105,15 +143,85 @@ func (c *dockerClient) PullImage(ctx context.Context, in *PullImageRequest, opts
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. // 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] type Docker_PullImageClient = grpc.ServerStreamingClient[JSONMessage]
func (c *dockerClient) InspectImage(ctx context.Context, in *InspectImageRequest, opts ...grpc.CallOption) (*InspectImageResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(InspectImageResponse)
err := c.cc.Invoke(ctx, Docker_InspectImage_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *dockerClient) InspectRemoteImage(ctx context.Context, in *InspectRemoteImageRequest, opts ...grpc.CallOption) (*InspectRemoteImageResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(InspectRemoteImageResponse)
err := c.cc.Invoke(ctx, Docker_InspectRemoteImage_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *dockerClient) CreateServiceContainer(ctx context.Context, in *CreateServiceContainerRequest, opts ...grpc.CallOption) (*CreateContainerResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(CreateContainerResponse)
err := c.cc.Invoke(ctx, Docker_CreateServiceContainer_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *dockerClient) InspectServiceContainer(ctx context.Context, in *InspectContainerRequest, opts ...grpc.CallOption) (*ServiceContainer, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ServiceContainer)
err := c.cc.Invoke(ctx, Docker_InspectServiceContainer_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *dockerClient) ListServiceContainers(ctx context.Context, in *ListServiceContainersRequest, opts ...grpc.CallOption) (*ListServiceContainersResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ListServiceContainersResponse)
err := c.cc.Invoke(ctx, Docker_ListServiceContainers_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *dockerClient) RemoveServiceContainer(ctx context.Context, in *RemoveContainerRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(emptypb.Empty)
err := c.cc.Invoke(ctx, Docker_RemoveServiceContainer_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
// DockerServer is the server API for Docker service. // DockerServer is the server API for Docker service.
// All implementations must embed UnimplementedDockerServer // All implementations must embed UnimplementedDockerServer
// for forward compatibility. // for forward compatibility.
type DockerServer interface { type DockerServer interface {
CreateContainer(context.Context, *CreateContainerRequest) (*CreateContainerResponse, error) CreateContainer(context.Context, *CreateContainerRequest) (*CreateContainerResponse, error)
InspectContainer(context.Context, *InspectContainerRequest) (*InspectContainerResponse, error)
StartContainer(context.Context, *StartContainerRequest) (*emptypb.Empty, error) StartContainer(context.Context, *StartContainerRequest) (*emptypb.Empty, error)
StopContainer(context.Context, *StopContainerRequest) (*emptypb.Empty, error)
ListContainers(context.Context, *ListContainersRequest) (*ListContainersResponse, error) ListContainers(context.Context, *ListContainersRequest) (*ListContainersResponse, error)
RemoveContainer(context.Context, *RemoveContainerRequest) (*emptypb.Empty, error) RemoveContainer(context.Context, *RemoveContainerRequest) (*emptypb.Empty, error)
PullImage(*PullImageRequest, grpc.ServerStreamingServer[JSONMessage]) error PullImage(*PullImageRequest, grpc.ServerStreamingServer[JSONMessage]) error
InspectImage(context.Context, *InspectImageRequest) (*InspectImageResponse, error)
// InspectRemoteImage returns the image metadata for an image in a remote registry using the machine's
// Docker auth credentials if necessary.
InspectRemoteImage(context.Context, *InspectRemoteImageRequest) (*InspectRemoteImageResponse, error)
CreateServiceContainer(context.Context, *CreateServiceContainerRequest) (*CreateContainerResponse, error)
InspectServiceContainer(context.Context, *InspectContainerRequest) (*ServiceContainer, error)
ListServiceContainers(context.Context, *ListServiceContainersRequest) (*ListServiceContainersResponse, error)
RemoveServiceContainer(context.Context, *RemoveContainerRequest) (*emptypb.Empty, error)
mustEmbedUnimplementedDockerServer() mustEmbedUnimplementedDockerServer()
} }
@@ -127,9 +235,15 @@ type UnimplementedDockerServer struct{}
func (UnimplementedDockerServer) CreateContainer(context.Context, *CreateContainerRequest) (*CreateContainerResponse, error) { func (UnimplementedDockerServer) CreateContainer(context.Context, *CreateContainerRequest) (*CreateContainerResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method CreateContainer not implemented") return nil, status.Errorf(codes.Unimplemented, "method CreateContainer not implemented")
} }
func (UnimplementedDockerServer) InspectContainer(context.Context, *InspectContainerRequest) (*InspectContainerResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method InspectContainer not implemented")
}
func (UnimplementedDockerServer) StartContainer(context.Context, *StartContainerRequest) (*emptypb.Empty, error) { func (UnimplementedDockerServer) StartContainer(context.Context, *StartContainerRequest) (*emptypb.Empty, error) {
return nil, status.Errorf(codes.Unimplemented, "method StartContainer not implemented") return nil, status.Errorf(codes.Unimplemented, "method StartContainer not implemented")
} }
func (UnimplementedDockerServer) StopContainer(context.Context, *StopContainerRequest) (*emptypb.Empty, error) {
return nil, status.Errorf(codes.Unimplemented, "method StopContainer not implemented")
}
func (UnimplementedDockerServer) ListContainers(context.Context, *ListContainersRequest) (*ListContainersResponse, error) { func (UnimplementedDockerServer) ListContainers(context.Context, *ListContainersRequest) (*ListContainersResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ListContainers not implemented") return nil, status.Errorf(codes.Unimplemented, "method ListContainers not implemented")
} }
@@ -139,6 +253,24 @@ func (UnimplementedDockerServer) RemoveContainer(context.Context, *RemoveContain
func (UnimplementedDockerServer) PullImage(*PullImageRequest, grpc.ServerStreamingServer[JSONMessage]) error { func (UnimplementedDockerServer) PullImage(*PullImageRequest, grpc.ServerStreamingServer[JSONMessage]) error {
return status.Errorf(codes.Unimplemented, "method PullImage not implemented") return status.Errorf(codes.Unimplemented, "method PullImage not implemented")
} }
func (UnimplementedDockerServer) InspectImage(context.Context, *InspectImageRequest) (*InspectImageResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method InspectImage not implemented")
}
func (UnimplementedDockerServer) InspectRemoteImage(context.Context, *InspectRemoteImageRequest) (*InspectRemoteImageResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method InspectRemoteImage not implemented")
}
func (UnimplementedDockerServer) CreateServiceContainer(context.Context, *CreateServiceContainerRequest) (*CreateContainerResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method CreateServiceContainer not implemented")
}
func (UnimplementedDockerServer) InspectServiceContainer(context.Context, *InspectContainerRequest) (*ServiceContainer, error) {
return nil, status.Errorf(codes.Unimplemented, "method InspectServiceContainer not implemented")
}
func (UnimplementedDockerServer) ListServiceContainers(context.Context, *ListServiceContainersRequest) (*ListServiceContainersResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ListServiceContainers not implemented")
}
func (UnimplementedDockerServer) RemoveServiceContainer(context.Context, *RemoveContainerRequest) (*emptypb.Empty, error) {
return nil, status.Errorf(codes.Unimplemented, "method RemoveServiceContainer not implemented")
}
func (UnimplementedDockerServer) mustEmbedUnimplementedDockerServer() {} func (UnimplementedDockerServer) mustEmbedUnimplementedDockerServer() {}
func (UnimplementedDockerServer) testEmbeddedByValue() {} func (UnimplementedDockerServer) testEmbeddedByValue() {}
@@ -178,6 +310,24 @@ func _Docker_CreateContainer_Handler(srv interface{}, ctx context.Context, dec f
return interceptor(ctx, in, info, handler) return interceptor(ctx, in, info, handler)
} }
func _Docker_InspectContainer_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(InspectContainerRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(DockerServer).InspectContainer(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: Docker_InspectContainer_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(DockerServer).InspectContainer(ctx, req.(*InspectContainerRequest))
}
return interceptor(ctx, in, info, handler)
}
func _Docker_StartContainer_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { func _Docker_StartContainer_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(StartContainerRequest) in := new(StartContainerRequest)
if err := dec(in); err != nil { if err := dec(in); err != nil {
@@ -196,6 +346,24 @@ func _Docker_StartContainer_Handler(srv interface{}, ctx context.Context, dec fu
return interceptor(ctx, in, info, handler) return interceptor(ctx, in, info, handler)
} }
func _Docker_StopContainer_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(StopContainerRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(DockerServer).StopContainer(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: Docker_StopContainer_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(DockerServer).StopContainer(ctx, req.(*StopContainerRequest))
}
return interceptor(ctx, in, info, handler)
}
func _Docker_ListContainers_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { func _Docker_ListContainers_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ListContainersRequest) in := new(ListContainersRequest)
if err := dec(in); err != nil { if err := dec(in); err != nil {
@@ -243,6 +411,114 @@ func _Docker_PullImage_Handler(srv interface{}, stream grpc.ServerStream) error
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. // 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] type Docker_PullImageServer = grpc.ServerStreamingServer[JSONMessage]
func _Docker_InspectImage_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(InspectImageRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(DockerServer).InspectImage(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: Docker_InspectImage_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(DockerServer).InspectImage(ctx, req.(*InspectImageRequest))
}
return interceptor(ctx, in, info, handler)
}
func _Docker_InspectRemoteImage_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(InspectRemoteImageRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(DockerServer).InspectRemoteImage(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: Docker_InspectRemoteImage_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(DockerServer).InspectRemoteImage(ctx, req.(*InspectRemoteImageRequest))
}
return interceptor(ctx, in, info, handler)
}
func _Docker_CreateServiceContainer_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(CreateServiceContainerRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(DockerServer).CreateServiceContainer(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: Docker_CreateServiceContainer_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(DockerServer).CreateServiceContainer(ctx, req.(*CreateServiceContainerRequest))
}
return interceptor(ctx, in, info, handler)
}
func _Docker_InspectServiceContainer_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(InspectContainerRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(DockerServer).InspectServiceContainer(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: Docker_InspectServiceContainer_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(DockerServer).InspectServiceContainer(ctx, req.(*InspectContainerRequest))
}
return interceptor(ctx, in, info, handler)
}
func _Docker_ListServiceContainers_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ListServiceContainersRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(DockerServer).ListServiceContainers(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: Docker_ListServiceContainers_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(DockerServer).ListServiceContainers(ctx, req.(*ListServiceContainersRequest))
}
return interceptor(ctx, in, info, handler)
}
func _Docker_RemoveServiceContainer_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(RemoveContainerRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(DockerServer).RemoveServiceContainer(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: Docker_RemoveServiceContainer_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(DockerServer).RemoveServiceContainer(ctx, req.(*RemoveContainerRequest))
}
return interceptor(ctx, in, info, handler)
}
// Docker_ServiceDesc is the grpc.ServiceDesc for Docker service. // Docker_ServiceDesc is the grpc.ServiceDesc for Docker service.
// It's only intended for direct use with grpc.RegisterService, // It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy) // and not to be introspected or modified (even as a copy)
@@ -254,10 +530,18 @@ var Docker_ServiceDesc = grpc.ServiceDesc{
MethodName: "CreateContainer", MethodName: "CreateContainer",
Handler: _Docker_CreateContainer_Handler, Handler: _Docker_CreateContainer_Handler,
}, },
{
MethodName: "InspectContainer",
Handler: _Docker_InspectContainer_Handler,
},
{ {
MethodName: "StartContainer", MethodName: "StartContainer",
Handler: _Docker_StartContainer_Handler, Handler: _Docker_StartContainer_Handler,
}, },
{
MethodName: "StopContainer",
Handler: _Docker_StopContainer_Handler,
},
{ {
MethodName: "ListContainers", MethodName: "ListContainers",
Handler: _Docker_ListContainers_Handler, Handler: _Docker_ListContainers_Handler,
@@ -266,6 +550,30 @@ var Docker_ServiceDesc = grpc.ServiceDesc{
MethodName: "RemoveContainer", MethodName: "RemoveContainer",
Handler: _Docker_RemoveContainer_Handler, Handler: _Docker_RemoveContainer_Handler,
}, },
{
MethodName: "InspectImage",
Handler: _Docker_InspectImage_Handler,
},
{
MethodName: "InspectRemoteImage",
Handler: _Docker_InspectRemoteImage_Handler,
},
{
MethodName: "CreateServiceContainer",
Handler: _Docker_CreateServiceContainer_Handler,
},
{
MethodName: "InspectServiceContainer",
Handler: _Docker_InspectServiceContainer_Handler,
},
{
MethodName: "ListServiceContainers",
Handler: _Docker_ListServiceContainers_Handler,
},
{
MethodName: "RemoveServiceContainer",
Handler: _Docker_RemoveServiceContainer_Handler,
},
}, },
Streams: []grpc.StreamDesc{ Streams: []grpc.StreamDesc{
{ {
+175 -110
View File
@@ -26,9 +26,10 @@ type MachineInfo struct {
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields unknownFields protoimpl.UnknownFields
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`
Network *NetworkConfig `protobuf:"bytes,3,opt,name=network,proto3" json:"network,omitempty"` Network *NetworkConfig `protobuf:"bytes,3,opt,name=network,proto3" json:"network,omitempty"`
PublicIp *IP `protobuf:"bytes,4,opt,name=public_ip,json=publicIp,proto3" json:"public_ip,omitempty"`
} }
func (x *MachineInfo) Reset() { func (x *MachineInfo) Reset() {
@@ -84,6 +85,13 @@ func (x *MachineInfo) GetNetwork() *NetworkConfig {
return nil return nil
} }
func (x *MachineInfo) GetPublicIp() *IP {
if x != nil {
return x.PublicIp
}
return nil
}
type NetworkConfig struct { type NetworkConfig struct {
state protoimpl.MessageState state protoimpl.MessageState
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
@@ -92,7 +100,7 @@ type NetworkConfig struct {
Subnet *IPPrefix `protobuf:"bytes,1,opt,name=subnet,proto3" json:"subnet,omitempty"` 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"` 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"` Endpoints []*IPPort `protobuf:"bytes,3,rep,name=endpoints,proto3" json:"endpoints,omitempty"`
PublicKey []byte `protobuf:"bytes,4,opt,name=publicKey,proto3" json:"publicKey,omitempty"` PublicKey []byte `protobuf:"bytes,4,opt,name=public_key,json=publicKey,proto3" json:"public_key,omitempty"`
} }
func (x *NetworkConfig) Reset() { func (x *NetworkConfig) Reset() {
@@ -162,6 +170,11 @@ type InitClusterRequest struct {
MachineName string `protobuf:"bytes,1,opt,name=machineName,proto3" json:"machineName,omitempty"` MachineName string `protobuf:"bytes,1,opt,name=machineName,proto3" json:"machineName,omitempty"`
Network *IPPrefix `protobuf:"bytes,2,opt,name=network,proto3" json:"network,omitempty"` Network *IPPrefix `protobuf:"bytes,2,opt,name=network,proto3" json:"network,omitempty"`
// Types that are assignable to PublicIpConfig:
//
// *InitClusterRequest_PublicIp
// *InitClusterRequest_PublicIpAuto
PublicIpConfig isInitClusterRequest_PublicIpConfig `protobuf_oneof:"public_ip_config"`
} }
func (x *InitClusterRequest) Reset() { func (x *InitClusterRequest) Reset() {
@@ -210,6 +223,43 @@ func (x *InitClusterRequest) GetNetwork() *IPPrefix {
return nil return nil
} }
func (m *InitClusterRequest) GetPublicIpConfig() isInitClusterRequest_PublicIpConfig {
if m != nil {
return m.PublicIpConfig
}
return nil
}
func (x *InitClusterRequest) GetPublicIp() *IP {
if x, ok := x.GetPublicIpConfig().(*InitClusterRequest_PublicIp); ok {
return x.PublicIp
}
return nil
}
func (x *InitClusterRequest) GetPublicIpAuto() bool {
if x, ok := x.GetPublicIpConfig().(*InitClusterRequest_PublicIpAuto); ok {
return x.PublicIpAuto
}
return false
}
type isInitClusterRequest_PublicIpConfig interface {
isInitClusterRequest_PublicIpConfig()
}
type InitClusterRequest_PublicIp struct {
PublicIp *IP `protobuf:"bytes,3,opt,name=public_ip,json=publicIp,proto3,oneof"`
}
type InitClusterRequest_PublicIpAuto struct {
PublicIpAuto bool `protobuf:"varint,4,opt,name=public_ip_auto,json=publicIpAuto,proto3,oneof"`
}
func (*InitClusterRequest_PublicIp) isInitClusterRequest_PublicIpConfig() {}
func (*InitClusterRequest_PublicIpAuto) isInitClusterRequest_PublicIpConfig() {}
type InitClusterResponse struct { type InitClusterResponse struct {
state protoimpl.MessageState state protoimpl.MessageState
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
@@ -590,87 +640,96 @@ var file_internal_machine_api_pb_machine_proto_rawDesc = []byte{
0x70, 0x74, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x24, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x70, 0x74, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x24, 0x69, 0x6e, 0x74, 0x65, 0x72,
0x6e, 0x61, 0x6c, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x6e, 0x61, 0x6c, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f,
0x70, 0x62, 0x2f, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x70, 0x62, 0x2f, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22,
0x5f, 0x0a, 0x0b, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x0e, 0x85, 0x01, 0x0a, 0x0b, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x12,
0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12,
0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e,
0x6d, 0x65, 0x12, 0x2c, 0x0a, 0x07, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x18, 0x03, 0x20, 0x61, 0x6d, 0x65, 0x12, 0x2c, 0x0a, 0x07, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x18, 0x03,
0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4e, 0x65, 0x74, 0x77, 0x6f,
0x6b, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x07, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x72, 0x6b, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x07, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72,
0x22, 0xad, 0x01, 0x0a, 0x0d, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x43, 0x6f, 0x6e, 0x66, 0x6b, 0x12, 0x24, 0x0a, 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x69, 0x70, 0x18, 0x04,
0x69, 0x67, 0x12, 0x25, 0x0a, 0x06, 0x73, 0x75, 0x62, 0x6e, 0x65, 0x74, 0x18, 0x01, 0x20, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x07, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x50, 0x52, 0x08, 0x70,
0x75, 0x62, 0x6c, 0x69, 0x63, 0x49, 0x70, 0x22, 0xae, 0x01, 0x0a, 0x0d, 0x4e, 0x65, 0x74, 0x77,
0x6f, 0x72, 0x6b, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x25, 0x0a, 0x06, 0x73, 0x75, 0x62,
0x6e, 0x65, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x61, 0x70, 0x69, 0x2e,
0x49, 0x50, 0x50, 0x72, 0x65, 0x66, 0x69, 0x78, 0x52, 0x06, 0x73, 0x75, 0x62, 0x6e, 0x65, 0x74,
0x12, 0x2c, 0x0a, 0x0d, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x5f, 0x69,
0x70, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x07, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x50,
0x52, 0x0c, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x49, 0x70, 0x12, 0x29,
0x0a, 0x09, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28,
0x0b, 0x32, 0x0b, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x50, 0x50, 0x6f, 0x72, 0x74, 0x52, 0x09,
0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x70, 0x75, 0x62,
0x6c, 0x69, 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x70,
0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x22, 0xc3, 0x01, 0x0a, 0x12, 0x49, 0x6e, 0x69,
0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12,
0x20, 0x0a, 0x0b, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x01,
0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x4e, 0x61, 0x6d,
0x65, 0x12, 0x27, 0x0a, 0x07, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x18, 0x02, 0x20, 0x01,
0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x50, 0x50, 0x72, 0x65, 0x66, 0x69, 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, 0x78, 0x52, 0x07, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x12, 0x26, 0x0a, 0x09, 0x70, 0x75,
0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x70, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x69, 0x70, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x07, 0x2e,
0x32, 0x07, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x50, 0x52, 0x0c, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x50, 0x48, 0x00, 0x52, 0x08, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63,
0x65, 0x6d, 0x65, 0x6e, 0x74, 0x49, 0x70, 0x12, 0x29, 0x0a, 0x09, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x49, 0x70, 0x12, 0x26, 0x0a, 0x0e, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x69, 0x70, 0x5f,
0x69, 0x6e, 0x74, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0b, 0x2e, 0x61, 0x70, 0x69, 0x61, 0x75, 0x74, 0x6f, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x48, 0x00, 0x52, 0x0c, 0x70, 0x75,
0x2e, 0x49, 0x50, 0x50, 0x6f, 0x72, 0x74, 0x52, 0x09, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x62, 0x6c, 0x69, 0x63, 0x49, 0x70, 0x41, 0x75, 0x74, 0x6f, 0x42, 0x12, 0x0a, 0x10, 0x70, 0x75,
0x74, 0x73, 0x12, 0x1c, 0x0a, 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x18, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x69, 0x70, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0x41,
0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x0a, 0x13, 0x49, 0x6e, 0x69, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x73,
0x22, 0x5f, 0x0a, 0x12, 0x49, 0x6e, 0x69, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x52, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2a, 0x0a, 0x07, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65,
0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x20, 0x0a, 0x0b, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d, 0x61, 0x63,
0x65, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x07, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e,
0x68, 0x69, 0x6e, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x27, 0x0a, 0x07, 0x6e, 0x65, 0x74, 0x77, 0x65, 0x22, 0x79, 0x0a, 0x12, 0x4a, 0x6f, 0x69, 0x6e, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72,
0x6f, 0x72, 0x6b, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2a, 0x0a, 0x07, 0x6d, 0x61, 0x63, 0x68, 0x69,
0x49, 0x50, 0x50, 0x72, 0x65, 0x66, 0x69, 0x78, 0x52, 0x07, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6e, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d,
0x6b, 0x22, 0x41, 0x0a, 0x13, 0x49, 0x6e, 0x69, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x07, 0x6d, 0x61, 0x63, 0x68,
0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2a, 0x0a, 0x07, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x12, 0x37, 0x0a, 0x0e, 0x6f, 0x74, 0x68, 0x65, 0x72, 0x5f, 0x6d, 0x61, 0x63,
0x69, 0x6e, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x61, 0x70,
0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x07, 0x6d, 0x61, 0x63, 0x69, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x0d, 0x6f,
0x68, 0x69, 0x6e, 0x65, 0x22, 0x79, 0x0a, 0x12, 0x4a, 0x6f, 0x69, 0x6e, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x68, 0x65, 0x72, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x22, 0x25, 0x0a, 0x0d,
0x74, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2a, 0x0a, 0x07, 0x6d, 0x61, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x14, 0x0a,
0x63, 0x68, 0x69, 0x6e, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x61, 0x70, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x6f,
0x69, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x07, 0x6d, 0x6b, 0x65, 0x6e, 0x22, 0xc3, 0x01, 0x0a, 0x07, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12,
0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x12, 0x37, 0x0a, 0x0e, 0x6f, 0x74, 0x68, 0x65, 0x72, 0x5f, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12,
0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x10, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e,
0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28,
0x52, 0x0d, 0x6f, 0x74, 0x68, 0x65, 0x72, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x22, 0x09, 0x52, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x12, 0x36, 0x0a, 0x0a, 0x63, 0x6f, 0x6e, 0x74, 0x61,
0x25, 0x0a, 0x0d, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x61, 0x70,
0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x69, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69,
0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0xc3, 0x01, 0x0a, 0x07, 0x53, 0x65, 0x72, 0x76, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x0a, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x1a,
0x63, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x48, 0x0a, 0x09, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x1d, 0x0a, 0x0a,
0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x18, 0x03, 0x52, 0x09, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x64, 0x12, 0x1c, 0x0a, 0x09, 0x63,
0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x12, 0x36, 0x0a, 0x0a, 0x63, 0x6f, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09,
0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x22, 0x27, 0x0a, 0x15, 0x49, 0x6e, 0x73,
0x2e, 0x61, 0x70, 0x69, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x43, 0x6f, 0x6e, 0x70, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65,
0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x0a, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02,
0x72, 0x73, 0x1a, 0x48, 0x0a, 0x09, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x69, 0x64, 0x22, 0x40, 0x0a, 0x16, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72,
0x1d, 0x0a, 0x0a, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x26, 0x0a, 0x07,
0x01, 0x28, 0x09, 0x52, 0x09, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x64, 0x12, 0x1c, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0c, 0x2e,
0x0a, 0x09, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x61, 0x70, 0x69, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x07, 0x73, 0x65, 0x72,
0x0c, 0x52, 0x09, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x22, 0x27, 0x0a, 0x15, 0x76, 0x69, 0x63, 0x65, 0x32, 0xc0, 0x02, 0x0a, 0x07, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65,
0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x12, 0x40, 0x0a, 0x0b, 0x49, 0x6e, 0x69, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x12,
0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x17, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x69, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65,
0x09, 0x52, 0x02, 0x69, 0x64, 0x22, 0x40, 0x0a, 0x16, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49,
0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x6e, 0x69, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
0x26, 0x0a, 0x07, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x73, 0x65, 0x12, 0x3e, 0x0a, 0x0b, 0x4a, 0x6f, 0x69, 0x6e, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65,
0x32, 0x0c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x07, 0x72, 0x12, 0x17, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4a, 0x6f, 0x69, 0x6e, 0x43, 0x6c, 0x75, 0x73,
0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x32, 0xc0, 0x02, 0x0a, 0x07, 0x4d, 0x61, 0x63, 0x68, 0x74, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f,
0x69, 0x6e, 0x65, 0x12, 0x40, 0x0a, 0x0b, 0x49, 0x6e, 0x69, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70,
0x65, 0x72, 0x12, 0x17, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x69, 0x74, 0x43, 0x6c, 0x75, 0x74, 0x79, 0x12, 0x33, 0x0a, 0x05, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x16, 0x2e, 0x67, 0x6f,
0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x61, 0x70, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d,
0x69, 0x2e, 0x49, 0x6e, 0x69, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x74, 0x79, 0x1a, 0x12, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52,
0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3e, 0x0a, 0x0b, 0x4a, 0x6f, 0x69, 0x6e, 0x43, 0x6c, 0x75, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x33, 0x0a, 0x07, 0x49, 0x6e, 0x73, 0x70, 0x65,
0x73, 0x74, 0x65, 0x72, 0x12, 0x17, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4a, 0x6f, 0x69, 0x6e, 0x43, 0x63, 0x74, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74,
0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x10, 0x2e, 0x61, 0x70, 0x69,
0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x49, 0x0a, 0x0e,
0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x33, 0x0a, 0x05, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x16, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x1a,
0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72, 0x76,
0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x12, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x54, 0x6f, 0x6b, 0x69, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x61, 0x70, 0x69,
0x65, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x33, 0x0a, 0x07, 0x49, 0x6e, 0x2e, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52,
0x73, 0x70, 0x65, 0x63, 0x74, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x37, 0x5a, 0x35, 0x67, 0x69, 0x74, 0x68, 0x75,
0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x10, 0x2e, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x70, 0x73, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x6b, 0x69,
0x61, 0x70, 0x69, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x2f, 0x75, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61,
0x49, 0x0a, 0x0e, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x6c, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x62,
0x65, 0x12, 0x1a, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
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 ( var (
@@ -697,37 +756,39 @@ var file_internal_machine_api_pb_machine_proto_goTypes = []any{
(*InspectServiceRequest)(nil), // 7: api.InspectServiceRequest (*InspectServiceRequest)(nil), // 7: api.InspectServiceRequest
(*InspectServiceResponse)(nil), // 8: api.InspectServiceResponse (*InspectServiceResponse)(nil), // 8: api.InspectServiceResponse
(*Service_Container)(nil), // 9: api.Service.Container (*Service_Container)(nil), // 9: api.Service.Container
(*IPPrefix)(nil), // 10: api.IPPrefix (*IP)(nil), // 10: api.IP
(*IP)(nil), // 11: api.IP (*IPPrefix)(nil), // 11: api.IPPrefix
(*IPPort)(nil), // 12: api.IPPort (*IPPort)(nil), // 12: api.IPPort
(*emptypb.Empty)(nil), // 13: google.protobuf.Empty (*emptypb.Empty)(nil), // 13: google.protobuf.Empty
} }
var file_internal_machine_api_pb_machine_proto_depIdxs = []int32{ var file_internal_machine_api_pb_machine_proto_depIdxs = []int32{
1, // 0: api.MachineInfo.network:type_name -> api.NetworkConfig 1, // 0: api.MachineInfo.network:type_name -> api.NetworkConfig
10, // 1: api.NetworkConfig.subnet:type_name -> api.IPPrefix 10, // 1: api.MachineInfo.public_ip:type_name -> api.IP
11, // 2: api.NetworkConfig.management_ip:type_name -> api.IP 11, // 2: api.NetworkConfig.subnet:type_name -> api.IPPrefix
12, // 3: api.NetworkConfig.endpoints:type_name -> api.IPPort 10, // 3: api.NetworkConfig.management_ip:type_name -> api.IP
10, // 4: api.InitClusterRequest.network:type_name -> api.IPPrefix 12, // 4: api.NetworkConfig.endpoints:type_name -> api.IPPort
0, // 5: api.InitClusterResponse.machine:type_name -> api.MachineInfo 11, // 5: api.InitClusterRequest.network:type_name -> api.IPPrefix
0, // 6: api.JoinClusterRequest.machine:type_name -> api.MachineInfo 10, // 6: api.InitClusterRequest.public_ip:type_name -> api.IP
0, // 7: api.JoinClusterRequest.other_machines:type_name -> api.MachineInfo 0, // 7: api.InitClusterResponse.machine:type_name -> api.MachineInfo
9, // 8: api.Service.containers:type_name -> api.Service.Container 0, // 8: api.JoinClusterRequest.machine:type_name -> api.MachineInfo
6, // 9: api.InspectServiceResponse.service:type_name -> api.Service 0, // 9: api.JoinClusterRequest.other_machines:type_name -> api.MachineInfo
2, // 10: api.Machine.InitCluster:input_type -> api.InitClusterRequest 9, // 10: api.Service.containers:type_name -> api.Service.Container
4, // 11: api.Machine.JoinCluster:input_type -> api.JoinClusterRequest 6, // 11: api.InspectServiceResponse.service:type_name -> api.Service
13, // 12: api.Machine.Token:input_type -> google.protobuf.Empty 2, // 12: api.Machine.InitCluster:input_type -> api.InitClusterRequest
13, // 13: api.Machine.Inspect:input_type -> google.protobuf.Empty 4, // 13: api.Machine.JoinCluster:input_type -> api.JoinClusterRequest
7, // 14: api.Machine.InspectService:input_type -> api.InspectServiceRequest 13, // 14: api.Machine.Token:input_type -> google.protobuf.Empty
3, // 15: api.Machine.InitCluster:output_type -> api.InitClusterResponse 13, // 15: api.Machine.Inspect:input_type -> google.protobuf.Empty
13, // 16: api.Machine.JoinCluster:output_type -> google.protobuf.Empty 7, // 16: api.Machine.InspectService:input_type -> api.InspectServiceRequest
5, // 17: api.Machine.Token:output_type -> api.TokenResponse 3, // 17: api.Machine.InitCluster:output_type -> api.InitClusterResponse
0, // 18: api.Machine.Inspect:output_type -> api.MachineInfo 13, // 18: api.Machine.JoinCluster:output_type -> google.protobuf.Empty
8, // 19: api.Machine.InspectService:output_type -> api.InspectServiceResponse 5, // 19: api.Machine.Token:output_type -> api.TokenResponse
15, // [15:20] is the sub-list for method output_type 0, // 20: api.Machine.Inspect:output_type -> api.MachineInfo
10, // [10:15] is the sub-list for method input_type 8, // 21: api.Machine.InspectService:output_type -> api.InspectServiceResponse
10, // [10:10] is the sub-list for extension type_name 17, // [17:22] is the sub-list for method output_type
10, // [10:10] is the sub-list for extension extendee 12, // [12:17] is the sub-list for method input_type
0, // [0:10] is the sub-list for field type_name 12, // [12:12] is the sub-list for extension type_name
12, // [12:12] is the sub-list for extension extendee
0, // [0:12] is the sub-list for field type_name
} }
func init() { file_internal_machine_api_pb_machine_proto_init() } func init() { file_internal_machine_api_pb_machine_proto_init() }
@@ -858,6 +919,10 @@ func file_internal_machine_api_pb_machine_proto_init() {
} }
} }
} }
file_internal_machine_api_pb_machine_proto_msgTypes[2].OneofWrappers = []any{
(*InitClusterRequest_PublicIp)(nil),
(*InitClusterRequest_PublicIpAuto)(nil),
}
type x struct{} type x struct{}
out := protoimpl.TypeBuilder{ out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{ File: protoimpl.DescBuilder{
+7 -1
View File
@@ -19,18 +19,24 @@ message MachineInfo {
string id = 1; string id = 1;
string name = 2; string name = 2;
NetworkConfig network = 3; NetworkConfig network = 3;
IP public_ip = 4;
} }
message NetworkConfig { message NetworkConfig {
IPPrefix subnet = 1; IPPrefix subnet = 1;
IP management_ip = 2; IP management_ip = 2;
repeated IPPort endpoints = 3; repeated IPPort endpoints = 3;
bytes publicKey = 4; bytes public_key = 4;
} }
message InitClusterRequest { message InitClusterRequest {
string machineName = 1; string machineName = 1;
IPPrefix network = 2; IPPrefix network = 2;
oneof public_ip_config {
IP public_ip = 3;
bool public_ip_auto = 4;
}
} }
message InitClusterResponse { message InitClusterResponse {
+1 -1
View File
@@ -5,7 +5,7 @@ import (
"google.golang.org/grpc/status" "google.golang.org/grpc/status"
"google.golang.org/protobuf/encoding/protowire" "google.golang.org/protobuf/encoding/protowire"
"google.golang.org/protobuf/proto" "google.golang.org/protobuf/proto"
"uncloud/internal/machine/api/pb" "github.com/psviderski/uncloud/internal/machine/api/pb"
) )
// One2ManyResponder converts upstream responses into messages from upstreams, so that multiple // One2ManyResponder converts upstream responses into messages from upstreams, so that multiple
+164
View File
@@ -0,0 +1,164 @@
package caddyfile
import (
"encoding/json"
"errors"
"fmt"
"log/slog"
"maps"
"net"
"net/http"
"slices"
"strconv"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
"github.com/caddyserver/caddy/v2/modules/caddyhttp/reverseproxy"
"github.com/psviderski/uncloud/internal/machine/docker"
"github.com/psviderski/uncloud/pkg/api"
)
func GenerateConfig(containers []api.ServiceContainer, verifyResponse string) (*caddy.Config, error) {
// Maps hostnames to lists of upstreams (container IP:port pairs).
httpHostUpstreams := make(map[string][]string)
httpsHostUpstreams := make(map[string][]string)
for _, ctr := range containers {
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(httpsHostUpstreams[port.Hostname], upstream)
default:
// 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: append(
hostUpstreamsToRoutes(httpHostUpstreams, &warnings),
// Add a route to respond with a static verification response at the /.uncloud-verify path.
verificationRoute(verifyResponse, &warnings),
),
}
servers["https"] = &caddyhttp.Server{
Listen: []string{fmt.Sprintf(":%d", caddyhttp.DefaultHTTPSPort)},
Routes: hostUpstreamsToRoutes(httpsHostUpstreams, &warnings),
}
httpApp := caddyhttp.App{
Servers: servers,
}
config := &caddy.Config{
AppsRaw: caddy.ModuleMap{
"http": caddyconfig.JSON(httpApp, &warnings),
},
}
var err error
if len(warnings) > 0 {
// warnings only contains errors from JSON marshaling, which are highly unlikely with correct code.
for _, w := range warnings {
err = errors.Join(err, errors.New(w.Message))
}
return nil, fmt.Errorf("marshal Caddy configuration: %w", err)
}
return config, nil
}
// hostUpstreamsToRoutes converts a map of hostnames to upstreams to a list of Caddy routes.
func hostUpstreamsToRoutes(hostUpstreams map[string][]string, warnings *[]caddyconfig.Warning) []caddyhttp.Route {
// Sort hostnames for deterministic output.
hostnames := slices.Collect(maps.Keys(hostUpstreams))
slices.Sort(hostnames)
routes := make([]caddyhttp.Route, 0, len(hostUpstreams))
for _, hostname := range hostnames {
upstreams := hostUpstreams[hostname]
upstreamPool := make([]*reverseproxy.Upstream, len(upstreams))
for i, upstream := range upstreams {
upstreamPool[i] = &reverseproxy.Upstream{
Dial: upstream,
}
}
handler := &reverseproxy.Handler{
Upstreams: upstreamPool,
}
routes = append(routes, caddyhttp.Route{
MatcherSetsRaw: caddyhttp.RawMatcherSets{
{
"host": caddyconfig.JSON(caddyhttp.MatchHost{hostname}, warnings),
},
},
HandlersRaw: []json.RawMessage{
caddyconfig.JSONModuleObject(handler, "handler", "reverse_proxy", warnings),
},
})
}
return routes
}
// verificationRoute returns a Caddy route that responds with the given static response at the /.uncloud-verify path.
func verificationRoute(response string, warnings *[]caddyconfig.Warning) caddyhttp.Route {
// Return the following route:
// {
// "match": [
// {
// "path": [
// "/.uncloud-verify"
// ]
// }
// ],
// "handle": [
// {
// "handler": "static_response",
// "body": "<response>",
// "status_code": 200
// }
// ]
// }
staticResponse := caddyhttp.StaticResponse{
StatusCode: caddyhttp.WeakString(strconv.Itoa(http.StatusOK)),
Body: response,
}
return caddyhttp.Route{
MatcherSetsRaw: caddyhttp.RawMatcherSets{
{
"path": caddyconfig.JSON(caddyhttp.MatchPath{VerifyPath}, warnings),
},
},
HandlersRaw: []json.RawMessage{
caddyconfig.JSONModuleObject(staticResponse, "handler", "static_response", warnings),
},
}
}
+362
View File
@@ -0,0 +1,362 @@
package caddyfile
import (
"strings"
"testing"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/network"
"github.com/psviderski/uncloud/internal/machine/docker"
"github.com/psviderski/uncloud/pkg/api"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestGenerateConfig(t *testing.T) {
tests := []struct {
name string
containers []api.ServiceContainer
want string
wantErr bool
}{
{
name: "empty containers",
containers: []api.ServiceContainer{},
want: `{
"servers": {
"http": {
"listen": [":80"],
"routes": [{
"match": [{"path": ["/.uncloud-verify"]}],
"handle": [{
"body": "verification-response-body",
"handler": "static_response",
"status_code": 200
}]
}]
},
"https": {
"listen": [":443"]
}
}
}`,
wantErr: false,
},
{
name: "HTTP container",
containers: []api.ServiceContainer{
newContainer("10.210.0.2", "app.example.com:8080/http"),
},
want: `{
"servers": {
"http": {
"listen": [":80"],
"routes": [
{
"match": [{"host": ["app.example.com"]}],
"handle": [{
"handler": "reverse_proxy",
"upstreams": [{"dial": "10.210.0.2:8080"}]
}]
},
{
"match": [{"path": ["/.uncloud-verify"]}],
"handle": [{
"body": "verification-response-body",
"handler": "static_response",
"status_code": 200
}]
}
]
},
"https": {
"listen": [":443"]
}
}
}`,
wantErr: false,
},
{
name: "load balancing multiple containers",
containers: []api.ServiceContainer{
newContainer("10.210.0.2", "app.example.com:8080/http"),
newContainer("10.210.0.3", "app.example.com:8080/http"),
},
want: `{
"servers": {
"http": {
"listen": [":80"],
"routes": [
{
"match": [{"host": ["app.example.com"]}],
"handle": [{
"handler": "reverse_proxy",
"upstreams": [
{"dial": "10.210.0.2:8080"},
{"dial": "10.210.0.3:8080"}
]
}]
},
{
"match": [{"path": ["/.uncloud-verify"]}],
"handle": [{
"body": "verification-response-body",
"handler": "static_response",
"status_code": 200
}]
}
]
},
"https": {
"listen": [":443"]
}
}
}`,
wantErr: false,
},
{
name: "HTTPS container",
containers: []api.ServiceContainer{
newContainer("10.210.0.2", "secure.example.com:8000/https"),
},
want: `{
"servers": {
"http": {
"listen": [":80"],
"routes": [
{
"match": [{"path": ["/.uncloud-verify"]}],
"handle": [{
"body": "verification-response-body",
"handler": "static_response",
"status_code": 200
}]
}
]
},
"https": {
"listen": [":443"],
"routes": [
{
"match": [{"host": ["secure.example.com"]}],
"handle": [{
"handler": "reverse_proxy",
"upstreams": [{"dial": "10.210.0.2:8000"}]
}]
}
]
}
}
}`,
wantErr: false,
},
{
name: "mixed HTTP and HTTPS",
containers: []api.ServiceContainer{
newContainer("10.210.0.2",
"app.example.com:8080/http",
"web.example.com:8000/http"),
newContainer("10.210.0.3",
"app.example.com:8080/http",
"secure.example.com:8888/https"),
newContainer("10.210.0.4",
"web.example.com:8000/http",
"secure.example.com:8888/https"),
newContainer("10.210.0.5",
"app.example.com:8080/http",
"web.example.com:8000/http",
"secure.example.com:8888/https"),
},
want: `{
"servers": {
"http": {
"listen": [":80"],
"routes": [
{
"match": [{"host": ["app.example.com"]}],
"handle": [{
"handler": "reverse_proxy",
"upstreams": [
{"dial": "10.210.0.2:8080"},
{"dial": "10.210.0.3:8080"},
{"dial": "10.210.0.5:8080"}
]
}]
},
{
"match": [{"host": ["web.example.com"]}],
"handle": [{
"handler": "reverse_proxy",
"upstreams": [
{"dial": "10.210.0.2:8000"},
{"dial": "10.210.0.4:8000"},
{"dial": "10.210.0.5:8000"}
]
}]
},
{
"match": [{"path": ["/.uncloud-verify"]}],
"handle": [{
"body": "verification-response-body",
"handler": "static_response",
"status_code": 200
}]
}
]
},
"https": {
"listen": [":443"],
"routes": [
{
"match": [{"host": ["secure.example.com"]}],
"handle": [{
"handler": "reverse_proxy",
"upstreams": [
{"dial": "10.210.0.3:8888"},
{"dial": "10.210.0.4:8888"},
{"dial": "10.210.0.5:8888"}
]
}]
}
]
}
}
}`,
wantErr: false,
},
{
name: "container without uncloud network ignored",
containers: []api.ServiceContainer{
newContainerWithoutNetwork("ignored.example.com:8080/http"),
},
want: `{
"servers": {
"http": {
"listen": [":80"],
"routes": [{
"match": [{"path": ["/.uncloud-verify"]}],
"handle": [{
"body": "verification-response-body",
"handler": "static_response",
"status_code": 200
}]
}]
},
"https": {
"listen": [":443"]
}
}
}`,
wantErr: false,
},
{
name: "container with invalid port ignored",
containers: []api.ServiceContainer{
newContainer("10.210.0.2", "invalid-port"),
},
want: `{
"servers": {
"http": {
"listen": [":80"],
"routes": [{
"match": [{"path": ["/.uncloud-verify"]}],
"handle": [{
"body": "verification-response-body",
"handler": "static_response",
"status_code": 200
}]
}]
},
"https": {
"listen": [":443"]
}
}
}`,
wantErr: false,
},
{
name: "containers with unsupported protocols ignored",
containers: []api.ServiceContainer{
newContainer("10.210.0.2", "5000/tcp"),
newContainer("10.210.0.3", "5000/udp"),
newContainer("10.210.0.4", "80:8080/tcp@host"),
},
want: `{
"servers": {
"http": {
"listen": [":80"],
"routes": [{
"match": [{"path": ["/.uncloud-verify"]}],
"handle": [{
"body": "verification-response-body",
"handler": "static_response",
"status_code": 200
}]
}]
},
"https": {
"listen": [":443"]
}
}
}`,
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
config, err := GenerateConfig(tt.containers, "verification-response-body")
if tt.wantErr {
assert.Error(t, err)
return
}
require.NoError(t, err)
require.Len(t, config.AppsRaw, 1, "Expected one http app")
require.Contains(t, config.AppsRaw, "http", "Expected http app")
assert.JSONEq(t, tt.want, string(config.AppsRaw["http"]), "Generated Caddy app config doesn't match")
})
}
}
func newContainer(ip string, ports ...string) api.ServiceContainer {
portsLabel := strings.Join(ports, ",")
return api.ServiceContainer{Container: api.Container{ContainerJSON: types.ContainerJSON{
ContainerJSONBase: &types.ContainerJSONBase{},
NetworkSettings: &types.NetworkSettings{
Networks: map[string]*network.EndpointSettings{
docker.NetworkName: {
IPAddress: ip,
},
},
},
Config: &container.Config{
Labels: map[string]string{
api.LabelServicePorts: portsLabel,
},
},
}}}
}
func newContainerWithoutNetwork(ports ...string) api.ServiceContainer {
portsLabel := strings.Join(ports, ",")
return api.ServiceContainer{Container: api.Container{ContainerJSON: types.ContainerJSON{
ContainerJSONBase: &types.ContainerJSONBase{},
NetworkSettings: &types.NetworkSettings{
Networks: map[string]*network.EndpointSettings{
"other-network": {
IPAddress: "172.17.0.2",
},
},
},
Config: &container.Config{
Labels: map[string]string{
api.LabelServicePorts: portsLabel,
},
},
}}}
}
+28 -116
View File
@@ -3,34 +3,31 @@ package caddyfile
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"errors"
"fmt" "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" "log/slog"
"net"
"os" "os"
"path/filepath" "path/filepath"
"strconv"
"uncloud/internal/api" "github.com/psviderski/uncloud/internal/fs"
"uncloud/internal/fs" "github.com/psviderski/uncloud/internal/machine/store"
"uncloud/internal/machine/docker" "github.com/psviderski/uncloud/pkg/api"
"uncloud/internal/machine/store"
) )
const CaddyGroup = "uncloud" const (
CaddyGroup = "uncloud"
VerifyPath = "/.uncloud-verify"
)
// Controller monitors container changes in the cluster store and generates a configuration file for Caddy reverse // Controller monitors container changes in the cluster store and generates a configuration file for Caddy reverse
// proxy. The generated Caddyfile allows Caddy to route external traffic to service containers across the internal // proxy. The generated Caddyfile allows Caddy to route external traffic to service containers across the internal
// network. // network.
type Controller struct { type Controller struct {
store *store.Store store *store.Store
path string path string
verifyResponse string
} }
func NewController(store *store.Store, path string) (*Controller, error) { func NewController(store *store.Store, path string, verifyResponse string) (*Controller, error) {
dir := filepath.Dir(path) dir := filepath.Dir(path)
if err := os.MkdirAll(dir, 0750); err != nil { if err := os.MkdirAll(dir, 0750); err != nil {
return nil, fmt.Errorf("create parent directory for Caddy configuration '%s': %w", dir, err) return nil, fmt.Errorf("create parent directory for Caddy configuration '%s': %w", dir, err)
@@ -40,8 +37,9 @@ func NewController(store *store.Store, path string) (*Controller, error) {
} }
return &Controller{ return &Controller{
store: store, store: store,
path: path, path: path,
verifyResponse: verifyResponse,
}, nil }, nil
} }
@@ -89,84 +87,26 @@ func (c *Controller) Run(ctx context.Context) error {
} }
} }
// filterAvailableContainers filters out containers that are likely unavailable from this machine. The availability // filterAvailableContainers filters out containers from this machine that are likely unavailable. The availability
// is determined by the cluster membership state of the machine that the container is running on. // 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. // TODO: implement machine membership check using Corrossion Admin client.
func (c *Controller) filterAvailableContainers(containerRecords []*store.ContainerRecord) ([]*api.Container, error) { func (c *Controller) filterAvailableContainers(
containers := make([]*api.Container, len(containerRecords)) containerRecords []store.ContainerRecord,
) ([]api.ServiceContainer, error) {
containers := make([]api.ServiceContainer, len(containerRecords))
for i, cr := range containerRecords { for i, cr := range containerRecords {
containers[i] = cr.Container containers[i] = api.ServiceContainer{
Container: cr.Container,
// TODO: restore ServiceSpec from the container record once it's saved in the store.
}
} }
return containers, nil return containers, nil
} }
func (c *Controller) generateConfig(containers []*api.Container) error { func (c *Controller) generateConfig(containers []api.ServiceContainer) error {
// Maps hostnames to lists of upstreams (container IP:port pairs). config, err := GenerateConfig(containers, c.verifyResponse)
httpHostUpstreams := make(map[string][]string) if err != nil {
httpsHostUpstreams := make(map[string][]string) return err
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, "", " ") configBytes, err := json.MarshalIndent(config, "", " ")
@@ -183,31 +123,3 @@ func (c *Controller) generateConfig(containers []*api.Container) error {
return nil 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
}
+22 -29
View File
@@ -11,11 +11,11 @@ import (
"log/slog" "log/slog"
"net/netip" "net/netip"
"time" "time"
"uncloud/internal/corrosion" "github.com/psviderski/uncloud/internal/corrosion"
"uncloud/internal/machine/api/pb" "github.com/psviderski/uncloud/internal/machine/api/pb"
"uncloud/internal/machine/network" "github.com/psviderski/uncloud/internal/machine/network"
"uncloud/internal/machine/store" "github.com/psviderski/uncloud/internal/machine/store"
"uncloud/internal/secret" "github.com/psviderski/uncloud/internal/secret"
) )
type Cluster struct { type Cluster struct {
@@ -110,6 +110,15 @@ func (c *Cluster) AddMachine(ctx context.Context, req *pb.AddMachineRequest) (*p
if len(req.Network.Endpoints) == 0 { if len(req.Network.Endpoints) == 0 {
return nil, status.Error(codes.InvalidArgument, "endpoints not set") return nil, status.Error(codes.InvalidArgument, "endpoints not set")
} }
if req.PublicIp != nil {
ip, err := req.PublicIp.ToAddr()
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "invalid public IP: %v", err)
}
if !ip.IsValid() {
return nil, status.Error(codes.InvalidArgument, "invalid public IP")
}
}
machines, err := c.store.ListMachines(ctx) machines, err := c.store.ListMachines(ctx)
if err != nil { if err != nil {
@@ -174,6 +183,7 @@ func (c *Cluster) AddMachine(ctx context.Context, req *pb.AddMachineRequest) (*p
Endpoints: req.Network.Endpoints, Endpoints: req.Network.Endpoints,
PublicKey: req.Network.PublicKey, PublicKey: req.Network.PublicKey,
}, },
PublicIp: req.PublicIp,
} }
// TODO: announce the new machine to the cluster members and achieve consensus. // 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. // We should perhaps not proceed if this machine is in a minority partition.
@@ -210,9 +220,13 @@ func (c *Cluster) ListMachines(ctx context.Context, _ *emptypb.Empty) (*pb.ListM
state := pb.MachineMember_DOWN state := pb.MachineMember_DOWN
addr, _ := m.Network.ManagementIp.ToAddr() addr, _ := m.Network.ManagementIp.ToAddr()
for _, s := range states { for _, s := range states {
if s.Addr.Addr().Compare(addr) == 0 && if s.Addr.Addr().Compare(addr) == 0 {
(s.State == corrosion.MembershipStateAlive || s.State == corrosion.MembershipStateSuspect) { switch s.State {
state = pb.MachineMember_UP case corrosion.MembershipStateAlive:
state = pb.MachineMember_UP
case corrosion.MembershipStateSuspect:
state = pb.MachineMember_SUSPECT
}
break break
} }
} }
@@ -228,24 +242,3 @@ func (c *Cluster) ListMachines(ctx context.Context, _ *emptypb.Empty) (*pb.ListM
return &pb.ListMachinesResponse{Machines: members}, nil return &pb.ListMachinesResponse{Machines: members}, nil
} }
//func (c *Cluster) ListServices(ctx context.Context, _ *emptypb.Empty) (*pb.ListServicesResponse, error) {
// if err := c.checkInitialised(ctx); err != nil {
// return nil, err
// }
//
// return &pb.ListServicesResponse{
// Messages: []*pb.Services{
// {
// Services: []*pb.Service{
// {
// Name: "service1",
// },
// {
// Name: "service2",
// },
// },
// },
// },
// }, nil
//}
+159
View File
@@ -0,0 +1,159 @@
package cluster
import (
"context"
"encoding/json"
"errors"
"github.com/psviderski/uncloud/internal/dns"
"github.com/psviderski/uncloud/internal/machine/api/pb"
"github.com/psviderski/uncloud/internal/machine/store"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/emptypb"
)
// uncloudDNSKey is the key used to store the details of the reserved domain in the store.
const uncloudDNSKey = "uncloud_dns"
type uncloudDNSDomain struct {
// Endpoint is the API endpoint of the Uncloud DNS service where the domain is reserved.
Endpoint string
Name string
// TODO: encrypt the token in the store.
Token string
}
func (c *Cluster) ReserveDomain(ctx context.Context, req *pb.ReserveDomainRequest) (*pb.Domain, error) {
if err := c.checkInitialised(ctx); err != nil {
return nil, err
}
if req.Endpoint == "" {
return nil, status.Error(codes.InvalidArgument, "API endpoint not set")
}
if _, err := c.storedDomain(ctx); err == nil {
return nil, status.Errorf(codes.AlreadyExists, "domain already reserved")
} else {
if s := status.Convert(err); s.Code() != codes.NotFound {
return nil, err
}
}
dnsClient := dns.NewClient()
name, token, err := dnsClient.ReserveDomain(req.Endpoint)
if err != nil {
return nil, status.Errorf(codes.Internal, err.Error())
}
domain := uncloudDNSDomain{
Endpoint: req.Endpoint,
Name: name,
Token: token,
}
domainJSON, err := json.Marshal(domain)
if err != nil {
return nil, status.Errorf(codes.Internal, "marshal reserved domain for store: %v", err)
}
if err = c.store.Put(ctx, uncloudDNSKey, domainJSON); err != nil {
return nil, status.Errorf(codes.Internal, "store reserved domain: %v", err)
}
return &pb.Domain{Name: name}, nil
}
func (c *Cluster) GetDomain(ctx context.Context, _ *emptypb.Empty) (*pb.Domain, error) {
if err := c.checkInitialised(ctx); err != nil {
return nil, err
}
domain, err := c.storedDomain(ctx)
if err != nil {
return nil, err
}
return &pb.Domain{Name: domain.Name}, nil
}
func (c *Cluster) storedDomain(ctx context.Context) (uncloudDNSDomain, error) {
var domain uncloudDNSDomain
var domainJSON []byte
if err := c.store.Get(ctx, uncloudDNSKey, &domainJSON); err != nil {
if errors.Is(err, store.ErrKeyNotFound) {
return domain, status.Errorf(codes.NotFound, "domain not found")
}
return domain, status.Errorf(codes.Internal, "get domain from store: %v", err)
}
if err := json.Unmarshal(domainJSON, &domain); err != nil {
return domain, status.Errorf(codes.Internal, "unmarshal domain: %v", err)
}
return domain, nil
}
func (c *Cluster) ReleaseDomain(ctx context.Context, _ *emptypb.Empty) (*pb.Domain, error) {
if err := c.checkInitialised(ctx); err != nil {
return nil, err
}
domain, err := c.storedDomain(ctx)
if err != nil {
return nil, err
}
if err = c.store.Delete(ctx, uncloudDNSKey); err != nil {
return nil, status.Errorf(codes.Internal, "delete domain from store: %v", err)
}
// TODO: implement and call Uncloud DNS endpoint to release/delete the domain.
return &pb.Domain{Name: domain.Name}, nil
}
func (c *Cluster) CreateDomainRecords(
ctx context.Context, req *pb.CreateDomainRecordsRequest,
) (*pb.CreateDomainRecordsResponse, error) {
if err := c.checkInitialised(ctx); err != nil {
return nil, err
}
domain, err := c.storedDomain(ctx)
if err != nil {
return nil, err
}
dnsClient := dns.NewClient()
recordsReq := make([]dns.RecordRequest, len(req.Records))
for i, r := range req.Records {
recordsReq[i] = dns.RecordRequest{
Name: r.Name,
Type: dns.RecordType(r.Type.String()),
Values: r.Values,
}
}
recordsResp, err := dnsClient.CreateRecords(domain.Endpoint, domain.Name, domain.Token, recordsReq)
if err != nil {
return nil, err
}
resp := &pb.CreateDomainRecordsResponse{
Records: make([]*pb.DNSRecord, len(recordsResp)),
}
for i, r := range recordsResp {
resp.Records[i] = &pb.DNSRecord{
Name: r.FQDN,
Values: r.Values,
}
switch r.Type {
case dns.RecordTypeA:
resp.Records[i].Type = pb.DNSRecord_A
case dns.RecordTypeAAAA:
resp.Records[i].Type = pb.DNSRecord_AAAA
}
}
return resp, nil
}
+1 -1
View File
@@ -2,7 +2,7 @@ package cluster
import ( import (
"fmt" "fmt"
"uncloud/internal/secret" "github.com/psviderski/uncloud/internal/secret"
) )
// NewMachineID generates a new unique machine ID. // NewMachineID generates a new unique machine ID.
+1 -1
View File
@@ -7,7 +7,7 @@ import (
"net/netip" "net/netip"
"os" "os"
"path/filepath" "path/filepath"
"uncloud/internal/fs" "github.com/psviderski/uncloud/internal/fs"
) )
const ( const (
+56
View File
@@ -0,0 +1,56 @@
package machine
import (
"fmt"
"os"
"github.com/jmoiron/sqlx"
_ "modernc.org/sqlite"
)
const DBFileName = "machine.db"
// NewDB creates a new connection to machine SQLite database and runs schema migrations if necessary.
func NewDB(path string) (*sqlx.DB, error) {
// Create the database file with 0600 permissions if it doesn't exist, or update permissions if exists.
if _, err := os.Stat(path); os.IsNotExist(err) {
file, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR, 0600)
if err != nil {
return nil, fmt.Errorf("create SQLite database '%s': %w", path, err)
}
file.Close()
} else {
if err = os.Chmod(path, 0600); err != nil {
return nil, fmt.Errorf("update SQLite database permissions '%s': %w", path, err)
}
}
// - Write-Ahead Logging (WAL) mode for better read/write performance.
// - Busy timeout (5s) to make concurrent writes wait on each other instead of failing immediately.
conn := path + "?_pragma=journal_mode=WAL&_pragma=synchronous=NORMAL&_pragma=busy_timeout=5000&_time_format=sqlite"
db, err := sqlx.Connect("sqlite", conn)
if err != nil {
return nil, fmt.Errorf("connect to SQLite database '%s': %w", conn, err)
}
schema := `
CREATE TABLE IF NOT EXISTS containers (
id TEXT NOT NULL PRIMARY KEY,
service_id TEXT NOT NULL,
service_name TEXT AS (json_extract(service_spec, '$.Name')),
service_spec TEXT NOT NULL CHECK (json_valid(service_spec)),
-- 'subsecond' modifier is used to store timestamps with millisecond precision.
created_at TIMESTAMP NOT NULL DEFAULT (datetime('subsecond')),
updated_at TIMESTAMP NOT NULL DEFAULT (datetime('subsecond'))
);
CREATE INDEX IF NOT EXISTS idx_containers_service_id ON containers (service_id);
CREATE INDEX IF NOT EXISTS idx_containers_service_name ON containers (service_name);
`
if _, err = db.Exec(schema); err != nil {
return nil, fmt.Errorf("create schema: %w", err)
}
return db, nil
}
+276 -15
View File
@@ -3,18 +3,23 @@ package docker
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"io"
"github.com/distribution/reference"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/network" "github.com/docker/docker/api/types/network"
"github.com/docker/docker/errdefs" "github.com/docker/docker/errdefs"
"github.com/docker/docker/pkg/jsonmessage" "github.com/docker/docker/pkg/jsonmessage"
regtypes "github.com/google/go-containerregistry/pkg/v1/types"
ocispec "github.com/opencontainers/image-spec/specs-go/v1" ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/psviderski/uncloud/internal/machine/api/pb"
"github.com/psviderski/uncloud/pkg/api"
"google.golang.org/grpc" "google.golang.org/grpc"
"google.golang.org/grpc/codes" "google.golang.org/grpc/codes"
"google.golang.org/grpc/status" "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. // Client is a gRPC client for the Docker service that provides a similar interface to the Docker HTTP client.
@@ -72,10 +77,26 @@ func (c *Client) CreateContainer(
Name: name, Name: name,
}) })
if err != nil { if err != nil {
if s, ok := status.FromError(err); ok { if status.Convert(err).Code() == codes.NotFound {
if s.Code() == codes.NotFound { return resp, errdefs.NotFound(err)
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
}
// InspectContainer returns the container information for the given container ID.
func (c *Client) InspectContainer(ctx context.Context, id string) (types.ContainerJSON, error) {
var resp types.ContainerJSON
grpcResp, err := c.grpcClient.InspectContainer(ctx, &pb.InspectContainerRequest{Id: id})
if err != nil {
if status.Convert(err).Code() == codes.NotFound {
return resp, errdefs.NotFound(err)
} }
return resp, err return resp, err
} }
@@ -98,10 +119,27 @@ func (c *Client) StartContainer(ctx context.Context, id string, opts container.S
Options: optsBytes, Options: optsBytes,
}) })
if err != nil { if err != nil {
if s, ok := status.FromError(err); ok { if status.Convert(err).Code() == codes.NotFound {
if s.Code() == codes.NotFound { return errdefs.NotFound(err)
return errdefs.NotFound(err) }
} }
return err
}
// StopContainer stops a container with the given ID and options.
func (c *Client) StopContainer(ctx context.Context, id string, opts container.StopOptions) error {
optsBytes, err := json.Marshal(opts)
if err != nil {
return fmt.Errorf("marshal options: %w", err)
}
_, err = c.grpcClient.StopContainer(ctx, &pb.StopContainerRequest{
Id: id,
Options: optsBytes,
})
if err != nil {
if status.Convert(err).Code() == codes.NotFound {
return errdefs.NotFound(err)
} }
} }
return err return err
@@ -109,7 +147,7 @@ func (c *Client) StartContainer(ctx context.Context, id string, opts container.S
type MachineContainers struct { type MachineContainers struct {
Metadata *pb.Metadata Metadata *pb.Metadata
Containers []types.Container Containers []types.ContainerJSON
} }
func (c *Client) ListContainers(ctx context.Context, opts container.ListOptions) ([]MachineContainers, error) { func (c *Client) ListContainers(ctx context.Context, opts container.ListOptions) ([]MachineContainers, error) {
@@ -150,10 +188,8 @@ func (c *Client) RemoveContainer(ctx context.Context, id string, opts container.
Options: optsBytes, Options: optsBytes,
}) })
if err != nil { if err != nil {
if s, ok := status.FromError(err); ok { if status.Convert(err).Code() == codes.NotFound {
if s.Code() == codes.NotFound { return errdefs.NotFound(err)
return errdefs.NotFound(err)
}
} }
} }
return err return err
@@ -196,3 +232,228 @@ func (c *Client) PullImage(ctx context.Context, image string) (<-chan PullImageM
return ch, nil return ch, nil
} }
// InspectImage returns the image information for the given image ID. The request may be sent to multiple machines.
func (c *Client) InspectImage(ctx context.Context, id string) ([]api.MachineImage, error) {
resp, err := c.grpcClient.InspectImage(ctx, &pb.InspectImageRequest{Id: id})
if err != nil {
// If the request was sent to only one machine, err is an actual error from the machine.
if status.Convert(err).Code() == codes.NotFound {
return nil, errdefs.NotFound(err)
}
return nil, err
}
notFoundCount := 0
for _, msg := range resp.Messages {
if msg.Metadata != nil && msg.Metadata.Status != nil && codes.Code(msg.Metadata.Status.Code) == codes.NotFound {
notFoundCount++
}
}
if len(resp.Messages) == notFoundCount {
return nil, errdefs.NotFound(fmt.Errorf("image not found: %s", id))
}
images := make([]api.MachineImage, len(resp.Messages))
for i, msg := range resp.Messages {
images[i].Metadata = msg.Metadata
if msg.Metadata != nil && msg.Metadata.Error != "" {
continue
}
if err = json.Unmarshal(msg.Image, &images[i].Image); err != nil {
return nil, fmt.Errorf("unmarshal image: %w", err)
}
}
return images, nil
}
// InspectRemoteImage returns the image metadata for an image in a remote registry using the machine's Docker auth
// credentials if necessary. If the response from a machine doesn't contain an error, the api.RemoteImage will either
// contain an IndexManifest or an ImageManifest.
func (c *Client) InspectRemoteImage(ctx context.Context, id string) ([]api.MachineRemoteImage, error) {
resp, err := c.grpcClient.InspectRemoteImage(ctx, &pb.InspectRemoteImageRequest{Id: id})
if err != nil {
return nil, err
}
images := make([]api.MachineRemoteImage, 0, len(resp.Messages))
var parseErr error
for _, msg := range resp.Messages {
mri, err := parseRemoteImageMessage(msg)
if err != nil {
parseErr = errors.Join(parseErr, err)
continue
}
images = append(images, mri)
}
return images, parseErr
}
type manifestMediaType struct {
MediaType regtypes.MediaType `json:"mediaType"`
}
func parseRemoteImageMessage(msg *pb.RemoteImage) (api.MachineRemoteImage, error) {
mri := api.MachineRemoteImage{
Metadata: msg.Metadata,
}
if msg.Metadata != nil && msg.Metadata.Error != "" {
return mri, nil
}
ref, err := reference.ParseNormalizedNamed(msg.Reference)
if err != nil {
// The reference is the string representation of a valid canonical reference from the server which
// is expected to be parseable.
return mri, fmt.Errorf("parse reference: %w", err)
}
if canonicalRef, ok := ref.(reference.Canonical); ok {
mri.Image.Reference = canonicalRef
} else {
return mri, fmt.Errorf("unexpected non-canonical image reference: %s", msg.Reference)
}
manifest := manifestMediaType{}
if err = json.Unmarshal(msg.Manifest, &manifest); err != nil {
return mri, fmt.Errorf("unmarshal manifest: %w", err)
}
switch {
case manifest.MediaType.IsIndex():
index := &ocispec.Index{}
if err = json.Unmarshal(msg.Manifest, index); err != nil {
return mri, fmt.Errorf("unmarshal index manifest: %w", err)
}
mri.Image.IndexManifest = index
case manifest.MediaType.IsImage():
image := &ocispec.Manifest{}
if err = json.Unmarshal(msg.Manifest, image); err != nil {
return mri, fmt.Errorf("unmarshal image manifest: %w", err)
}
mri.Image.ImageManifest = image
default:
return mri, fmt.Errorf("unexpected manifest media type: %s", manifest.MediaType)
}
return mri, nil
}
// CreateServiceContainer creates a new container for the service with the given specifications.
func (c *Client) CreateServiceContainer(
ctx context.Context, serviceID string, spec api.ServiceSpec, containerName string,
) (container.CreateResponse, error) {
var resp container.CreateResponse
specBytes, err := json.Marshal(spec)
if err != nil {
return resp, fmt.Errorf("marshal service spec: %w", err)
}
grpcResp, err := c.grpcClient.CreateServiceContainer(ctx, &pb.CreateServiceContainerRequest{
ServiceId: serviceID,
ServiceSpec: specBytes,
ContainerName: containerName,
})
if err != nil {
if status.Convert(err).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
}
// InspectServiceContainer returns the container information and service specification that was used to create the
// container with the given ID.
func (c *Client) InspectServiceContainer(ctx context.Context, id string) (api.ServiceContainer, error) {
var resp api.ServiceContainer
grpcResp, err := c.grpcClient.InspectServiceContainer(ctx, &pb.InspectContainerRequest{Id: id})
if err != nil {
if status.Convert(err).Code() == codes.NotFound {
return resp, errdefs.NotFound(err)
}
return resp, err
}
if err = json.Unmarshal(grpcResp.Container, &resp.Container); err != nil {
return resp, fmt.Errorf("unmarshal container: %w", err)
}
if err = json.Unmarshal(grpcResp.ServiceSpec, &resp.ServiceSpec); err != nil {
return resp, fmt.Errorf("unmarshal service spec: %w", err)
}
return resp, nil
}
type MachineServiceContainers struct {
Metadata *pb.Metadata
Containers []api.ServiceContainer
}
// ListServiceContainers returns all containers on requested machines that belong to the service with the given
// name or ID. If serviceNameOrID is empty, all service containers are returned.
func (c *Client) ListServiceContainers(
ctx context.Context, serviceNameOrID string, opts container.ListOptions,
) ([]MachineServiceContainers, error) {
optsBytes, err := json.Marshal(opts)
if err != nil {
return nil, fmt.Errorf("marshal options: %w", err)
}
resp, err := c.grpcClient.ListServiceContainers(ctx, &pb.ListServiceContainersRequest{
ServiceId: serviceNameOrID,
Options: optsBytes,
})
if err != nil {
return nil, err
}
machineContainers := make([]MachineServiceContainers, len(resp.Messages))
for i, msg := range resp.Messages {
machineContainers[i].Metadata = msg.Metadata
if msg.Metadata != nil && msg.Metadata.Error != "" {
continue
}
containers := make([]api.ServiceContainer, len(msg.Containers))
for j, sc := range msg.Containers {
if err = json.Unmarshal(sc.Container, &containers[j].Container); err != nil {
return nil, fmt.Errorf("unmarshal container: %w", err)
}
if err = json.Unmarshal(sc.ServiceSpec, &containers[j].ServiceSpec); err != nil {
return nil, fmt.Errorf("unmarshal service spec: %w", err)
}
}
machineContainers[i].Containers = containers
}
return machineContainers, nil
}
// RemoveServiceContainer stops (kills after grace period) and removes a service container with the given ID.
// A service container is a container that has been created with CreateServiceContainer.
func (c *Client) RemoveServiceContainer(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.RemoveServiceContainer(ctx, &pb.RemoveContainerRequest{
Id: id,
Options: optsBytes,
})
if err != nil {
if status.Convert(err).Code() == codes.NotFound {
return errdefs.NotFound(err)
}
}
return err
}
+17 -7
View File
@@ -8,10 +8,10 @@ import (
"github.com/docker/docker/api/types/events" "github.com/docker/docker/api/types/events"
"github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/filters"
"github.com/docker/docker/client" "github.com/docker/docker/client"
"github.com/psviderski/uncloud/internal/machine/store"
"github.com/psviderski/uncloud/pkg/api"
"log/slog" "log/slog"
"time" "time"
"uncloud/internal/api"
"uncloud/internal/machine/store"
) )
const ( const (
@@ -149,8 +149,9 @@ func (m *Manager) syncContainersToStore(ctx context.Context) error {
if err != nil { if err != nil {
return fmt.Errorf("list containers from store: %w", err) return fmt.Errorf("list containers from store: %w", err)
} }
// List only Uncloud service containers identified by their labels. // List only Uncloud service containers identified by their labels.
containers, err := m.client.ContainerList(ctx, dockercontainer.ListOptions{ containerSummaries, err := m.client.ContainerList(ctx, dockercontainer.ListOptions{
Filters: filters.NewArgs( Filters: filters.NewArgs(
filters.Arg("label", api.LabelServiceID), filters.Arg("label", api.LabelServiceID),
filters.Arg("label", api.LabelServiceName), filters.Arg("label", api.LabelServiceName),
@@ -161,11 +162,21 @@ func (m *Manager) syncContainersToStore(ctx context.Context) error {
return fmt.Errorf("list Docker containers: %w", err) return fmt.Errorf("list Docker containers: %w", err)
} }
// Delete containers that are not present in the Docker daemon from the store. // Inspect each container to get the full container details.
containers := make([]api.Container, len(containerSummaries))
for i, cs := range containerSummaries {
ctr, err := m.client.ContainerInspect(ctx, cs.ID)
if err != nil {
return fmt.Errorf("inspect container '%s': %w", cs.ID, err)
}
containers[i] = api.Container{ContainerJSON: ctr}
}
// Delete containers from the store that are no longer present in the Docker daemon.
var deleteIDs []string var deleteIDs []string
for _, sc := range storeContainers { for _, sc := range storeContainers {
found := false found := false
for i, _ := range containers { for i := range containers {
if containers[i].ID == sc.Container.ID { if containers[i].ID == sc.Container.ID {
found = true found = true
break break
@@ -184,8 +195,7 @@ func (m *Manager) syncContainersToStore(ctx context.Context) error {
} }
// Create or update the current Docker containers in the store. // Create or update the current Docker containers in the store.
for _, dc := range containers { for _, c := range containers {
c := &api.Container{Container: dc}
if err = m.store.CreateOrUpdateContainer(ctx, c, m.machineID); err != nil { 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)) storeErr = errors.Join(storeErr, fmt.Errorf("create or update container %q: %w", c.ID, err))
} }
+1 -1
View File
@@ -9,6 +9,6 @@ import (
) )
// EnsureUncloudNetwork is a stub for darwin. // EnsureUncloudNetwork is a stub for darwin.
func (d *Manager) EnsureUncloudNetwork(ctx context.Context, subnet netip.Prefix) error { func (m *Manager) EnsureUncloudNetwork(ctx context.Context, subnet netip.Prefix) error {
return fmt.Errorf("not supported on darwin") return fmt.Errorf("not supported on darwin")
} }
+6 -6
View File
@@ -8,16 +8,16 @@ import (
"github.com/docker/docker/libnetwork/iptables" "github.com/docker/docker/libnetwork/iptables"
"log/slog" "log/slog"
"net/netip" "net/netip"
"uncloud/internal/machine/network" "github.com/psviderski/uncloud/internal/machine/network"
) )
// EnsureUncloudNetwork creates the Docker bridge network NetworkName with the provided machine subnet // EnsureUncloudNetwork creates the Docker bridge network NetworkName with the provided machine subnet
// if it doesn't exist. If the network exists but has a different subnet, it removes and recreates the network. // if it doesn't exist. If the network exists but has a different subnet, it removes and recreates the network.
// It also configures iptables to allow container access from the WireGuard network. // It also configures iptables to allow container access from the WireGuard network.
func (d *Manager) EnsureUncloudNetwork(ctx context.Context, subnet netip.Prefix) error { func (m *Manager) EnsureUncloudNetwork(ctx context.Context, subnet netip.Prefix) error {
// Ensure the Docker network 'uncloud' is created with the correct subnet. // Ensure the Docker network 'uncloud' is created with the correct subnet.
needsCreation := false needsCreation := false
nw, err := d.client.NetworkInspect(ctx, NetworkName, dnetwork.InspectOptions{}) nw, err := m.client.NetworkInspect(ctx, NetworkName, dnetwork.InspectOptions{})
if err != nil { if err != nil {
if !client.IsErrNotFound(err) { if !client.IsErrNotFound(err) {
return fmt.Errorf("inspect Docker network %q: %w", NetworkName, err) return fmt.Errorf("inspect Docker network %q: %w", NetworkName, err)
@@ -29,7 +29,7 @@ func (d *Manager) EnsureUncloudNetwork(ctx context.Context, subnet netip.Prefix)
slog.Info( slog.Info(
"Removing Docker network with old subnet.", "name", NetworkName, "subnet", nw.IPAM.Config[0].Subnet, "Removing Docker network with old subnet.", "name", NetworkName, "subnet", nw.IPAM.Config[0].Subnet,
) )
if err = d.client.NetworkRemove(ctx, NetworkName); err != nil { if err = m.client.NetworkRemove(ctx, NetworkName); err != nil {
// It can still fail if the network is in use by a container. Leave it to the user to resolve the issue. // It can still fail if the network is in use by a container. Leave it to the user to resolve the issue.
return fmt.Errorf("remove Docker network %q: %w", NetworkName, err) return fmt.Errorf("remove Docker network %q: %w", NetworkName, err)
} }
@@ -37,7 +37,7 @@ func (d *Manager) EnsureUncloudNetwork(ctx context.Context, subnet netip.Prefix)
} }
if needsCreation { if needsCreation {
if _, err = d.client.NetworkCreate( if _, err = m.client.NetworkCreate(
ctx, NetworkName, dnetwork.CreateOptions{ ctx, NetworkName, dnetwork.CreateOptions{
Driver: "bridge", Driver: "bridge",
Scope: "local", Scope: "local",
@@ -54,7 +54,7 @@ func (d *Manager) EnsureUncloudNetwork(ctx context.Context, subnet netip.Prefix)
} }
slog.Info("Docker network created.", "name", NetworkName, "subnet", subnet.String()) slog.Info("Docker network created.", "name", NetworkName, "subnet", subnet.String())
if nw, err = d.client.NetworkInspect(ctx, NetworkName, dnetwork.InspectOptions{}); err != nil { if nw, err = m.client.NetworkInspect(ctx, NetworkName, dnetwork.InspectOptions{}); err != nil {
return fmt.Errorf("inspect Docker network %q: %w", NetworkName, err) return fmt.Errorf("inspect Docker network %q: %w", NetworkName, err)
} }
} }
+420 -14
View File
@@ -2,31 +2,54 @@ package docker
import ( import (
"context" "context"
"database/sql"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt"
"io"
"log/slog"
"regexp"
"strconv"
"strings"
"github.com/distribution/reference"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/image" "github.com/docker/docker/api/types/image"
"github.com/docker/docker/api/types/network" "github.com/docker/docker/api/types/network"
"github.com/docker/docker/client" "github.com/docker/docker/client"
"github.com/docker/go-connections/nat"
"github.com/google/go-containerregistry/pkg/authn"
"github.com/google/go-containerregistry/pkg/name"
"github.com/google/go-containerregistry/pkg/v1/remote"
"github.com/jmoiron/sqlx"
"github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1" ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/psviderski/uncloud/internal/machine/api/pb"
"github.com/psviderski/uncloud/internal/secret"
"github.com/psviderski/uncloud/pkg/api"
"google.golang.org/grpc" "google.golang.org/grpc"
"google.golang.org/grpc/codes" "google.golang.org/grpc/codes"
"google.golang.org/grpc/status" "google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/emptypb" "google.golang.org/protobuf/types/known/emptypb"
"io"
"uncloud/internal/machine/api/pb"
) )
var fullDockerIDRegex = regexp.MustCompile(`^[a-f0-9]{64}$`)
// Server implements the gRPC Docker service that proxies requests to the Docker daemon. // Server implements the gRPC Docker service that proxies requests to the Docker daemon.
type Server struct { type Server struct {
pb.UnimplementedDockerServer pb.UnimplementedDockerServer
client *client.Client client *client.Client
db *sqlx.DB
} }
// NewServer creates a new Docker gRPC server with the provided Docker client. // NewServer creates a new Docker gRPC server with the provided Docker client.
func NewServer(cli *client.Client) *Server { func NewServer(cli *client.Client, db *sqlx.DB) *Server {
return &Server{client: cli} return &Server{
client: cli,
db: db,
}
} }
// CreateContainer creates a new container based on the given configuration. // CreateContainer creates a new container based on the given configuration.
@@ -53,9 +76,9 @@ func (s *Server) CreateContainer(ctx context.Context, req *pb.CreateContainerReq
resp, err := s.client.ContainerCreate(ctx, &config, &hostConfig, &networkConfig, &platform, req.Name) resp, err := s.client.ContainerCreate(ctx, &config, &hostConfig, &networkConfig, &platform, req.Name)
if err != nil { if err != nil {
if client.IsErrNotFound(err) { if client.IsErrNotFound(err) {
return nil, status.Errorf(codes.NotFound, "create container: %v", err) return nil, status.Errorf(codes.NotFound, err.Error())
} }
return nil, status.Errorf(codes.Internal, "create container: %v", err) return nil, status.Errorf(codes.Internal, err.Error())
} }
respBytes, err := json.Marshal(resp) respBytes, err := json.Marshal(resp)
@@ -66,6 +89,24 @@ func (s *Server) CreateContainer(ctx context.Context, req *pb.CreateContainerReq
return &pb.CreateContainerResponse{Response: respBytes}, nil return &pb.CreateContainerResponse{Response: respBytes}, nil
} }
// InspectContainer returns the container information for the given container ID.
func (s *Server) InspectContainer(ctx context.Context, req *pb.InspectContainerRequest) (*pb.InspectContainerResponse, error) {
resp, err := s.client.ContainerInspect(ctx, req.Id)
if err != nil {
if client.IsErrNotFound(err) {
return nil, status.Errorf(codes.NotFound, err.Error())
}
return nil, status.Errorf(codes.Internal, err.Error())
}
respBytes, err := json.Marshal(resp)
if err != nil {
return nil, status.Errorf(codes.Internal, "marshal response: %v", err)
}
return &pb.InspectContainerResponse{Response: respBytes}, nil
}
// StartContainer starts a container with the given ID and options. // StartContainer starts a container with the given ID and options.
func (s *Server) StartContainer(ctx context.Context, req *pb.StartContainerRequest) (*emptypb.Empty, error) { func (s *Server) StartContainer(ctx context.Context, req *pb.StartContainerRequest) (*emptypb.Empty, error) {
var opts container.StartOptions var opts container.StartOptions
@@ -77,9 +118,28 @@ func (s *Server) StartContainer(ctx context.Context, req *pb.StartContainerReque
if err := s.client.ContainerStart(ctx, req.Id, opts); err != nil { if err := s.client.ContainerStart(ctx, req.Id, opts); err != nil {
if client.IsErrNotFound(err) { if client.IsErrNotFound(err) {
return nil, status.Errorf(codes.NotFound, "start container: %v", err) return nil, status.Errorf(codes.NotFound, err.Error())
} }
return nil, status.Errorf(codes.Internal, "start container: %v", err) return nil, status.Errorf(codes.Internal, err.Error())
}
return &emptypb.Empty{}, nil
}
// StopContainer stops a container with the given ID and options.
func (s *Server) StopContainer(ctx context.Context, req *pb.StopContainerRequest) (*emptypb.Empty, error) {
var opts container.StopOptions
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.ContainerStop(ctx, req.Id, opts); err != nil {
if client.IsErrNotFound(err) {
return nil, status.Errorf(codes.NotFound, err.Error())
}
return nil, status.Errorf(codes.Internal, err.Error())
} }
return &emptypb.Empty{}, nil return &emptypb.Empty{}, nil
@@ -107,9 +167,21 @@ func (s *Server) ListContainers(ctx context.Context, req *pb.ListContainersReque
} }
} }
containers, err := s.client.ContainerList(ctx, opts) containerSummaries, err := s.client.ContainerList(ctx, opts)
if err != nil { if err != nil {
return nil, status.Errorf(codes.Internal, "list container: %v", err) return nil, status.Errorf(codes.Internal, err.Error())
}
containers := make([]types.ContainerJSON, 0, len(containerSummaries))
for _, cs := range containerSummaries {
c, err := s.client.ContainerInspect(ctx, cs.ID)
if err != nil {
if client.IsErrNotFound(err) {
// The listed container may have been removed while we were inspecting other containers.
continue
}
return nil, status.Errorf(codes.Internal, "inspect container %s: %v", cs.ID, err)
}
containers = append(containers, c)
} }
containersBytes, err := json.Marshal(containers) containersBytes, err := json.Marshal(containers)
@@ -137,9 +209,9 @@ func (s *Server) RemoveContainer(ctx context.Context, req *pb.RemoveContainerReq
if err := s.client.ContainerRemove(ctx, req.Id, opts); err != nil { if err := s.client.ContainerRemove(ctx, req.Id, opts); err != nil {
if client.IsErrNotFound(err) { if client.IsErrNotFound(err) {
return nil, status.Errorf(codes.NotFound, "remove container: %v", err) return nil, status.Errorf(codes.NotFound, err.Error())
} }
return nil, status.Errorf(codes.Internal, "remove container: %v", err) return nil, status.Errorf(codes.Internal, err.Error())
} }
return &emptypb.Empty{}, nil return &emptypb.Empty{}, nil
@@ -158,7 +230,7 @@ func (s *Server) PullImage(req *pb.PullImageRequest, stream grpc.ServerStreaming
respBody, err := s.client.ImagePull(ctx, req.Image, opts) respBody, err := s.client.ImagePull(ctx, req.Image, opts)
if err != nil { if err != nil {
return status.Errorf(codes.Internal, "pull image: %v", err) return status.Errorf(codes.Internal, err.Error())
} }
defer respBody.Close() defer respBody.Close()
@@ -189,7 +261,341 @@ func (s *Server) PullImage(req *pb.PullImageRequest, stream grpc.ServerStreaming
case err = <-errCh: case err = <-errCh:
return err return err
case <-ctx.Done(): case <-ctx.Done():
return status.Errorf(codes.Canceled, "pull image: %v", ctx.Err()) return status.Errorf(codes.Canceled, ctx.Err().Error())
} }
} }
} }
// InspectImage returns the image information for the given image ID.
func (s *Server) InspectImage(ctx context.Context, req *pb.InspectImageRequest) (*pb.InspectImageResponse, error) {
resp, _, err := s.client.ImageInspectWithRaw(ctx, req.Id)
if err != nil {
if client.IsErrNotFound(err) {
return nil, status.Errorf(codes.NotFound, err.Error())
}
return nil, status.Errorf(codes.Internal, err.Error())
}
respBytes, err := json.Marshal(resp)
if err != nil {
return nil, status.Errorf(codes.Internal, "marshal response: %v", err)
}
return &pb.InspectImageResponse{
Messages: []*pb.Image{
{
Image: respBytes,
},
},
}, nil
}
// InspectRemoteImage returns the image metadata for an image in a remote registry using the machine's Docker auth
// credentials if necessary.
func (s *Server) InspectRemoteImage(
_ context.Context, req *pb.InspectRemoteImageRequest,
) (*pb.InspectRemoteImageResponse, error) {
ref, err := name.ParseReference(req.Id)
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "parse image: %v", err)
}
desc, err := remote.Get(ref, remote.WithAuthFromKeychain(authn.DefaultKeychain))
if err != nil {
return nil, status.Errorf(codes.Internal, "fetch image manifest: %v", err)
}
namedRef, err := reference.ParseNormalizedNamed(ref.String())
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "parse image: %v", err)
}
var canonicalRef reference.Canonical
if _, ok := namedRef.(reference.Canonical); ok {
canonicalRef = namedRef.(reference.Canonical)
} else {
if canonicalRef, err = reference.WithDigest(namedRef, digest.Digest(desc.Digest.String())); err != nil {
return nil, status.Errorf(codes.Internal, "add digest to image: %v", err)
}
}
return &pb.InspectRemoteImageResponse{
Messages: []*pb.RemoteImage{
{
Reference: reference.FamiliarString(canonicalRef),
Manifest: desc.Manifest,
},
},
}, nil
}
// CreateServiceContainer creates a new container for the service with the given specifications.
func (s *Server) CreateServiceContainer(
ctx context.Context, req *pb.CreateServiceContainerRequest,
) (*pb.CreateContainerResponse, error) {
if !api.ValidateServiceID(req.ServiceId) {
return nil, status.Errorf(codes.InvalidArgument, "invalid service ID: '%s'", req.ServiceId)
}
var spec api.ServiceSpec
if err := json.Unmarshal(req.ServiceSpec, &spec); err != nil {
return nil, status.Errorf(codes.InvalidArgument, "unmarshal service spec: %v", err)
}
spec = spec.SetDefaults()
if err := spec.Validate(); err != nil {
return nil, status.Errorf(codes.InvalidArgument, "invalid service spec: %v", err)
}
containerName := req.ContainerName
if containerName == "" {
suffix, err := secret.RandomAlphaNumeric(4)
if err != nil {
return nil, fmt.Errorf("generate random suffix: %w", err)
}
containerName = fmt.Sprintf("%s-%s", spec.Name, suffix)
}
config := &container.Config{
Cmd: spec.Container.Command,
Entrypoint: spec.Container.Entrypoint,
Hostname: containerName,
Image: spec.Container.Image,
Labels: map[string]string{
api.LabelServiceID: req.ServiceId,
api.LabelServiceName: spec.Name,
api.LabelServiceMode: spec.Mode,
api.LabelManaged: "",
},
}
if spec.Mode == "" {
config.Labels[api.LabelServiceMode] = api.ServiceModeReplicated
}
// TODO: do not set the ports as container labels once migrated to retrieve them from the spec in DB.
var err error
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 nil, 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,
// Always restart service containers if they exit or a machine restarts.
// For one-off containers and batch jobs we plan to use a different service type/mode.
RestartPolicy: container.RestartPolicy{
Name: container.RestartPolicyAlways,
},
}
networkConfig := &network.NetworkingConfig{
EndpointsConfig: map[string]*network.EndpointSettings{
NetworkName: {},
},
}
resp, err := s.client.ContainerCreate(ctx, config, hostConfig, networkConfig, nil, containerName)
if err != nil {
if client.IsErrNotFound(err) {
return nil, status.Error(codes.NotFound, err.Error())
}
return nil, status.Error(codes.Internal, err.Error())
}
respBytes, err := json.Marshal(resp)
if err != nil {
return nil, status.Errorf(codes.Internal, "marshal response: %v", err)
}
// Store the container spec in the database or remove the container with its anonymous volumes if storing fails.
removeContainer := func() {
_ = s.client.ContainerRemove(ctx, resp.ID, container.RemoveOptions{RemoveVolumes: true})
}
specBytes, err := json.Marshal(spec)
if err != nil {
removeContainer()
return nil, status.Errorf(codes.Internal, "marshal service spec: %v", err)
}
if _, err = s.db.ExecContext(ctx,
`INSERT INTO containers (id, service_id, service_spec) VALUES ($1, $2, $3)`,
resp.ID, req.ServiceId, string(specBytes)); err != nil {
removeContainer()
return nil, status.Errorf(codes.Internal, "store container in database: %v", err)
}
return &pb.CreateContainerResponse{Response: respBytes}, nil
}
// InspectServiceContainer returns the container information and service specification that was used to create the
// container with the given ID.
func (s *Server) InspectServiceContainer(
ctx context.Context, req *pb.InspectContainerRequest,
) (*pb.ServiceContainer, error) {
ctr, err := s.client.ContainerInspect(ctx, req.Id)
if err != nil {
if client.IsErrNotFound(err) {
return nil, status.Errorf(codes.NotFound, err.Error())
}
return nil, status.Errorf(codes.Internal, err.Error())
}
ctrBytes, err := json.Marshal(ctr)
if err != nil {
return nil, status.Errorf(codes.Internal, "marshal response: %v", err)
}
var specBytes []byte
err = s.db.QueryRowContext(ctx, `SELECT service_spec FROM containers WHERE id = $1`, ctr.ID).Scan(&specBytes)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, status.Errorf(codes.NotFound, "service spec not found for container: '%s'", ctr.ID)
}
return nil, status.Errorf(codes.Internal, "get service spec for container '%s' from machine database: %v",
ctr.ID, err)
}
return &pb.ServiceContainer{
Container: ctrBytes,
ServiceSpec: specBytes,
}, nil
}
// ListServiceContainers returns all containers that belong to the service with the given name or ID.
// If req.ServiceId is empty, all service containers are returned.
func (s *Server) ListServiceContainers(
ctx context.Context, req *pb.ListServiceContainersRequest,
) (*pb.ListServiceContainersResponse, 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
} else {
opts.Filters = filters.NewArgs()
}
}
// Only uncloud-managed containers that belong to some service.
opts.Filters.Add("label", api.LabelServiceID)
opts.Filters.Add("label", api.LabelManaged)
containerSummaries, err := s.client.ContainerList(ctx, opts)
if err != nil {
return nil, status.Error(codes.Internal, err.Error())
}
containers := make([]*pb.ServiceContainer, 0, len(containerSummaries))
for _, cs := range containerSummaries {
if req.ServiceId != "" &&
cs.Labels[api.LabelServiceID] != req.ServiceId && cs.Labels[api.LabelServiceName] != req.ServiceId {
continue
}
ctr, err := s.client.ContainerInspect(ctx, cs.ID)
if err != nil {
if client.IsErrNotFound(err) {
// The listed container may have been removed while we were inspecting other containers.
continue
}
return nil, status.Errorf(codes.Internal, "inspect container %s: %v", cs.ID, err)
}
ctrBytes, err := json.Marshal(ctr)
if err != nil {
return nil, status.Errorf(codes.Internal, "marshal container: %v", err)
}
var specBytes []byte
err = s.db.QueryRowContext(ctx, `SELECT service_spec FROM containers WHERE id = $1`, ctr.ID).Scan(&specBytes)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
// If this happens, there is a bug in the code, or someone manually removed the container from the DB,
// or created a managed container out of band.
slog.Error("Service container not found in machine database.", "id", ctr.ID)
// Just ignore such a container to not fail the list operation as it's not easily recoverable.
continue
}
return nil, status.Errorf(codes.Internal, "get service spec for container '%s' from machine database: %v",
ctr.ID, err)
}
containers = append(containers, &pb.ServiceContainer{
Container: ctrBytes,
ServiceSpec: specBytes,
})
}
return &pb.ListServiceContainersResponse{
Messages: []*pb.MachineServiceContainers{
{
Containers: containers,
},
},
}, nil
}
// RemoveServiceContainer stops (kills after grace period) and removes a service container with the given ID.
// The difference between this method and RemoveContainer is that it also removes the container from the machine
// database.
func (s *Server) RemoveServiceContainer(ctx context.Context, req *pb.RemoveContainerRequest) (*emptypb.Empty, error) {
ctrID := req.Id
// If the ID is not a full Docker ID, inspect the container to get its full ID.
if !fullDockerIDRegex.MatchString(req.Id) {
ctr, err := s.client.ContainerInspect(ctx, req.Id)
if err != nil {
if client.IsErrNotFound(err) {
return nil, status.Error(codes.NotFound, err.Error())
}
return nil, status.Error(codes.Internal, err.Error())
}
ctrID = ctr.ID
}
resp, err := s.RemoveContainer(ctx, req)
if err != nil {
return nil, err
}
if _, err = s.db.ExecContext(ctx, `DELETE FROM containers WHERE id = $1`, ctrID); err != nil {
slog.Error("Failed to remove container from machine database.", "err", err, "id", ctrID)
// Do not return an error because the container has already been removed from the Docker daemon.
// The orphaned db record will be ignored and eventually cleaned up by the garbage collector.
}
return resp, nil
}
+52 -34
View File
@@ -5,32 +5,35 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "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"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/emptypb"
"log/slog" "log/slog"
"net" "net"
"net/netip" "net/netip"
"os" "os"
"os/user" "os/user"
"path/filepath" "path/filepath"
"slices"
"strconv" "strconv"
"uncloud/internal/corrosion"
"uncloud/internal/docker" "github.com/docker/docker/client"
"uncloud/internal/fs" "github.com/docker/go-connections/sockets"
"uncloud/internal/machine/api/pb" "github.com/psviderski/uncloud/internal/corrosion"
apiproxy "uncloud/internal/machine/api/proxy" "github.com/psviderski/uncloud/internal/docker"
"uncloud/internal/machine/caddyfile" "github.com/psviderski/uncloud/internal/fs"
"uncloud/internal/machine/cluster" "github.com/psviderski/uncloud/internal/machine/api/pb"
"uncloud/internal/machine/corroservice" apiproxy "github.com/psviderski/uncloud/internal/machine/api/proxy"
machinedocker "uncloud/internal/machine/docker" "github.com/psviderski/uncloud/internal/machine/caddyfile"
"uncloud/internal/machine/network" "github.com/psviderski/uncloud/internal/machine/cluster"
"uncloud/internal/machine/store" "github.com/psviderski/uncloud/internal/machine/corroservice"
machinedocker "github.com/psviderski/uncloud/internal/machine/docker"
"github.com/psviderski/uncloud/internal/machine/network"
"github.com/psviderski/uncloud/internal/machine/store"
"github.com/psviderski/uncloud/pkg/api"
"github.com/siderolabs/grpc-proxy/proxy"
"golang.org/x/sync/errgroup"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/emptypb"
) )
const ( const (
@@ -56,9 +59,9 @@ type Config struct {
// DockerClient manages system and user containers using the local Docker daemon. // DockerClient manages system and user containers using the local Docker daemon.
DockerClient *client.Client DockerClient *client.Client
// CaddyfilePath specifies where the machine generates the Caddy reverse proxy configuration file for routing // CaddyConfigPath specifies where the machine generates the Caddy reverse proxy configuration file for routing
// external traffic to service containers across the internal network. Default is DataDir/caddy/Caddyfile. // external traffic to service containers across the internal network. Default is DataDir/caddy/caddy.json.
CaddyfilePath string CaddyConfigPath string
} }
// SetDefaults returns a new Config with default values set where not provided. // SetDefaults returns a new Config with default values set where not provided.
@@ -121,8 +124,8 @@ func (c *Config) SetDefaults() (*Config, error) {
} }
} }
if cfg.CaddyfilePath == "" { if cfg.CaddyConfigPath == "" {
cfg.CaddyfilePath = filepath.Join(cfg.DataDir, "caddy", "caddy.json") cfg.CaddyConfigPath = filepath.Join(cfg.DataDir, "caddy", "caddy.json")
} }
return &cfg, nil return &cfg, nil
@@ -208,7 +211,13 @@ func NewMachine(config *Config) (*Machine, error) {
if err != nil { if err != nil {
return nil, fmt.Errorf("create Docker client: %w", err) return nil, fmt.Errorf("create Docker client: %w", err)
} }
dockerServer := machinedocker.NewServer(dockerCli)
dbFilePath := filepath.Join(config.DataDir, DBFileName)
db, err := NewDB(dbFilePath)
if err != nil {
return nil, fmt.Errorf("init machine database: %w", err)
}
dockerServer := machinedocker.NewServer(dockerCli, db)
// Init a local gRPC proxy server that proxies requests to the local or remote machine API servers. // Init a local gRPC proxy server that proxies requests to the local or remote machine API servers.
proxyDirector := apiproxy.NewDirector(config.MachineSockPath, APIPort) proxyDirector := apiproxy.NewDirector(config.MachineSockPath, APIPort)
@@ -357,7 +366,9 @@ func (m *Machine) Run(ctx context.Context) error {
), ),
) )
caddyfileCtrl, err := caddyfile.NewController(m.store, m.config.CaddyfilePath) // Create a new Caddyfile controller for managing the Caddy reverse proxy configuration.
// It will also serve the current machine ID at /.uncloud-verify to verify Caddy reachability.
caddyfileCtrl, err := caddyfile.NewController(m.store, m.config.CaddyConfigPath, m.state.ID)
if err != nil { if err != nil {
return fmt.Errorf("create Caddyfile controller: %w", err) return fmt.Errorf("create Caddyfile controller: %w", err)
} }
@@ -536,9 +547,9 @@ func (m *Machine) InitCluster(ctx context.Context, req *pb.InitClusterRequest) (
if err != nil { if err != nil {
return nil, status.Errorf(codes.Internal, "list routable IPs: %v", err) return nil, status.Errorf(codes.Internal, "list routable IPs: %v", err)
} }
publicIP, err := network.GetPublicIP() publicIP, pubIPErr := network.GetPublicIP()
// Ignore the error if failed to get the public IP using API services. // Ignore the error if failed to get the public IP using API services.
if err == nil { if pubIPErr == nil && !slices.Contains(ips, publicIP) {
ips = append(ips, publicIP) ips = append(ips, publicIP)
} }
endpoints := make([]*pb.IPPort, len(ips)) endpoints := make([]*pb.IPPort, len(ips))
@@ -556,6 +567,12 @@ func (m *Machine) InitCluster(ctx context.Context, req *pb.InitClusterRequest) (
PublicKey: m.state.Network.PublicKey, PublicKey: m.state.Network.PublicKey,
}, },
} }
if req.GetPublicIp() != nil {
addReq.PublicIp = req.GetPublicIp()
} else if req.GetPublicIpAuto() && pubIPErr == nil {
addReq.PublicIp = pb.NewIP(publicIP)
}
addResp, err := m.cluster.AddMachine(ctx, addReq) addResp, err := m.cluster.AddMachine(ctx, addReq)
if err != nil { if err != nil {
return nil, status.Errorf(codes.Internal, "add machine to cluster: %v", err) return nil, status.Errorf(codes.Internal, "add machine to cluster: %v", err)
@@ -674,7 +691,7 @@ func (m *Machine) Token(_ context.Context, _ *emptypb.Empty) (*pb.TokenResponse,
} }
publicIP, err := network.GetPublicIP() publicIP, err := network.GetPublicIP()
// Ignore the error if failed to get the public IP using API services. // Ignore the error if failed to get the public IP using API services.
if err == nil { if err == nil && !slices.Contains(ips, publicIP) {
ips = append(ips, publicIP) ips = append(ips, publicIP)
} }
endpoints := make([]netip.AddrPort, len(ips)) endpoints := make([]netip.AddrPort, len(ips))
@@ -682,7 +699,7 @@ func (m *Machine) Token(_ context.Context, _ *emptypb.Empty) (*pb.TokenResponse,
endpoints[i] = netip.AddrPortFrom(ip, network.WireGuardPort) endpoints[i] = netip.AddrPortFrom(ip, network.WireGuardPort)
} }
token := NewToken(m.state.Network.PublicKey, endpoints) token := NewToken(m.state.Network.PublicKey, publicIP, endpoints)
tokenStr, err := token.String() tokenStr, err := token.String()
if err != nil { if err != nil {
return nil, status.Error(codes.Internal, err.Error()) return nil, status.Error(codes.Internal, err.Error())
@@ -703,7 +720,7 @@ func (m *Machine) Inspect(_ context.Context, _ *emptypb.Empty) (*pb.MachineInfo,
}, nil }, nil
} }
// InspectService returns detailed information about a service and its containers. // InspectService returns detailed information about a service and its containers stored in the cluster store.
func (m *Machine) InspectService( func (m *Machine) InspectService(
ctx context.Context, req *pb.InspectServiceRequest, ctx context.Context, req *pb.InspectServiceRequest,
) (*pb.InspectServiceResponse, error) { ) (*pb.InspectServiceResponse, error) {
@@ -735,10 +752,11 @@ func (m *Machine) InspectService(
} }
} }
ctr := api.ServiceContainer{Container: records[0].Container}
svc := &pb.Service{ svc := &pb.Service{
Id: records[0].Container.ServiceID(), Id: ctr.ServiceID(),
Name: records[0].Container.ServiceName(), Name: ctr.ServiceName(),
Mode: records[0].Container.ServiceMode(), Mode: ctr.ServiceMode(),
Containers: containers, Containers: containers,
} }
return &pb.InspectServiceResponse{Service: svc}, nil return &pb.InspectServiceResponse{Service: svc}, nil
+6 -6
View File
@@ -14,12 +14,12 @@ import (
"slices" "slices"
"strconv" "strconv"
"time" "time"
"uncloud/internal/machine/api/pb" "github.com/psviderski/uncloud/internal/machine/api/pb"
"uncloud/internal/machine/caddyfile" "github.com/psviderski/uncloud/internal/machine/caddyfile"
"uncloud/internal/machine/corroservice" "github.com/psviderski/uncloud/internal/machine/corroservice"
"uncloud/internal/machine/docker" "github.com/psviderski/uncloud/internal/machine/docker"
"uncloud/internal/machine/network" "github.com/psviderski/uncloud/internal/machine/network"
"uncloud/internal/machine/store" "github.com/psviderski/uncloud/internal/machine/store"
) )
const ( const (
+1 -1
View File
@@ -2,10 +2,10 @@ package network
import ( import (
"fmt" "fmt"
"github.com/psviderski/uncloud/internal/secret"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes" "golang.zx2c4.com/wireguard/wgctrl/wgtypes"
"net" "net"
"net/netip" "net/netip"
"uncloud/internal/secret"
) )
type Config struct { type Config struct {
+1 -1
View File
@@ -2,9 +2,9 @@ package network
import ( import (
"fmt" "fmt"
"github.com/psviderski/uncloud/internal/secret"
"net" "net"
"net/netip" "net/netip"
"uncloud/internal/secret"
) )
// MachineIP returns the IP address of the machine which is the first address in the subnet. // MachineIP returns the IP address of the machine which is the first address in the subnet.
+1 -1
View File
@@ -9,7 +9,7 @@ import (
"net" "net"
"net/netip" "net/netip"
"time" "time"
"uncloud/internal/secret" "github.com/psviderski/uncloud/internal/secret"
) )
const ( const (
+1 -1
View File
@@ -5,7 +5,7 @@ import (
"golang.zx2c4.com/wireguard/wgctrl/wgtypes" "golang.zx2c4.com/wireguard/wgctrl/wgtypes"
"net/netip" "net/netip"
"time" "time"
"uncloud/internal/secret" "github.com/psviderski/uncloud/internal/secret"
) )
const ( const (
+1 -1
View File
@@ -17,7 +17,7 @@ import (
"slices" "slices"
"sync" "sync"
"time" "time"
"uncloud/internal/secret" "github.com/psviderski/uncloud/internal/secret"
) )
type WireGuardNetwork struct { type WireGuardNetwork struct {
+1 -1
View File
@@ -6,7 +6,7 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"sync" "sync"
"uncloud/internal/machine/network" "github.com/psviderski/uncloud/internal/machine/network"
) )
const ( const (
+16 -13
View File
@@ -5,10 +5,10 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
sq "github.com/Masterminds/squirrel" sq "github.com/Masterminds/squirrel"
"github.com/psviderski/uncloud/pkg/api"
"log/slog" "log/slog"
"strings" "strings"
"time" "time"
"uncloud/internal/api"
) )
const ( const (
@@ -22,7 +22,7 @@ const (
) )
type ContainerRecord struct { type ContainerRecord struct {
Container *api.Container Container api.Container
MachineID string MachineID string
SyncStatus string SyncStatus string
UpdatedAt time.Time UpdatedAt time.Time
@@ -47,8 +47,11 @@ type DeleteOptions struct {
// CreateOrUpdateContainer creates a new container record or updates an existing one in the store database. // 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. // 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 { func (s *Store) CreateOrUpdateContainer(ctx context.Context, ctr api.Container, machineID string) error {
cJSON, err := json.Marshal(c) // Remove the environment variables from the container record before storing it in the database
// to avoid leaking secrets.
ctr.Config.Env = nil
cJSON, err := json.Marshal(ctr)
if err != nil { if err != nil {
return fmt.Errorf("marshal container: %w", err) return fmt.Errorf("marshal container: %w", err)
} }
@@ -63,19 +66,19 @@ func (s *Store) CreateOrUpdateContainer(ctx context.Context, c *api.Container, m
updated_at = excluded.updated_at updated_at = excluded.updated_at
WHERE containers.container != excluded.container WHERE containers.container != excluded.container
OR containers.machine_id != excluded.machine_id`, OR containers.machine_id != excluded.machine_id`,
c.ID, string(cJSON), machineID, SyncStatusSynced) ctr.ID, string(cJSON), machineID, SyncStatusSynced)
if err != nil { if err != nil {
return fmt.Errorf("upsert query: %w", err) return fmt.Errorf("upsert query: %w", err)
} }
if res.RowsAffected > 0 { if res.RowsAffected > 0 {
slog.Debug("Container record updated in store DB.", "id", c.ID, "machine_id", machineID) slog.Debug("Container record updated in store DB.", "id", ctr.ID, "machine_id", machineID)
} }
return nil return nil
} }
// ListContainers returns a list of container records from the store database that match the given options. // 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) { func (s *Store) ListContainers(ctx context.Context, opts ListOptions) ([]ContainerRecord, error) {
q := sq.Select("container", "machine_id", "sync_status", "updated_at").From("containers"). q := sq.Select("container", "machine_id", "sync_status", "updated_at").From("containers").
Where(sq.Eq{"sync_status": SyncStatusSynced}) Where(sq.Eq{"sync_status": SyncStatusSynced})
@@ -105,7 +108,7 @@ func (s *Store) ListContainers(ctx context.Context, opts ListOptions) ([]*Contai
} }
defer rows.Close() defer rows.Close()
var containers []*ContainerRecord var containers []ContainerRecord
var cJSON, machineID, syncStatus, updatedAtStr string var cJSON, machineID, syncStatus, updatedAtStr string
var updatedAt time.Time var updatedAt time.Time
@@ -121,8 +124,8 @@ func (s *Store) ListContainers(ctx context.Context, opts ListOptions) ([]*Contai
if updatedAt, err = time.Parse(time.DateTime, updatedAtStr); err != nil { if updatedAt, err = time.Parse(time.DateTime, updatedAtStr); err != nil {
return nil, fmt.Errorf("parse updated_at: %w", err) return nil, fmt.Errorf("parse updated_at: %w", err)
} }
containers = append(containers, &ContainerRecord{ containers = append(containers, ContainerRecord{
Container: &c, Container: c,
MachineID: machineID, MachineID: machineID,
SyncStatus: syncStatus, SyncStatus: syncStatus,
UpdatedAt: updatedAt, UpdatedAt: updatedAt,
@@ -158,7 +161,7 @@ func (s *Store) DeleteContainers(ctx context.Context, opts DeleteOptions) error
// SubscribeContainers returns a list of containers and a channel that signals changes to the list. The channel doesn't // 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. // 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) { func (s *Store) SubscribeContainers(ctx context.Context) ([]ContainerRecord, <-chan struct{}, error) {
// TODO: figure out whether we need sync_status at all. // TODO: figure out whether we need sync_status at all.
q := sq.Select("container", "machine_id", "sync_status", "updated_at").From("containers"). q := sq.Select("container", "machine_id", "sync_status", "updated_at").From("containers").
Where(sq.Eq{"sync_status": SyncStatusSynced}) Where(sq.Eq{"sync_status": SyncStatusSynced})
@@ -172,7 +175,7 @@ func (s *Store) SubscribeContainers(ctx context.Context) ([]*ContainerRecord, <-
return nil, nil, err return nil, nil, err
} }
var containers []*ContainerRecord var containers []ContainerRecord
var cJSON, updatedAtStr string var cJSON, updatedAtStr string
rows := sub.Rows() rows := sub.Rows()
@@ -188,7 +191,7 @@ func (s *Store) SubscribeContainers(ctx context.Context) ([]*ContainerRecord, <-
if cr.UpdatedAt, err = time.Parse(time.DateTime, updatedAtStr); err != nil { if cr.UpdatedAt, err = time.Parse(time.DateTime, updatedAtStr); err != nil {
return nil, nil, fmt.Errorf("parse updated_at: %w", err) return nil, nil, fmt.Errorf("parse updated_at: %w", err)
} }
containers = append(containers, &cr) containers = append(containers, cr)
} }
events, err := sub.Changes() events, err := sub.Changes()
if err != nil { if err != nil {
+2 -2
View File
@@ -22,8 +22,8 @@ CREATE TABLE containers
-- container is a JSON-serialized api.Container struct. -- container is a JSON-serialized api.Container struct.
container TEXT NOT NULL DEFAULT '{}' CHECK (json_valid(container)), container TEXT NOT NULL DEFAULT '{}' CHECK (json_valid(container)),
machine_id TEXT NOT NULL DEFAULT '', machine_id TEXT NOT NULL DEFAULT '',
service_id TEXT AS (json_extract(container, '$.Labels."uncloud.service.id"')), service_id TEXT AS (json_extract(container, '$.Config.Labels."uncloud.service.id"')),
service_name TEXT AS (json_extract(container, '$.Labels."uncloud.service.name"')), service_name TEXT AS (json_extract(container, '$.Config.Labels."uncloud.service.name"')),
-- sync_status indicates if the record reflects the actual Docker state of the container. -- sync_status indicates if the record reflects the actual Docker state of the container.
sync_status TEXT NOT NULL DEFAULT '', sync_status TEXT NOT NULL DEFAULT '',
-- updated_at is the last time the record was updated. -- updated_at is the last time the record was updated.
+11 -3
View File
@@ -7,8 +7,8 @@ import (
"fmt" "fmt"
"google.golang.org/protobuf/encoding/protojson" "google.golang.org/protobuf/encoding/protojson"
"log/slog" "log/slog"
"uncloud/internal/corrosion" "github.com/psviderski/uncloud/internal/corrosion"
"uncloud/internal/machine/api/pb" "github.com/psviderski/uncloud/internal/machine/api/pb"
) )
var ( var (
@@ -49,6 +49,11 @@ func (s *Store) Put(ctx context.Context, key string, value any) error {
return err return err
} }
func (s *Store) Delete(ctx context.Context, key string) error {
_, err := s.corro.ExecContext(ctx, "DELETE FROM cluster WHERE key = ?", key)
return err
}
func (s *Store) CreateMachine(ctx context.Context, m *pb.MachineInfo) error { func (s *Store) CreateMachine(ctx context.Context, m *pb.MachineInfo) error {
mJSON, err := protojson.Marshal(m) mJSON, err := protojson.Marshal(m)
if err != nil { if err != nil {
@@ -74,10 +79,13 @@ func (s *Store) ListMachines(ctx context.Context) ([]*pb.MachineInfo, error) {
if err = rows.Scan(&mJSON); err != nil { if err = rows.Scan(&mJSON); err != nil {
return nil, err return nil, err
} }
protojsonParser := protojson.UnmarshalOptions{DiscardUnknown: true}
var m pb.MachineInfo var m pb.MachineInfo
if err = protojson.Unmarshal([]byte(mJSON), &m); err != nil { if err = protojsonParser.Unmarshal([]byte(mJSON), &m); err != nil {
return nil, fmt.Errorf("unmarshal machine info: %w", err) return nil, fmt.Errorf("unmarshal machine info: %w", err)
} }
if err = m.Network.Validate(); err != nil { if err = m.Network.Validate(); err != nil {
slog.Error("Invalid network configuration for machine in store", "id", m.Id, "err", err) slog.Error("Invalid network configuration for machine in store", "id", m.Id, "err", err)
continue continue
+4 -2
View File
@@ -6,7 +6,7 @@ import (
"fmt" "fmt"
"net/netip" "net/netip"
"strings" "strings"
"uncloud/internal/secret" "github.com/psviderski/uncloud/internal/secret"
) )
const ( const (
@@ -16,13 +16,15 @@ const (
// Token represents the machine's token for joining a cluster. // Token represents the machine's token for joining a cluster.
type Token struct { type Token struct {
PublicKey secret.Secret PublicKey secret.Secret
PublicIP netip.Addr
Endpoints []netip.AddrPort Endpoints []netip.AddrPort
} }
// NewToken creates a new machine token with the given public key and endpoints. // NewToken creates a new machine token with the given public key and endpoints.
func NewToken(publicKey secret.Secret, endpoints []netip.AddrPort) Token { func NewToken(publicKey secret.Secret, publicIP netip.Addr, endpoints []netip.AddrPort) Token {
return Token{ return Token{
PublicKey: publicKey, PublicKey: publicKey,
PublicIP: publicIP,
Endpoints: endpoints, Endpoints: endpoints,
} }
} }
+6 -2
View File
@@ -2,12 +2,13 @@ package sshexec
import ( import (
"fmt" "fmt"
"golang.org/x/crypto/ssh"
"golang.org/x/crypto/ssh/agent"
"net" "net"
"os" "os"
"strconv" "strconv"
"time" "time"
"golang.org/x/crypto/ssh"
"golang.org/x/crypto/ssh/agent"
) )
func Connect(user, host string, port int, sshKeyPath string) (*ssh.Client, error) { func Connect(user, host string, port int, sshKeyPath string) (*ssh.Client, error) {
@@ -34,6 +35,9 @@ func Connect(user, host string, port int, sshKeyPath string) (*ssh.Client, error
} }
keyAuth, err := privateKeyAuth(sshKeyPath) keyAuth, err := privateKeyAuth(sshKeyPath)
if err != nil {
return nil, err
}
config := &ssh.ClientConfig{ config := &ssh.ClientConfig{
User: user, User: user,
Auth: []ssh.AuthMethod{keyAuth}, Auth: []ssh.AuthMethod{keyAuth},
+4 -4
View File
@@ -12,9 +12,9 @@ import (
"google.golang.org/protobuf/types/known/emptypb" "google.golang.org/protobuf/types/known/emptypb"
"net/netip" "net/netip"
"time" "time"
"uncloud/internal/machine" "github.com/psviderski/uncloud/internal/machine"
"uncloud/internal/machine/api/pb" "github.com/psviderski/uncloud/internal/machine/api/pb"
"uncloud/internal/machine/cluster" "github.com/psviderski/uncloud/internal/machine/cluster"
) )
const ( const (
@@ -89,7 +89,7 @@ func (p *Provisioner) initCluster(ctx context.Context, machines []Machine) error
// Init a new cluster on the first machine. // Init a new cluster on the first machine.
initMachine := machines[0] initMachine := machines[0]
if err := p.WaitMachineReady(ctx, initMachine, 5*time.Second); err != nil { if err := p.WaitMachineReady(ctx, initMachine, 30*time.Second); err != nil {
return fmt.Errorf("wait for machine %q to be ready: %w", initMachine.Name, err) return fmt.Errorf("wait for machine %q to be ready: %w", initMachine.Name, err)
} }
+1 -1
View File
@@ -2,7 +2,7 @@ package ucind
import ( import (
"fmt" "fmt"
"uncloud/internal/cli/config" "github.com/psviderski/uncloud/internal/cli/config"
) )
type ConfigUpdater struct { type ConfigUpdater struct {
+3 -3
View File
@@ -9,14 +9,14 @@ import (
"github.com/docker/docker/api/types/image" "github.com/docker/docker/api/types/image"
dockerclient "github.com/docker/docker/client" dockerclient "github.com/docker/docker/client"
"github.com/docker/go-connections/nat" "github.com/docker/go-connections/nat"
"github.com/psviderski/uncloud/internal/secret"
"github.com/psviderski/uncloud/pkg/client"
"github.com/psviderski/uncloud/pkg/client/connector"
"google.golang.org/protobuf/types/known/emptypb" "google.golang.org/protobuf/types/known/emptypb"
"io" "io"
"net" "net"
"net/netip" "net/netip"
"time" "time"
"uncloud/internal/cli/client"
"uncloud/internal/cli/client/connector"
"uncloud/internal/secret"
) )
const ( const (
+67
View File
@@ -0,0 +1,67 @@
package api
import (
"context"
"fmt"
"slices"
"github.com/docker/docker/api/types/container"
"github.com/psviderski/uncloud/internal/machine/api/pb"
"google.golang.org/grpc/metadata"
)
type Client interface {
ContainerClient
DNSClient
ImageClient
MachineClient
ServiceClient
}
type ContainerClient interface {
CreateContainer(
ctx context.Context, serviceID string, spec ServiceSpec, machineID string,
) (container.CreateResponse, error)
InspectContainer(ctx context.Context, serviceNameOrID, containerNameOrID string) (MachineServiceContainer, error)
RemoveContainer(ctx context.Context, serviceNameOrID, containerNameOrID string, opts container.RemoveOptions) error
StartContainer(ctx context.Context, serviceNameOrID, containerNameOrID string) error
StopContainer(ctx context.Context, serviceNameOrID, containerNameOrID string, opts container.StopOptions) error
}
type DNSClient interface {
GetDomain(ctx context.Context) (string, error)
}
type ImageClient interface {
InspectImage(ctx context.Context, id string) ([]MachineImage, error)
InspectRemoteImage(ctx context.Context, id string) ([]MachineRemoteImage, error)
}
type MachineClient interface {
InspectMachine(ctx context.Context, id string) (*pb.MachineMember, error)
ListMachines(ctx context.Context) ([]*pb.MachineMember, error)
}
type ServiceClient interface {
InspectService(ctx context.Context, id string) (Service, error)
}
// ProxyMachinesContext returns a new context that proxies gRPC requests to the specified machines.
// If namesOrIDs is nil, all machines are included.
func ProxyMachinesContext(ctx context.Context, cli MachineClient, namesOrIDs []string) (context.Context, error) {
machines, err := cli.ListMachines(ctx)
if err != nil {
return nil, fmt.Errorf("list machines: %w", err)
}
md := metadata.New(nil)
for _, m := range machines {
if namesOrIDs == nil ||
slices.Contains(namesOrIDs, m.Machine.Name) || slices.Contains(namesOrIDs, m.Machine.Id) {
machineIP, _ := m.Machine.Network.ManagementIp.ToAddr()
md.Append("machines", machineIP.String())
}
}
return metadata.NewOutgoingContext(ctx, md), nil
}
+186
View File
@@ -0,0 +1,186 @@
package api
import (
"encoding/json"
"fmt"
"strings"
"time"
"github.com/docker/docker/api/types"
"github.com/docker/go-units"
)
const (
LabelManaged = "uncloud.managed"
LabelServiceID = "uncloud.service.id"
LabelServiceName = "uncloud.service.name"
LabelServiceMode = "uncloud.service.mode"
LabelServicePorts = "uncloud.service.ports"
)
type Container struct {
types.ContainerJSON
}
// Healthy determines if the container is running and healthy.
// A running container with no health check configured is considered healthy.
func (c *Container) Healthy() bool {
if !c.State.Running || c.State.Paused || c.State.Restarting {
return false
}
// If there's no health status (no health check configured), container is considered healthy.
if c.State.Health == nil {
return true
}
return c.State.Health.Status == types.Healthy
}
// HumanState returns a human-readable description of the container's state. Based on the Docker implementation:
// https://github.com/moby/moby/blob/b343d235a0a1f30c8f05b1d651238e72158dc25d/container/state.go#L79-L113
func (c *Container) HumanState() (string, error) {
startedAt, err := time.Parse(time.RFC3339Nano, c.State.StartedAt)
if err != nil {
return "", fmt.Errorf("parse started time: %w", err)
}
finishedAt, err := time.Parse(time.RFC3339Nano, c.State.FinishedAt)
if err != nil {
return "", fmt.Errorf("parse finished time: %w", err)
}
if c.State.Running {
if c.State.Paused {
return fmt.Sprintf("Up %s (Paused)", units.HumanDuration(time.Now().UTC().Sub(startedAt))), nil
}
if c.State.Restarting {
return fmt.Sprintf("Restarting (%d) %s ago",
c.State.ExitCode, units.HumanDuration(time.Now().UTC().Sub(finishedAt))), nil
}
if c.State.Health != nil {
status := c.State.Health.Status
if status == types.Starting {
status = "health: " + status
}
return fmt.Sprintf("Up %s (%s)", units.HumanDuration(time.Now().UTC().Sub(startedAt)), status), nil
}
return fmt.Sprintf("Up %s", units.HumanDuration(time.Now().UTC().Sub(startedAt))), nil
}
if c.State.Status == "removing" {
return "Removal In Progress", nil
}
if c.State.Dead {
return "Dead", nil
}
if startedAt.IsZero() {
return "Created", nil
}
if finishedAt.IsZero() {
return "", nil
}
return fmt.Sprintf("Exited (%d) %s ago",
c.State.ExitCode, units.HumanDuration(time.Now().UTC().Sub(finishedAt))), nil
}
func (c *Container) UnmarshalJSON(data []byte) error {
// A temporary type that's identical to Container but doesn't have the UnmarshalJSON method.
type ContainerAlias Container
var temp ContainerAlias
if err := json.Unmarshal(data, &temp); err != nil {
return err
}
*c = Container(temp)
c.Name = strings.TrimPrefix(c.Name, "/")
return nil
}
type ServiceContainer struct {
Container
ServiceSpec ServiceSpec
}
type MachineContainer struct {
MachineID string
Container Container
}
// ServiceID returns the ID of the service this container belongs to.
func (c *ServiceContainer) ServiceID() string {
return c.Config.Labels[LabelServiceID]
}
// ServiceName returns the name of the service this container belongs to.
func (c *ServiceContainer) ServiceName() string {
return c.Config.Labels[LabelServiceName]
}
// ServiceMode returns the replication mode of the service this container belongs to.
func (c *ServiceContainer) ServiceMode() string {
return c.Config.Labels[LabelServiceMode]
}
// ServicePorts returns the ports this container publishes as part of its service.
func (c *ServiceContainer) ServicePorts() ([]PortSpec, error) {
encoded, ok := c.Config.Labels[LabelServicePorts]
if !ok {
return nil, nil
}
if strings.TrimSpace(encoded) == "" {
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
}
// ConflictingServicePorts returns a list of service ports that conflict with the given ports.
func (c *ServiceContainer) ConflictingServicePorts(ports []PortSpec) ([]PortSpec, error) {
svcPorts, err := c.ServicePorts()
if err != nil {
return nil, fmt.Errorf("get service ports: %w", err)
}
var conflicting []PortSpec
for _, p := range ports {
if p.Mode != PortModeHost {
continue
}
// Two host ports conflict if they have the same published port number and protocol, and either:
// * At least one host IP is not set (meaning it uses all interfaces)
// * Both host IPs are identical
for _, svcPort := range svcPorts {
if svcPort.Mode != PortModeHost ||
svcPort.PublishedPort != p.PublishedPort ||
svcPort.Protocol != p.Protocol {
continue
}
if !svcPort.HostIP.IsValid() || !p.HostIP.IsValid() || svcPort.HostIP.Compare(p.HostIP) == 0 {
conflicting = append(conflicting, p)
}
}
}
return conflicting, nil
}
+301
View File
@@ -0,0 +1,301 @@
package api
import (
"net/netip"
"testing"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestContainer_Healthy(t *testing.T) {
t.Parallel()
t.Run("exited", func(t *testing.T) {
t.Parallel()
c := &Container{ContainerJSON: types.ContainerJSON{
ContainerJSONBase: &types.ContainerJSONBase{
State: &types.ContainerState{
Running: false,
Dead: false,
ExitCode: 0,
},
},
}}
assert.False(t, c.Healthy())
})
t.Run("running with no health check", func(t *testing.T) {
t.Parallel()
c := &Container{ContainerJSON: types.ContainerJSON{
ContainerJSONBase: &types.ContainerJSONBase{
State: &types.ContainerState{
Running: true,
},
},
}}
assert.True(t, c.Healthy())
})
t.Run("running and healthy", func(t *testing.T) {
t.Parallel()
c := &Container{ContainerJSON: types.ContainerJSON{
ContainerJSONBase: &types.ContainerJSONBase{
State: &types.ContainerState{
Running: true,
Health: &types.Health{
Status: types.Healthy,
},
},
},
}}
assert.True(t, c.Healthy())
})
t.Run("running but unhealthy", func(t *testing.T) {
t.Parallel()
c := &Container{ContainerJSON: types.ContainerJSON{
ContainerJSONBase: &types.ContainerJSONBase{
State: &types.ContainerState{
Running: true,
Health: &types.Health{
Status: types.Unhealthy,
},
},
},
}}
assert.False(t, c.Healthy())
})
t.Run("running with health starting", func(t *testing.T) {
t.Parallel()
c := &Container{ContainerJSON: types.ContainerJSON{
ContainerJSONBase: &types.ContainerJSONBase{
State: &types.ContainerState{
Running: true,
Health: &types.Health{
Status: "starting",
},
},
},
}}
assert.False(t, c.Healthy())
})
t.Run("dead", func(t *testing.T) {
t.Parallel()
c := &Container{ContainerJSON: types.ContainerJSON{
ContainerJSONBase: &types.ContainerJSONBase{
State: &types.ContainerState{
Dead: true,
Running: false,
},
},
}}
assert.False(t, c.Healthy())
})
t.Run("restarting", func(t *testing.T) {
t.Parallel()
c := &Container{ContainerJSON: types.ContainerJSON{
ContainerJSONBase: &types.ContainerJSONBase{
State: &types.ContainerState{
Restarting: true,
Running: true,
ExitCode: 1,
},
},
}}
assert.False(t, c.Healthy())
})
t.Run("paused", func(t *testing.T) {
t.Parallel()
c := &Container{ContainerJSON: types.ContainerJSON{
ContainerJSONBase: &types.ContainerJSONBase{
State: &types.ContainerState{
Paused: true,
Running: true,
},
},
}}
assert.False(t, c.Healthy())
})
}
func TestContainer_ConflictingServicePorts(t *testing.T) {
tests := []struct {
name string
containerPorts string
checkPorts []PortSpec
want []PortSpec
wantErr bool
}{
{
name: "no conflicts when container has no ports",
containerPorts: "",
checkPorts: []PortSpec{
{Mode: PortModeHost, PublishedPort: 8080, ContainerPort: 80, Protocol: ProtocolTCP},
},
want: nil,
wantErr: false,
},
{
name: "host mode ports with same published port and protocol conflict",
containerPorts: "8080:80/tcp@host",
checkPorts: []PortSpec{
{Mode: PortModeHost, PublishedPort: 8080, ContainerPort: 80, Protocol: ProtocolTCP},
},
want: []PortSpec{
{Mode: PortModeHost, PublishedPort: 8080, ContainerPort: 80, Protocol: ProtocolTCP},
},
wantErr: false,
},
{
name: "host mode ports with same port but different protocols don't conflict",
containerPorts: "8080:80/tcp@host",
checkPorts: []PortSpec{
{Mode: PortModeHost, PublishedPort: 8080, ContainerPort: 80, Protocol: ProtocolUDP},
},
want: nil,
wantErr: false,
},
{
name: "multiple protocols on same port don't conflict",
containerPorts: "8080:80/tcp@host,8080:80/udp@host",
checkPorts: []PortSpec{
{Mode: PortModeHost, PublishedPort: 8080, ContainerPort: 80, Protocol: ProtocolUDP},
},
want: []PortSpec{
{Mode: PortModeHost, PublishedPort: 8080, ContainerPort: 80, Protocol: ProtocolUDP},
},
wantErr: false,
},
{
name: "host mode ports with different published ports don't conflict",
containerPorts: "8080:80/tcp@host",
checkPorts: []PortSpec{
{Mode: PortModeHost, PublishedPort: 8081, ContainerPort: 80, Protocol: ProtocolTCP},
},
want: nil,
wantErr: false,
},
{
name: "host mode ports with same published port but different host IPs don't conflict",
containerPorts: "127.0.0.1:8080:80/tcp@host",
checkPorts: []PortSpec{
{
Mode: PortModeHost,
HostIP: netip.MustParseAddr("127.0.0.2"),
PublishedPort: 8080,
ContainerPort: 80,
Protocol: ProtocolTCP,
},
},
want: nil,
wantErr: false,
},
{
name: "host mode ports with same published port, protocol, and host IP conflict",
containerPorts: "127.0.0.1:8080:80/tcp@host",
checkPorts: []PortSpec{
{
Mode: PortModeHost,
HostIP: netip.MustParseAddr("127.0.0.1"),
PublishedPort: 8080,
ContainerPort: 80,
Protocol: ProtocolTCP,
},
},
want: []PortSpec{
{
Mode: PortModeHost,
HostIP: netip.MustParseAddr("127.0.0.1"),
PublishedPort: 8080,
ContainerPort: 80,
Protocol: ProtocolTCP,
},
},
wantErr: false,
},
{
name: "host mode port with no host IP conflicts with specific host IP on same port and protocol",
containerPorts: "8080:80/tcp@host",
checkPorts: []PortSpec{
{
Mode: PortModeHost,
HostIP: netip.MustParseAddr("127.0.0.1"),
PublishedPort: 8080,
ContainerPort: 80,
Protocol: ProtocolTCP,
},
},
want: []PortSpec{
{
Mode: PortModeHost,
HostIP: netip.MustParseAddr("127.0.0.1"),
PublishedPort: 8080,
ContainerPort: 80,
Protocol: ProtocolTCP,
},
},
wantErr: false,
},
{
name: "host mode port with no host IP doesn't conflict with different protocol",
containerPorts: "8080:80/tcp@host",
checkPorts: []PortSpec{
{
Mode: PortModeHost,
HostIP: netip.MustParseAddr("127.0.0.1"),
PublishedPort: 8080,
ContainerPort: 80,
Protocol: ProtocolUDP,
},
},
want: nil,
wantErr: false,
},
{
name: "ingress mode ports don't conflict with host mode ports",
containerPorts: "8080:80/tcp",
checkPorts: []PortSpec{
{Mode: PortModeHost, PublishedPort: 8080, ContainerPort: 80, Protocol: ProtocolTCP},
},
want: nil,
wantErr: false,
},
{
name: "container with invalid port spec returns error",
containerPorts: "invalid:port:spec",
checkPorts: []PortSpec{
{Mode: PortModeHost, PublishedPort: 8080, ContainerPort: 80, Protocol: ProtocolTCP},
},
want: nil,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctr := &ServiceContainer{Container: Container{ContainerJSON: types.ContainerJSON{
Config: &container.Config{
Labels: map[string]string{
LabelServicePorts: tt.containerPorts,
},
},
}}}
got, err := ctr.ConflictingServicePorts(tt.checkPorts)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
assert.Equal(t, tt.want, got)
})
}
}
+5
View File
@@ -0,0 +1,5 @@
package api
import "errors"
var ErrNotFound = errors.New("not found")
+28
View File
@@ -0,0 +1,28 @@
package api
import (
"github.com/distribution/reference"
"github.com/docker/docker/api/types"
v1 "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/psviderski/uncloud/internal/machine/api/pb"
)
type MachineImage struct {
Metadata *pb.Metadata
Image types.ImageInspect
}
// MachineRemoteImage represents an image in a remote registry fetched by a particular machine.
type MachineRemoteImage struct {
Metadata *pb.Metadata
Image RemoteImage
}
// RemoteImage represents an image in a remote registry. The canonical reference includes the image digest.
// Either IndexManifest or ImageManifest must be set depending on whether the reference points to an index
// (multi-platform image) or a single-platform image.
type RemoteImage struct {
Reference reference.Canonical
IndexManifest *v1.Index
ImageManifest *v1.Manifest
}
+30 -6
View File
@@ -3,6 +3,7 @@ package api
import ( import (
"fmt" "fmt"
"net/netip" "net/netip"
"slices"
"strconv" "strconv"
"strings" "strings"
) )
@@ -62,9 +63,6 @@ func (p *PortSpec) Validate() error {
return fmt.Errorf("invalid hostname '%s': %w", p.Hostname, err) 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: case PortModeHost:
if p.PublishedPort == 0 { if p.PublishedPort == 0 {
return fmt.Errorf("published port is required in %s mode", PortModeHost) return fmt.Errorf("published port is required in %s mode", PortModeHost)
@@ -216,9 +214,7 @@ func ParsePortSpec(port string) (PortSpec, error) {
return spec, fmt.Errorf("invalid host IP '%s': %w", parts[0], err) return spec, fmt.Errorf("invalid host IP '%s': %w", parts[0], err)
} }
} else { } else {
if parts[0] == "" { // Hostname may be empty.
return spec, fmt.Errorf("hostname must not be empty")
}
spec.Hostname = parts[0] spec.Hostname = parts[0]
} }
@@ -266,3 +262,31 @@ func validateHostname(hostname string) error {
} }
return nil return nil
} }
// PortsEqual returns true if the two port sets are equal. The order of the ports is not important.
func PortsEqual(a, b []PortSpec) bool {
if len(a) != len(b) {
return false
}
var err error
aSerialised := make([]string, len(a))
bSerialised := make([]string, len(b))
for i := range a {
aSerialised[i], err = a[i].String()
if err != nil {
return false
}
bSerialised[i], err = b[i].String()
if err != nil {
return false
}
}
slices.Sort(aSerialised)
slices.Sort(bSerialised)
return slices.Equal(aSerialised, bSerialised)
}
@@ -50,6 +50,22 @@ func TestPortSpec_Validate(t *testing.T) {
Mode: PortModeIngress, Mode: PortModeIngress,
}, },
}, },
{
name: "ingress mode without hostname http",
spec: PortSpec{
ContainerPort: 8080,
Protocol: ProtocolHTTP,
Mode: PortModeIngress,
},
},
{
name: "ingress mode without hostname https",
spec: PortSpec{
ContainerPort: 8080,
Protocol: ProtocolHTTPS,
Mode: PortModeIngress,
},
},
{ {
name: "ingress mode with hostname and http", name: "ingress mode with hostname and http",
spec: PortSpec{ spec: PortSpec{
@@ -164,24 +180,6 @@ func TestPortSpec_Validate(t *testing.T) {
}, },
wantErr: "invalid hostname 'app': must be a valid domain name containing at least one dot", 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", name: "host IP in ingress mode",
spec: PortSpec{ spec: PortSpec{
@@ -468,6 +466,15 @@ func TestParsePortSpec(t *testing.T) {
Mode: PortModeIngress, Mode: PortModeIngress,
}, },
}, },
{
name: "container port http without hostname",
port: "8080/http",
expected: PortSpec{
ContainerPort: 8080,
Protocol: ProtocolHTTP,
Mode: PortModeIngress,
},
},
{ {
name: "hostname and container port http", name: "hostname and container port http",
port: "app.example.com:8080/http", port: "app.example.com:8080/http",
@@ -602,21 +609,6 @@ func TestParsePortSpec(t *testing.T) {
port: "app.example.com:invalid:8080", port: "app.example.com:invalid:8080",
wantErr: "invalid published port", 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", name: "invalid hostname",
port: "app:8080/http", port: "app:8080/http",
+252
View File
@@ -0,0 +1,252 @@
package api
import (
"encoding/json"
"fmt"
"maps"
"reflect"
"regexp"
"slices"
"github.com/distribution/reference"
"github.com/psviderski/uncloud/internal/machine/api/pb"
)
const (
ServiceModeReplicated = "replicated"
ServiceModeGlobal = "global"
// PullPolicyAlways means the image is always pulled from the registry.
PullPolicyAlways = "always"
// PullPolicyMissing means the image is pulled from the registry only if it's not available on the machine where
// a container is started. This is the default pull policy.
// TODO: make each machine aware of the images on other machines and it possible to pull from them.
// Pull from the registry only if the image is missing on all machines.
PullPolicyMissing = "missing"
// PullPolicyNever means the image is never pulled from the registry. A service with this pull policy can only be
// deployed to machines where the image is already available.
// TODO: see the TODO above for PullPolicyMissing. Pull from other machines in the cluster if available.
PullPolicyNever = "never"
)
var serviceIDRegexp = regexp.MustCompile("^[0-9a-f]{32}$")
func ValidateServiceID(id string) bool {
return serviceIDRegexp.MatchString(id)
}
// ServiceSpec defines the desired state of a service.
// ATTENTION: after changing this struct, verify if deploy.EvalContainerSpecChange needs to be updated.
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
// Replicas is the number of containers to run for the service. Only valid for a replicated service.
Replicas uint `json:",omitempty"`
}
func (s *ServiceSpec) SetDefaults() ServiceSpec {
spec := s.Clone()
if spec.Mode == "" {
spec.Mode = ServiceModeReplicated
}
// Ensure the replicated service has at least one replica.
if spec.Mode == ServiceModeReplicated && spec.Replicas == 0 {
spec.Replicas = 1
}
spec.Container = spec.Container.SetDefaults()
return spec
}
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 the service name is a valid DNS label.
for _, p := range s.Ports {
if (p.Mode == "" || p.Mode == PortModeIngress) &&
p.Protocol != ProtocolHTTP && p.Protocol != ProtocolHTTPS {
return fmt.Errorf("unsupported protocol for ingress port %d: %s", p.ContainerPort, p.Protocol)
}
}
// TODO: validate there is no conflict between ports.
return nil
}
func (s *ServiceSpec) Clone() ServiceSpec {
spec := *s
if s.Ports != nil {
spec.Ports = make([]PortSpec, len(s.Ports))
copy(spec.Ports, s.Ports)
}
spec.Container = s.Container.Clone()
return spec
}
// ContainerSpec defines the desired state of a container in a service.
// ATTENTION: after changing this struct, verify if deploy.EvalContainerSpecChange needs to be updated.
type ContainerSpec struct {
// Command overrides the default CMD of the image to be executed when running a container.
Command []string
// Entrypoint overrides the default ENTRYPOINT of the image.
Entrypoint []string
Image string
// Run a custom init inside the container. If nil, use the daemon's configured settings.
Init *bool
// PullPolicy determines when to pull the image from the registry or use the image already available in the cluster.
// Default is PullPolicyMissing if empty.
PullPolicy string
// List of volumes to bind mount into the container.
Volumes []string
}
// SetDefaults returns a copy of the container spec with default values set.
func (s *ContainerSpec) SetDefaults() ContainerSpec {
spec := s.Clone()
if spec.PullPolicy == "" {
spec.PullPolicy = PullPolicyMissing
}
return spec
}
func (s *ContainerSpec) Validate() error {
if _, err := reference.ParseDockerRef(s.Image); err != nil {
return fmt.Errorf("invalid image: %w", err)
}
return nil
}
func (s *ContainerSpec) Equals(spec ContainerSpec) bool {
orig := s.SetDefaults()
spec = spec.SetDefaults()
slices.Sort(orig.Volumes)
slices.Sort(spec.Volumes)
return reflect.DeepEqual(orig, spec)
}
func (s *ContainerSpec) Clone() ContainerSpec {
spec := *s
if s.Command != nil {
spec.Command = make([]string, len(s.Command))
copy(spec.Command, s.Command)
}
if s.Entrypoint != nil {
spec.Entrypoint = make([]string, len(s.Entrypoint))
copy(spec.Entrypoint, s.Entrypoint)
}
if s.Volumes != nil {
spec.Volumes = make([]string, len(s.Volumes))
copy(spec.Volumes, s.Volumes)
}
return spec
}
type Service struct {
ID string
Name string
Mode string
Containers []MachineServiceContainer
}
type MachineServiceContainer struct {
MachineID string
Container ServiceContainer
}
// Endpoints returns the exposed HTTP and HTTPS endpoints of the service.
func (s *Service) Endpoints() []string {
endpoints := make(map[string]struct{})
// Container specs may differ between containers in the same service, e.g. during a rolling update,
// so we need to collect all unique endpoints.
for _, ctr := range s.Containers {
ports, err := ctr.Container.ServicePorts()
if err != nil {
continue
}
for _, port := range ports {
protocol := ""
switch port.Protocol {
case ProtocolHTTP:
protocol = "http"
case ProtocolHTTPS:
protocol = "https"
default:
continue
}
if port.Hostname == "" {
// There shouldn't be http(s) ports without a hostname but just in case ignore them.
continue
}
endpoint := fmt.Sprintf("%s://%s", protocol, port.Hostname)
if port.PublishedPort != 0 {
// For non-standard ports (80/443), include the port in the URL.
if !(port.Protocol == ProtocolHTTP && port.PublishedPort == 80) &&
!(port.Protocol == ProtocolHTTPS && port.PublishedPort == 443) {
endpoint += fmt.Sprintf(":%d", port.PublishedPort)
}
}
endpoint += fmt.Sprintf(" → :%d", port.ContainerPort)
endpoints[endpoint] = struct{}{}
}
}
return slices.Sorted(maps.Keys(endpoints))
}
func ServiceFromProto(s *pb.Service) (Service, error) {
var err error
containers := make([]MachineServiceContainer, 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) (MachineServiceContainer, error) {
var c Container
if err := json.Unmarshal(sc.Container, &c); err != nil {
return MachineServiceContainer{}, fmt.Errorf("unmarshal container: %w", err)
}
return MachineServiceContainer{
MachineID: sc.MachineId,
Container: ServiceContainer{Container: c},
}, nil
}
+102
View File
@@ -0,0 +1,102 @@
package client
import (
"fmt"
"github.com/Masterminds/semver"
"github.com/distribution/reference"
"github.com/google/go-containerregistry/pkg/name"
"github.com/google/go-containerregistry/pkg/v1/remote"
"github.com/psviderski/uncloud/pkg/api"
"github.com/psviderski/uncloud/pkg/client/deploy"
"regexp"
)
const (
CaddyServiceName = "caddy"
// CaddyImage is the official Caddy Docker image on Docker Hub: https://hub.docker.com/_/caddy
CaddyImage = "caddy"
)
var caddyImageTagRegex = regexp.MustCompile(`^2\.\d+\.\d+$`)
// NewCaddyDeployment creates a new deployment for a Caddy reverse proxy service.
// The service is deployed in global mode to all machines in the cluster. If the image is not provided, the latest
// version of the official Caddy Docker image is used.
func (cli *Client) NewCaddyDeployment(image string, filter deploy.MachineFilter) (*deploy.Deployment, error) {
latest, err := LatestCaddyImage()
if err != nil {
return nil, fmt.Errorf("look up latest Caddy image: %w", err)
}
if image == "" {
image = reference.FamiliarString(latest)
}
spec := api.ServiceSpec{
Container: api.ContainerSpec{
Command: []string{"caddy", "run", "-c", "/config/caddy.json", "--watch"},
Image: image,
Volumes: []string{"/var/lib/uncloud/caddy:/config"},
},
Mode: api.ServiceModeGlobal,
Name: CaddyServiceName,
Ports: []api.PortSpec{
{
PublishedPort: 80,
ContainerPort: 80,
Protocol: api.ProtocolTCP,
Mode: api.PortModeHost,
},
{
PublishedPort: 443,
ContainerPort: 443,
Protocol: api.ProtocolTCP,
Mode: api.PortModeHost,
},
},
}
return cli.NewDeployment(spec, &deploy.RollingStrategy{MachineFilter: filter}), nil
}
// LatestCaddyImage returns the latest image of the official Caddy Docker image on Docker Hub.
// The latest image is determined by the latest version tag 2.x.x.
func LatestCaddyImage() (reference.NamedTagged, error) {
repo, err := name.NewRepository(CaddyImage)
if err != nil {
return nil, fmt.Errorf("parse image: %w", err)
}
tags, err := remote.List(repo)
if err != nil {
return nil, fmt.Errorf("list image tags: %w", err)
}
// Default to the 'latest' tag but try to find the latest version tag 2.x.x.
latestTag := "latest"
var latestVersion *semver.Version
for _, t := range tags {
if !caddyImageTagRegex.MatchString(t) {
continue
}
v, err := semver.NewVersion(t)
if err != nil {
continue
}
if latestVersion == nil || v.GreaterThan(latestVersion) {
latestVersion = v
latestTag = t
}
}
image, err := reference.ParseDockerRef(CaddyImage)
if err != nil {
return nil, fmt.Errorf("parse image: %w", err)
}
imageWithTag, err := reference.WithTag(image, latestTag)
if err != nil {
return nil, fmt.Errorf("set image tag: %w", err)
}
return imageWithTag, nil
}
+17
View File
@@ -0,0 +1,17 @@
package client
import (
"github.com/distribution/reference"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"testing"
)
func TestLatestCaddyImage(t *testing.T) {
t.Parallel()
image, err := LatestCaddyImage()
require.NoError(t, err)
assert.Regexp(t, `^caddy:2\.\d+\.\d+$`, reference.FamiliarString(image))
}
@@ -4,14 +4,15 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"github.com/docker/cli/cli/streams"
"google.golang.org/grpc"
"os" "os"
"uncloud/internal/machine/api/pb"
"uncloud/internal/machine/docker"
)
var ErrNotFound = errors.New("not found") "github.com/docker/cli/cli/streams"
"github.com/psviderski/uncloud/internal/machine/api/pb"
"github.com/psviderski/uncloud/internal/machine/docker"
"github.com/psviderski/uncloud/pkg/api"
"google.golang.org/grpc"
"google.golang.org/grpc/metadata"
)
// Client is a client for the machine API. // Client is a client for the machine API.
type Client struct { type Client struct {
@@ -20,11 +21,12 @@ type Client struct {
pb.MachineClient pb.MachineClient
pb.ClusterClient pb.ClusterClient
*DockerClient // Docker is a namespaced client for the Docker service to distinguish Uncloud-specific service container operations
// from generic Docker operations.
Docker *docker.Client
} }
// DockerClient is a type alias for the Docker client to embed it in Client with a more specific name. var _ api.Client = (*Client)(nil)
type DockerClient = docker.Client
// Connector is an interface for establishing a connection to the machine API. // Connector is an interface for establishing a connection to the machine API.
type Connector interface { type Connector interface {
@@ -46,7 +48,7 @@ func New(ctx context.Context, connector Connector) (*Client, error) {
c.MachineClient = pb.NewMachineClient(c.conn) c.MachineClient = pb.NewMachineClient(c.conn)
c.ClusterClient = pb.NewClusterClient(c.conn) c.ClusterClient = pb.NewClusterClient(c.conn)
c.DockerClient = docker.NewClient(c.conn) c.Docker = docker.NewClient(c.conn)
return c, nil return c, nil
} }
@@ -58,3 +60,10 @@ func (cli *Client) Close() error {
func (cli *Client) progressOut() *streams.Out { func (cli *Client) progressOut() *streams.Out {
return streams.NewOut(os.Stdout) return streams.NewOut(os.Stdout)
} }
// proxyToMachine returns a new context that proxies gRPC requests to the specified machine.
func proxyToMachine(ctx context.Context, machine *pb.MachineInfo) context.Context {
machineIP, _ := machine.Network.ManagementIp.ToAddr()
md := metadata.Pairs("machines", machineIP.String())
return metadata.NewOutgoingContext(ctx, md)
}
+31
View File
@@ -0,0 +1,31 @@
package client
import (
"context"
"github.com/psviderski/uncloud/internal/machine/api/pb"
"github.com/psviderski/uncloud/pkg/api"
"google.golang.org/protobuf/types/known/emptypb"
)
func (cli *Client) InspectMachine(ctx context.Context, id string) (*pb.MachineMember, error) {
machines, err := cli.ListMachines(ctx)
if err != nil {
return nil, err
}
for _, m := range machines {
if m.Machine.Id == id || m.Machine.Name == id {
return m, nil
}
}
return nil, api.ErrNotFound
}
func (cli *Client) ListMachines(ctx context.Context) ([]*pb.MachineMember, error) {
resp, err := cli.ClusterClient.ListMachines(ctx, &emptypb.Empty{})
if err != nil {
return nil, err
}
return resp.Machines, nil
}
+112
View File
@@ -0,0 +1,112 @@
package compose
import (
"context"
"errors"
"fmt"
"github.com/compose-spec/compose-go/v2/graph"
"github.com/compose-spec/compose-go/v2/types"
"github.com/psviderski/uncloud/pkg/api"
"github.com/psviderski/uncloud/pkg/client/deploy"
)
type Client interface {
api.DNSClient
deploy.Client
}
type Deployment struct {
Client Client
Project *types.Project
SpecResolver *deploy.ServiceSpecResolver
plan *deploy.SequenceOperation
}
func NewDeployment(ctx context.Context, cli Client, project *types.Project) (*Deployment, error) {
domain, err := cli.GetDomain(ctx)
if err != nil && !errors.Is(err, api.ErrNotFound) {
return nil, fmt.Errorf("get cluster domain: %w", err)
}
resolver := &deploy.ServiceSpecResolver{
// If the domain is not found (not reserved), an empty domain is used for the resolver.
ClusterDomain: domain,
// TODO: provide an image resolver.
}
return &Deployment{
Client: cli,
Project: project,
SpecResolver: resolver,
}, nil
}
func (d *Deployment) Plan(ctx context.Context) (deploy.SequenceOperation, error) {
if d.plan != nil {
return *d.plan, nil
}
plan := deploy.SequenceOperation{}
err := graph.InDependencyOrder(ctx, d.Project,
func(ctx context.Context, name string, _ types.ServiceConfig) error {
spec, err := d.ServiceSpec(name)
if err != nil {
return fmt.Errorf("convert compose service '%s' to service spec: %w", name, err)
}
// TODO: properly handle depends_on conditions in the service deployment plan as the first operation.
deployment := deploy.NewDeployment(d.Client, spec, nil)
if err != nil {
return fmt.Errorf("create deployment for service '%s': %w", name, err)
}
servicePlan, err := deployment.Plan(ctx)
if err != nil {
return fmt.Errorf("create deployment plan for service '%s': %w", name, err)
}
// Skip no-op (up-to-date) service plans.
if len(servicePlan.Operations) > 0 {
plan.Operations = append(plan.Operations, &servicePlan)
}
return nil
})
if err != nil {
d.plan = &plan
}
return plan, err
}
// ServiceSpec returns the service specification for the given compose service that is ready for deployment.
func (d *Deployment) ServiceSpec(name string) (api.ServiceSpec, error) {
service, err := d.Project.GetService(name)
if err != nil {
return api.ServiceSpec{}, fmt.Errorf("get config for compose service '%s': %w", name, err)
}
spec, err := ServiceSpecFromCompose(name, service)
if err != nil {
return spec, fmt.Errorf("convert compose service '%s' to service spec: %w", name, err)
}
// TODO: resolve the image to a digest and supported platforms using an image resolver that broadcasts requests
// to all machines in the cluster. If service.PullPolicy is "missing":
// - Broadcast request if any machine contains a particular image and resolve it to image@digest.
// - If not found, broadcast request to resolve an image using a registry, and resolve it to image@digest.
// TODO: configure placement filter based on the supported platforms of the image.
// TODO: maybe instantiate ImageResolver here based on PullPolicy of each service?
return spec, nil
}
func (d *Deployment) Run(ctx context.Context) error {
plan, err := d.Plan(ctx)
if err != nil {
return fmt.Errorf("create plan: %w", err)
}
return plan.Execute(ctx, d.Client)
}
+43
View File
@@ -0,0 +1,43 @@
package compose
import (
"fmt"
"github.com/compose-spec/compose-go/v2/types"
"github.com/psviderski/uncloud/pkg/api"
)
const PortsExtensionKey = "x-ports"
type PortsSource []string
// TransformServicesPortsExtension transforms the ports extension of all services in the project by replacing a string
// representation of each port with a parsed PortSpec.
func transformServicesPortsExtension(project *types.Project) (*types.Project, error) {
return project.WithServicesTransform(func(name string, service types.ServiceConfig) (types.ServiceConfig, error) {
ports, ok := service.Extensions[PortsExtensionKey].(PortsSource)
if !ok {
return service, nil
}
specs, err := transformPortsExtension(ports)
if err != nil {
return service, err
}
service.Extensions[PortsExtensionKey] = specs
return service, nil
})
}
func transformPortsExtension(ports PortsSource) ([]api.PortSpec, error) {
var specs []api.PortSpec
for _, port := range ports {
spec, err := api.ParsePortSpec(port)
if err != nil {
return specs, fmt.Errorf("parse port %q: %w", port, err)
}
specs = append(specs, spec)
}
return specs, nil
}
+39
View File
@@ -0,0 +1,39 @@
package compose
// TODO: make compose, cli, and api packages public.
import (
"context"
"fmt"
composecli "github.com/compose-spec/compose-go/v2/cli"
"github.com/compose-spec/compose-go/v2/types"
)
func LoadProject(ctx context.Context, paths []string) (*types.Project, error) {
options, err := composecli.NewProjectOptions(
paths,
// First apply os.Environment, always wins.
composecli.WithOsEnv,
// Read dot env file to populate project environment.
composecli.WithDotEnv,
// Get compose file path set by COMPOSE_FILE.
composecli.WithConfigFileEnv,
// If none was selected, get default compose-ports-long.yaml file from current dir or parent folders.
composecli.WithDefaultConfigPath,
composecli.WithExtension(PortsExtensionKey, PortsSource{}),
)
if err != nil {
return nil, fmt.Errorf("create compose parser options: %w", err)
}
project, err := options.LoadProject(ctx)
if err != nil {
return nil, err
}
if project, err = transformServicesPortsExtension(project); err != nil {
return nil, err
}
return project, nil
}

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