mirror of
https://github.com/psviderski/uncloud.git
synced 2026-08-26 19:13:34 +00:00
Compare commits
456
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d78d038fd1 | ||
|
|
624caf037b | ||
|
|
ff1a8fbcf9 | ||
|
|
3aa8fead76 | ||
|
|
4075f85b72 | ||
|
|
59f6fa75ca | ||
|
|
8b66a11394 | ||
|
|
fa3d992dbc | ||
|
|
a7d8afb052 | ||
|
|
0d820bc75a | ||
|
|
6a4e63dbd4 | ||
|
|
7699b530c2 | ||
|
|
f1abbbea6d | ||
|
|
683ad22358 | ||
|
|
8f00e0082e | ||
|
|
c34542a9d8 | ||
|
|
88db1cb6a9 | ||
|
|
dfa6e001c5 | ||
|
|
f49ec465d0 | ||
|
|
1ffe9acdd7 | ||
|
|
e35e69b4be | ||
|
|
11c2e641f1 | ||
|
|
bb97537df7 | ||
|
|
9f4b8179b5 | ||
|
|
f7b079c4f2 | ||
|
|
3648187219 | ||
|
|
93346301fb | ||
|
|
cc86c8660c | ||
|
|
2963436538 | ||
|
|
6e326277bb | ||
|
|
469d6fadcd | ||
|
|
5f214247ca | ||
|
|
0b8495e964 | ||
|
|
6068e24f4f | ||
|
|
7861d56dff | ||
|
|
7bc69b9a9e | ||
|
|
38800924ed | ||
|
|
cf7e333579 | ||
|
|
769f5e47c3 | ||
|
|
6e1165ec57 | ||
|
|
8d16da596c | ||
|
|
d6c06828b3 | ||
|
|
fe93d91894 | ||
|
|
3f70018e7c | ||
|
|
70e9385032 | ||
|
|
db3016fe89 | ||
|
|
c56385602e | ||
|
|
fbbf839258 | ||
|
|
eee28e2ead | ||
|
|
ef57b62625 | ||
|
|
e854bbb3e1 | ||
|
|
938e7d05c0 | ||
|
|
6c2759f315 | ||
|
|
72bf34632f | ||
|
|
7db8bba6ea | ||
|
|
badb8ecd3c | ||
|
|
9413f1c0b1 | ||
|
|
d8818ba770 | ||
|
|
440215b811 | ||
|
|
02a418aafb | ||
|
|
33550be65e | ||
|
|
edad2429c5 | ||
|
|
b605a64fbe | ||
|
|
16c876d83f | ||
|
|
d6ff62159d | ||
|
|
fcf164dda1 | ||
|
|
e5957d9d6b | ||
|
|
143e68cc95 | ||
|
|
835c310987 | ||
|
|
5fce84d33c | ||
|
|
45497fd20c | ||
|
|
f08d9fe405 | ||
|
|
565a8d8366 | ||
|
|
343357adf9 | ||
|
|
8ee551ca8f | ||
|
|
948201cb6f | ||
|
|
f71fc57f34 | ||
|
|
1acb0ff110 | ||
|
|
03c2681792 | ||
|
|
f3cb6657ae | ||
|
|
4532b985d4 | ||
|
|
000a8a1d3e | ||
|
|
7705db2c4f | ||
|
|
590a1b231f | ||
|
|
8709ac0ed5 | ||
|
|
f8ebd94a79 | ||
|
|
87331a9265 | ||
|
|
2f5a2c6344 | ||
|
|
97e032cb7b | ||
|
|
410a95ec88 | ||
|
|
df4ab40e12 | ||
|
|
c6205d4f57 | ||
|
|
13dea92311 | ||
|
|
9100c22e7f | ||
|
|
6125553a97 | ||
|
|
c9452f1509 | ||
|
|
25b173128a | ||
|
|
4e40a93991 | ||
|
|
9fc54c9d64 | ||
|
|
cacfca7a88 | ||
|
|
b3885b0888 | ||
|
|
3ada89c4f8 | ||
|
|
20b2ca2ea5 | ||
|
|
1bbefdd0d2 | ||
|
|
f467fad84c | ||
|
|
0e80f2e5b2 | ||
|
|
64f6a4f3d3 | ||
|
|
0975cbab66 | ||
|
|
615022c9ac | ||
|
|
ecbd4378e4 | ||
|
|
1becd033e6 | ||
|
|
1cba1eef0a | ||
|
|
3893005866 | ||
|
|
1911e03c82 | ||
|
|
641b11a92d | ||
|
|
7c7530f977 | ||
|
|
6e7fe405ef | ||
|
|
03c5c054e1 | ||
|
|
c42968c812 | ||
|
|
ffef41081b | ||
|
|
58b51b974c | ||
|
|
2d923380a3 | ||
|
|
3910101a88 | ||
|
|
7a9d0cf9db | ||
|
|
042dd594e0 | ||
|
|
1471a94162 | ||
|
|
b1abd07dde | ||
|
|
3757813b20 | ||
|
|
f1a150821c | ||
|
|
874af1ba10 | ||
|
|
2ad098c612 | ||
|
|
4172cb826f | ||
|
|
041d51b74f | ||
|
|
fb01604d21 | ||
|
|
bd8bcbd2f9 | ||
|
|
07501813c8 | ||
|
|
1bbcfa8519 | ||
|
|
e8715fce38 | ||
|
|
a0a66f34c4 | ||
|
|
b799b7518e | ||
|
|
2571498ed2 | ||
|
|
5bcbfc332d | ||
|
|
34574e41fc | ||
|
|
c7355afa3d | ||
|
|
a907fdb1af | ||
|
|
c9ba4adccb | ||
|
|
a0551d2d64 | ||
|
|
76129ea056 | ||
|
|
6ffd35f596 | ||
|
|
0ef776e7f1 | ||
|
|
0b22b2136a | ||
|
|
e1530bb6f3 | ||
|
|
7fc07ce4bd | ||
|
|
eb42082955 | ||
|
|
8719e37856 | ||
|
|
e1cc39b0d5 | ||
|
|
efe646f54c | ||
|
|
e130d803e4 | ||
|
|
56e7e8baa2 | ||
|
|
b963ef39d1 | ||
|
|
1df0fa290f | ||
|
|
c3bb0c9f49 | ||
|
|
cf65cecd9e | ||
|
|
fff17dbb60 | ||
|
|
586a4fa129 | ||
|
|
2a82f75cad | ||
|
|
805afd0d28 | ||
|
|
11098ea7d1 | ||
|
|
90e357a1dc | ||
|
|
91d58bc701 | ||
|
|
69d3a511d6 | ||
|
|
c3123820b2 | ||
|
|
2e49d94fad | ||
|
|
bb51e01c5c | ||
|
|
cdb2884e4e | ||
|
|
137abde808 | ||
|
|
342969b7f4 | ||
|
|
d9e350c4dd | ||
|
|
3f4f263c05 | ||
|
|
0760ba3b18 | ||
|
|
0896bd27fb | ||
|
|
a16b2d1c74 | ||
|
|
52117954a8 | ||
|
|
435a794fa5 | ||
|
|
9642863d7b | ||
|
|
6bb9bdeb5a | ||
|
|
30f1e135f1 | ||
|
|
58ecc17ac1 | ||
|
|
4c239ed79a | ||
|
|
968dd50705 | ||
|
|
d7f437107d | ||
|
|
72548fbd1b | ||
|
|
a5f6ec4a68 | ||
|
|
4cc68e3133 | ||
|
|
2bacebe900 | ||
|
|
09bea1d12d | ||
|
|
5e9b883ad5 | ||
|
|
1046396edc | ||
|
|
35804458b0 | ||
|
|
6bb64ac5dc | ||
|
|
e5f68947f9 | ||
|
|
289d964b2a | ||
|
|
d0fd9db258 | ||
|
|
0ea997e826 | ||
|
|
2d84059d7f | ||
|
|
ecec42ea60 | ||
|
|
af4629b679 | ||
|
|
f6cf5cc250 | ||
|
|
f37bfd01fe | ||
|
|
d9897e5d1e | ||
|
|
bcb67a4073 | ||
|
|
bcb5a2a173 | ||
|
|
c3b88077ac | ||
|
|
98e1bdc9b5 | ||
|
|
e13408d79f | ||
|
|
8610322682 | ||
|
|
0be42f1f59 | ||
|
|
dcbfcbd0fb | ||
|
|
d0e46186ed | ||
|
|
625a8baed3 | ||
|
|
98db5ed440 | ||
|
|
c4398f9f06 | ||
|
|
a53fa2c1d8 | ||
|
|
5e2b2ac836 | ||
|
|
43f28284f1 | ||
|
|
b17fd7531f | ||
|
|
9859be50a6 | ||
|
|
f209851986 | ||
|
|
4da51636fd | ||
|
|
76a9f8e8b6 | ||
|
|
a477db6b16 | ||
|
|
04fd1b1425 | ||
|
|
c0bf1f2930 | ||
|
|
0bf759333a | ||
|
|
b482d836aa | ||
|
|
3d2d9b78e8 | ||
|
|
99219dc376 | ||
|
|
408703360c | ||
|
|
ac48b8bee6 | ||
|
|
e3a310397b | ||
|
|
b2f7c6fb33 | ||
|
|
653f47f507 | ||
|
|
5b66cec627 | ||
|
|
0d9a2b6e07 | ||
|
|
fb68303825 | ||
|
|
cfe5b018f3 | ||
|
|
b451f2cf7c | ||
|
|
7d25bbafdf | ||
|
|
58e1c2eea3 | ||
|
|
6c791840bd | ||
|
|
3af97306e1 | ||
|
|
d645958631 | ||
|
|
48bc94828e | ||
|
|
a43f4bf5df | ||
|
|
22f705d6c9 | ||
|
|
efcb9ee947 | ||
|
|
a347b32b65 | ||
|
|
7e865b61c2 | ||
|
|
b959f5d9f3 | ||
|
|
df64a00813 | ||
|
|
07dc5c373d | ||
|
|
a6e8616b36 | ||
|
|
7041244728 | ||
|
|
bee101aa3e | ||
|
|
21e70dd846 | ||
|
|
91f870989a | ||
|
|
bda1123ae6 | ||
|
|
190d0de61f | ||
|
|
f2e730796f | ||
|
|
bb77ff9246 | ||
|
|
5061097dff | ||
|
|
911522c388 | ||
|
|
3ee7cb6e8e | ||
|
|
2e00632959 | ||
|
|
8e53fd46fa | ||
|
|
2835edadeb | ||
|
|
f7c4490c10 | ||
|
|
b3102480c0 | ||
|
|
cbe7c563cd | ||
|
|
b016252821 | ||
|
|
6c197085d0 | ||
|
|
c29fa571e1 | ||
|
|
092fe254aa | ||
|
|
6951221b88 | ||
|
|
fa54584128 | ||
|
|
d922f7bfed | ||
|
|
18ec186e81 | ||
|
|
2a898b3eee | ||
|
|
042b44cbb6 | ||
|
|
3f7726e5fb | ||
|
|
2b3f325075 | ||
|
|
be2a26e626 | ||
|
|
4479490db2 | ||
|
|
7556f8cb12 | ||
|
|
bfbcec34e8 | ||
|
|
8eea34d8dc | ||
|
|
6b80ef9db8 | ||
|
|
2134f3c1d9 | ||
|
|
0652123bc7 | ||
|
|
4de7ecd747 | ||
|
|
e8908ced9e | ||
|
|
deedbcb8a9 | ||
|
|
8bfb3b80ed | ||
|
|
848abbd2f8 | ||
|
|
9cb4b35546 | ||
|
|
26df6740e3 | ||
|
|
537789902b | ||
|
|
4088fdfd84 | ||
|
|
01a71b4b38 | ||
|
|
73d41c75b3 | ||
|
|
f85b5dd01d | ||
|
|
1fb8a3d7a0 | ||
|
|
3ce9c8be04 | ||
|
|
3ac166e6cb | ||
|
|
88ce2d5181 | ||
|
|
8bddbec7bd | ||
|
|
a40fd613e8 | ||
|
|
99c9aabdfd | ||
|
|
5504951a8a | ||
|
|
2727c3c97e | ||
|
|
7d47d274a0 | ||
|
|
a4247b0097 | ||
|
|
11612a802c | ||
|
|
d7bad5ed46 | ||
|
|
c0dff56378 | ||
|
|
00dbbec62b | ||
|
|
9755a6973d | ||
|
|
16120b80ba | ||
|
|
386e4e25f3 | ||
|
|
43ddb5c363 | ||
|
|
417427b4d5 | ||
|
|
5da1fc5387 | ||
|
|
5621a84e99 | ||
|
|
8db7ac84a0 | ||
|
|
77890b8a8a | ||
|
|
daada3bbe9 | ||
|
|
21d55dc69a | ||
|
|
6eb373e4de | ||
|
|
6a4a7f2e6a | ||
|
|
349dd0d2ef | ||
|
|
cf5818d963 | ||
|
|
faf3378659 | ||
|
|
bb967461d0 | ||
|
|
0b5dd2f7ba | ||
|
|
d5f0bca4a0 | ||
|
|
bdd034d169 | ||
|
|
48767125f4 | ||
|
|
035f44896d | ||
|
|
3d5fe0a824 | ||
|
|
6d1c3aa5b2 | ||
|
|
cd60d5178f | ||
|
|
7aa7a9fea1 | ||
|
|
73785007cb | ||
|
|
24a824b890 | ||
|
|
085ee285e8 | ||
|
|
6daa1291b5 | ||
|
|
c3d3a4242e | ||
|
|
871fd8c035 | ||
|
|
a6ccd6055a | ||
|
|
ca607ee82d | ||
|
|
1c8e60aac0 | ||
|
|
d02fc4d981 | ||
|
|
410f74425c | ||
|
|
6e0df53f13 | ||
|
|
e9d07687f2 | ||
|
|
95175d10c5 | ||
|
|
df25b0d098 | ||
|
|
5c1a3dd694 | ||
|
|
6d39174b51 | ||
|
|
e851f25b12 | ||
|
|
3c1d2201ce | ||
|
|
b4c2ec3a2a | ||
|
|
bf10b21da5 | ||
|
|
2819ba63aa | ||
|
|
54a8b09c9f | ||
|
|
24371eb401 | ||
|
|
9c9e143dc8 | ||
|
|
19d53c4f67 | ||
|
|
40f929847c | ||
|
|
e73e38957a | ||
|
|
e82b78df49 | ||
|
|
741378d0b7 | ||
|
|
d31595820d | ||
|
|
035601d366 | ||
|
|
5c96f75958 | ||
|
|
2c62e1f6df | ||
|
|
22bca16226 | ||
|
|
a4be45186d | ||
|
|
5e79697505 | ||
|
|
6736839ccb | ||
|
|
913fb2da0c | ||
|
|
bd0e7f44b2 | ||
|
|
81cf5f5605 | ||
|
|
cedafd069d | ||
|
|
93dfa5ca9d | ||
|
|
c70c8a6ec1 | ||
|
|
5c74b09f45 | ||
|
|
1316dd3ffc | ||
|
|
4493bb3c27 | ||
|
|
b23699470b | ||
|
|
34316c77cf | ||
|
|
c2b466b046 | ||
|
|
d36429e6e5 | ||
|
|
17b96a8744 | ||
|
|
a54666a7ac | ||
|
|
5cb765d286 | ||
|
|
5bf3cba2d6 | ||
|
|
8a3f0032ae | ||
|
|
5fe77c688b | ||
|
|
61dc97228d | ||
|
|
e6df331fb8 | ||
|
|
9dbcf1e902 | ||
|
|
2f6c988125 | ||
|
|
411c3398ce | ||
|
|
3a644771ca | ||
|
|
2eff7d1e22 | ||
|
|
f06dd865fa | ||
|
|
ee580df6af | ||
|
|
2c0cf28509 | ||
|
|
7b98ed34df | ||
|
|
5094149406 | ||
|
|
2fd1ee203a | ||
|
|
336bdf9f4b | ||
|
|
c80c75e4e1 | ||
|
|
e5bda92ec6 | ||
|
|
fd3ad91136 | ||
|
|
94186b2ae7 | ||
|
|
1bcb8836ef | ||
|
|
840943b3b0 | ||
|
|
518eae9bd2 | ||
|
|
06a4001080 | ||
|
|
b73577e1a0 | ||
|
|
c3ba43a8fa | ||
|
|
c275273617 | ||
|
|
b688742e5a | ||
|
|
4ec87db8e8 | ||
|
|
c6358474ac | ||
|
|
8fbf406007 | ||
|
|
e123f7214e | ||
|
|
3287691a31 | ||
|
|
6f9fc69c2e | ||
|
|
0effba0adb | ||
|
|
667ecadd30 | ||
|
|
68845e7607 | ||
|
|
6d4698fc1d | ||
|
|
708e14a4e0 | ||
|
|
c80de16ebe | ||
|
|
3e9718f08c | ||
|
|
69c9167355 | ||
|
|
b25938acc1 | ||
|
|
1ec3376be5 | ||
|
|
aa236aac10 | ||
|
|
9fcc319c3a | ||
|
|
2f7463ebe3 | ||
|
|
498d35e866 | ||
|
|
ba67b96734 |
@@ -0,0 +1,12 @@
|
|||||||
|
# Ignore everything and use an allowlist of what is allowed to not package any secrets by accident.
|
||||||
|
*
|
||||||
|
|
||||||
|
# Allow files and directories.
|
||||||
|
!cmd/
|
||||||
|
!internal/
|
||||||
|
!pkg/
|
||||||
|
!scripts/docker/
|
||||||
|
!go.*
|
||||||
|
|
||||||
|
# Ignore unnecessary files inside allowed directories.
|
||||||
|
**/.DS_Store
|
||||||
+12
-2
@@ -8,6 +8,9 @@
|
|||||||
*.so
|
*.so
|
||||||
*.dylib
|
*.dylib
|
||||||
|
|
||||||
|
# OS X
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
# Test binary, built with `go test -c`
|
# Test binary, built with `go test -c`
|
||||||
*.test
|
*.test
|
||||||
|
|
||||||
@@ -24,5 +27,12 @@ go.work.sum
|
|||||||
.env
|
.env
|
||||||
.idea/
|
.idea/
|
||||||
|
|
||||||
data
|
/data/
|
||||||
dist/
|
/dist/
|
||||||
|
/uc
|
||||||
|
|
||||||
|
# Web
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# VS Code
|
||||||
|
.vscode
|
||||||
|
|||||||
+27
-4
@@ -24,6 +24,8 @@ builds:
|
|||||||
goarch:
|
goarch:
|
||||||
- amd64
|
- amd64
|
||||||
- arm64
|
- arm64
|
||||||
|
ldflags:
|
||||||
|
- -s -w -X github.com/psviderski/uncloud/internal/version.version={{ .Version }}
|
||||||
|
|
||||||
- id: uncloudd
|
- id: uncloudd
|
||||||
main: ./cmd/uncloudd
|
main: ./cmd/uncloudd
|
||||||
@@ -35,28 +37,34 @@ builds:
|
|||||||
goarch:
|
goarch:
|
||||||
- amd64
|
- amd64
|
||||||
- arm64
|
- arm64
|
||||||
|
ldflags:
|
||||||
|
- -s -w -X github.com/psviderski/uncloud/internal/version.version={{ .Version }}
|
||||||
|
|
||||||
archives:
|
archives:
|
||||||
- id: uncloud
|
- id: uncloud
|
||||||
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
|
||||||
{{- else}}{{.Os}}{{end}}_
|
{{- else}}{{.Os}}{{end}}_
|
||||||
{{- .Arch }}"
|
{{- .Arch }}
|
||||||
# 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 +78,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 }}"
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
[tools]
|
[tools]
|
||||||
go = "1.22"
|
go = "1.23"
|
||||||
|
|
||||||
[env]
|
[env]
|
||||||
_.file = ".env"
|
_.file = ".env"
|
||||||
|
|||||||
+51
@@ -0,0 +1,51 @@
|
|||||||
|
ARG ALPINE_VERSION=3.20.3
|
||||||
|
|
||||||
|
FROM golang:1.23.2-alpine AS uncloudd
|
||||||
|
|
||||||
|
ARG TARGETOS
|
||||||
|
ARG TARGETARCH
|
||||||
|
|
||||||
|
ENV CGO_ENABLED=0
|
||||||
|
|
||||||
|
WORKDIR /build
|
||||||
|
# Download and cache dependencies and only redownload them in subsequent builds if they change.
|
||||||
|
COPY go.mod go.sum ./
|
||||||
|
RUN go mod download && go mod verify
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
RUN GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -o uncloudd cmd/uncloudd/main.go
|
||||||
|
|
||||||
|
|
||||||
|
FROM alpine:${ALPINE_VERSION} AS corrosion-download
|
||||||
|
RUN wget -q -O /tmp/corrosion.tar.gz \
|
||||||
|
https://github.com/psviderski/corrosion/releases/latest/download/corrosion-aarch64-unknown-linux-gnu.tar.gz \
|
||||||
|
&& tar -xzf /tmp/corrosion.tar.gz -C /tmp \
|
||||||
|
&& install /tmp/corrosion /usr/local/bin/corrosion \
|
||||||
|
&& rm /tmp/corrosion.tar.gz /tmp/corrosion
|
||||||
|
|
||||||
|
FROM chainguard/wolfi-base:latest AS corrosion
|
||||||
|
COPY --from=corrosion-download /usr/local/bin/corrosion /usr/local/bin/corrosion
|
||||||
|
CMD ["corrosion", "agent"]
|
||||||
|
|
||||||
|
|
||||||
|
FROM alpine:${ALPINE_VERSION} AS corrosion-image-tarball
|
||||||
|
ARG CORROSION_IMAGE="ghcr.io/psviderski/corrosion:latest"
|
||||||
|
|
||||||
|
RUN apk --no-cache add crane
|
||||||
|
RUN crane pull "${CORROSION_IMAGE}" /corrosion.tar
|
||||||
|
|
||||||
|
|
||||||
|
# Uncloud-in-Docker (ucind) image for running Uncloud test clusters using Docker.
|
||||||
|
FROM docker:27.3.1-dind AS ucind
|
||||||
|
# Create system group and user 'uncloud'.
|
||||||
|
RUN addgroup -S uncloud && adduser -SHD -h /nonexistent -G uncloud -g "" uncloud
|
||||||
|
RUN apk --no-cache add \
|
||||||
|
socat \
|
||||||
|
wireguard-tools
|
||||||
|
|
||||||
|
COPY --from=corrosion-image-tarball /corrosion.tar /images/corrosion.tar
|
||||||
|
COPY scripts/docker/dind scripts/docker/entrypoint.sh /usr/local/bin/
|
||||||
|
COPY --from=uncloudd /build/uncloudd /usr/local/bin/
|
||||||
|
|
||||||
|
ENTRYPOINT ["entrypoint.sh"]
|
||||||
|
CMD ["uncloudd"]
|
||||||
@@ -1,16 +1,62 @@
|
|||||||
.PHONY: build
|
CORROSION_IMAGE ?= ghcr.io/psviderski/corrosion:latest
|
||||||
uncloud-dev:
|
UCIND_IMAGE ?= ghcr.io/psviderski/ucind:latest
|
||||||
|
|
||||||
|
update-dev:
|
||||||
GOOS=linux GOARCH=amd64 go build -o uncloudd-linux-amd64 ./cmd/uncloudd && \
|
GOOS=linux GOARCH=amd64 go build -o uncloudd-linux-amd64 ./cmd/uncloudd && \
|
||||||
scp uncloudd-linux-amd64 spy@192.168.40.243:~/ && \
|
scp uncloudd-linux-amd64 spy@192.168.40.243:~/ && \
|
||||||
ssh spy@192.168.40.243 sudo install ./uncloudd-linux-amd64 /usr/local/bin/uncloudd
|
ssh spy@192.168.40.243 sudo install ./uncloudd-linux-amd64 /usr/local/bin/uncloudd
|
||||||
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 && \
|
||||||
GOOS=linux GOARCH=amd64 go build -o uncloud-linux-amd64 ./cmd/uncloud && \
|
rm uncloudd-linux-amd64
|
||||||
scp uncloud-linux-amd64 spy@192.168.40.243:~/ && \
|
# GOOS=linux GOARCH=arm64 go build -o uncloudd-linux-arm64 ./cmd/uncloudd && \
|
||||||
ssh spy@192.168.40.243 sudo install ./uncloud-linux-amd64 /usr/local/bin/uncloud
|
# scp uncloudd-linux-arm64 ubuntu@152.67.101.197:~/ && \
|
||||||
scp uncloud-linux-amd64 spy@192.168.40.176:~/ && \
|
# ssh ubuntu@152.67.101.197 sudo install ./uncloudd-linux-arm64 /usr/local/bin/uncloudd && \
|
||||||
ssh spy@192.168.40.176 sudo install ./uncloud-linux-amd64 /usr/local/bin/uncloud
|
# rm uncloudd-linux-arm64
|
||||||
|
|
||||||
|
update-restart-dev:
|
||||||
|
GOOS=linux GOARCH=amd64 go build -o uncloudd-linux-amd64 ./cmd/uncloudd && \
|
||||||
|
scp uncloudd-linux-amd64 spy@192.168.40.243:~/ && \
|
||||||
|
ssh spy@192.168.40.243 "sudo install ./uncloudd-linux-amd64 /usr/local/bin/uncloudd && sudo systemctl restart uncloud" && \
|
||||||
|
scp uncloudd-linux-amd64 spy@192.168.40.176:~/ && \
|
||||||
|
ssh spy@192.168.40.176 "sudo install ./uncloudd-linux-amd64 /usr/local/bin/uncloudd && sudo systemctl restart uncloud" && \
|
||||||
|
rm uncloudd-linux-amd64
|
||||||
|
# GOOS=linux GOARCH=arm64 go build -o uncloudd-linux-arm64 ./cmd/uncloudd && \
|
||||||
|
# scp uncloudd-linux-arm64 ubuntu@152.67.101.197:~/ && \
|
||||||
|
# ssh ubuntu@152.67.101.197 "sudo install ./uncloudd-linux-arm64 /usr/local/bin/uncloudd && sudo systemctl restart uncloud" && \
|
||||||
|
# rm uncloudd-linux-arm64
|
||||||
|
|
||||||
|
reset-dev:
|
||||||
|
ssh spy@192.168.40.243 "sudo systemctl stop uncloud && sudo rm -rf /var/lib/uncloud"
|
||||||
|
ssh spy@192.168.40.176 "sudo systemctl stop uncloud && sudo rm -rf /var/lib/uncloud"
|
||||||
|
ssh ubuntu@152.67.101.197 "sudo systemctl stop uncloud && sudo rm -rf /var/lib/uncloud"
|
||||||
|
|
||||||
|
demo-reset:
|
||||||
|
rm -fv ~/.config/uncloud/config.yaml
|
||||||
|
ssh ubuntu@152.67.101.197 "AUTO_CONFIRM=true sudo -E uncloud-uninstall && docker rmi caddy:2.9.1"
|
||||||
|
ssh root@5.223.45.199 "AUTO_CONFIRM=true sudo -E uncloud-uninstall"
|
||||||
|
ssh spy@192.168.40.243 "AUTO_CONFIRM=true sudo -E uncloud-uninstall"
|
||||||
|
|
||||||
|
.PHONY: ucind-cluster
|
||||||
|
ucind-cluster:
|
||||||
|
go run ./cmd/ucind cluster rm && go run ./cmd/ucind cluster create -m 3
|
||||||
|
|
||||||
.PHONY: proto
|
.PHONY: proto
|
||||||
proto:
|
proto:
|
||||||
protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative internal/machine/api/pb/*.proto
|
protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative \
|
||||||
|
--proto_path=. --proto_path=internal/machine/api/vendor internal/machine/api/pb/*.proto
|
||||||
|
|
||||||
|
.PHONY: corrosion-image
|
||||||
|
corrosion-image:
|
||||||
|
docker build -t "$(CORROSION_IMAGE)" --target corrosion .
|
||||||
|
|
||||||
|
.PHONY: corrosion-image-push
|
||||||
|
corrosion-image-push: corrosion-image
|
||||||
|
docker push "$(CORROSION_IMAGE)"
|
||||||
|
|
||||||
|
.PHONY: ucind-image
|
||||||
|
ucind-image:
|
||||||
|
docker build -t "$(UCIND_IMAGE)" --target ucind .
|
||||||
|
|
||||||
|
.PHONY: ucind-image-push
|
||||||
|
ucind-image-push: ucind-image
|
||||||
|
docker push "$(UCIND_IMAGE)"
|
||||||
|
|||||||
@@ -1,9 +1,288 @@
|
|||||||
# Uncloud
|
<div align="center">
|
||||||
|
<img src="./website/images/logo.svg" height="100" alt="Uncloud logo"/>
|
||||||
|
<h1>Uncloud</h1>
|
||||||
|
<p><strong>Docker simplicity. Multi-machine power.</strong></p>
|
||||||
|
|
||||||
Uncloud is a lightweight tool for deploying and managing containerised applications across a network of Docker hosts. It
|
<p>
|
||||||
bridges the gap between simple Docker deployments and complex cloud platforms and container orchestrators like
|
<a href="https://discord.gg/eR35KQJhPu"><img src="https://img.shields.io/badge/discord-5865F2.svg?style=for-the-badge&logo=discord&logoColor=white" alt="Join Discord"></a>
|
||||||
Kubernetes.
|
<a href="https://x.com/psviderski"><img src="https://img.shields.io/badge/follow-black?style=for-the-badge&logo=X&logoColor=while" alt="Follow on X"></a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
Our goal is to become the go-to self-hosted alternative to platforms like [Fly.io](https://fly.io) and
|
Uncloud is a lightweight clustering and container orchestration tool that lets you deploy and manage web apps across
|
||||||
[Render](https://render.com/). Whether you're using bare metal servers, Raspberry Pis, or cloud VMs, Uncloud transforms
|
cloud VMs and bare metal with minimised cluster management overhead. It creates a secure WireGuard mesh network between
|
||||||
your hardware into a personal cloud platform.
|
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
|
||||||
|
synchronised copy of the cluster state through peer-to-peer communication, keeping cluster operations functional even if
|
||||||
|
some machines go offline.
|
||||||
|
|
||||||
|
Uncloud is the solution for developers who want the flexibility of self-hosted infrastructure without the operational
|
||||||
|
complexity of Kubernetes.
|
||||||
|
|
||||||
|
## ✨ Features
|
||||||
|
|
||||||
|
* **Deploy anywhere**: Combine cloud VMs, dedicated servers, and bare metal into a unified computing environment —
|
||||||
|
regardless of location or provider.
|
||||||
|
* **Docker Compose**: Familiar [Docker Compose](https://compose-spec.io/) format for defining services and volumes. No
|
||||||
|
need to learn a new bespoke DSL.
|
||||||
|
* **Zero-downtime deployments**: Rolling updates without service interruption. Automatic rollback on failure is coming
|
||||||
|
soon.
|
||||||
|
* **Service discovery**: Built-in DNS server resolves service names to container IPs.
|
||||||
|
* **Persistent storage**: Run stateful services with Docker volumes managed across machines.
|
||||||
|
* **Zero-config private network**: Automatic WireGuard mesh with peer discovery and NAT traversal. Containers get unique
|
||||||
|
IPs for direct cross-machine communication.
|
||||||
|
* **No control plane**: Fully decentralised design eliminates single points of failure and reduces operational overhead.
|
||||||
|
* **Imperative over declarative**: Favoring imperative operations over state reconciliation simplifies both the mental
|
||||||
|
model and troubleshooting.
|
||||||
|
* **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.
|
||||||
|
|
||||||
|
## 🎬 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>
|
||||||
|
|
||||||
|
## 💫 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 controlling your data and maintaining the cloud-like experience
|
||||||
|
you love.
|
||||||
|
* **Stay simple as you grow**: Start with a single machine and add more whenever you need without changing your
|
||||||
|
workflow. No worrying about highly-available control planes or complex YAML configurations.
|
||||||
|
* **Build with proven primitives**: Get production-grade networking, deployment primitives, service discovery, load
|
||||||
|
balancing, and ingress with HTTPS out of the box without becoming a distributed systems expert.
|
||||||
|
* **Support sustainable computing** 🌿: Minimise system overhead to maximise resources available for your applications.
|
||||||
|
|
||||||
|
Uncloud's goal is to make deployment and management of containerised applications feel as seamless as using a cloud
|
||||||
|
platform, whether you're running on a $5 VPS, a spare Mac mini, or a rack of bare metal servers.
|
||||||
|
|
||||||
|
## 🚀 Quick start
|
||||||
|
|
||||||
|
1. Install Uncloud CLI:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
brew install psviderski/tap/uncloud
|
||||||
|
|
||||||
|
# or using curl (macOS/Linux)
|
||||||
|
curl -fsS https://get.uncloud.run/install.sh | sh
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Initialise your first machine:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uc machine init root@your-server-ip
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Deploy your app from a Docker image and publish its container port 8000 as HTTPS using `app.example.com` domain:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uc run -p app.example.com:8000/https image/my-app
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Create a DNS A record in your DNS provider (Cloudflare, Namecheap, etc.) that points `app.example.com` to your
|
||||||
|
server's IP address. Allow a few minutes for DNS propagation.
|
||||||
|
|
||||||
|
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 is a diagram of an Uncloud multi-provider cluster of 3 machines:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Peek under the hood to see what happens when you run certain commands.</summary>
|
||||||
|
|
||||||
|
**When you initialise a new cluster on a machine:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ uc machine init --name oracle-vm ubuntu@152.67.101.197
|
||||||
|
|
||||||
|
Downloading Uncloud install script: https://raw.githubusercontent.com/psviderski/uncloud/refs/heads/main/scripts/install.sh
|
||||||
|
⏳ Running Uncloud install script...
|
||||||
|
✓ Docker is already installed.
|
||||||
|
⏳ Installing Docker...
|
||||||
|
...
|
||||||
|
✓ Docker installed successfully.
|
||||||
|
✓ Linux user and group 'uncloud' created.
|
||||||
|
✓ Linux user 'ubuntu' added to group 'uncloud'.
|
||||||
|
⏳ Installing Uncloud binaries...
|
||||||
|
⏳ Downloading uncloudd binary: https://github.com/psviderski/uncloud/releases/latest/download/uncloudd_linux_arm64.tar.gz
|
||||||
|
✓ uncloudd binary installed: /usr/local/bin/uncloudd
|
||||||
|
⏳ Downloading uninstall script: https://raw.githubusercontent.com/psviderski/uncloud/refs/heads/main/scripts/uninstall.sh
|
||||||
|
✓ uncloud-uninstall script installed: /usr/local/bin/uncloud-uninstall
|
||||||
|
✓ Systemd unit file created: /etc/systemd/system/uncloud.service
|
||||||
|
Created symlink /etc/systemd/system/multi-user.target.wants/uncloud.service → /etc/systemd/system/uncloud.service.
|
||||||
|
⏳ Downloading uncloud-corrosion binary: https://github.com/psviderski/corrosion/releases/latest/download/corrosion-aarch64-unknown-linux-gnu.tar.gz
|
||||||
|
✓ uncloud-corrosion binary installed: /usr/local/bin/uncloud-corrosion
|
||||||
|
✓ Systemd unit file created: /etc/systemd/system/uncloud-corrosion.service
|
||||||
|
⏳ Starting Uncloud machine daemon (uncloud.service)...
|
||||||
|
✓ Uncloud machine daemon started.
|
||||||
|
✓ Uncloud installed on the machine successfully! 🎉
|
||||||
|
Cluster "default" initialised with machine "oracle-vm"
|
||||||
|
Waiting for the machine to be ready...
|
||||||
|
|
||||||
|
Reserved cluster domain: xuw3xd.cluster.uncloud.run
|
||||||
|
[+] Deploying service caddy 1/1
|
||||||
|
✔ Container caddy-c47x on oracle-vm Started 0.9s
|
||||||
|
|
||||||
|
Updating cluster domain records in Uncloud DNS to point to machines running caddy service...
|
||||||
|
[+] Verifying internet access to caddy service 1/1
|
||||||
|
✔ Machine oracle-vm (152.67.101.197) Reachable 0.1s
|
||||||
|
|
||||||
|
DNS records updated to use only the internet-reachable machines running caddy service:
|
||||||
|
*.xuw3xd.cluster.uncloud.run A → 152.67.101.197
|
||||||
|
```
|
||||||
|
|
||||||
|
1. The CLI SSHs into the machine and installs Docker, the `uncloudd` machine daemon and
|
||||||
|
[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.
|
||||||
|
|
||||||
|
**When you add another machine:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ uc machine add --name hetzner-server root@5.223.45.199
|
||||||
|
Downloading Uncloud install script: https://raw.githubusercontent.com/psviderski/uncloud/refs/heads/main/scripts/install.sh
|
||||||
|
⏳ Running Uncloud install script...
|
||||||
|
✓ Docker is already installed.
|
||||||
|
✓ Linux user and group 'uncloud' created.
|
||||||
|
⏳ Installing Uncloud binaries...
|
||||||
|
⏳ Downloading uncloudd binary: https://github.com/psviderski/uncloud/releases/latest/download/uncloudd_linux_amd64.tar.gz
|
||||||
|
✓ uncloudd binary installed: /usr/local/bin/uncloudd
|
||||||
|
⏳ Downloading uninstall script: https://raw.githubusercontent.com/psviderski/uncloud/refs/heads/main/scripts/uninstall.sh
|
||||||
|
✓ uncloud-uninstall script installed: /usr/local/bin/uncloud-uninstall
|
||||||
|
✓ Systemd unit file created: /etc/systemd/system/uncloud.service
|
||||||
|
Created symlink /etc/systemd/system/multi-user.target.wants/uncloud.service → /etc/systemd/system/uncloud.service.
|
||||||
|
⏳ Downloading uncloud-corrosion binary: https://github.com/psviderski/corrosion/releases/latest/download/corrosion-x86_64-unknown-linux-gnu.tar.gz
|
||||||
|
✓ uncloud-corrosion binary installed: /usr/local/bin/uncloud-corrosion
|
||||||
|
✓ Systemd unit file created: /etc/systemd/system/uncloud-corrosion.service
|
||||||
|
⏳ Starting Uncloud machine daemon (uncloud.service)...
|
||||||
|
✓ Uncloud machine daemon started.
|
||||||
|
✓ Uncloud installed on the machine successfully! 🎉
|
||||||
|
Machine "hetzner-server" added to cluster
|
||||||
|
Waiting for the machine to be ready...
|
||||||
|
|
||||||
|
[+] Deploying service caddy 1/1
|
||||||
|
✔ Container caddy-d36c on hetzner-server Started 1.0s
|
||||||
|
|
||||||
|
Updating cluster domain records in Uncloud DNS to point to machines running caddy service...
|
||||||
|
[+] Verifying internet access to caddy service 2/2
|
||||||
|
✔ Machine hetzner-server (5.223.45.199) Reachable 0.2s
|
||||||
|
✔ Machine oracle-vm (152.67.101.197) Reachable 0.1s
|
||||||
|
|
||||||
|
DNS records updated to use only the internet-reachable machines running caddy service:
|
||||||
|
*.xuw3xd.cluster.uncloud.run A → 152.67.101.197, 5.223.45.199
|
||||||
|
|
||||||
|
$ uc machine ls
|
||||||
|
NAME STATE ADDRESS PUBLIC IP WIREGUARD ENDPOINTS
|
||||||
|
oracle-vm Up 10.210.0.1/24 152.67.101.197 10.0.0.95:51820, 152.67.101.197:51820
|
||||||
|
hetzner-server Up 10.210.1.1/24 5.223.45.199 5.223.45.199:51820, [2a01:4ff:2f0:128b::1]:51820
|
||||||
|
```
|
||||||
|
|
||||||
|
1. The second machine gets provisioned just like the first. A non-root SSH user will need `sudo` access.
|
||||||
|
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.
|
||||||
|
</details>
|
||||||
|
|
||||||
|
## 🏗 Project status
|
||||||
|
|
||||||
|
Uncloud is currently in active development and is **not ready for production use**. Features may change significantly
|
||||||
|
and there may be breaking changes between releases.
|
||||||
|
|
||||||
|
We'd love your input! Here's how you can contribute:
|
||||||
|
|
||||||
|
* 🐛 Found a bug? [Open an issue](https://github.com/psviderski/uncloud/issues)
|
||||||
|
* 💡 Have ideas or need help? [Join our Discord community](https://discord.gg/eR35KQJhPu) where we discuss features,
|
||||||
|
roadmap, implementation details, and help each other out.
|
||||||
|
|
||||||
|
## 📫 Stay updated
|
||||||
|
|
||||||
|
* Join our [Discord server](https://discord.gg/eR35KQJhPu) for real-time discussions, support, and updates.
|
||||||
|
* Follow [@psviderski](https://x.com/psviderski) on X/Twitter.
|
||||||
|
* Subscribe to [my newsletter](https://uncloud.run/#subscribe) to follow the progress, get early insights into new
|
||||||
|
features, and be the first to know when it's ready for production use.
|
||||||
|
* Watch this repository for releases.
|
||||||
|
|
||||||
|
## ❤️ 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>
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package cluster
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/psviderski/uncloud/internal/ucind"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewCreateCommand() *cobra.Command {
|
||||||
|
opts := ucind.CreateClusterOptions{}
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "create [NAME]",
|
||||||
|
Short: "Create a new cluster.",
|
||||||
|
Args: cobra.MaximumNArgs(1),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
p := cmd.Context().Value("provisioner").(*ucind.Provisioner)
|
||||||
|
|
||||||
|
name := DefaultClusterName
|
||||||
|
if len(args) > 0 {
|
||||||
|
name = args[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := p.CreateCluster(cmd.Context(), name, opts); err != nil {
|
||||||
|
return fmt.Errorf("create cluster '%s': %w", name, err)
|
||||||
|
}
|
||||||
|
fmt.Printf("Cluster '%s' created.\n", name)
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Flags().IntVarP(&opts.Machines, "machines", "m", 1, "Number of machines to create.")
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package cluster
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/psviderski/uncloud/internal/ucind"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewRemoveCommand() *cobra.Command {
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "rm [NAME]",
|
||||||
|
Short: "Remove a cluster.",
|
||||||
|
Args: cobra.MaximumNArgs(1),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
p := cmd.Context().Value("provisioner").(*ucind.Provisioner)
|
||||||
|
|
||||||
|
name := DefaultClusterName
|
||||||
|
if len(args) > 0 {
|
||||||
|
name = args[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := p.RemoveCluster(cmd.Context(), name); err != nil {
|
||||||
|
return fmt.Errorf("remove cluster '%s': %w", name, err)
|
||||||
|
}
|
||||||
|
fmt.Printf("Cluster '%s' removed.\n", name)
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package cluster
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
const DefaultClusterName = "ucind-default"
|
||||||
|
|
||||||
|
func NewRootCommand() *cobra.Command {
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "cluster",
|
||||||
|
Short: "Manage local Docker-based clusters.",
|
||||||
|
}
|
||||||
|
cmd.AddCommand(
|
||||||
|
NewCreateCommand(),
|
||||||
|
//NewListCommand(),
|
||||||
|
NewRemoveCommand(),
|
||||||
|
)
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/docker/docker/client"
|
||||||
|
"github.com/psviderski/uncloud/cmd/ucind/cluster"
|
||||||
|
"github.com/psviderski/uncloud/internal/ucind"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
var configPath string
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "ucind",
|
||||||
|
Short: "A CLI tool for running Uncloud test clusters using Docker.",
|
||||||
|
Long: "A CLI tool for running Uncloud test clusters using Docker.\n" +
|
||||||
|
"Machines in a ucind cluster are Docker containers running a ucind image. All machines within a cluster " +
|
||||||
|
"are connected to the same Docker network.",
|
||||||
|
SilenceUsage: true,
|
||||||
|
SilenceErrors: true,
|
||||||
|
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("create Docker client: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasPrefix(configPath, "~/") {
|
||||||
|
home, err := os.UserHomeDir()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("get user home directory to resolve %q: %w", configPath, err)
|
||||||
|
}
|
||||||
|
configPath = strings.Replace(configPath, "~", home, 1)
|
||||||
|
}
|
||||||
|
configUpdater := ucind.NewConfigUpdater(configPath)
|
||||||
|
|
||||||
|
p := ucind.NewProvisioner(cli, configUpdater)
|
||||||
|
// Persist the provisioner in the context so it can be used by subcommands.
|
||||||
|
cmd.SetContext(context.WithValue(cmd.Context(), "provisioner", p))
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: allow to override using UNCLOUD_CONFIG env var.
|
||||||
|
cmd.PersistentFlags().StringVar(&configPath, "uncloud-config", "~/.config/uncloud/config.yaml",
|
||||||
|
"path to the Uncloud configuration file.")
|
||||||
|
_ = cmd.MarkPersistentFlagFilename("uncloud-config", "yaml", "yml")
|
||||||
|
|
||||||
|
cmd.AddCommand(
|
||||||
|
cluster.NewRootCommand(),
|
||||||
|
)
|
||||||
|
cobra.CheckErr(cmd.Execute())
|
||||||
|
}
|
||||||
@@ -0,0 +1,200 @@
|
|||||||
|
package caddy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"maps"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/docker/cli/cli/streams"
|
||||||
|
"github.com/docker/compose/v2/pkg/progress"
|
||||||
|
"github.com/psviderski/uncloud/internal/cli"
|
||||||
|
"github.com/psviderski/uncloud/internal/machine/api/pb"
|
||||||
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
|
"github.com/psviderski/uncloud/pkg/client"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
type deployOptions struct {
|
||||||
|
image string
|
||||||
|
machines []string
|
||||||
|
context string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDeployCommand() *cobra.Command {
|
||||||
|
opts := deployOptions{}
|
||||||
|
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "deploy",
|
||||||
|
Short: "Deploy or upgrade Caddy reverse proxy across all machines in the cluster.",
|
||||||
|
Long: "Deploy or upgrade Caddy reverse proxy across all machines in the cluster.\n" +
|
||||||
|
"A rolling update is performed when updating existing containers to minimise disruption.",
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
uncli := cmd.Context().Value("cli").(*cli.CLI)
|
||||||
|
return runDeploy(cmd.Context(), uncli, opts)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Flags().StringVar(&opts.image, "image", "",
|
||||||
|
"Caddy Docker image to deploy. (default caddy:LATEST_VERSION)")
|
||||||
|
cmd.Flags().StringSliceVarP(&opts.machines, "machine", "m", nil,
|
||||||
|
"Machine names to deploy to. Can be specified multiple times or as a comma-separated "+
|
||||||
|
"list of machine names. (default is all machines)")
|
||||||
|
cmd.Flags().StringVarP(
|
||||||
|
&opts.context, "context", "c", "",
|
||||||
|
"Name of the cluster context to deploy to. (default is the current context)",
|
||||||
|
)
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func runDeploy(ctx context.Context, uncli *cli.CLI, opts deployOptions) error {
|
||||||
|
clusterClient, err := uncli.ConnectCluster(ctx, opts.context)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("connect to cluster: %w", err)
|
||||||
|
}
|
||||||
|
defer clusterClient.Close()
|
||||||
|
|
||||||
|
svc, err := clusterClient.InspectService(ctx, client.CaddyServiceName)
|
||||||
|
if err != nil {
|
||||||
|
if !errors.Is(err, api.ErrNotFound) {
|
||||||
|
return fmt.Errorf("inspect caddy service: %w", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("Service: %s (not running)\n", client.CaddyServiceName)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("Service: %s (%s mode)\n", svc.Name, svc.Mode)
|
||||||
|
|
||||||
|
// Collect unique images of all containers in the running caddy service.
|
||||||
|
images := make(map[string]struct{}, len(svc.Containers))
|
||||||
|
for _, c := range svc.Containers {
|
||||||
|
images[c.Container.Config.Image] = struct{}{}
|
||||||
|
}
|
||||||
|
currentImages := slices.Collect(maps.Keys(images))
|
||||||
|
|
||||||
|
if len(currentImages) > 1 {
|
||||||
|
commaSeparatedImages := strings.Join(currentImages, ", ")
|
||||||
|
fmt.Printf("Current images (multiple versions detected): %s\n", commaSeparatedImages)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("Current image: %s\n", currentImages[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if opts.image != "" {
|
||||||
|
fmt.Printf("Target image: %s\n", opts.image)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Println("Preparing a deployment plan...")
|
||||||
|
|
||||||
|
placement := api.Placement{
|
||||||
|
Machines: cli.ExpandCommaSeparatedValues(opts.machines),
|
||||||
|
}
|
||||||
|
d, err := clusterClient.NewCaddyDeployment(opts.image, placement)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("create caddy deployment: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if opts.image == "" {
|
||||||
|
fmt.Printf("Target image: %s (latest stable)\n", d.Spec.Container.Image)
|
||||||
|
}
|
||||||
|
|
||||||
|
plan, err := d.Plan(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("plan caddy deployment: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(plan.Operations) == 0 {
|
||||||
|
fmt.Printf("%s service is up to date.\n", client.CaddyServiceName)
|
||||||
|
} else {
|
||||||
|
if svc.ID == "" {
|
||||||
|
if len(opts.machines) > 0 {
|
||||||
|
fmt.Println("This will run a Caddy container on each selected machine.")
|
||||||
|
} else {
|
||||||
|
fmt.Println("This will run a Caddy container on each machine.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialise a machine and container name resolver to properly format the plan output.
|
||||||
|
resolver, err := clusterClient.ServiceOperationNameResolver(ctx, svc)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("create machine and container name resolver for service operations: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Println("Deployment plan:")
|
||||||
|
fmt.Println(plan.Format(resolver))
|
||||||
|
fmt.Println()
|
||||||
|
|
||||||
|
confirmed, err := cli.Confirm()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("confirm deployment: %w", err)
|
||||||
|
}
|
||||||
|
if !confirmed {
|
||||||
|
fmt.Println("Cancelled. No changes were made.")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
err = progress.RunWithTitle(ctx, func(ctx context.Context) error {
|
||||||
|
if _, err = d.Run(ctx); err != nil {
|
||||||
|
return fmt.Errorf("deploy caddy: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}, uncli.ProgressOut(), fmt.Sprintf("Deploying service %s (%s mode)", d.Spec.Name, d.Spec.Mode))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println()
|
||||||
|
return UpdateDomainRecords(ctx, clusterClient, uncli.ProgressOut())
|
||||||
|
}
|
||||||
|
|
||||||
|
func UpdateDomainRecords(ctx context.Context, clusterClient *client.Client, progressOut *streams.Out) error {
|
||||||
|
domain, err := clusterClient.GetDomain(ctx)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, api.ErrNotFound) {
|
||||||
|
fmt.Println("Skipping DNS records update as no cluster domain is reserved (see 'uc dns').")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return fmt.Errorf("get cluster domain: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("Updating cluster domain records in Uncloud DNS to point to machines running caddy service...")
|
||||||
|
// TODO: split the method into two: one to get the records and one to update them to ask for update confirmation.
|
||||||
|
|
||||||
|
var records []*pb.DNSRecord
|
||||||
|
err = progress.RunWithTitle(ctx, func(ctx context.Context) error {
|
||||||
|
var err error
|
||||||
|
records, err = clusterClient.CreateIngressRecords(ctx, client.CaddyServiceName)
|
||||||
|
return err
|
||||||
|
}, progressOut, "Verifying internet access to caddy service")
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, client.ErrNoReachableMachines) {
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Printf("DNS records for domain '%s' could not be updated as there are no internet-reachable "+
|
||||||
|
"machines running caddy containers.\n", domain)
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Println("Possible solutions:")
|
||||||
|
fmt.Println("- Ensure your machines have public IP addresses")
|
||||||
|
fmt.Println("- Use --public-ip flag when adding machines to override the automatically detected IPs")
|
||||||
|
fmt.Println("- Check firewall settings on your machines")
|
||||||
|
fmt.Println("- Configure port forwarding if behind NAT")
|
||||||
|
fmt.Println("- Retry Caddy deployment with 'uc caddy deploy' after resolving connectivity issues")
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Println("Your services won't be accessible from the internet until at least one machine " +
|
||||||
|
"becomes reachable. If you aren't planning to expose any services publicly, you can release " +
|
||||||
|
"the domain by running 'uc dns release'.")
|
||||||
|
}
|
||||||
|
return fmt.Errorf("failed to update DNS records pointing to caddy service: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Println("DNS records updated to use only the internet-reachable machines running caddy service:")
|
||||||
|
for _, r := range records {
|
||||||
|
fmt.Printf(" %s %s → %s\n", r.Name, r.Type, strings.Join(r.Values, ", "))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
package context
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"maps"
|
||||||
|
"os"
|
||||||
|
"slices"
|
||||||
|
"text/tabwriter"
|
||||||
|
|
||||||
|
"github.com/psviderski/uncloud/internal/cli"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewListCommand() *cobra.Command {
|
||||||
|
var clusterContext string
|
||||||
|
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "ls",
|
||||||
|
Aliases: []string{"list"},
|
||||||
|
Short: "List available cluster contexts.",
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
uncli := cmd.Context().Value("cli").(*cli.CLI)
|
||||||
|
return list(uncli)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Flags().StringVarP(
|
||||||
|
&clusterContext, "context", "c", "",
|
||||||
|
"Name of the cluster context. (default is the current context)",
|
||||||
|
)
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func list(uncli *cli.CLI) error {
|
||||||
|
if uncli.Config == nil {
|
||||||
|
return fmt.Errorf("context management is not available: Uncloud configuration file is not being used")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(uncli.Config.Contexts) == 0 {
|
||||||
|
fmt.Println("No contexts found")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
contextNames := slices.Sorted(maps.Keys(uncli.Config.Contexts))
|
||||||
|
currentContext := uncli.Config.CurrentContext
|
||||||
|
|
||||||
|
tw := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', 0)
|
||||||
|
fmt.Fprintln(tw, "NAME\tCURRENT\tCONNECTIONS")
|
||||||
|
|
||||||
|
for _, name := range contextNames {
|
||||||
|
current := ""
|
||||||
|
if name == currentContext {
|
||||||
|
current = "*"
|
||||||
|
}
|
||||||
|
connCount := len(uncli.Config.Contexts[name].Connections)
|
||||||
|
fmt.Fprintf(tw, "%s\t%s\t%d\n", name, current, connCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
return tw.Flush()
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package context
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/psviderski/uncloud/internal/cli"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewRootCommand() *cobra.Command {
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "ctx",
|
||||||
|
Aliases: []string{"context"},
|
||||||
|
Short: "Switch between different cluster contexts. Contains subcommands to manage contexts.",
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
uncli := cmd.Context().Value("cli").(*cli.CLI)
|
||||||
|
return selectContext(uncli)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.AddCommand(
|
||||||
|
NewListCommand(),
|
||||||
|
NewUseCommand(),
|
||||||
|
)
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
package context
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"maps"
|
||||||
|
"slices"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/huh"
|
||||||
|
"github.com/psviderski/uncloud/internal/cli"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewUseCommand() *cobra.Command {
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "use [CONTEXT]",
|
||||||
|
Short: "Switch to a different cluster context.",
|
||||||
|
Long: "Switch to a different cluster context. If no context is provided, " +
|
||||||
|
"a list of available contexts will be displayed for selection.",
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
uncli := cmd.Context().Value("cli").(*cli.CLI)
|
||||||
|
|
||||||
|
if len(args) == 1 {
|
||||||
|
if err := uncli.SetCurrentContext(args[0]); err != nil {
|
||||||
|
return fmt.Errorf("failed to set the current cluster context to '%s': %w", args[0], err)
|
||||||
|
}
|
||||||
|
fmt.Printf("Current cluster context is now '%s'.\n", args[0])
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return selectContext(uncli)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func selectContext(uncli *cli.CLI) error {
|
||||||
|
if uncli.Config == nil {
|
||||||
|
return fmt.Errorf("context management is not available: Uncloud configuration file is not being used")
|
||||||
|
}
|
||||||
|
if len(uncli.Config.Contexts) == 0 {
|
||||||
|
return fmt.Errorf("no contexts found in Uncloud config (%s)", uncli.Config.Path())
|
||||||
|
}
|
||||||
|
|
||||||
|
contextNames := slices.Sorted(maps.Keys(uncli.Config.Contexts))
|
||||||
|
|
||||||
|
var selected string
|
||||||
|
form := huh.NewForm(
|
||||||
|
huh.NewGroup(
|
||||||
|
huh.NewSelect[string]().
|
||||||
|
Title("Select a cluster context").
|
||||||
|
Options(buildContextOptions(contextNames, uncli.Config.CurrentContext)...).
|
||||||
|
Value(&selected),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
if err := form.Run(); err != nil {
|
||||||
|
return fmt.Errorf("select cluster context: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := uncli.SetCurrentContext(selected); err != nil {
|
||||||
|
return fmt.Errorf("set current cluster context: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Current cluster context is now '%s'.\n", selected)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildContextOptions(contexts []string, current string) []huh.Option[string] {
|
||||||
|
options := make([]huh.Option[string], len(contexts))
|
||||||
|
|
||||||
|
for i, ctx := range contexts {
|
||||||
|
opt := huh.NewOption(ctx, ctx)
|
||||||
|
if ctx == current {
|
||||||
|
opt.Key += " (current)"
|
||||||
|
opt = opt.Selected(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
options[i] = opt
|
||||||
|
}
|
||||||
|
|
||||||
|
return options
|
||||||
|
}
|
||||||
@@ -0,0 +1,159 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
composecli "github.com/compose-spec/compose-go/v2/cli"
|
||||||
|
"github.com/docker/compose/v2/pkg/progress"
|
||||||
|
"github.com/psviderski/uncloud/internal/cli"
|
||||||
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
|
"github.com/psviderski/uncloud/pkg/client"
|
||||||
|
"github.com/psviderski/uncloud/pkg/client/compose"
|
||||||
|
"github.com/psviderski/uncloud/pkg/client/deploy"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
type deployOptions struct {
|
||||||
|
files []string
|
||||||
|
profiles []string
|
||||||
|
services []string
|
||||||
|
|
||||||
|
context string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDeployCommand creates a new command to deploy services from a Compose file.
|
||||||
|
func NewDeployCommand() *cobra.Command {
|
||||||
|
opts := deployOptions{}
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "deploy [FLAGS] [SERVICE...]",
|
||||||
|
Short: "Deploy services from a Compose file.",
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
uncli := cmd.Context().Value("cli").(*cli.CLI)
|
||||||
|
|
||||||
|
if len(args) > 0 {
|
||||||
|
opts.services = args
|
||||||
|
}
|
||||||
|
|
||||||
|
return runDeploy(cmd.Context(), uncli, opts)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Flags().StringSliceVarP(&opts.files, "file", "f", nil,
|
||||||
|
"One or more Compose files to deploy services from. (default compose.yaml)")
|
||||||
|
cmd.Flags().StringSliceVarP(&opts.profiles, "profile", "p", nil,
|
||||||
|
"One or more Compose profiles to enable.")
|
||||||
|
cmd.Flags().StringVarP(&opts.context, "context", "c", "",
|
||||||
|
"Name of the cluster context to deploy to (default is the current context)")
|
||||||
|
// TODO: Consider adding a filter flag to specify which machines to deploy to but keep the rest running.
|
||||||
|
// Could be useful to test a new version on a subset of machines before rolling out to all.
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
// projectOpts returns the project options for the Compose file(s).
|
||||||
|
func projectOpts(opts deployOptions) []composecli.ProjectOptionsFn {
|
||||||
|
projectOpts := []composecli.ProjectOptionsFn{}
|
||||||
|
|
||||||
|
if len(opts.profiles) > 0 {
|
||||||
|
projectOpts = append(projectOpts, composecli.WithDefaultProfiles(opts.profiles...))
|
||||||
|
}
|
||||||
|
|
||||||
|
return projectOpts
|
||||||
|
}
|
||||||
|
|
||||||
|
// runDeploy parses the Compose file(s) and deploys the services.
|
||||||
|
func runDeploy(ctx context.Context, uncli *cli.CLI, opts deployOptions) error {
|
||||||
|
projectOpts := projectOpts(opts)
|
||||||
|
|
||||||
|
project, err := compose.LoadProject(ctx, opts.files, projectOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("load compose file(s): %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(opts.services) > 0 {
|
||||||
|
// Includes service dependencies by default. This is the default docker compose behavior.
|
||||||
|
project, err = project.WithSelectedServices(opts.services)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("select services: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clusterClient, err := uncli.ConnectCluster(ctx, opts.context)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("connect to cluster: %w", err)
|
||||||
|
}
|
||||||
|
defer clusterClient.Close()
|
||||||
|
|
||||||
|
composeDeploy, err := compose.NewDeployment(ctx, clusterClient, project)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("create compose deployment: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
plan, err := composeDeploy.Plan(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("plan deployment: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(plan.Operations) == 0 {
|
||||||
|
fmt.Println("Services are up to date.")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("Deployment plan:")
|
||||||
|
if err = printPlan(ctx, clusterClient, plan); err != nil {
|
||||||
|
return fmt.Errorf("print deployment plan: %w", err)
|
||||||
|
}
|
||||||
|
fmt.Println()
|
||||||
|
|
||||||
|
confirmed, err := cli.Confirm()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("confirm deployment: %w", err)
|
||||||
|
}
|
||||||
|
if !confirmed {
|
||||||
|
fmt.Println("Cancelled. No changes were made.")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return progress.RunWithTitle(ctx, func(ctx context.Context) error {
|
||||||
|
if err := plan.Execute(ctx, clusterClient); err != nil {
|
||||||
|
return fmt.Errorf("deploy services: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}, uncli.ProgressOut(), "Deploying services")
|
||||||
|
}
|
||||||
|
|
||||||
|
func printPlan(ctx context.Context, cli *client.Client, plan deploy.SequenceOperation) error {
|
||||||
|
for _, op := range plan.Operations {
|
||||||
|
svcPlan, ok := op.(*deploy.Plan)
|
||||||
|
if !ok {
|
||||||
|
fmt.Println("- " + op.Format(nil))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
svc, err := cli.InspectService(ctx, svcPlan.ServiceID)
|
||||||
|
if err != nil && !errors.Is(err, api.ErrNotFound) {
|
||||||
|
return fmt.Errorf("inspect service: %w", err)
|
||||||
|
}
|
||||||
|
// Initialise a machine and container name resolver to properly format the service plan output.
|
||||||
|
resolver, err := cli.ServiceOperationNameResolver(ctx, svc)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("create machine and container name resolver for service operations: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("- Deploy service [name=%s]\n", svcPlan.ServiceName)
|
||||||
|
fmt.Println(indent(svcPlan.Format(resolver), " "))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func indent(text, prefix string) string {
|
||||||
|
lines := strings.Split(text, "\n")
|
||||||
|
for i, line := range lines {
|
||||||
|
lines[i] = prefix + line
|
||||||
|
}
|
||||||
|
return strings.Join(lines, "\n")
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
package dns
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/psviderski/uncloud/internal/cli"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"google.golang.org/grpc/codes"
|
||||||
|
"google.golang.org/grpc/status"
|
||||||
|
"google.golang.org/protobuf/types/known/emptypb"
|
||||||
|
)
|
||||||
|
|
||||||
|
type releaseOptions struct {
|
||||||
|
context string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewReleaseCommand() *cobra.Command {
|
||||||
|
opts := releaseOptions{}
|
||||||
|
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "release",
|
||||||
|
Short: "Release the reserved cluster domain.",
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
uncli := cmd.Context().Value("cli").(*cli.CLI)
|
||||||
|
return release(cmd.Context(), uncli, opts)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Flags().StringVarP(
|
||||||
|
&opts.context, "context", "c", "",
|
||||||
|
"Name of the cluster context. (default is the current context)",
|
||||||
|
)
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func release(ctx context.Context, uncli *cli.CLI, opts releaseOptions) error {
|
||||||
|
client, err := uncli.ConnectCluster(ctx, opts.context)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("connect to cluster: %w", err)
|
||||||
|
}
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
domain, err := client.ReleaseDomain(ctx, &emptypb.Empty{})
|
||||||
|
if err != nil {
|
||||||
|
if status.Convert(err).Code() == codes.NotFound {
|
||||||
|
return errors.New("no domain reserved")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Released cluster domain: %s\n", domain.Name)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
package dns
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/psviderski/uncloud/cmd/uncloud/caddy"
|
||||||
|
"github.com/psviderski/uncloud/internal/cli"
|
||||||
|
"github.com/psviderski/uncloud/internal/machine/api/pb"
|
||||||
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
|
"github.com/psviderski/uncloud/pkg/client"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"google.golang.org/grpc/codes"
|
||||||
|
"google.golang.org/grpc/status"
|
||||||
|
)
|
||||||
|
|
||||||
|
const DefaultUncloudDNSAPIEndpoint = "https://dns.uncloud.run/v1"
|
||||||
|
|
||||||
|
type reserveOptions struct {
|
||||||
|
endpoint string
|
||||||
|
context string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewReserveCommand() *cobra.Command {
|
||||||
|
opts := reserveOptions{}
|
||||||
|
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "reserve",
|
||||||
|
Short: "Reserve a cluster domain in Uncloud DNS.",
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
uncli := cmd.Context().Value("cli").(*cli.CLI)
|
||||||
|
return reserve(cmd.Context(), uncli, opts)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Flags().StringVar(&opts.endpoint, "endpoint", DefaultUncloudDNSAPIEndpoint,
|
||||||
|
"API endpoint for the Uncloud DNS service.")
|
||||||
|
cmd.Flags().StringVarP(
|
||||||
|
&opts.context, "context", "c", "",
|
||||||
|
"Name of the cluster context. (default is the current context)",
|
||||||
|
)
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func reserve(ctx context.Context, uncli *cli.CLI, opts reserveOptions) error {
|
||||||
|
clusterClient, err := uncli.ConnectCluster(ctx, opts.context)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("connect to cluster: %w", err)
|
||||||
|
}
|
||||||
|
defer clusterClient.Close()
|
||||||
|
|
||||||
|
domain, err := clusterClient.ReserveDomain(ctx, &pb.ReserveDomainRequest{Endpoint: opts.endpoint})
|
||||||
|
if err != nil {
|
||||||
|
if status.Convert(err).Code() == codes.AlreadyExists {
|
||||||
|
return errors.New("domain already reserved")
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Reserved cluster domain: %s\n", domain.Name)
|
||||||
|
|
||||||
|
// Update cluster domain records in Uncloud DNS to point to machines running caddy service if it has been deployed.
|
||||||
|
if _, err = clusterClient.InspectService(ctx, client.CaddyServiceName); err != nil {
|
||||||
|
if errors.Is(err, api.ErrNotFound) {
|
||||||
|
fmt.Println("Deploy the Caddy reverse proxy service ('uc caddy deploy') to enable internet access " +
|
||||||
|
"to your services via the reserved or your custom domain.")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return fmt.Errorf("inspect caddy service: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return caddy.UpdateDomainRecords(ctx, clusterClient, uncli.ProgressOut())
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
package dns
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/psviderski/uncloud/internal/cli"
|
||||||
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
type showOptions struct {
|
||||||
|
context string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewShowCommand() *cobra.Command {
|
||||||
|
opts := showOptions{}
|
||||||
|
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "show",
|
||||||
|
Short: "Print the cluster domain name.",
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
uncli := cmd.Context().Value("cli").(*cli.CLI)
|
||||||
|
return show(cmd.Context(), uncli, opts)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Flags().StringVarP(
|
||||||
|
&opts.context, "context", "c", "",
|
||||||
|
"Name of the cluster context. (default is the current context)",
|
||||||
|
)
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func show(ctx context.Context, uncli *cli.CLI, opts showOptions) error {
|
||||||
|
clusterClient, err := uncli.ConnectCluster(ctx, opts.context)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("connect to cluster: %w", err)
|
||||||
|
}
|
||||||
|
defer clusterClient.Close()
|
||||||
|
|
||||||
|
domain, err := clusterClient.GetDomain(ctx)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, api.ErrNotFound) {
|
||||||
|
return errors.New("no domain reserved")
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println(domain)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
+144
-10
@@ -1,16 +1,31 @@
|
|||||||
package machine
|
package machine
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/netip"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/cenkalti/backoff/v4"
|
||||||
|
"github.com/docker/compose/v2/pkg/progress"
|
||||||
|
"github.com/psviderski/uncloud/cmd/uncloud/caddy"
|
||||||
|
"github.com/psviderski/uncloud/internal/cli"
|
||||||
|
"github.com/psviderski/uncloud/internal/cli/config"
|
||||||
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
|
"github.com/psviderski/uncloud/pkg/client"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"uncloud/internal/cli"
|
"google.golang.org/grpc/codes"
|
||||||
"uncloud/internal/cli/config"
|
"google.golang.org/grpc/status"
|
||||||
)
|
)
|
||||||
|
|
||||||
type addOptions struct {
|
type addOptions struct {
|
||||||
name string
|
name string
|
||||||
sshKey string
|
noCaddy bool
|
||||||
cluster string
|
publicIP string
|
||||||
|
sshKey string
|
||||||
|
context string
|
||||||
|
version string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAddCommand() *cobra.Command {
|
func NewAddCommand() *cobra.Command {
|
||||||
@@ -33,17 +48,136 @@ 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_*)",
|
||||||
|
)
|
||||||
|
cmd.Flags().StringVar(
|
||||||
|
&opts.version, "version", "latest",
|
||||||
|
"Version of the Uncloud daemon to install on the machine.",
|
||||||
)
|
)
|
||||||
cmd.Flags().StringVarP(
|
cmd.Flags().StringVarP(
|
||||||
&opts.cluster, "cluster", "c", "",
|
&opts.context, "context", "c", "",
|
||||||
"Name of the cluster to add the machine to (default is the current cluster)",
|
"Name of the cluster context to add the machine to. (default is the current context)",
|
||||||
)
|
)
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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, cli.AddMachineOptions{
|
||||||
|
Context: opts.context,
|
||||||
|
MachineName: opts.name,
|
||||||
|
PublicIP: publicIP,
|
||||||
|
RemoteMachine: remoteMachine,
|
||||||
|
Version: opts.version,
|
||||||
|
})
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: scale the existing Caddy service to the new machine instead of running a new deployment
|
||||||
|
// that may cause a small downtime.
|
||||||
|
d, err := machineClient.NewCaddyDeployment(caddyImage, api.Placement{})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("create caddy deployment: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = progress.RunWithTitle(ctx, func(ctx context.Context) error {
|
||||||
|
if _, err = d.Run(ctx); err != nil {
|
||||||
|
return fmt.Errorf("deploy caddy: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}, uncli.ProgressOut(), fmt.Sprintf("Deploying service %s", d.Spec.Name))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println()
|
||||||
|
return caddy.UpdateDomainRecords(ctx, machineClient, uncli.ProgressOut())
|
||||||
|
}
|
||||||
|
|
||||||
|
func waitClusterInitialised(ctx context.Context, client *client.Client) error {
|
||||||
|
boff := backoff.WithContext(backoff.NewExponentialBackOff(
|
||||||
|
backoff.WithMaxInterval(1*time.Second),
|
||||||
|
backoff.WithMaxElapsedTime(5*time.Minute),
|
||||||
|
), ctx)
|
||||||
|
|
||||||
|
check := func() error {
|
||||||
|
_, err := client.ListMachines(ctx, nil)
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
statusErr := status.Convert(err)
|
||||||
|
if statusErr.Code() == codes.FailedPrecondition {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return backoff.Permanent(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return backoff.Retry(check, boff)
|
||||||
|
}
|
||||||
|
|||||||
+127
-27
@@ -1,30 +1,41 @@
|
|||||||
package machine
|
package machine
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"uncloud/internal/cli"
|
|
||||||
"uncloud/internal/cli/config"
|
"github.com/docker/compose/v2/pkg/progress"
|
||||||
"uncloud/internal/machine/network"
|
"github.com/psviderski/uncloud/cmd/uncloud/caddy"
|
||||||
|
"github.com/psviderski/uncloud/cmd/uncloud/dns"
|
||||||
|
"github.com/psviderski/uncloud/internal/cli"
|
||||||
|
"github.com/psviderski/uncloud/internal/cli/config"
|
||||||
|
"github.com/psviderski/uncloud/internal/machine/api/pb"
|
||||||
|
"github.com/psviderski/uncloud/internal/machine/cluster"
|
||||||
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
type initOptions struct {
|
type initOptions struct {
|
||||||
name string
|
dnsEndpoint string
|
||||||
network string
|
name string
|
||||||
userPublicKey string
|
network string
|
||||||
|
noCaddy bool
|
||||||
sshKey string
|
noDNS bool
|
||||||
cluster string
|
publicIP string
|
||||||
|
sshKey string
|
||||||
|
version string
|
||||||
|
context string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewInitCommand() *cobra.Command {
|
func NewInitCommand() *cobra.Command {
|
||||||
opts := initOptions{}
|
opts := initOptions{}
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "init [USER@HOST:PORT]",
|
Use: "init [USER@HOST:PORT]",
|
||||||
|
Short: "Initialise a new cluster with a remote machine as the first member.",
|
||||||
|
// TODO: include usage examples of initialising a remote machine.
|
||||||
|
// TODO: support initialising a cluster on the local machine.
|
||||||
Args: cobra.MaximumNArgs(1),
|
Args: cobra.MaximumNArgs(1),
|
||||||
// TODO: include usage examples of initialising a local and remote machine.
|
|
||||||
Short: "Initialise a new cluster that consists of the local or remote machine.",
|
|
||||||
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)
|
||||||
|
|
||||||
@@ -41,30 +52,119 @@ 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", network.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().StringVar(&opts.userPublicKey, "user-pubkey", "",
|
|
||||||
// "User's public key which will be able to access the cluster (hex-encoded)")
|
|
||||||
|
|
||||||
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().StringVar(
|
||||||
|
&opts.version, "version", "latest",
|
||||||
|
"Version of the Uncloud daemon to install on the machine.",
|
||||||
)
|
)
|
||||||
cmd.Flags().StringVarP(
|
cmd.Flags().StringVarP(
|
||||||
&opts.cluster, "cluster", "c", "",
|
&opts.context, "context", "c", "default",
|
||||||
"Name of the cluster in the local config if initialising a remote machine",
|
"Name of the created context for the initialised cluster in the Uncloud config.",
|
||||||
)
|
)
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func initCluster(ctx context.Context, uncli *cli.CLI, remoteMachine *cli.RemoteMachine, opts initOptions) error {
|
||||||
|
netPrefix, err := netip.ParsePrefix(opts.network)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("parse network CIDR: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var publicIP *netip.Addr
|
||||||
|
switch opts.publicIP {
|
||||||
|
case "auto":
|
||||||
|
publicIP = &netip.Addr{}
|
||||||
|
case "", "none":
|
||||||
|
publicIP = nil
|
||||||
|
default:
|
||||||
|
ip, err := netip.ParseAddr(opts.publicIP)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("parse public IP: %w", err)
|
||||||
|
}
|
||||||
|
publicIP = &ip
|
||||||
|
}
|
||||||
|
|
||||||
|
client, err := uncli.InitCluster(ctx, cli.InitClusterOptions{
|
||||||
|
Context: opts.context,
|
||||||
|
MachineName: opts.name,
|
||||||
|
Network: netPrefix,
|
||||||
|
PublicIP: publicIP,
|
||||||
|
RemoteMachine: remoteMachine,
|
||||||
|
Version: opts.version,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
if opts.noCaddy && opts.noDNS {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deploy the Caddy service to the initialised machine.
|
||||||
|
// The creation of a deployment plan talks to cluster API. Since the API needs a few moments to become available
|
||||||
|
// after cluster initialisation, we keep the user informed during this wait.
|
||||||
|
fmt.Println("Waiting for the machine to be ready...")
|
||||||
|
fmt.Println()
|
||||||
|
|
||||||
|
if !opts.noDNS {
|
||||||
|
domain, err := client.ReserveDomain(ctx, &pb.ReserveDomainRequest{Endpoint: opts.dnsEndpoint})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("reserve cluster domain in Uncloud DNS: %w", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("Reserved cluster domain: %s\n", domain.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !opts.noCaddy {
|
||||||
|
d, err := client.NewCaddyDeployment("", api.Placement{})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("create caddy deployment: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = progress.RunWithTitle(ctx, func(ctx context.Context) error {
|
||||||
|
if _, err = d.Run(ctx); err != nil {
|
||||||
|
return fmt.Errorf("deploy caddy: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}, uncli.ProgressOut(), fmt.Sprintf("Deploying service %s", d.Spec.Name))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println()
|
||||||
|
return caddy.UpdateDomainRecords(ctx, client, uncli.ProgressOut())
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
package machine
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"uncloud/internal/cli"
|
|
||||||
)
|
|
||||||
|
|
||||||
func NewListCommand() *cobra.Command {
|
|
||||||
opts := addOptions{}
|
|
||||||
cmd := &cobra.Command{
|
|
||||||
Use: "list",
|
|
||||||
Aliases: []string{"ls"},
|
|
||||||
Short: "List machines in a cluster",
|
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
|
||||||
uncli := cmd.Context().Value("cli").(*cli.CLI)
|
|
||||||
return uncli.ListMachines(cmd.Context(), opts.cluster)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
cmd.Flags().StringVarP(
|
|
||||||
&opts.cluster, "cluster", "c", "",
|
|
||||||
"Name of the cluster (default is the current cluster)",
|
|
||||||
)
|
|
||||||
return cmd
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
package machine
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/netip"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"text/tabwriter"
|
||||||
|
|
||||||
|
"github.com/psviderski/uncloud/internal/cli"
|
||||||
|
"github.com/psviderski/uncloud/internal/machine/network"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewListCommand() *cobra.Command {
|
||||||
|
var clusterContext string
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "ls",
|
||||||
|
Aliases: []string{"list"},
|
||||||
|
Short: "List machines in a cluster.",
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
uncli := cmd.Context().Value("cli").(*cli.CLI)
|
||||||
|
return list(cmd.Context(), uncli, clusterContext)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
cmd.Flags().StringVarP(
|
||||||
|
&clusterContext, "context", "c", "",
|
||||||
|
"Name of the cluster context. (default is the current context)",
|
||||||
|
)
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func list(ctx context.Context, uncli *cli.CLI, clusterName string) error {
|
||||||
|
client, err := uncli.ConnectCluster(ctx, clusterName)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("connect to cluster: %w", err)
|
||||||
|
}
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
machines, err := client.ListMachines(ctx, nil)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("list machines: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print the list of machines in a table format.
|
||||||
|
tw := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', 0)
|
||||||
|
// Print header.
|
||||||
|
if _, err = fmt.Fprintln(tw, "NAME\tSTATE\tADDRESS\tPUBLIC IP\tWIREGUARD ENDPOINTS"); err != nil {
|
||||||
|
return fmt.Errorf("write header: %w", err)
|
||||||
|
}
|
||||||
|
// Print rows.
|
||||||
|
for _, member := range machines {
|
||||||
|
m := member.Machine
|
||||||
|
subnet, _ := m.Network.Subnet.ToPrefix()
|
||||||
|
subnet = netip.PrefixFrom(network.MachineIP(subnet), subnet.Bits())
|
||||||
|
|
||||||
|
publicIP := "-"
|
||||||
|
if m.PublicIp != nil {
|
||||||
|
ip, _ := m.PublicIp.ToAddr()
|
||||||
|
publicIP = ip.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
endpoints := make([]string, len(m.Network.Endpoints))
|
||||||
|
for i, ep := range m.Network.Endpoints {
|
||||||
|
addrPort, _ := ep.ToAddrPort()
|
||||||
|
endpoints[i] = addrPort.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err = fmt.Fprintf(
|
||||||
|
tw, "%s\t%s\t%s\t%s\t%s\n", m.Name, capitalise(member.State.String()), subnet, publicIP, strings.Join(endpoints, ", "),
|
||||||
|
); err != nil {
|
||||||
|
return fmt.Errorf("write row: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return tw.Flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
// capitalise returns a string where the first character is upper case, and the rest is lower case.
|
||||||
|
func capitalise(s string) string {
|
||||||
|
if s == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return strings.ToUpper(s[:1]) + strings.ToLower(s[1:])
|
||||||
|
}
|
||||||
@@ -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 {
|
||||||
@@ -31,7 +31,7 @@ func NewTokenCommand() *cobra.Command {
|
|||||||
}
|
}
|
||||||
|
|
||||||
cmd.Flags().StringVarP(&opts.dataDir, "data-dir", "d", machine.DefaultDataDir,
|
cmd.Flags().StringVarP(&opts.dataDir, "data-dir", "d", machine.DefaultDataDir,
|
||||||
"Directory for storing persistent machine state")
|
"Directory for storing persistent machine state.")
|
||||||
_ = cmd.MarkFlagDirname("data-dir")
|
_ = cmd.MarkFlagDirname("data-dir")
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
|
|||||||
+60
-15
@@ -3,44 +3,89 @@ package main
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/spf13/cobra"
|
"net/netip"
|
||||||
"os"
|
|
||||||
"strings"
|
"strings"
|
||||||
"uncloud/cmd/uncloud/machine"
|
|
||||||
"uncloud/internal/cli"
|
"github.com/psviderski/uncloud/cmd/uncloud/caddy"
|
||||||
|
cmdcontext "github.com/psviderski/uncloud/cmd/uncloud/context"
|
||||||
|
"github.com/psviderski/uncloud/cmd/uncloud/dns"
|
||||||
|
"github.com/psviderski/uncloud/cmd/uncloud/machine"
|
||||||
|
"github.com/psviderski/uncloud/cmd/uncloud/service"
|
||||||
|
"github.com/psviderski/uncloud/cmd/uncloud/volume"
|
||||||
|
"github.com/psviderski/uncloud/internal/cli"
|
||||||
|
"github.com/psviderski/uncloud/internal/cli/config"
|
||||||
|
"github.com/psviderski/uncloud/internal/fs"
|
||||||
|
"github.com/psviderski/uncloud/internal/version"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type globalOptions struct {
|
||||||
|
configPath string
|
||||||
|
connect string
|
||||||
|
}
|
||||||
|
|
||||||
func main() {
|
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.",
|
||||||
|
Version: version.String(),
|
||||||
SilenceUsage: true,
|
SilenceUsage: true,
|
||||||
SilenceErrors: true,
|
SilenceErrors: true,
|
||||||
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
|
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
|
||||||
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("initialise CLI: %w", err)
|
||||||
}
|
}
|
||||||
cmd.SetContext(context.WithValue(cmd.Context(), "cli", uncli))
|
cmd.SetContext(context.WithValue(cmd.Context(), "cli", uncli))
|
||||||
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.yaml",
|
||||||
"path to the Uncloud configuration file")
|
"Path to the Uncloud configuration file.")
|
||||||
_ = cmd.MarkPersistentFlagFilename("uncloud-config", "toml")
|
_ = cmd.MarkPersistentFlagFilename("uncloud-config", "yaml", "yml")
|
||||||
|
// TODO: make --context a global flag and pass it as a value of the command context.
|
||||||
|
|
||||||
cmd.AddCommand(
|
cmd.AddCommand(
|
||||||
|
NewDeployCommand(),
|
||||||
|
caddy.NewRootCommand(),
|
||||||
|
cmdcontext.NewRootCommand(),
|
||||||
|
dns.NewRootCommand(),
|
||||||
machine.NewRootCommand(),
|
machine.NewRootCommand(),
|
||||||
|
service.NewRootCommand(),
|
||||||
|
service.NewInspectCommand(),
|
||||||
|
service.NewListCommand(),
|
||||||
|
service.NewRmCommand(),
|
||||||
|
service.NewRunCommand(),
|
||||||
|
service.NewScaleCommand(),
|
||||||
|
volume.NewRootCommand(),
|
||||||
)
|
)
|
||||||
cobra.CheckErr(cmd.Execute())
|
cobra.CheckErr(cmd.Execute())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,103 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"text/tabwriter"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/docker/docker/pkg/stringid"
|
||||||
|
"github.com/docker/go-units"
|
||||||
|
|
||||||
|
"github.com/psviderski/uncloud/internal/cli"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
type inspectOptions struct {
|
||||||
|
service string
|
||||||
|
cluster string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewInspectCommand() *cobra.Command {
|
||||||
|
opts := inspectOptions{}
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "inspect SERVICE",
|
||||||
|
Short: "Display detailed information on a service.",
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
uncli := cmd.Context().Value("cli").(*cli.CLI)
|
||||||
|
opts.service = args[0]
|
||||||
|
return inspect(cmd.Context(), uncli, opts)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
cmd.Flags().StringVarP(
|
||||||
|
&opts.cluster, "context", "c", "",
|
||||||
|
"Name of the cluster context. (default is the current context)",
|
||||||
|
)
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func inspect(ctx context.Context, uncli *cli.CLI, opts inspectOptions) error {
|
||||||
|
client, err := uncli.ConnectCluster(ctx, opts.cluster)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("connect to cluster: %w", err)
|
||||||
|
}
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
svc, err := client.InspectService(ctx, opts.service)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("inspect service: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
machines, err := client.ListMachines(ctx, nil)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("list machines: %w", err)
|
||||||
|
}
|
||||||
|
machinesNamesByID := make(map[string]string)
|
||||||
|
for _, m := range machines {
|
||||||
|
machinesNamesByID[m.Machine.Id] = m.Machine.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("ID: %s\n", svc.ID)
|
||||||
|
fmt.Printf("Name: %s\n", svc.Name)
|
||||||
|
fmt.Printf("Mode: %s\n", svc.Mode)
|
||||||
|
fmt.Println()
|
||||||
|
|
||||||
|
// Print the list of containers in a table format.
|
||||||
|
tw := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', 0)
|
||||||
|
if _, err = fmt.Fprintln(tw, "CONTAINER ID\tIMAGE\tCREATED\tSTATUS\tMACHINE"); err != nil {
|
||||||
|
return fmt.Errorf("write header: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, ctr := range svc.Containers {
|
||||||
|
createdAt, 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"
|
||||||
|
|
||||||
|
machine := machinesNamesByID[ctr.MachineID]
|
||||||
|
if machine == "" {
|
||||||
|
machine = ctr.MachineID
|
||||||
|
}
|
||||||
|
state, err := ctr.Container.HumanState()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("get human state: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = fmt.Fprintf(
|
||||||
|
tw,
|
||||||
|
"%s\t%s\t%s\t%s\t%s\n",
|
||||||
|
stringid.TruncateID(ctr.Container.ID),
|
||||||
|
ctr.Container.Config.Image,
|
||||||
|
created,
|
||||||
|
state,
|
||||||
|
machine,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("write row: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return tw.Flush()
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"text/tabwriter"
|
||||||
|
|
||||||
|
"github.com/psviderski/uncloud/internal/cli"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewListCommand() *cobra.Command {
|
||||||
|
// TODO(lhf): rename to context
|
||||||
|
var cluster string
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "ls",
|
||||||
|
Aliases: []string{"list"},
|
||||||
|
Short: "List services.",
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
uncli := cmd.Context().Value("cli").(*cli.CLI)
|
||||||
|
return list(cmd.Context(), uncli, cluster)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
cmd.Flags().StringVarP(
|
||||||
|
&cluster, "context", "c", "",
|
||||||
|
"Name of the cluster context. (default is the current context)",
|
||||||
|
)
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func list(ctx context.Context, uncli *cli.CLI, clusterName string) error {
|
||||||
|
client, err := uncli.ConnectCluster(ctx, clusterName)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("connect to cluster: %w", err)
|
||||||
|
}
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
services, err := client.ListServices(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("list services: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
serviceNames := make(map[string]struct{}, len(services))
|
||||||
|
haveDuplicateNames := false
|
||||||
|
for _, svc := range services {
|
||||||
|
if _, exists := serviceNames[svc.Name]; exists {
|
||||||
|
haveDuplicateNames = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
serviceNames[svc.Name] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print the list of services in a table format.
|
||||||
|
tw := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', 0)
|
||||||
|
|
||||||
|
// Include the ID column if there are duplicate service names to differentiate them.
|
||||||
|
if haveDuplicateNames {
|
||||||
|
if _, err = fmt.Fprintf(tw, "ID\t"); err != nil {
|
||||||
|
return fmt.Errorf("write header: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if _, err = fmt.Fprintln(tw, "NAME\tMODE\tREPLICAS\tENDPOINTS"); err != nil {
|
||||||
|
return fmt.Errorf("write header: %w", err)
|
||||||
|
}
|
||||||
|
for _, s := range services {
|
||||||
|
endpointsSlice := s.Endpoints()
|
||||||
|
endpoints := strings.Join(endpointsSlice, ", ")
|
||||||
|
|
||||||
|
if haveDuplicateNames {
|
||||||
|
if _, err = fmt.Fprintf(tw, "%s\t", s.ID); err != nil {
|
||||||
|
return fmt.Errorf("write row: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if _, err = fmt.Fprintf(tw, "%s\t%s\t%d\t%s\n", s.Name, s.Mode, len(s.Containers), endpoints); err != nil {
|
||||||
|
return fmt.Errorf("write row: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return tw.Flush()
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/docker/compose/v2/pkg/progress"
|
||||||
|
"github.com/psviderski/uncloud/internal/cli"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
type rmOptions struct {
|
||||||
|
services []string
|
||||||
|
cluster string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRmCommand() *cobra.Command {
|
||||||
|
opts := rmOptions{}
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "rm SERVICE [SERVICE...]",
|
||||||
|
Aliases: []string{"remove", "delete"},
|
||||||
|
Short: "Remove one or more services.",
|
||||||
|
Args: cobra.MinimumNArgs(1),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
uncli := cmd.Context().Value("cli").(*cli.CLI)
|
||||||
|
opts.services = args
|
||||||
|
return rm(cmd.Context(), uncli, opts)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
cmd.Flags().StringVarP(
|
||||||
|
&opts.cluster, "context", "c", "",
|
||||||
|
"Name of the cluster context. (default is the current context)",
|
||||||
|
)
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func rm(ctx context.Context, uncli *cli.CLI, opts rmOptions) error {
|
||||||
|
client, err := uncli.ConnectCluster(ctx, opts.cluster)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("connect to cluster: %w", err)
|
||||||
|
}
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
for _, s := range opts.services {
|
||||||
|
err = progress.RunWithTitle(ctx, func(ctx context.Context) error {
|
||||||
|
if err = client.RemoveService(ctx, s); err != nil {
|
||||||
|
return fmt.Errorf("remove service '%s': %w", s, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}, uncli.ProgressOut(), "Removing service "+s)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewRootCommand() *cobra.Command {
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "service",
|
||||||
|
Aliases: []string{"svc"},
|
||||||
|
Short: "Manage services in an Uncloud cluster.",
|
||||||
|
}
|
||||||
|
cmd.AddCommand(
|
||||||
|
NewInspectCommand(),
|
||||||
|
NewListCommand(),
|
||||||
|
NewRmCommand(),
|
||||||
|
NewRunCommand(),
|
||||||
|
NewScaleCommand(),
|
||||||
|
)
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
@@ -0,0 +1,362 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
dockeropts "github.com/docker/cli/opts"
|
||||||
|
"github.com/docker/compose/v2/pkg/progress"
|
||||||
|
"github.com/docker/docker/daemon/names"
|
||||||
|
"github.com/psviderski/uncloud/internal/cli"
|
||||||
|
"github.com/psviderski/uncloud/internal/secret"
|
||||||
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
|
"github.com/psviderski/uncloud/pkg/client/deploy"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
type runOptions struct {
|
||||||
|
command []string
|
||||||
|
cpu dockeropts.NanoCPUs
|
||||||
|
entrypoint string
|
||||||
|
entrypointChanged bool
|
||||||
|
env []string
|
||||||
|
image string
|
||||||
|
machines []string
|
||||||
|
memory dockeropts.MemBytes
|
||||||
|
mode string
|
||||||
|
name string
|
||||||
|
privileged bool
|
||||||
|
publish []string
|
||||||
|
pull string
|
||||||
|
replicas uint
|
||||||
|
user string
|
||||||
|
volumes []string
|
||||||
|
|
||||||
|
cluster string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRunCommand() *cobra.Command {
|
||||||
|
opts := runOptions{}
|
||||||
|
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "run IMAGE [COMMAND...]",
|
||||||
|
Short: "Run a service.",
|
||||||
|
Args: cobra.MinimumNArgs(1),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
uncli := cmd.Context().Value("cli").(*cli.CLI)
|
||||||
|
|
||||||
|
opts.entrypointChanged = cmd.Flag("entrypoint").Changed
|
||||||
|
opts.image = args[0]
|
||||||
|
if len(args) > 1 {
|
||||||
|
opts.command = args[1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
return run(cmd.Context(), uncli, opts)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Flags().VarP(&opts.cpu, "cpu", "",
|
||||||
|
"Maximum number of CPU cores a service container can use. Fractional values are allowed: "+
|
||||||
|
"0.5 for half a core or 2.25 for two and a quarter cores.")
|
||||||
|
cmd.Flags().StringVar(&opts.entrypoint, "entrypoint", "",
|
||||||
|
"Overwrite the default ENTRYPOINT of the image. Pass an empty string \"\" to reset it.")
|
||||||
|
cmd.Flags().StringSliceVarP(&opts.env, "env", "e", nil,
|
||||||
|
"Set an environment variable for service containers. Can be specified multiple times.\n"+
|
||||||
|
"Format: VAR=value or just VAR to use the value from the local environment.")
|
||||||
|
cmd.Flags().StringVar(&opts.mode, "mode", api.ServiceModeReplicated,
|
||||||
|
fmt.Sprintf("Replication mode of the service: either '%s' (a specified number of containers across "+
|
||||||
|
"the machines) or '%s' (one container on every machine).",
|
||||||
|
api.ServiceModeReplicated, api.ServiceModeGlobal))
|
||||||
|
cmd.Flags().StringSliceVarP(&opts.machines, "machine", "m", nil,
|
||||||
|
"Placement constraint by machine names, limiting which machines the service can run on. Can be specified "+
|
||||||
|
"multiple times or as a comma-separated list of machine names. (default is any suitable machine)")
|
||||||
|
cmd.Flags().VarP(&opts.memory, "memory", "",
|
||||||
|
"Maximum amount of memory a service container can use. Value is a positive integer with optional unit suffix "+
|
||||||
|
"(b, k, m, g). Default unit is bytes if no suffix specified.\n"+
|
||||||
|
"Examples: 1073741824, 1024m, 1g (all equal 1 gibibyte)")
|
||||||
|
cmd.Flags().StringVarP(&opts.name, "name", "n", "",
|
||||||
|
"Assign a name to the service. A random name is generated if not specified.")
|
||||||
|
cmd.Flags().BoolVar(&opts.privileged, "privileged", false,
|
||||||
|
"Give extended privileges to service containers. This is a security risk and should be used with caution.")
|
||||||
|
cmd.Flags().StringSliceVarP(&opts.publish, "publish", "p", nil,
|
||||||
|
"Publish a service port to make it accessible outside the cluster. Can be specified multiple times.\n"+
|
||||||
|
"Format: [hostname:][load_balancer_port:]container_port[/protocol] or [host_ip:]:host_port:container_port[/protocol]@host\n"+
|
||||||
|
"Supported protocols: tcp, udp, http, https (default is tcp). If a hostname for http(s) port is not specified\n"+
|
||||||
|
"and a cluster domain is reserved, service-name.cluster-domain will be used as the hostname.\n"+
|
||||||
|
"Examples:\n"+
|
||||||
|
" -p 8080/https Publish port 8080 as HTTPS via load balancer with default service-name.cluster-domain hostname\n"+
|
||||||
|
" -p app.example.com:8080/https Publish port 8080 as HTTPS via load balancer with custom hostname\n"+
|
||||||
|
" -p 9000:8080 Publish port 8080 as TCP port 9000 via load balancer\n"+
|
||||||
|
" -p 53:5353/udp@host Bind UDP port 5353 to host port 53")
|
||||||
|
cmd.Flags().StringVar(&opts.pull, "pull", api.PullPolicyMissing,
|
||||||
|
fmt.Sprintf("Pull image from the registry before running service containers ('%s', '%s', '%s').",
|
||||||
|
api.PullPolicyAlways, api.PullPolicyMissing, api.PullPolicyNever))
|
||||||
|
cmd.Flags().UintVar(&opts.replicas, "replicas", 1,
|
||||||
|
"Number of containers to run for the service. Only valid for a replicated service.")
|
||||||
|
cmd.Flags().StringVarP(&opts.user, "user", "u", "",
|
||||||
|
"User name or UID and optionally group name or GID used for running the command inside service containers.\n"+
|
||||||
|
"Format: USER[:GROUP] or UID[:GID]. If not specified, the user is set to the default user of the image.")
|
||||||
|
cmd.Flags().StringSliceVarP(&opts.volumes, "volume", "v", nil,
|
||||||
|
"Mount a data volume or host path into service containers. Service containers will be scheduled on the machine(s) where\n"+
|
||||||
|
"the volume is located. Can be specified multiple times.\n"+
|
||||||
|
"Format: volume_name:/container/path[:ro|volume-nocopy] or /host/path:/container/path[:ro]\n"+
|
||||||
|
"Examples:\n"+
|
||||||
|
" -v postgres-data:/var/lib/postgresql/data Mount volume 'postgres-data' to /var/lib/postgresql/data in container\n"+
|
||||||
|
" -v /data/uploads:/app/uploads Bind mount /data/uploads host directory to /app/uploads in container\n"+
|
||||||
|
" -v /host/path:/container/path:ro Bind mount a host directory or file as read-only")
|
||||||
|
|
||||||
|
cmd.Flags().StringVarP(
|
||||||
|
&opts.cluster, "context", "c", "",
|
||||||
|
"Name of the cluster context to run the service in. (default is the current context)",
|
||||||
|
)
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func run(ctx context.Context, uncli *cli.CLI, opts runOptions) error {
|
||||||
|
spec, err := prepareServiceSpec(opts)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
clusterClient, err := uncli.ConnectCluster(ctx, opts.cluster)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("connect to cluster: %w", err)
|
||||||
|
}
|
||||||
|
defer clusterClient.Close()
|
||||||
|
|
||||||
|
var resp api.RunServiceResponse
|
||||||
|
err = progress.RunWithTitle(ctx, func(ctx context.Context) error {
|
||||||
|
resp, err = clusterClient.RunService(ctx, spec)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("run service: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}, uncli.ProgressOut(), fmt.Sprintf("Running service %s (%s mode)", spec.Name, spec.Mode))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
svc, err := clusterClient.InspectService(ctx, resp.ID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("inspect service: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
endpoints := svc.Endpoints()
|
||||||
|
if len(endpoints) > 0 {
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Printf("%s endpoints:\n", svc.Name)
|
||||||
|
for _, endpoint := range endpoints {
|
||||||
|
fmt.Printf(" • %s\n", endpoint)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func prepareServiceSpec(opts runOptions) (api.ServiceSpec, error) {
|
||||||
|
var spec api.ServiceSpec
|
||||||
|
|
||||||
|
env, err := parseEnv(opts.env)
|
||||||
|
if err != nil {
|
||||||
|
return spec, err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch opts.mode {
|
||||||
|
case api.ServiceModeReplicated, api.ServiceModeGlobal:
|
||||||
|
default:
|
||||||
|
return spec, fmt.Errorf("invalid replication mode: '%s'", opts.mode)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch opts.pull {
|
||||||
|
case api.PullPolicyAlways, api.PullPolicyMissing, api.PullPolicyNever:
|
||||||
|
default:
|
||||||
|
return spec, fmt.Errorf("invalid pull policy: '%s'", opts.pull)
|
||||||
|
}
|
||||||
|
|
||||||
|
ports := make([]api.PortSpec, len(opts.publish))
|
||||||
|
for i, publishPort := range opts.publish {
|
||||||
|
port, err := api.ParsePortSpec(publishPort)
|
||||||
|
if err != nil {
|
||||||
|
return spec, fmt.Errorf("invalid service port '%s': %w", publishPort, err)
|
||||||
|
}
|
||||||
|
ports[i] = port
|
||||||
|
}
|
||||||
|
|
||||||
|
volumes, mounts, err := parseVolumeFlags(opts.volumes)
|
||||||
|
if err != nil {
|
||||||
|
return spec, err
|
||||||
|
}
|
||||||
|
|
||||||
|
placement := api.Placement{
|
||||||
|
Machines: cli.ExpandCommaSeparatedValues(opts.machines),
|
||||||
|
}
|
||||||
|
|
||||||
|
spec = api.ServiceSpec{
|
||||||
|
Container: api.ContainerSpec{
|
||||||
|
Command: opts.command,
|
||||||
|
Env: env,
|
||||||
|
Image: opts.image,
|
||||||
|
Privileged: opts.privileged,
|
||||||
|
PullPolicy: opts.pull,
|
||||||
|
Resources: api.ContainerResources{
|
||||||
|
CPU: opts.cpu.Value(),
|
||||||
|
Memory: opts.memory.Value(),
|
||||||
|
},
|
||||||
|
User: opts.user,
|
||||||
|
VolumeMounts: mounts,
|
||||||
|
},
|
||||||
|
Mode: opts.mode,
|
||||||
|
Name: opts.name,
|
||||||
|
Placement: placement,
|
||||||
|
Ports: ports,
|
||||||
|
Replicas: opts.replicas,
|
||||||
|
Volumes: volumes,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Overwrite the default ENTRYPOINT of the image or reset it if an empty string is passed.
|
||||||
|
if opts.entrypoint != "" {
|
||||||
|
spec.Container.Entrypoint = []string{opts.entrypoint}
|
||||||
|
} else if opts.entrypointChanged {
|
||||||
|
spec.Container.Entrypoint = []string{""}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = spec.Validate(); err != nil {
|
||||||
|
return spec, fmt.Errorf("invalid service configuration: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate a service name if not specified to be able to include it in the progress title.
|
||||||
|
if spec.Name == "" {
|
||||||
|
spec.Name, err = deploy.GenerateServiceName(spec.Container.Image)
|
||||||
|
if err != nil {
|
||||||
|
return spec, fmt.Errorf("generate service name: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return spec, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseEnv parses the environment variables from the command line arguments.
|
||||||
|
// It supports two formats: "VAR=value" or just "VAR" to use the value from the local environment.
|
||||||
|
func parseEnv(env []string) (api.EnvVars, error) {
|
||||||
|
envVars := make(api.EnvVars)
|
||||||
|
for _, e := range env {
|
||||||
|
key, value, hasValue := strings.Cut(e, "=")
|
||||||
|
if key == "" {
|
||||||
|
return nil, fmt.Errorf("invalid environment variable: '%s'", e)
|
||||||
|
}
|
||||||
|
|
||||||
|
if hasValue {
|
||||||
|
envVars[key] = value
|
||||||
|
} else {
|
||||||
|
if localEnvValue, ok := os.LookupEnv(key); ok {
|
||||||
|
envVars[key] = localEnvValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return envVars, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseVolumeFlags parses volume flag values in Docker CLI format and returns VolumeSpecs and VolumeMounts.
|
||||||
|
// It handles both named volumes (volume_name:/container/path[:ro|volume-nocopy])
|
||||||
|
// and bind mounts (/host/path:/container/path[:ro]).
|
||||||
|
func parseVolumeFlags(volumes []string) ([]api.VolumeSpec, []api.VolumeMount, error) {
|
||||||
|
specs := make([]api.VolumeSpec, 0, len(volumes))
|
||||||
|
mounts := make([]api.VolumeMount, 0, len(volumes))
|
||||||
|
|
||||||
|
// Track volume names to avoid duplicate specs.
|
||||||
|
seenVolumes := make(map[string]struct{})
|
||||||
|
|
||||||
|
for _, vol := range volumes {
|
||||||
|
spec, mount, err := parseVolumeFlagValue(vol)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("invalid volume mount '%s': %w", vol, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := seenVolumes[spec.Name]; !ok {
|
||||||
|
specs = append(specs, spec)
|
||||||
|
seenVolumes[spec.Name] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
mounts = append(mounts, mount)
|
||||||
|
}
|
||||||
|
|
||||||
|
return specs, mounts, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseVolumeFlagValue(volume string) (api.VolumeSpec, api.VolumeMount, error) {
|
||||||
|
var spec api.VolumeSpec
|
||||||
|
var mount api.VolumeMount
|
||||||
|
|
||||||
|
parts := strings.Split(volume, ":")
|
||||||
|
switch len(parts) {
|
||||||
|
case 1:
|
||||||
|
return spec, mount, fmt.Errorf("invalid format, must contain at least one separator ':'")
|
||||||
|
case 2, 3:
|
||||||
|
// Format: (volume_name|/host/path):/container/path[:opts]
|
||||||
|
if !strings.HasPrefix(parts[1], "/") {
|
||||||
|
return spec, mount, fmt.Errorf("invalid container mount path: '%s', must be absolute path", parts[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
mount.ContainerPath = parts[1]
|
||||||
|
volumeNoCopy := false
|
||||||
|
|
||||||
|
if len(parts) == 3 {
|
||||||
|
opts := strings.Split(parts[2], ",")
|
||||||
|
for _, opt := range opts {
|
||||||
|
switch opt {
|
||||||
|
case "ro", "readonly":
|
||||||
|
mount.ReadOnly = true
|
||||||
|
case "volume-nocopy":
|
||||||
|
volumeNoCopy = true
|
||||||
|
default:
|
||||||
|
return spec, mount, fmt.Errorf("invalid option: '%s'", opt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasPrefix(parts[0], "/") {
|
||||||
|
// Host path bind mount: /host/path:/container/path
|
||||||
|
suffix, err := secret.RandomAlphaNumeric(4)
|
||||||
|
if err != nil {
|
||||||
|
return spec, mount, fmt.Errorf("generate random suffix: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
spec = api.VolumeSpec{
|
||||||
|
Name: "bind-" + suffix,
|
||||||
|
Type: api.VolumeTypeBind,
|
||||||
|
BindOptions: &api.BindOptions{
|
||||||
|
HostPath: parts[0],
|
||||||
|
CreateHostPath: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Named volume mount: volume_name:/container/path
|
||||||
|
volumeName := parts[0]
|
||||||
|
if !names.RestrictedNamePattern.MatchString(volumeName) {
|
||||||
|
return spec, mount, fmt.Errorf("volume name '%s' includes invalid characters, only '%s' are allowed. "+
|
||||||
|
"If you intended to pass a host directory or file, use absolute path",
|
||||||
|
volumeName, names.RestrictedNameChars)
|
||||||
|
}
|
||||||
|
|
||||||
|
spec = api.VolumeSpec{
|
||||||
|
Name: volumeName,
|
||||||
|
Type: api.VolumeTypeVolume,
|
||||||
|
VolumeOptions: &api.VolumeOptions{
|
||||||
|
Name: volumeName,
|
||||||
|
NoCopy: volumeNoCopy,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mount.VolumeName = spec.Name
|
||||||
|
default:
|
||||||
|
return spec, mount, fmt.Errorf("invalid format, must container at most 2 separators ':'")
|
||||||
|
}
|
||||||
|
|
||||||
|
return spec, mount, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/huh"
|
||||||
|
"github.com/docker/compose/v2/pkg/progress"
|
||||||
|
"github.com/psviderski/uncloud/internal/cli"
|
||||||
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
type scaleOptions struct {
|
||||||
|
service string
|
||||||
|
replicas uint
|
||||||
|
cluster string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewScaleCommand() *cobra.Command {
|
||||||
|
opts := scaleOptions{}
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "scale SERVICE REPLICAS",
|
||||||
|
Short: "Scale a replicated service by changing the number of replicas.",
|
||||||
|
Long: "Scale a replicated service by changing the number of replicas. Scaling down requires confirmation.",
|
||||||
|
Args: cobra.ExactArgs(2),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
uncli := cmd.Context().Value("cli").(*cli.CLI)
|
||||||
|
|
||||||
|
opts.service = args[0]
|
||||||
|
replicas, err := strconv.ParseUint(args[1], 10, 0)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid number of replicas: %w", err)
|
||||||
|
}
|
||||||
|
opts.replicas = uint(replicas)
|
||||||
|
|
||||||
|
return scale(cmd.Context(), uncli, opts)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Flags().StringVarP(
|
||||||
|
&opts.cluster, "context", "c", "",
|
||||||
|
"Name of the cluster context. (default is the current context)",
|
||||||
|
)
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func scale(ctx context.Context, uncli *cli.CLI, opts scaleOptions) error {
|
||||||
|
if opts.replicas == 0 {
|
||||||
|
return fmt.Errorf(
|
||||||
|
"scaling to zero replicas is not supported. This would effectively remove the service without preserving "+
|
||||||
|
"its configuration, making it impossible to scale back up. Uncloud derives the service configuration "+
|
||||||
|
"from existing containers. Use 'uc rm %s' instead if you want to remove the service",
|
||||||
|
opts.service,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
clusterClient, err := uncli.ConnectCluster(ctx, opts.cluster)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("connect to cluster: %w", err)
|
||||||
|
}
|
||||||
|
defer clusterClient.Close()
|
||||||
|
|
||||||
|
svc, err := clusterClient.InspectService(ctx, opts.service)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("inspect service '%s': %w", opts.service, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if svc.Mode != api.ServiceModeReplicated {
|
||||||
|
return fmt.Errorf("scaling is only supported for services in %s mode, service '%s' is in %s mode",
|
||||||
|
api.ServiceModeReplicated, svc.Name, svc.Mode)
|
||||||
|
}
|
||||||
|
|
||||||
|
currentReplicas := uint(len(svc.Containers))
|
||||||
|
|
||||||
|
if currentReplicas == opts.replicas {
|
||||||
|
fmt.Printf("Service '%s' already has %d replicas. No changes required.\n", svc.Name, currentReplicas)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Check if all containers have the same spec. If not, prompt user to choose which one to scale.
|
||||||
|
// This can happen if a service deployment failed midway and some containers were not updated.
|
||||||
|
spec := svc.Containers[0].Container.ServiceSpec
|
||||||
|
spec.Replicas = opts.replicas
|
||||||
|
deployment := clusterClient.NewDeployment(spec, nil)
|
||||||
|
plan, err := deployment.Plan(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("plan deployment: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(plan.Operations) == 0 {
|
||||||
|
fmt.Printf("Service '%s' is already scaled to %d replicas.\n", svc.Name, opts.replicas)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if opts.replicas < currentReplicas {
|
||||||
|
// Initialise a machine and container name resolver to properly format the plan output.
|
||||||
|
resolver, err := clusterClient.ServiceOperationNameResolver(ctx, svc)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("create machine and container name resolver for service operations: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Scaling plan for service %s (%d → %d replicas):\n", svc.Name, currentReplicas, opts.replicas)
|
||||||
|
fmt.Println(plan.Format(resolver))
|
||||||
|
fmt.Println()
|
||||||
|
|
||||||
|
// Ask for confirmation before scaling down as it may cause data loss.
|
||||||
|
confirmed, err := confirm()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("confirm scaling: %w", err)
|
||||||
|
}
|
||||||
|
if !confirmed {
|
||||||
|
fmt.Println("Cancelled. No changes were made.")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
title := fmt.Sprintf("Scaling service %s (%d → %d replicas)", svc.Name, currentReplicas, opts.replicas)
|
||||||
|
err = progress.RunWithTitle(ctx, func(ctx context.Context) error {
|
||||||
|
if _, err = deployment.Run(ctx); err != nil {
|
||||||
|
return fmt.Errorf("deploy service: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}, uncli.ProgressOut(), title)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func confirm() (bool, error) {
|
||||||
|
var confirmed bool
|
||||||
|
form := huh.NewForm(
|
||||||
|
huh.NewGroup(
|
||||||
|
huh.NewConfirm().
|
||||||
|
Title(
|
||||||
|
"Do you want to continue?",
|
||||||
|
).
|
||||||
|
Affirmative("Yes!").
|
||||||
|
Negative("No").
|
||||||
|
Value(&confirmed),
|
||||||
|
),
|
||||||
|
).WithAccessible(true)
|
||||||
|
if err := form.Run(); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return confirmed, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
package volume
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/huh"
|
||||||
|
"github.com/docker/docker/api/types/volume"
|
||||||
|
"github.com/psviderski/uncloud/internal/cli"
|
||||||
|
"github.com/psviderski/uncloud/internal/machine/api/pb"
|
||||||
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
type createOptions struct {
|
||||||
|
driver string
|
||||||
|
driverOpts []string
|
||||||
|
labels []string
|
||||||
|
machine string
|
||||||
|
context string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCreateCommand() *cobra.Command {
|
||||||
|
opts := createOptions{}
|
||||||
|
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "create VOLUME_NAME",
|
||||||
|
Short: "Create a volume on a specific machine.",
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
uncli := cmd.Context().Value("cli").(*cli.CLI)
|
||||||
|
opts.driver = strings.TrimSpace(opts.driver)
|
||||||
|
|
||||||
|
volumeName := args[0]
|
||||||
|
if volumeName == "" {
|
||||||
|
return fmt.Errorf("volume name is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
return create(cmd.Context(), uncli, volumeName, opts)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Flags().StringVarP(&opts.driver, "driver", "d", "local",
|
||||||
|
"Volume driver to use.")
|
||||||
|
cmd.Flags().StringSliceVarP(&opts.driverOpts, "opt", "o", nil,
|
||||||
|
"Driver specific options in the form of 'key=value' pairs. Can be specified multiple times.")
|
||||||
|
cmd.Flags().StringSliceVarP(&opts.labels, "label", "l", nil,
|
||||||
|
"Labels to assign to the volume in the form of 'key=value' pairs. Can be specified multiple times.")
|
||||||
|
cmd.Flags().StringVarP(&opts.machine, "machine", "m", "",
|
||||||
|
"Name or ID of the machine to create the volume on.")
|
||||||
|
cmd.Flags().StringVarP(&opts.context, "context", "c", "",
|
||||||
|
"Name of the cluster context. (default is the current context)")
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func create(ctx context.Context, uncli *cli.CLI, name string, opts createOptions) error {
|
||||||
|
client, err := uncli.ConnectCluster(ctx, opts.context)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("connect to cluster: %w", err)
|
||||||
|
}
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
// Parse driver options.
|
||||||
|
driverOpts := make(map[string]string)
|
||||||
|
for _, opt := range opts.driverOpts {
|
||||||
|
k, v, ok := strings.Cut(opt, "=")
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("invalid driver option format: '%s' (expected key=value)", opt)
|
||||||
|
}
|
||||||
|
driverOpts[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse labels.
|
||||||
|
labels := make(map[string]string)
|
||||||
|
for _, label := range opts.labels {
|
||||||
|
k, v, ok := strings.Cut(label, "=")
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("invalid label format: '%s' (expected key=value)", label)
|
||||||
|
}
|
||||||
|
labels[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
// List machines and filter by the specified machine name or ID.
|
||||||
|
// If no machine is specified, prompt the user to select one.
|
||||||
|
machines, err := client.ListMachines(ctx, nil)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("list machines: %w", err)
|
||||||
|
}
|
||||||
|
var selectedMachine *pb.MachineInfo
|
||||||
|
|
||||||
|
if opts.machine == "" {
|
||||||
|
if len(machines) == 1 {
|
||||||
|
selectedMachine = machines[0].Machine
|
||||||
|
} else {
|
||||||
|
if selectedMachine, err = promptSelectMachine(ctx, machines); err != nil {
|
||||||
|
return fmt.Errorf("select machine: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
m := machines.FindByNameOrID(opts.machine)
|
||||||
|
if m == nil {
|
||||||
|
return fmt.Errorf("machine '%s' not found", opts.machine)
|
||||||
|
}
|
||||||
|
selectedMachine = m.Machine
|
||||||
|
}
|
||||||
|
|
||||||
|
createOpts := volume.CreateOptions{
|
||||||
|
Name: name,
|
||||||
|
Driver: opts.driver,
|
||||||
|
DriverOpts: driverOpts,
|
||||||
|
Labels: labels,
|
||||||
|
}
|
||||||
|
vol, err := client.CreateVolume(ctx, selectedMachine.Id, createOpts)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("create volume '%s' on machine '%s': %w", name, selectedMachine.Name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Volume '%s' created on machine '%s'.\n", vol.Volume.Name, vol.MachineName)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func promptSelectMachine(ctx context.Context, machines api.MachineMembersList) (*pb.MachineInfo, error) {
|
||||||
|
options := make([]huh.Option[*pb.MachineInfo], len(machines))
|
||||||
|
for i, m := range machines {
|
||||||
|
options[i] = huh.NewOption(m.Machine.Name, m.Machine)
|
||||||
|
}
|
||||||
|
slices.SortFunc(options, func(a, b huh.Option[*pb.MachineInfo]) int {
|
||||||
|
return strings.Compare(a.Key, b.Key)
|
||||||
|
})
|
||||||
|
|
||||||
|
var selected *pb.MachineInfo
|
||||||
|
form := huh.NewForm(
|
||||||
|
huh.NewGroup(
|
||||||
|
huh.NewSelect[*pb.MachineInfo]().
|
||||||
|
Title("Select a machine to create the volume on (or specify with --machine flag)").
|
||||||
|
Options(options...).
|
||||||
|
Value(&selected),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
if err := form.RunWithContext(ctx); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return selected, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
package volume
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/psviderski/uncloud/internal/cli"
|
||||||
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
type inspectOptions struct {
|
||||||
|
machine string
|
||||||
|
context string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewInspectCommand() *cobra.Command {
|
||||||
|
opts := inspectOptions{}
|
||||||
|
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "inspect VOLUME_NAME",
|
||||||
|
Short: "Display detailed information on a volume.",
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
uncli := cmd.Context().Value("cli").(*cli.CLI)
|
||||||
|
return inspect(cmd.Context(), uncli, args[0], opts)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Flags().StringVarP(&opts.machine, "machine", "m", "",
|
||||||
|
"Name or ID of the machine where the volume is located. "+
|
||||||
|
"If not specified, the volume will be searched across all machines.")
|
||||||
|
cmd.Flags().StringVarP(&opts.context, "context", "c", "",
|
||||||
|
"Name of the cluster context. (default is the current context)")
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func inspect(ctx context.Context, uncli *cli.CLI, name string, opts inspectOptions) error {
|
||||||
|
client, err := uncli.ConnectCluster(ctx, opts.context)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("connect to cluster: %w", err)
|
||||||
|
}
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
filter := &api.VolumeFilter{
|
||||||
|
Names: []string{name},
|
||||||
|
}
|
||||||
|
if opts.machine != "" {
|
||||||
|
filter.Machines = []string{opts.machine}
|
||||||
|
}
|
||||||
|
|
||||||
|
volumes, err := client.ListVolumes(ctx, filter)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("list volumes: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(volumes) == 0 {
|
||||||
|
if opts.machine != "" {
|
||||||
|
return fmt.Errorf("volume '%s' not found on machine '%s'", name, opts.machine)
|
||||||
|
}
|
||||||
|
return fmt.Errorf("volume '%s' not found on any machine", name)
|
||||||
|
}
|
||||||
|
if len(volumes) > 1 {
|
||||||
|
fmt.Printf("Volume '%s' found on multiple machines:\n", name)
|
||||||
|
for _, v := range volumes {
|
||||||
|
fmt.Printf(" • %s\n", v.MachineName)
|
||||||
|
}
|
||||||
|
return errors.New("specify --machine flag to choose which machine to use")
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := json.MarshalIndent(volumes[0], "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("marshal volume: %w", err)
|
||||||
|
}
|
||||||
|
fmt.Println(string(data))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
package volume
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
"text/tabwriter"
|
||||||
|
|
||||||
|
"github.com/psviderski/uncloud/internal/cli"
|
||||||
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
type listOptions struct {
|
||||||
|
machines []string
|
||||||
|
quiet bool
|
||||||
|
context string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewListCommand() *cobra.Command {
|
||||||
|
opts := listOptions{}
|
||||||
|
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "ls",
|
||||||
|
Aliases: []string{"list"},
|
||||||
|
Short: "List volumes across all machines in the cluster.",
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
uncli := cmd.Context().Value("cli").(*cli.CLI)
|
||||||
|
return list(cmd.Context(), uncli, opts)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Flags().StringSliceVarP(&opts.machines, "machine", "m", nil,
|
||||||
|
"Filter volumes by machine name or ID. Can be specified multiple times or as a comma-separated list. "+
|
||||||
|
"(default is include all machines)")
|
||||||
|
cmd.Flags().BoolVarP(&opts.quiet, "quiet", "q", false,
|
||||||
|
"Only display volume names.")
|
||||||
|
|
||||||
|
cmd.Flags().StringVarP(&opts.context, "context", "c", "",
|
||||||
|
"Name of the cluster context. (default is the current context)")
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func list(ctx context.Context, uncli *cli.CLI, opts listOptions) error {
|
||||||
|
client, err := uncli.ConnectCluster(ctx, opts.context)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("connect to cluster: %w", err)
|
||||||
|
}
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
// Apply machine filter if specified.
|
||||||
|
var filter *api.VolumeFilter
|
||||||
|
if len(opts.machines) > 0 {
|
||||||
|
machines := cli.ExpandCommaSeparatedValues(opts.machines)
|
||||||
|
filter = &api.VolumeFilter{
|
||||||
|
Machines: machines,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
volumes, err := client.ListVolumes(ctx, filter)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("list volumes: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(volumes) == 0 {
|
||||||
|
if !opts.quiet {
|
||||||
|
fmt.Println("No volumes found.")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort the volumes by name first, then by machine name.
|
||||||
|
slices.SortFunc(volumes, func(a, b api.MachineVolume) int {
|
||||||
|
cmp := strings.Compare(a.Volume.Name, b.Volume.Name)
|
||||||
|
if cmp != 0 {
|
||||||
|
return cmp
|
||||||
|
}
|
||||||
|
return strings.Compare(a.MachineName, b.MachineName)
|
||||||
|
})
|
||||||
|
|
||||||
|
// If quiet mode, just print volume names.
|
||||||
|
if opts.quiet {
|
||||||
|
for _, v := range volumes {
|
||||||
|
fmt.Println(v.Volume.Name)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print the volumes in a table format.
|
||||||
|
tw := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', 0)
|
||||||
|
fmt.Fprintln(tw, "NAME\tDRIVER\tMACHINE")
|
||||||
|
|
||||||
|
for _, v := range volumes {
|
||||||
|
fmt.Fprintf(tw, "%s\t%s\t%s\n",
|
||||||
|
v.Volume.Name,
|
||||||
|
v.Volume.Driver,
|
||||||
|
v.MachineName,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return tw.Flush()
|
||||||
|
}
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
package volume
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/psviderski/uncloud/internal/cli"
|
||||||
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
type removeOptions struct {
|
||||||
|
force bool
|
||||||
|
machines []string
|
||||||
|
yes bool
|
||||||
|
context string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRemoveCommand() *cobra.Command {
|
||||||
|
opts := removeOptions{}
|
||||||
|
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "rm VOLUME_NAME [VOLUME_NAME...]",
|
||||||
|
Aliases: []string{"remove", "delete"},
|
||||||
|
Short: "Remove one or more volumes.",
|
||||||
|
Long: "Remove one or more volumes. You cannot remove a volume that is in use by a container.",
|
||||||
|
Args: cobra.MinimumNArgs(1),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
uncli := cmd.Context().Value("cli").(*cli.CLI)
|
||||||
|
return remove(cmd.Context(), uncli, args, opts)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Flags().BoolVarP(&opts.force, "force", "f", false,
|
||||||
|
"Force the removal of one or more volumes.")
|
||||||
|
cmd.Flags().StringSliceVarP(&opts.machines, "machine", "m", nil,
|
||||||
|
"Name or ID of the machine to remove one or more volumes from. "+
|
||||||
|
"Can be specified multiple times or as a comma-separated list.\n"+
|
||||||
|
"If not specified, the found volume(s) will be removed from all machines.")
|
||||||
|
cmd.Flags().BoolVarP(&opts.yes, "yes", "y", false,
|
||||||
|
"Do not prompt for confirmation before removing the volume(s).")
|
||||||
|
|
||||||
|
cmd.Flags().StringVarP(&opts.context, "context", "c", "",
|
||||||
|
"Name of the cluster context. (default is the current context)")
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func remove(ctx context.Context, uncli *cli.CLI, names []string, opts removeOptions) error {
|
||||||
|
client, err := uncli.ConnectCluster(ctx, opts.context)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("connect to cluster: %w", err)
|
||||||
|
}
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
filter := &api.VolumeFilter{
|
||||||
|
Names: names,
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(opts.machines) > 0 {
|
||||||
|
machines := cli.ExpandCommaSeparatedValues(opts.machines)
|
||||||
|
filter.Machines = machines
|
||||||
|
}
|
||||||
|
|
||||||
|
volumes, err := client.ListVolumes(ctx, filter)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("list volumes: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(volumes) == 0 {
|
||||||
|
if len(names) == 1 {
|
||||||
|
return fmt.Errorf("volume '%s' not found", names[0])
|
||||||
|
}
|
||||||
|
return fmt.Errorf("no volumes found matching the specified names")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Confirm removal if not using --yes flag.
|
||||||
|
if !opts.yes {
|
||||||
|
fmt.Println("The following volumes will be removed:")
|
||||||
|
for _, v := range volumes {
|
||||||
|
fmt.Printf(" • '%s' on machine '%s'\n", v.Volume.Name, v.MachineName)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println()
|
||||||
|
confirmed, err := cli.Confirm()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("confirm removal: %w", err)
|
||||||
|
}
|
||||||
|
if !confirmed {
|
||||||
|
fmt.Println("Cancelled. No volumes were removed.")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the volumes one by one collecting errors.
|
||||||
|
var removeErr error
|
||||||
|
for _, v := range volumes {
|
||||||
|
if err = client.RemoveVolume(ctx, v.MachineID, v.Volume.Name, opts.force); err != nil {
|
||||||
|
if !errors.Is(err, api.ErrNotFound) {
|
||||||
|
removeErr = errors.Join(removeErr, fmt.Errorf("failed to remove volume '%s' on machine '%s': %w",
|
||||||
|
v.Volume.Name, v.MachineName, err))
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Volume '%s' removed from machine '%s'.\n", v.Volume.Name, v.MachineName)
|
||||||
|
}
|
||||||
|
|
||||||
|
return removeErr
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package volume
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewRootCommand() *cobra.Command {
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "volume",
|
||||||
|
Short: "Manage volumes in an Uncloud cluster.",
|
||||||
|
}
|
||||||
|
cmd.AddCommand(
|
||||||
|
NewCreateCommand(),
|
||||||
|
NewInspectCommand(),
|
||||||
|
NewListCommand(),
|
||||||
|
NewRemoveCommand(),
|
||||||
|
)
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
+11
-4
@@ -2,22 +2,29 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"syscall"
|
"syscall"
|
||||||
"uncloud/internal/daemon"
|
|
||||||
"uncloud/internal/machine"
|
"github.com/psviderski/uncloud/internal/daemon"
|
||||||
|
"github.com/psviderski/uncloud/internal/log"
|
||||||
|
"github.com/psviderski/uncloud/internal/machine"
|
||||||
|
"github.com/psviderski/uncloud/internal/version"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
slog.SetLogLoggerLevel(slog.LevelDebug)
|
logger := slog.New(log.NewSlogTextHandler(os.Stderr, &slog.HandlerOptions{
|
||||||
|
Level: slog.LevelDebug,
|
||||||
|
}))
|
||||||
|
slog.SetDefault(logger)
|
||||||
|
|
||||||
var dataDir string
|
var dataDir string
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "uncloudd",
|
Use: "uncloudd",
|
||||||
Short: "Uncloud machine daemon.",
|
Short: "Uncloud machine daemon.",
|
||||||
|
Version: version.String(),
|
||||||
SilenceUsage: true,
|
SilenceUsage: true,
|
||||||
SilenceErrors: true,
|
SilenceErrors: true,
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# Dependencies
|
||||||
|
/node_modules
|
||||||
|
|
||||||
|
# Production
|
||||||
|
/build
|
||||||
|
|
||||||
|
# Generated files
|
||||||
|
.docusaurus
|
||||||
|
.cache-loader
|
||||||
|
|
||||||
|
# Misc
|
||||||
|
.DS_Store
|
||||||
|
.env.local
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
# Website
|
||||||
|
|
||||||
|
This website is built using [Docusaurus](https://docusaurus.io/), a modern static website generator.
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
|
```
|
||||||
|
$ yarn
|
||||||
|
```
|
||||||
|
|
||||||
|
### Local Development
|
||||||
|
|
||||||
|
```
|
||||||
|
$ yarn start
|
||||||
|
```
|
||||||
|
|
||||||
|
This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server.
|
||||||
|
|
||||||
|
### Build
|
||||||
|
|
||||||
|
```
|
||||||
|
$ yarn build
|
||||||
|
```
|
||||||
|
|
||||||
|
This command generates static content into the `build` directory and can be served using any static contents hosting service.
|
||||||
|
|
||||||
|
### Deployment
|
||||||
|
|
||||||
|
Using SSH:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ USE_SSH=true yarn deploy
|
||||||
|
```
|
||||||
|
|
||||||
|
Not using SSH:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ GIT_USER=<Your GitHub username> yarn deploy
|
||||||
|
```
|
||||||
|
|
||||||
|
If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch.
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
---
|
||||||
|
slug: /
|
||||||
|
---
|
||||||
|
|
||||||
|
# Overview
|
||||||
|
|
||||||
|
Uncloud is lightweight tool for creating a cluster of machines running Docker and deploying web applications across
|
||||||
|
them. It is designed to be simple and easy to use, while still providing the essential features to manage and scale
|
||||||
|
your apps in production.
|
||||||
@@ -0,0 +1,153 @@
|
|||||||
|
// @ts-check
|
||||||
|
// `@type` JSDoc annotations allow editor autocompletion and type checking
|
||||||
|
// (when paired with `@ts-check`).
|
||||||
|
// There are various equivalent ways to declare your Docusaurus config.
|
||||||
|
// See: https://docusaurus.io/docs/api/docusaurus-config
|
||||||
|
|
||||||
|
import {themes as prismThemes} from 'prism-react-renderer';
|
||||||
|
|
||||||
|
// This runs in Node.js - Don't use client-side code here (browser APIs, JSX...)
|
||||||
|
|
||||||
|
/** @type {import('@docusaurus/types').Config} */
|
||||||
|
const config = {
|
||||||
|
title: 'Uncloud Docs',
|
||||||
|
tagline: 'Dinosaurs are cool',
|
||||||
|
favicon: 'img/favicon.png',
|
||||||
|
|
||||||
|
// Set the production url of your site here
|
||||||
|
url: 'https://docs.uncloud.run',
|
||||||
|
// Set the /<baseUrl>/ pathname under which your site is served
|
||||||
|
// For GitHub pages deployment, it is often '/<projectName>/'
|
||||||
|
baseUrl: '/',
|
||||||
|
|
||||||
|
// GitHub pages deployment config.
|
||||||
|
// If you aren't using GitHub pages, you don't need these.
|
||||||
|
organizationName: '', // Usually your GitHub org/user name.
|
||||||
|
projectName: '', // Usually your repo name.
|
||||||
|
|
||||||
|
onBrokenLinks: 'throw',
|
||||||
|
onBrokenMarkdownLinks: 'warn',
|
||||||
|
|
||||||
|
// Even if you don't use internationalization, you can use this field to set
|
||||||
|
// useful metadata like html lang. For example, if your site is Chinese, you
|
||||||
|
// may want to replace "en" with "zh-Hans".
|
||||||
|
i18n: {
|
||||||
|
defaultLocale: 'en',
|
||||||
|
locales: ['en'],
|
||||||
|
},
|
||||||
|
|
||||||
|
themes: [
|
||||||
|
[
|
||||||
|
'@easyops-cn/docusaurus-search-local',
|
||||||
|
/** @type {import("@easyops-cn/docusaurus-search-local").PluginOptions} */
|
||||||
|
({
|
||||||
|
docsRouteBasePath: '/',
|
||||||
|
hashed: true,
|
||||||
|
highlightSearchTermsOnTargetPage: true,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
presets: [
|
||||||
|
[
|
||||||
|
'classic',
|
||||||
|
/** @type {import('@docusaurus/preset-classic').Options} */
|
||||||
|
({
|
||||||
|
docs: {
|
||||||
|
// Remove this to remove the "edit this page" links.
|
||||||
|
editUrl: 'https://github.com/psviderski/uncloud/edit/main/docs/',
|
||||||
|
// Serve the docs at the site's root.
|
||||||
|
routeBasePath: '/',
|
||||||
|
showLastUpdateTime: true,
|
||||||
|
sidebarPath: './sidebars.js',
|
||||||
|
},
|
||||||
|
// blog: {
|
||||||
|
// showReadingTime: true,
|
||||||
|
// feedOptions: {
|
||||||
|
// type: ['rss', 'atom'],
|
||||||
|
// xslt: true,
|
||||||
|
// },
|
||||||
|
// // Please change this to your repo.
|
||||||
|
// // Remove this to remove the "edit this page" links.
|
||||||
|
// editUrl:
|
||||||
|
// 'https://github.com/facebook/docusaurus/tree/main/packages/create-docusaurus/templates/shared/',
|
||||||
|
// // Useful options to enforce blogging best practices
|
||||||
|
// onInlineTags: 'warn',
|
||||||
|
// onInlineAuthors: 'warn',
|
||||||
|
// onUntruncatedBlogPosts: 'warn',
|
||||||
|
// },
|
||||||
|
theme: {
|
||||||
|
customCss: './src/css/custom.css',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
themeConfig:
|
||||||
|
/** @type {import('@docusaurus/preset-classic').ThemeConfig} */
|
||||||
|
({
|
||||||
|
colorMode: {
|
||||||
|
// The color mode when user first visits the site.
|
||||||
|
defaultMode: "light",
|
||||||
|
// Whether to use the prefers-color-scheme media-query, using user system preferences,
|
||||||
|
// instead of the hardcoded defaultMode.
|
||||||
|
respectPrefersColorScheme: true,
|
||||||
|
},
|
||||||
|
// The meta image URL for the site (og:image and twitter:image meta tags).
|
||||||
|
// Relative to your site's "static" directory. Cannot be SVGs. Can be external URLs too.
|
||||||
|
image: 'img/social-card.png',
|
||||||
|
navbar: {
|
||||||
|
title: 'Uncloud Docs',
|
||||||
|
logo: {
|
||||||
|
alt: 'Uncloud Logo',
|
||||||
|
src: 'img/logo.svg',
|
||||||
|
},
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
type: 'search',
|
||||||
|
position: 'right',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href: 'https://github.com/psviderski/uncloud',
|
||||||
|
label: 'GitHub',
|
||||||
|
position: 'right',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
footer: {
|
||||||
|
style: 'light',
|
||||||
|
links: [
|
||||||
|
{
|
||||||
|
title: 'Uncloud',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
html: 'Made with ❤️ by <a href="https://github.com/psviderski">@psviderski</a> in Australia'
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Community',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
label: 'Discord',
|
||||||
|
href: 'https://discord.gg/eR35KQJhPu',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'X',
|
||||||
|
href: 'https://x.com/psviderski',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'GitHub',
|
||||||
|
href: 'https://github.com/psviderski/uncloud',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
prism: {
|
||||||
|
theme: prismThemes.github,
|
||||||
|
darkTheme: prismThemes.dracula,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
Generated
+18434
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,45 @@
|
|||||||
|
{
|
||||||
|
"name": "docs",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"docusaurus": "docusaurus",
|
||||||
|
"start": "docusaurus start",
|
||||||
|
"build": "docusaurus build",
|
||||||
|
"swizzle": "docusaurus swizzle",
|
||||||
|
"deploy": "docusaurus deploy",
|
||||||
|
"clear": "docusaurus clear",
|
||||||
|
"serve": "docusaurus serve",
|
||||||
|
"write-translations": "docusaurus write-translations",
|
||||||
|
"write-heading-ids": "docusaurus write-heading-ids"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@docusaurus/core": "3.7.0",
|
||||||
|
"@docusaurus/preset-classic": "3.7.0",
|
||||||
|
"@easyops-cn/docusaurus-search-local": "^0.49.2",
|
||||||
|
"@mdx-js/react": "^3.0.0",
|
||||||
|
"clsx": "^2.0.0",
|
||||||
|
"prism-react-renderer": "^2.3.0",
|
||||||
|
"react": "^19.0.0",
|
||||||
|
"react-dom": "^19.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@docusaurus/module-type-aliases": "3.7.0",
|
||||||
|
"@docusaurus/types": "3.7.0"
|
||||||
|
},
|
||||||
|
"browserslist": {
|
||||||
|
"production": [
|
||||||
|
">0.5%",
|
||||||
|
"not dead",
|
||||||
|
"not op_mini all"
|
||||||
|
],
|
||||||
|
"development": [
|
||||||
|
"last 3 chrome version",
|
||||||
|
"last 3 firefox version",
|
||||||
|
"last 5 safari version"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
// @ts-check
|
||||||
|
|
||||||
|
// This runs in Node.js - Don't use client-side code here (browser APIs, JSX...)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creating a sidebar enables you to:
|
||||||
|
- create an ordered group of docs
|
||||||
|
- render a sidebar for each doc of that group
|
||||||
|
- provide next/previous navigation
|
||||||
|
|
||||||
|
The sidebars can be generated from the filesystem, or explicitly defined here.
|
||||||
|
|
||||||
|
Create as many sidebars as you want.
|
||||||
|
|
||||||
|
@type {import('@docusaurus/plugin-content-docs').SidebarsConfig}
|
||||||
|
*/
|
||||||
|
const sidebars = {
|
||||||
|
// By default, Docusaurus generates a sidebar from the docs folder structure
|
||||||
|
tutorialSidebar: [{type: 'autogenerated', dirName: '.'}],
|
||||||
|
|
||||||
|
// But you can create a sidebar manually
|
||||||
|
/*
|
||||||
|
tutorialSidebar: [
|
||||||
|
'intro',
|
||||||
|
'hello',
|
||||||
|
{
|
||||||
|
type: 'category',
|
||||||
|
label: 'Tutorial',
|
||||||
|
items: ['tutorial-basics/create-a-document'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
*/
|
||||||
|
};
|
||||||
|
|
||||||
|
export default sidebars;
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
/**
|
||||||
|
* Any CSS included here will be global. The classic template
|
||||||
|
* bundles Infima by default. Infima is a CSS framework designed to
|
||||||
|
* work well for content-centric websites.
|
||||||
|
*/
|
||||||
|
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;700&display=swap');
|
||||||
|
|
||||||
|
/* You can override the default Infima variables here. */
|
||||||
|
:root {
|
||||||
|
--ifm-font-family-base: 'Inter', sans-serif;
|
||||||
|
--ifm-heading-font-family: 'Inter', sans-serif;
|
||||||
|
|
||||||
|
--ifm-color-primary: #2e8555;
|
||||||
|
--ifm-color-primary-dark: #29784c;
|
||||||
|
--ifm-color-primary-darker: #277148;
|
||||||
|
--ifm-color-primary-darkest: #205d3b;
|
||||||
|
--ifm-color-primary-light: #33925d;
|
||||||
|
--ifm-color-primary-lighter: #359962;
|
||||||
|
--ifm-color-primary-lightest: #3cad6e;
|
||||||
|
--ifm-code-font-size: 95%;
|
||||||
|
--docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* For readability concerns, you should choose a lighter palette in dark mode. */
|
||||||
|
[data-theme='dark'] {
|
||||||
|
--ifm-color-primary: #25c2a0;
|
||||||
|
--ifm-color-primary-dark: #21af90;
|
||||||
|
--ifm-color-primary-darker: #1fa588;
|
||||||
|
--ifm-color-primary-darkest: #1a8870;
|
||||||
|
--ifm-color-primary-light: #29d5b0;
|
||||||
|
--ifm-color-primary-lighter: #32d8b4;
|
||||||
|
--ifm-color-primary-lightest: #4fddbf;
|
||||||
|
--docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: var(--ifm-font-weight-normal);
|
||||||
|
}
|
||||||
Vendored
BIN
Binary file not shown.
|
After Width: | Height: | Size: 1.2 KiB |
Vendored
+7
@@ -0,0 +1,7 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 80 80">
|
||||||
|
<g>
|
||||||
|
<rect x="0" y="0" width="80" height="80" rx="10" fill="#18181B"/>
|
||||||
|
<path d="M20 32 L20 48 L36 48 L36 32 M60 32 L45 32 L45 48 L60 48" stroke="white" stroke-width="6" fill="none"
|
||||||
|
stroke-linecap="square" stroke-linejoin="miter"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 341 B |
Vendored
BIN
Binary file not shown.
|
After Width: | Height: | Size: 88 KiB |
@@ -0,0 +1,59 @@
|
|||||||
|
# User Guide
|
||||||
|
|
||||||
|
## Initialising a cluster
|
||||||
|
|
||||||
|
To begin setting up a new Uncloud cluster, create the desired nodes. Ensure that their firewall allows the required ports.
|
||||||
|
|
||||||
|
### Ports
|
||||||
|
|
||||||
|
Nodes running Uncloud with a standard configuration must have the following inbound ports allowed in the firewall:
|
||||||
|
|
||||||
|
* 51820/udp (WireGuard meshnet)
|
||||||
|
* 22/tcp (default SSH management port)
|
||||||
|
* 80/tcp (required for challenge if uncloud.run DNS is enabled, pass --no-dns during init to disable)
|
||||||
|
|
||||||
|
In addition, any ports for any workloads you want to expose will need to be open.
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
|
||||||
|
Uncloud stores its configuration in `~/.config/uncloud/config.yaml`. If you wish to reinitialise a cluster, simply remove it from this config.
|
||||||
|
|
||||||
|
### Initialisation
|
||||||
|
|
||||||
|
Begin by initialising the first node in your cluster with `uc machine init [USER@HOST:PORT]`. If you do not have a need for Caddy reverse proxy, you may disable this feature with `--no-caddy`.
|
||||||
|
|
||||||
|
This command will idempotently install Docker, uncloudd, uncloud-corrosion. If Caddy is enabled, it will set up a reverse proxy. If Uncloud DNS is enabled, it will create a DNS A record for the machine's public IP address under `*.[CLUSTER ID].cluster.uncloud.run`.
|
||||||
|
|
||||||
|
If you wish to uninstall Uncloud and its components, run `uncloud-uninstall`.
|
||||||
|
|
||||||
|
### Adding a node
|
||||||
|
|
||||||
|
Just like the initialisation of the first node, a node can be added to the cluster with `uc machine add`.
|
||||||
|
|
||||||
|
### DNS
|
||||||
|
|
||||||
|
Uncloud (uncloud.run) DNS can be managed with the `uc dns` subcommand.
|
||||||
|
|
||||||
|
* To reserve a domain name, run `uc dns reserve`
|
||||||
|
* To release a domain name, run `uc dns release`.
|
||||||
|
* To see the domain name, run `uc dns show`
|
||||||
|
|
||||||
|
### Running a service
|
||||||
|
|
||||||
|
Services on an Uncloud cluster can be managed with `uc service`.
|
||||||
|
|
||||||
|
You can run a service with two replicas that expose port 80 like the following:
|
||||||
|
|
||||||
|
```
|
||||||
|
uc service run -p 80/http --replicas 2 nginxdemos/hello
|
||||||
|
```
|
||||||
|
|
||||||
|
This requires the Caddy reverse proxy to be deployed. If it wasn't previously, it can be deployed with `uc caddy deploy`.
|
||||||
|
|
||||||
|
Since this doesn't specify a service name, a random one will be generated.
|
||||||
|
|
||||||
|
The service can be deleted by its service name:
|
||||||
|
|
||||||
|
```
|
||||||
|
uc service rm hello-gsdo
|
||||||
|
```
|
||||||
@@ -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 (
|
||||||
|
|||||||
@@ -1,16 +1,30 @@
|
|||||||
module uncloud
|
module github.com/psviderski/uncloud
|
||||||
|
|
||||||
go 1.22.2
|
go 1.23.0
|
||||||
|
|
||||||
toolchain go1.22.5
|
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/caddyserver/caddy/v2 v2.8.4
|
||||||
|
github.com/cenkalti/backoff/v4 v4.3.0
|
||||||
github.com/charmbracelet/huh v0.6.0
|
github.com/charmbracelet/huh v0.6.0
|
||||||
|
github.com/charmbracelet/lipgloss v0.13.0
|
||||||
|
github.com/compose-spec/compose-go/v2 v2.4.5
|
||||||
github.com/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/docker/docker v27.3.0+incompatible
|
github.com/distribution/reference v0.6.0
|
||||||
|
github.com/docker/cli v27.5.0+incompatible
|
||||||
|
github.com/docker/compose/v2 v2.31.0
|
||||||
|
github.com/docker/docker v27.4.0-rc.2+incompatible
|
||||||
github.com/docker/go-connections v0.5.0
|
github.com/docker/go-connections v0.5.0
|
||||||
|
github.com/docker/go-units v0.5.0
|
||||||
|
github.com/goccy/go-yaml v1.17.1
|
||||||
|
github.com/google/go-cmp v0.7.0
|
||||||
|
github.com/google/go-containerregistry v0.20.2
|
||||||
github.com/hashicorp/memberlist v0.5.1
|
github.com/hashicorp/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
|
||||||
@@ -20,65 +34,115 @@ 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/miekg/dns v1.1.65
|
||||||
|
github.com/opencontainers/go-digest v1.0.0
|
||||||
|
github.com/opencontainers/image-spec v1.1.0
|
||||||
github.com/siderolabs/discovery-api v0.1.4
|
github.com/siderolabs/discovery-api v0.1.4
|
||||||
github.com/siderolabs/discovery-client v0.1.9
|
github.com/siderolabs/discovery-client v0.1.9
|
||||||
|
github.com/siderolabs/grpc-proxy v0.5.1
|
||||||
github.com/spf13/cobra v1.8.1
|
github.com/spf13/cobra v1.8.1
|
||||||
|
github.com/stretchr/testify v1.10.0
|
||||||
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.27.0
|
golang.org/x/crypto v0.33.0
|
||||||
golang.org/x/sync v0.8.0
|
golang.org/x/net v0.35.0
|
||||||
golang.org/x/sys v0.25.0
|
golang.org/x/sync v0.11.0
|
||||||
|
golang.org/x/sys v0.31.0
|
||||||
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173
|
golang.zx2c4.com/wireguard 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/grpc v1.66.0
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576
|
||||||
google.golang.org/protobuf v1.34.2
|
google.golang.org/grpc v1.68.1
|
||||||
|
google.golang.org/protobuf v1.36.3
|
||||||
|
modernc.org/sqlite v1.36.3
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
filippo.io/edwards25519 v1.1.0 // indirect
|
||||||
|
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 // indirect
|
||||||
|
github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 // indirect
|
||||||
|
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
|
||||||
github.com/Masterminds/goutils v1.1.1 // indirect
|
github.com/Masterminds/goutils v1.1.1 // indirect
|
||||||
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/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
|
||||||
github.com/armon/go-radix v1.0.0 // indirect
|
github.com/armon/go-radix v1.0.0 // indirect
|
||||||
|
github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b // indirect
|
||||||
github.com/atotto/clipboard v0.1.4 // indirect
|
github.com/atotto/clipboard v0.1.4 // indirect
|
||||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||||
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
github.com/bgentry/speakeasy v0.2.0 // indirect
|
github.com/bgentry/speakeasy v0.2.0 // indirect
|
||||||
|
github.com/buger/goterm v1.0.4 // indirect
|
||||||
|
github.com/caddyserver/certmagic v0.21.4 // indirect
|
||||||
|
github.com/caddyserver/zerossl v0.1.3 // indirect
|
||||||
github.com/catppuccin/go v0.2.0 // indirect
|
github.com/catppuccin/go v0.2.0 // indirect
|
||||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
|
||||||
github.com/cespare/xxhash v1.1.0 // indirect
|
github.com/cespare/xxhash v1.1.0 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
github.com/charmbracelet/bubbles v0.20.0 // indirect
|
github.com/charmbracelet/bubbles v0.20.0 // indirect
|
||||||
github.com/charmbracelet/bubbletea v1.1.1 // indirect
|
github.com/charmbracelet/bubbletea v1.1.1 // indirect
|
||||||
github.com/charmbracelet/lipgloss v0.13.0 // indirect
|
|
||||||
github.com/charmbracelet/x/ansi v0.3.2 // indirect
|
github.com/charmbracelet/x/ansi v0.3.2 // indirect
|
||||||
github.com/charmbracelet/x/exp/strings v0.0.0-20240919170804-a4978c8e603a // indirect
|
github.com/charmbracelet/x/exp/strings v0.0.0-20240919170804-a4978c8e603a // indirect
|
||||||
github.com/charmbracelet/x/term v0.2.0 // indirect
|
github.com/charmbracelet/x/term v0.2.0 // indirect
|
||||||
|
github.com/chzyer/readline v1.5.1 // indirect
|
||||||
|
github.com/cloudflare/cfssl v1.6.4 // indirect
|
||||||
|
github.com/containerd/console v1.0.4 // indirect
|
||||||
|
github.com/containerd/containerd v1.7.24 // indirect
|
||||||
|
github.com/containerd/containerd/api v1.7.19 // indirect
|
||||||
|
github.com/containerd/continuity v0.4.4 // indirect
|
||||||
|
github.com/containerd/errdefs v0.3.0 // indirect
|
||||||
github.com/containerd/log v0.1.0 // indirect
|
github.com/containerd/log v0.1.0 // indirect
|
||||||
|
github.com/containerd/platforms v0.2.1 // indirect
|
||||||
|
github.com/containerd/stargz-snapshotter/estargz v0.16.3 // indirect
|
||||||
|
github.com/containerd/ttrpc v1.2.5 // indirect
|
||||||
|
github.com/containerd/typeurl/v2 v2.2.0 // indirect
|
||||||
|
github.com/cpuguy83/go-md2man/v2 v2.0.6 // 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/v2 v2.2007.4 // indirect
|
||||||
github.com/dgraph-io/ristretto v0.1.1 // indirect
|
github.com/dgraph-io/ristretto v0.1.1 // indirect
|
||||||
github.com/distribution/reference v0.6.0 // indirect
|
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 // indirect
|
||||||
|
github.com/docker/buildx v0.18.0 // indirect
|
||||||
|
github.com/docker/distribution v2.8.3+incompatible // indirect
|
||||||
|
github.com/docker/docker-credential-helpers v0.8.2 // indirect
|
||||||
|
github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c // indirect
|
||||||
github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c // indirect
|
github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c // indirect
|
||||||
github.com/docker/go-units v0.5.0 // indirect
|
github.com/docker/go-metrics v0.0.1 // indirect
|
||||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
||||||
github.com/fatih/color v1.17.0 // indirect
|
github.com/fatih/color v1.17.0 // indirect
|
||||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||||
|
github.com/fvbommel/sortorder v1.1.0 // indirect
|
||||||
|
github.com/go-jose/go-jose/v3 v3.0.3 // indirect
|
||||||
|
github.com/go-kit/kit v0.13.0 // indirect
|
||||||
|
github.com/go-kit/log v0.2.1 // indirect
|
||||||
|
github.com/go-logfmt/logfmt v0.6.0 // indirect
|
||||||
github.com/go-logr/logr v1.4.2 // indirect
|
github.com/go-logr/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.8.1 // indirect
|
||||||
|
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
|
||||||
|
github.com/go-viper/mapstructure/v2 v2.0.0 // indirect
|
||||||
github.com/godbus/dbus/v5 v5.1.0 // indirect
|
github.com/godbus/dbus/v5 v5.1.0 // indirect
|
||||||
|
github.com/gofrs/flock v0.12.1 // indirect
|
||||||
github.com/gogo/protobuf v1.3.2 // indirect
|
github.com/gogo/protobuf v1.3.2 // indirect
|
||||||
github.com/golang/glog v1.2.2 // indirect
|
github.com/golang/glog v1.2.2 // indirect
|
||||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||||
github.com/golang/protobuf v1.5.4 // indirect
|
github.com/golang/protobuf v1.5.4 // indirect
|
||||||
github.com/golang/snappy v0.0.4 // indirect
|
github.com/golang/snappy v0.0.4 // indirect
|
||||||
github.com/google/btree v1.1.2 // indirect
|
github.com/google/btree v1.1.2 // indirect
|
||||||
|
github.com/google/cel-go v0.20.1 // indirect
|
||||||
github.com/google/flatbuffers v24.3.25+incompatible // indirect
|
github.com/google/flatbuffers v24.3.25+incompatible // indirect
|
||||||
github.com/google/go-cmp v0.6.0 // indirect
|
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e // indirect
|
||||||
|
github.com/google/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/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
|
||||||
@@ -86,12 +150,14 @@ require (
|
|||||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||||
github.com/hashicorp/go-sockaddr v1.0.6 // indirect
|
github.com/hashicorp/go-sockaddr v1.0.6 // indirect
|
||||||
github.com/hashicorp/go-syslog v1.0.0 // indirect
|
github.com/hashicorp/go-syslog v1.0.0 // indirect
|
||||||
|
github.com/hashicorp/go-version v1.7.0 // indirect
|
||||||
github.com/hashicorp/golang-lru v1.0.2 // indirect
|
github.com/hashicorp/golang-lru v1.0.2 // indirect
|
||||||
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
|
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
|
||||||
github.com/hashicorp/logutils v1.0.0 // indirect
|
github.com/hashicorp/logutils v1.0.0 // indirect
|
||||||
github.com/hashicorp/mdns v1.0.5 // indirect
|
github.com/hashicorp/mdns v1.0.5 // indirect
|
||||||
github.com/huandu/xstrings v1.5.0 // indirect
|
github.com/huandu/xstrings v1.5.0 // indirect
|
||||||
github.com/imdario/mergo v0.3.16 // indirect
|
github.com/imdario/mergo v0.3.16 // indirect
|
||||||
|
github.com/in-toto/in-toto-golang v0.5.0 // indirect
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
github.com/ipfs/bbloom v0.0.4 // indirect
|
github.com/ipfs/bbloom v0.0.4 // indirect
|
||||||
github.com/ipfs/go-block-format v0.2.0 // indirect
|
github.com/ipfs/go-block-format v0.2.0 // indirect
|
||||||
@@ -101,30 +167,52 @@ require (
|
|||||||
github.com/ipld/go-codec-dagpb v1.6.0 // indirect
|
github.com/ipld/go-codec-dagpb v1.6.0 // indirect
|
||||||
github.com/ipld/go-ipld-prime v0.21.0 // indirect
|
github.com/ipld/go-ipld-prime v0.21.0 // indirect
|
||||||
github.com/ishidawataru/sctp v0.0.0-20230406120618-7ff4192f6ff2 // indirect
|
github.com/ishidawataru/sctp v0.0.0-20230406120618-7ff4192f6ff2 // indirect
|
||||||
|
github.com/jackc/chunkreader/v2 v2.0.1 // indirect
|
||||||
|
github.com/jackc/pgconn v1.14.3 // indirect
|
||||||
|
github.com/jackc/pgio v1.0.0 // indirect
|
||||||
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
|
github.com/jackc/pgproto3/v2 v2.3.3 // indirect
|
||||||
|
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
|
||||||
|
github.com/jackc/pgtype v1.14.0 // indirect
|
||||||
|
github.com/jackc/pgx/v4 v4.18.3 // indirect
|
||||||
github.com/jbenet/goprocess v0.1.4 // indirect
|
github.com/jbenet/goprocess v0.1.4 // indirect
|
||||||
github.com/josharian/native v1.1.0 // indirect
|
github.com/josharian/native v1.1.0 // indirect
|
||||||
github.com/klauspost/compress v1.17.9 // indirect
|
github.com/klauspost/compress v1.17.11 // indirect
|
||||||
github.com/klauspost/cpuid/v2 v2.2.8 // indirect
|
github.com/klauspost/cpuid/v2 v2.2.9 // indirect
|
||||||
|
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect
|
||||||
|
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect
|
||||||
|
github.com/libdns/libdns v0.2.2 // indirect
|
||||||
github.com/libp2p/go-buffer-pool v0.1.0 // indirect
|
github.com/libp2p/go-buffer-pool v0.1.0 // indirect
|
||||||
github.com/libp2p/go-libp2p v0.35.4 // indirect
|
github.com/libp2p/go-libp2p v0.35.4 // indirect
|
||||||
github.com/libp2p/go-libp2p-pubsub v0.11.0 // indirect
|
github.com/libp2p/go-libp2p-pubsub v0.11.0 // indirect
|
||||||
github.com/libp2p/go-msgio v0.3.0 // indirect
|
github.com/libp2p/go-msgio v0.3.0 // indirect
|
||||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||||
|
github.com/manifoldco/promptui v0.9.0 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/mattn/go-localereader v0.0.1 // indirect
|
github.com/mattn/go-localereader v0.0.1 // indirect
|
||||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||||
|
github.com/mattn/go-shellwords v1.0.12 // indirect
|
||||||
github.com/mdlayher/genetlink v1.3.2 // indirect
|
github.com/mdlayher/genetlink v1.3.2 // indirect
|
||||||
github.com/mdlayher/netlink v1.7.2 // indirect
|
github.com/mdlayher/netlink v1.7.2 // indirect
|
||||||
github.com/mdlayher/socket v0.5.1 // indirect
|
github.com/mdlayher/socket v0.5.1 // indirect
|
||||||
github.com/miekg/dns v1.1.62 // indirect
|
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
|
||||||
|
github.com/mholt/acmez/v2 v2.0.3 // indirect
|
||||||
|
github.com/miekg/pkcs11 v1.1.1 // indirect
|
||||||
github.com/minio/sha256-simd v1.0.1 // indirect
|
github.com/minio/sha256-simd v1.0.1 // indirect
|
||||||
github.com/mitchellh/cli v1.1.5 // indirect
|
github.com/mitchellh/cli v1.1.5 // indirect
|
||||||
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/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
|
||||||
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||||
|
github.com/moby/buildkit v0.17.2 // indirect
|
||||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||||
|
github.com/moby/locker v1.0.1 // indirect
|
||||||
|
github.com/moby/patternmatcher v0.6.0 // indirect
|
||||||
|
github.com/moby/sys/sequential v0.6.0 // indirect
|
||||||
|
github.com/moby/sys/signal v0.7.1 // indirect
|
||||||
github.com/moby/term v0.5.0 // indirect
|
github.com/moby/term v0.5.0 // indirect
|
||||||
github.com/morikuni/aec v1.0.0 // indirect
|
github.com/morikuni/aec v1.0.0 // indirect
|
||||||
github.com/mr-tron/base58 v1.2.0 // indirect
|
github.com/mr-tron/base58 v1.2.0 // indirect
|
||||||
@@ -140,37 +228,94 @@ require (
|
|||||||
github.com/multiformats/go-multihash v0.2.3 // indirect
|
github.com/multiformats/go-multihash v0.2.3 // indirect
|
||||||
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/opencontainers/go-digest v1.0.0 // indirect
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||||
github.com/opencontainers/image-spec v1.1.0 // indirect
|
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||||
|
github.com/onsi/ginkgo/v2 v2.22.0 // indirect
|
||||||
|
github.com/pelletier/go-toml v1.9.5 // indirect
|
||||||
|
github.com/pires/go-proxyproto v0.7.0 // indirect
|
||||||
github.com/pkg/errors v0.9.1 // indirect
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
|
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/polydawn/refmt v0.89.0 // indirect
|
github.com/polydawn/refmt v0.89.0 // indirect
|
||||||
github.com/posener/complete v1.2.3 // indirect
|
github.com/posener/complete v1.2.3 // indirect
|
||||||
|
github.com/prometheus/client_golang v1.20.5 // indirect
|
||||||
|
github.com/prometheus/client_model v0.6.1 // indirect
|
||||||
|
github.com/prometheus/common v0.61.0 // indirect
|
||||||
|
github.com/prometheus/procfs v0.15.1 // indirect
|
||||||
|
github.com/quic-go/qpack v0.5.1 // indirect
|
||||||
|
github.com/quic-go/quic-go v0.48.2 // indirect
|
||||||
|
github.com/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/russross/blackfriday/v2 v2.1.0 // indirect
|
||||||
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 // indirect
|
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 // indirect
|
||||||
|
github.com/secure-systems-lab/go-securesystemslib v0.4.0 // indirect
|
||||||
|
github.com/shibumi/go-pathspec v1.3.0 // indirect
|
||||||
github.com/shopspring/decimal v1.4.0 // indirect
|
github.com/shopspring/decimal v1.4.0 // indirect
|
||||||
|
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
|
||||||
github.com/siderolabs/gen v0.4.8 // indirect
|
github.com/siderolabs/gen v0.4.8 // indirect
|
||||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||||
|
github.com/slackhq/nebula v1.6.1 // indirect
|
||||||
|
github.com/smallstep/certificates v0.26.1 // indirect
|
||||||
|
github.com/smallstep/nosql v0.6.1 // indirect
|
||||||
|
github.com/smallstep/pkcs7 v0.0.0-20231024181729-3b98ecc1ca81 // indirect
|
||||||
|
github.com/smallstep/scep v0.0.0-20231024192529-aee96d7ad34d // indirect
|
||||||
|
github.com/smallstep/truststore v0.13.0 // indirect
|
||||||
github.com/spaolacci/murmur3 v1.1.0 // indirect
|
github.com/spaolacci/murmur3 v1.1.0 // indirect
|
||||||
github.com/spf13/cast v1.7.0 // indirect
|
github.com/spf13/cast v1.7.0 // indirect
|
||||||
github.com/spf13/pflag v1.0.5 // indirect
|
github.com/spf13/pflag v1.0.5 // indirect
|
||||||
|
github.com/stoewer/go-strcase v1.2.0 // indirect
|
||||||
|
github.com/tailscale/tscert v0.0.0-20240517230440-bbccfbf48933 // indirect
|
||||||
|
github.com/theupdateframework/notary v0.7.0 // indirect
|
||||||
|
github.com/tonistiigi/dchapes-mode v0.0.0-20241001053921-ca0759fec205 // indirect
|
||||||
|
github.com/tonistiigi/fsutil v0.0.0-20241028165955-397af5306b5c // indirect
|
||||||
|
github.com/tonistiigi/go-csvvalue v0.0.0-20240710180619-ddb21b71c0b4 // indirect
|
||||||
|
github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea // indirect
|
||||||
|
github.com/tonistiigi/vt100 v0.0.0-20240514184818-90bafcd6abab // indirect
|
||||||
|
github.com/urfave/cli v1.22.16 // indirect
|
||||||
|
github.com/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
|
||||||
|
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/contrib/instrumentation/net/http/otelhttp v0.52.0 // indirect
|
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||||
go.opentelemetry.io/otel v1.30.0 // indirect
|
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 // indirect
|
||||||
go.opentelemetry.io/otel/metric v1.30.0 // indirect
|
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.46.1 // indirect
|
||||||
go.opentelemetry.io/otel/trace v1.30.0 // indirect
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 // indirect
|
||||||
|
go.opentelemetry.io/otel v1.33.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.44.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/metric v1.33.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/sdk v1.33.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/sdk/metric v1.32.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/trace v1.33.0 // indirect
|
||||||
|
go.opentelemetry.io/proto/otlp v1.4.0 // indirect
|
||||||
|
go.step.sm/cli-utils v0.9.0 // indirect
|
||||||
|
go.step.sm/crypto v0.45.0 // indirect
|
||||||
|
go.step.sm/linkedca v0.20.1 // indirect
|
||||||
|
go.uber.org/automaxprocs v1.5.3 // indirect
|
||||||
|
go.uber.org/mock v0.5.0 // indirect
|
||||||
go.uber.org/multierr v1.11.0 // indirect
|
go.uber.org/multierr v1.11.0 // indirect
|
||||||
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect
|
go.uber.org/zap/exp v0.2.0 // indirect
|
||||||
golang.org/x/mod v0.21.0 // indirect
|
golang.org/x/crypto/x509roots/fallback v0.0.0-20240507223354-67b13616a595 // indirect
|
||||||
golang.org/x/net v0.29.0 // indirect
|
golang.org/x/exp v0.0.0-20241215155358-4a5509556b9e // indirect
|
||||||
golang.org/x/text v0.18.0 // indirect
|
golang.org/x/mod v0.23.0 // indirect
|
||||||
golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 // indirect
|
golang.org/x/term v0.29.0 // indirect
|
||||||
golang.org/x/tools v0.25.0 // indirect
|
golang.org/x/text v0.22.0 // indirect
|
||||||
|
golang.org/x/time v0.8.0 // indirect
|
||||||
|
golang.org/x/tools v0.30.0 // indirect
|
||||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
|
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20240827150818-7e3bb234dfed // indirect
|
google.golang.org/genproto v0.0.0-20240401170217-c3f982113cda // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240827150818-7e3bb234dfed // indirect
|
google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 // indirect
|
||||||
gotest.tools/v3 v3.5.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
|
||||||
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
|
||||||
)
|
)
|
||||||
|
|||||||
+286
-222
@@ -4,262 +4,307 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/charmbracelet/huh"
|
|
||||||
"google.golang.org/protobuf/types/known/emptypb"
|
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
|
||||||
"text/tabwriter"
|
"github.com/charmbracelet/huh"
|
||||||
"uncloud/internal/cli/client"
|
"github.com/docker/cli/cli/streams"
|
||||||
"uncloud/internal/cli/client/connector"
|
"github.com/psviderski/uncloud/internal/cli/config"
|
||||||
"uncloud/internal/cli/config"
|
"github.com/psviderski/uncloud/internal/fs"
|
||||||
"uncloud/internal/machine"
|
"github.com/psviderski/uncloud/internal/machine"
|
||||||
"uncloud/internal/machine/api/pb"
|
"github.com/psviderski/uncloud/internal/machine/api/pb"
|
||||||
"uncloud/internal/secret"
|
"github.com/psviderski/uncloud/internal/sshexec"
|
||||||
"uncloud/internal/sshexec"
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
|
"github.com/psviderski/uncloud/pkg/client"
|
||||||
|
"github.com/psviderski/uncloud/pkg/client/connector"
|
||||||
|
"google.golang.org/grpc/codes"
|
||||||
|
"google.golang.org/grpc/status"
|
||||||
|
"google.golang.org/protobuf/types/known/emptypb"
|
||||||
)
|
)
|
||||||
|
|
||||||
const defaultClusterName = "default"
|
const defaultContextName = "default"
|
||||||
|
|
||||||
var (
|
|
||||||
ErrNotFound = errors.New("not found")
|
|
||||||
)
|
|
||||||
|
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cli *CLI) CreateCluster(name string, userPrivateKey secret.Secret) error {
|
func (cli *CLI) CreateContext(name string) error {
|
||||||
if _, ok := cli.config.Clusters[name]; ok {
|
if _, ok := cli.Config.Contexts[name]; ok {
|
||||||
return fmt.Errorf("cluster %q already exists", name)
|
return fmt.Errorf("context '%s' already exists", name)
|
||||||
}
|
}
|
||||||
if userPrivateKey == nil {
|
cli.Config.Contexts[name] = &config.Context{
|
||||||
user, err := client.NewUser(nil)
|
Name: name,
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("generate user: %w", err)
|
|
||||||
}
|
|
||||||
userPrivateKey = user.PrivateKey()
|
|
||||||
}
|
}
|
||||||
|
return cli.Config.Save()
|
||||||
cli.config.Clusters[name] = &config.Cluster{
|
|
||||||
Name: name,
|
|
||||||
UserPrivateKey: userPrivateKey,
|
|
||||||
}
|
|
||||||
return cli.config.Save()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cli *CLI) CreateDefaultCluster() error {
|
func (cli *CLI) SetCurrentContext(name string) error {
|
||||||
if err := cli.CreateCluster(defaultClusterName, nil); err != nil {
|
if _, ok := cli.Config.Contexts[name]; !ok {
|
||||||
return err
|
return api.ErrNotFound
|
||||||
}
|
}
|
||||||
return cli.SetCurrentCluster(defaultClusterName)
|
cli.Config.CurrentContext = name
|
||||||
|
return cli.Config.Save()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cli *CLI) SetCurrentCluster(name string) error {
|
// ConnectCluster connects to a cluster using the given context name or the current context if not specified.
|
||||||
if _, ok := cli.config.Clusters[name]; !ok {
|
// If the CLI was initialised with a machine connection, the config is ignored and the connection is used instead.
|
||||||
return ErrNotFound
|
func (cli *CLI) ConnectCluster(ctx context.Context, contextName string) (*client.Client, error) {
|
||||||
|
if cli.conn != nil {
|
||||||
|
return connectCluster(ctx, *cli.conn)
|
||||||
}
|
}
|
||||||
cli.config.CurrentCluster = name
|
|
||||||
return cli.config.Save()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cli *CLI) ConnectCluster(ctx context.Context, clusterName string) (*client.Client, error) {
|
if len(cli.Config.Contexts) == 0 {
|
||||||
if len(cli.config.Clusters) == 0 {
|
return nil, fmt.Errorf(
|
||||||
return nil, errors.New(
|
"no cluster contexts found in the Uncloud config (%s). "+
|
||||||
"no clusters found in the Uncloud config. " +
|
"Please initialise a cluster with 'uncloud machine init' first",
|
||||||
"Please initialise a cluster with `uncloud machine init` first",
|
cli.Config.Path(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if clusterName == "" {
|
if contextName == "" {
|
||||||
// If the cluster is not specified, use the current cluster if set.
|
// If the cluster is not specified, use the current cluster if set.
|
||||||
if cli.config.CurrentCluster == "" {
|
if cli.Config.CurrentContext == "" {
|
||||||
return nil, errors.New(
|
|
||||||
"the current cluster is not set in the Uncloud config. " +
|
|
||||||
"Please specify a cluster with the --cluster flag or set current_cluster in the config",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if _, ok := cli.config.Clusters[cli.config.CurrentCluster]; !ok {
|
|
||||||
return nil, fmt.Errorf(
|
return nil, fmt.Errorf(
|
||||||
"current cluster %q not found in the config. "+
|
"the current cluster context is not set in the Uncloud config (%s). "+
|
||||||
"Please specify a cluster with the --cluster flag or update current_cluster in the config",
|
"Please specify the context with the '--context' flag or set 'current_context' in the config",
|
||||||
cli.config.CurrentCluster,
|
cli.Config.Path(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
clusterName = cli.config.CurrentCluster
|
if _, ok := cli.Config.Contexts[cli.Config.CurrentContext]; !ok {
|
||||||
|
return nil, fmt.Errorf(
|
||||||
|
"current cluster context '%s' not found in the Uncloud config (%s). "+
|
||||||
|
"Please specify the context with the '--context' flag or update 'current_context' in the config",
|
||||||
|
cli.Config.CurrentContext,
|
||||||
|
cli.Config.Path(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
contextName = cli.Config.CurrentContext
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg, ok := cli.config.Clusters[clusterName]
|
cfg, ok := cli.Config.Contexts[contextName]
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, fmt.Errorf("cluster %q not found in the config", clusterName)
|
return nil, fmt.Errorf("cluster context '%s' not found in the Uncloud config (%s)",
|
||||||
|
contextName, cli.Config.Path())
|
||||||
}
|
}
|
||||||
if len(cfg.Connections) == 0 {
|
if len(cfg.Connections) == 0 {
|
||||||
return nil, fmt.Errorf("no connection configurations found for cluster %q in the config", clusterName)
|
return nil, fmt.Errorf(
|
||||||
|
"no connection configurations found for cluster context '%s' in the Uncloud config (%s)",
|
||||||
|
contextName, cli.Config.Path(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: iterate over all connections and try to connect to the cluster using the first successful connection.
|
// TODO: iterate over all connections and try to connect to the cluster using the first successful connection.
|
||||||
conn := cfg.Connections[0]
|
conn := cfg.Connections[0]
|
||||||
user, host, port, err := conn.SSH.Parse()
|
|
||||||
|
c, err := connectCluster(ctx, conn)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("parse SSH connection %q: %w", conn.SSH, err)
|
return nil, fmt.Errorf("connect to cluster (context '%s'): %w", contextName, err)
|
||||||
}
|
}
|
||||||
sshConfig := &connector.SSHConnectorConfig{
|
|
||||||
User: user,
|
return c, nil
|
||||||
Host: host,
|
|
||||||
Port: port,
|
|
||||||
}
|
|
||||||
return client.New(ctx, connector.NewSSHConnector(sshConfig))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cli *CLI) InitCluster(
|
func connectCluster(ctx context.Context, conn config.MachineConnection) (*client.Client, error) {
|
||||||
ctx context.Context, remoteMachine *RemoteMachine, clusterName, machineName string, netPrefix netip.Prefix,
|
if conn.SSH != "" {
|
||||||
) error {
|
user, host, port, err := conn.SSH.Parse()
|
||||||
if remoteMachine != nil {
|
if err != nil {
|
||||||
return cli.initRemoteMachine(ctx, *remoteMachine, clusterName, machineName, netPrefix)
|
return nil, fmt.Errorf("parse SSH connection %q: %w", conn.SSH, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
keyPath := fs.ExpandHomeDir(conn.SSHKeyFile)
|
||||||
|
|
||||||
|
sshConfig := &connector.SSHConnectorConfig{
|
||||||
|
User: user,
|
||||||
|
Host: host,
|
||||||
|
Port: port,
|
||||||
|
KeyPath: keyPath,
|
||||||
|
}
|
||||||
|
return client.New(ctx, connector.NewSSHConnector(sshConfig))
|
||||||
|
} else if conn.TCP != nil && conn.TCP.IsValid() {
|
||||||
|
return client.New(ctx, connector.NewTCPConnector(*conn.TCP))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, errors.New("connection configuration is invalid")
|
||||||
|
}
|
||||||
|
|
||||||
|
type InitClusterOptions struct {
|
||||||
|
Context string
|
||||||
|
MachineName string
|
||||||
|
Network netip.Prefix
|
||||||
|
PublicIP *netip.Addr
|
||||||
|
RemoteMachine *RemoteMachine
|
||||||
|
Version string
|
||||||
|
}
|
||||||
|
|
||||||
|
// InitCluster initialises a new cluster on a remote machine and returns a client to interact with the cluster.
|
||||||
|
// The client should be closed after use by the caller.
|
||||||
|
func (cli *CLI) InitCluster(ctx context.Context, opts InitClusterOptions) (*client.Client, error) {
|
||||||
|
if opts.RemoteMachine != nil {
|
||||||
|
return cli.initRemoteMachine(ctx, opts)
|
||||||
}
|
}
|
||||||
// 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, opts InitClusterOptions) (*client.Client, error) {
|
||||||
ctx context.Context, remoteMachine RemoteMachine, clusterName, machineName string, netPrefix netip.Prefix,
|
contextName := opts.Context
|
||||||
) error {
|
if contextName == "" {
|
||||||
if clusterName == "" {
|
contextName = defaultContextName
|
||||||
clusterName = defaultClusterName
|
|
||||||
}
|
}
|
||||||
if _, ok := cli.config.Clusters[clusterName]; ok {
|
if _, ok := cli.Config.Contexts[contextName]; ok {
|
||||||
return fmt.Errorf("cluster %q already exists", clusterName)
|
return nil, fmt.Errorf("cluster context '%s' already exists", contextName)
|
||||||
}
|
|
||||||
user, err := client.NewUser(nil)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("generate cluster user: %w", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Provision the remote machine by installing the Uncloud daemon and dependencies over SSH.
|
machineClient, err := cli.provisionRemoteMachine(ctx, *opts.RemoteMachine, opts.Version)
|
||||||
sshClient, err := sshexec.Connect(remoteMachine.User, remoteMachine.Host, remoteMachine.Port, remoteMachine.KeyPath)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf(
|
return nil, err
|
||||||
"SSH login to remote machine %s: %w",
|
|
||||||
config.NewSSHDestination(remoteMachine.User, remoteMachine.Host, remoteMachine.Port), err,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
exec := sshexec.NewRemote(sshClient)
|
// Ensure machineClient is closed on error.
|
||||||
// Install and run the Uncloud daemon and dependencies on the remote machine.
|
defer func() {
|
||||||
if err = provisionMachine(ctx, exec); err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("provision machine: %w", err)
|
machineClient.Close()
|
||||||
}
|
}
|
||||||
|
}()
|
||||||
// Create a machine API client over the SSH connection to the remote machine.
|
|
||||||
machineClient, err := client.New(ctx, connector.NewSSHConnectorFromClient(sshClient))
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("connect to remote machine: %w", err)
|
|
||||||
}
|
|
||||||
defer 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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check machine meets all necessary system requirements before proceeding.
|
||||||
|
checkResp, err := machineClient.CheckPrerequisites(ctx, &emptypb.Empty{})
|
||||||
|
// TODO(lhf): remove Unimplemented check when v0.9.0 is released.
|
||||||
|
if err != nil {
|
||||||
|
if status.Convert(err).Code() != codes.Unimplemented {
|
||||||
|
return nil, fmt.Errorf("check machine prerequisites: %w", err)
|
||||||
|
}
|
||||||
|
} else if !checkResp.Satisfied {
|
||||||
|
return nil, fmt.Errorf("machine prerequisites not satisfied: %s", checkResp.Error)
|
||||||
|
}
|
||||||
|
|
||||||
req := &pb.InitClusterRequest{
|
req := &pb.InitClusterRequest{
|
||||||
MachineName: machineName,
|
MachineName: opts.MachineName,
|
||||||
Network: pb.NewIPPrefix(netPrefix),
|
Network: pb.NewIPPrefix(opts.Network),
|
||||||
User: &pb.User{
|
|
||||||
Network: &pb.NetworkConfig{
|
|
||||||
ManagementIp: pb.NewIP(user.ManagementIP()),
|
|
||||||
PublicKey: user.PublicKey(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
resp, err := machineClient.InitCluster(ctx, req)
|
if opts.PublicIP != nil {
|
||||||
if err != nil {
|
if opts.PublicIP.IsValid() {
|
||||||
return fmt.Errorf("init cluster: %w", err)
|
req.PublicIpConfig = &pb.InitClusterRequest_PublicIp{PublicIp: pb.NewIP(*opts.PublicIP)}
|
||||||
}
|
} else {
|
||||||
fmt.Printf("Cluster %q initialised with machine %q\n", clusterName, resp.Machine.Name)
|
// Invalid or in other words zero IP means to automatically detect the public IP.
|
||||||
|
req.PublicIpConfig = &pb.InitClusterRequest_PublicIpAuto{PublicIpAuto: true}
|
||||||
if err = cli.CreateCluster(clusterName, user.PrivateKey()); err != nil {
|
|
||||||
return fmt.Errorf("save cluster to config: %w", err)
|
|
||||||
}
|
|
||||||
// Set the current cluster to the just created one if it is the only cluster in the config.
|
|
||||||
if len(cli.config.Clusters) == 1 {
|
|
||||||
if err = cli.SetCurrentCluster(clusterName); err != nil {
|
|
||||||
return fmt.Errorf("set current cluster: %w", err)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Save the machine's SSH connection details in the cluster config.
|
|
||||||
|
resp, err := machineClient.InitCluster(ctx, req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("init cluster: %w", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("Cluster initialised with machine '%s' and saved as context '%s' in your local config (%s)\n",
|
||||||
|
resp.Machine.Name, contextName, cli.Config.Path())
|
||||||
|
if err = cli.CreateContext(contextName); err != nil {
|
||||||
|
return nil, fmt.Errorf("save cluster context to config: %w", err)
|
||||||
|
}
|
||||||
|
if err = cli.SetCurrentContext(contextName); err != nil {
|
||||||
|
return nil, fmt.Errorf("set current cluster context: %w", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("Current cluster context is now '%s'.\n", contextName)
|
||||||
|
|
||||||
|
// Save the machine's SSH connection details in the context config.
|
||||||
connCfg := config.MachineConnection{
|
connCfg := config.MachineConnection{
|
||||||
SSH: config.NewSSHDestination(remoteMachine.User, remoteMachine.Host, remoteMachine.Port),
|
SSH: config.NewSSHDestination(opts.RemoteMachine.User, opts.RemoteMachine.Host, opts.RemoteMachine.Port),
|
||||||
|
SSHKeyFile: opts.RemoteMachine.KeyPath,
|
||||||
}
|
}
|
||||||
cli.config.Clusters[clusterName].Connections = append(cli.config.Clusters[clusterName].Connections, connCfg)
|
cli.Config.Contexts[contextName].Connections = append(cli.Config.Contexts[contextName].Connections, connCfg)
|
||||||
if err = cli.config.Save(); err != nil {
|
if err = cli.Config.Save(); err != nil {
|
||||||
return 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 {
|
type AddMachineOptions struct {
|
||||||
c, err := cli.ConnectCluster(ctx, clusterName)
|
Context string
|
||||||
|
MachineName string
|
||||||
|
PublicIP *netip.Addr
|
||||||
|
RemoteMachine RemoteMachine
|
||||||
|
Version string
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddMachine provisions a remote machine and adds it to the cluster. It returns a client to interact with the machine
|
||||||
|
// which should be closed after use by the caller.
|
||||||
|
func (cli *CLI) AddMachine(ctx context.Context, opts AddMachineOptions) (*client.Client, error) {
|
||||||
|
contextName := opts.Context
|
||||||
|
if contextName == "" {
|
||||||
|
contextName = cli.Config.CurrentContext
|
||||||
|
}
|
||||||
|
c, err := cli.ConnectCluster(ctx, contextName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("connect to cluster: %w", err)
|
return nil, fmt.Errorf("connect to cluster (context '%s'): %w", contextName, err)
|
||||||
|
}
|
||||||
|
defer c.Close()
|
||||||
|
|
||||||
|
machineClient, err := cli.provisionRemoteMachine(ctx, opts.RemoteMachine, opts.Version)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
defer func() {
|
defer func() {
|
||||||
_ = c.Close()
|
if err != nil {
|
||||||
|
machineClient.Close()
|
||||||
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// Provision the remote machine by installing the Uncloud daemon and dependencies over SSH.
|
|
||||||
sshClient, err := sshexec.Connect(remoteMachine.User, remoteMachine.Host, remoteMachine.Port, remoteMachine.KeyPath)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf(
|
|
||||||
"SSH login to remote machine %s: %w",
|
|
||||||
config.NewSSHDestination(remoteMachine.User, remoteMachine.Host, remoteMachine.Port), err,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
exec := sshexec.NewRemote(sshClient)
|
|
||||||
// Install and run the Uncloud daemon and dependencies on the remote machine.
|
|
||||||
if err = provisionMachine(ctx, exec); err != nil {
|
|
||||||
return fmt.Errorf("provision machine: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a machine API client over the SSH connection to the remote machine.
|
|
||||||
machineClient, err := client.New(ctx, connector.NewSSHConnectorFromClient(sshClient))
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("connect to remote machine: %w", err)
|
|
||||||
}
|
|
||||||
defer 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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check machine meets all necessary system requirements before proceeding.
|
||||||
|
checkResp, err := machineClient.CheckPrerequisites(ctx, &emptypb.Empty{})
|
||||||
|
// TODO(lhf): remove Unimplemented check when v0.9.0 is released.
|
||||||
|
if err != nil {
|
||||||
|
if status.Convert(err).Code() != codes.Unimplemented {
|
||||||
|
return nil, fmt.Errorf("check machine prerequisites: %w", err)
|
||||||
|
}
|
||||||
|
} else if !checkResp.Satisfied {
|
||||||
|
return nil, fmt.Errorf("machine prerequisites not satisfied: %s", checkResp.Error)
|
||||||
|
}
|
||||||
|
|
||||||
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.
|
||||||
@@ -268,26 +313,35 @@ func (cli *CLI) AddMachine(ctx context.Context, remoteMachine RemoteMachine, clu
|
|||||||
endpoints[i] = pb.NewIPPort(addrPort)
|
endpoints[i] = pb.NewIPPort(addrPort)
|
||||||
}
|
}
|
||||||
addReq := &pb.AddMachineRequest{
|
addReq := &pb.AddMachineRequest{
|
||||||
Name: machineName,
|
Name: opts.MachineName,
|
||||||
Network: &pb.NetworkConfig{
|
Network: &pb.NetworkConfig{
|
||||||
Endpoints: endpoints,
|
Endpoints: endpoints,
|
||||||
PublicKey: token.PublicKey,
|
PublicKey: token.PublicKey,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
if opts.PublicIP != nil {
|
||||||
|
if opts.PublicIP.IsValid() {
|
||||||
|
addReq.PublicIp = pb.NewIP(*opts.PublicIP)
|
||||||
|
} else if token.PublicIP.IsValid() {
|
||||||
|
// Invalid or in other words zero IP means to use an automatically detected public IP from the token.
|
||||||
|
addReq.PublicIp = pb.NewIP(token.PublicIP)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
addResp, err := c.AddMachine(ctx, addReq)
|
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 (context '%s'): %w", contextName, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// List other machines in the cluster to include them in the join request.
|
// List other machines in the cluster to include them in the join request.
|
||||||
listResp, err := c.ListMachines(ctx, &emptypb.Empty{})
|
machines, err := c.ListMachines(ctx, nil)
|
||||||
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(listResp.Machines)-1)
|
otherMachines := make([]*pb.MachineInfo, 0, len(machines)-1)
|
||||||
for _, m := range listResp.Machines {
|
for _, m := range machines {
|
||||||
if m.Id != addResp.Machine.Id {
|
if m.Machine.Id != addResp.Machine.Id {
|
||||||
otherMachines = append(otherMachines, m)
|
otherMachines = append(otherMachines, m.Machine)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -297,24 +351,67 @@ 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)
|
// TODO: fix empty context name when using the current context (contextName == "").
|
||||||
|
fmt.Printf("Machine '%s' added to the cluster (context '%s').\n", addResp.Machine.Name, contextName)
|
||||||
|
|
||||||
// Save the machine's SSH connection details in the cluster config.
|
// Save the machine's SSH connection details in the context config.
|
||||||
connCfg := config.MachineConnection{
|
connCfg := config.MachineConnection{
|
||||||
SSH: config.NewSSHDestination(remoteMachine.User, remoteMachine.Host, remoteMachine.Port),
|
SSH: config.NewSSHDestination(opts.RemoteMachine.User, opts.RemoteMachine.Host, opts.RemoteMachine.Port),
|
||||||
|
SSHKeyFile: opts.RemoteMachine.KeyPath,
|
||||||
}
|
}
|
||||||
if clusterName == "" {
|
if contextName == "" {
|
||||||
clusterName = cli.config.CurrentCluster
|
contextName = cli.Config.CurrentContext
|
||||||
}
|
}
|
||||||
cli.config.Clusters[clusterName].Connections = append(cli.config.Clusters[clusterName].Connections, connCfg)
|
cli.Config.Contexts[contextName].Connections = append(cli.Config.Contexts[contextName].Connections, connCfg)
|
||||||
if err = cli.config.Save(); err != nil {
|
if err = cli.Config.Save(); err != nil {
|
||||||
return 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
|
||||||
|
// a machine API client to interact with the machine. The client should be closed after use by the caller.
|
||||||
|
// The version parameter specifies the version of the Uncloud daemon to install. If empty, the latest version is used.
|
||||||
|
func (cli *CLI) provisionRemoteMachine(
|
||||||
|
ctx context.Context, remoteMachine RemoteMachine, version string,
|
||||||
|
) (*client.Client, error) {
|
||||||
|
// Provision the remote machine by installing the Uncloud daemon and dependencies over SSH.
|
||||||
|
sshClient, err := sshexec.Connect(remoteMachine.User, remoteMachine.Host, remoteMachine.Port, remoteMachine.KeyPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf(
|
||||||
|
"SSH login to remote machine %s: %w",
|
||||||
|
config.NewSSHDestination(remoteMachine.User, remoteMachine.Host, remoteMachine.Port), err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
exec := sshexec.NewRemote(sshClient)
|
||||||
|
// Install and run the Uncloud daemon and dependencies on the remote machine.
|
||||||
|
if err = provisionMachine(ctx, exec, version); err != nil {
|
||||||
|
return nil, fmt.Errorf("provision machine: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var machineClient *client.Client
|
||||||
|
if remoteMachine.User == "root" {
|
||||||
|
// Create a machine API client over the established SSH connection to the remote machine.
|
||||||
|
machineClient, err = client.New(ctx, connector.NewSSHConnectorFromClient(sshClient))
|
||||||
|
} else {
|
||||||
|
// Since the user is not root, we need to establish a new SSH connection to make the user's addition
|
||||||
|
// to the uncloud group effective, thus allowing access to the Uncloud daemon Unix socket.
|
||||||
|
sshConfig := &connector.SSHConnectorConfig{
|
||||||
|
User: remoteMachine.User,
|
||||||
|
Host: remoteMachine.Host,
|
||||||
|
Port: remoteMachine.Port,
|
||||||
|
KeyPath: remoteMachine.KeyPath,
|
||||||
|
}
|
||||||
|
machineClient, err = client.New(ctx, connector.NewSSHConnector(sshConfig))
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("connect to remote machine: %w", err)
|
||||||
|
}
|
||||||
|
return machineClient, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cli *CLI) promptResetMachine() error {
|
func (cli *CLI) promptResetMachine() error {
|
||||||
@@ -329,7 +426,7 @@ func (cli *CLI) promptResetMachine() error {
|
|||||||
Negative("No").
|
Negative("No").
|
||||||
Value(&confirm),
|
Value(&confirm),
|
||||||
),
|
),
|
||||||
)
|
).WithAccessible(true)
|
||||||
if err := form.Run(); err != nil {
|
if err := form.Run(); err != nil {
|
||||||
return fmt.Errorf("prompt user to confirm: %w", err)
|
return fmt.Errorf("prompt user to confirm: %w", err)
|
||||||
}
|
}
|
||||||
@@ -338,44 +435,11 @@ func (cli *CLI) promptResetMachine() error {
|
|||||||
return fmt.Errorf("remote machine is already initialised as a cluster member")
|
return fmt.Errorf("remote machine is already initialised as a cluster member")
|
||||||
}
|
}
|
||||||
// TODO: implement resetting the remote machine.
|
// TODO: implement resetting the remote machine.
|
||||||
return fmt.Errorf("resetting the remote machine is not implemented yet")
|
return fmt.Errorf("resetting the remote machine is not implemented yet. " +
|
||||||
|
"Please manually run 'uncloud-uninstall' on the remote machine to fully uninstall Uncloud from it")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cli *CLI) ListMachines(ctx context.Context, clusterName string) error {
|
// ProgressOut returns an output stream for progress writer.
|
||||||
c, err := cli.ConnectCluster(ctx, clusterName)
|
func (cli *CLI) ProgressOut() *streams.Out {
|
||||||
if err != nil {
|
return streams.NewOut(os.Stdout)
|
||||||
return fmt.Errorf("connect to cluster: %w", err)
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
_ = c.Close()
|
|
||||||
}()
|
|
||||||
|
|
||||||
listResp, err := c.ListMachines(ctx, &emptypb.Empty{})
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("list machines: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Print the list of machines in a table format.
|
|
||||||
tw := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', 0)
|
|
||||||
// Print header.
|
|
||||||
// TODO: print ADDRESS instead of SUBNET which is a machine IP with prefix.
|
|
||||||
if _, err = fmt.Fprintln(tw, "NAME\tSUBNET\tPUBLIC KEY\tENDPOINTS"); err != nil {
|
|
||||||
return fmt.Errorf("write header: %w", err)
|
|
||||||
}
|
|
||||||
// Print rows.
|
|
||||||
for _, m := range listResp.Machines {
|
|
||||||
subnet, _ := m.Network.Subnet.ToPrefix()
|
|
||||||
endpoints := make([]string, len(m.Network.Endpoints))
|
|
||||||
for i, ep := range m.Network.Endpoints {
|
|
||||||
addrPort, _ := ep.ToAddrPort()
|
|
||||||
endpoints[i] = addrPort.String()
|
|
||||||
}
|
|
||||||
publicKey := secret.Secret(m.Network.PublicKey)
|
|
||||||
if _, err = fmt.Fprintf(
|
|
||||||
tw, "%s\t%s\t%s\t%s\n", m.Name, subnet, publicKey, strings.Join(endpoints, ", "),
|
|
||||||
); err != nil {
|
|
||||||
return fmt.Errorf("write row: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return tw.Flush()
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,46 +0,0 @@
|
|||||||
package client
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"google.golang.org/grpc"
|
|
||||||
"uncloud/internal/machine/api/pb"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Client is a client for the machine API.
|
|
||||||
type Client struct {
|
|
||||||
connector Connector
|
|
||||||
conn *grpc.ClientConn
|
|
||||||
|
|
||||||
pb.MachineClient
|
|
||||||
pb.ClusterClient
|
|
||||||
}
|
|
||||||
|
|
||||||
// Connector is an interface for establishing a connection to the machine API.
|
|
||||||
type Connector interface {
|
|
||||||
Connect(ctx context.Context) (*grpc.ClientConn, error)
|
|
||||||
Close() error
|
|
||||||
}
|
|
||||||
|
|
||||||
// New creates a new client for the machine API. The connector is used to establish the connection
|
|
||||||
// either locally or remotely. The client is responsible for closing the connector.
|
|
||||||
func New(ctx context.Context, connector Connector) (*Client, error) {
|
|
||||||
c := &Client{
|
|
||||||
connector: connector,
|
|
||||||
}
|
|
||||||
var err error
|
|
||||||
c.conn, err = connector.Connect(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("connect to machine: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
c.MachineClient = pb.NewMachineClient(c.conn)
|
|
||||||
c.ClusterClient = pb.NewClusterClient(c.conn)
|
|
||||||
return c, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) Close() error {
|
|
||||||
err := c.conn.Close()
|
|
||||||
return errors.Join(err, c.connector.Close())
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
package config
|
|
||||||
|
|
||||||
import "uncloud/internal/secret"
|
|
||||||
|
|
||||||
type Cluster struct {
|
|
||||||
Name string `toml:"-"`
|
|
||||||
Connections []MachineConnection `toml:"connections"`
|
|
||||||
// UserPrivateKey is the user's WireGuard private key used to connect to cluster machines.
|
|
||||||
UserPrivateKey secret.Secret `toml:"user_private_key"`
|
|
||||||
}
|
|
||||||
@@ -2,14 +2,15 @@ package config
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/BurntSushi/toml"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/goccy/go-yaml"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Clusters map[string]*Cluster `toml:"clusters"`
|
CurrentContext string `yaml:"current_context"`
|
||||||
CurrentCluster string `toml:"current_cluster"`
|
Contexts map[string]*Context `yaml:"contexts"`
|
||||||
|
|
||||||
// path is the file path config is read from.
|
// path is the file path config is read from.
|
||||||
path string
|
path string
|
||||||
@@ -18,10 +19,10 @@ type Config struct {
|
|||||||
func NewFromFile(path string) (*Config, error) {
|
func NewFromFile(path string) (*Config, error) {
|
||||||
_, err := os.Stat(path)
|
_, err := os.Stat(path)
|
||||||
if err != nil && !os.IsNotExist(err) {
|
if err != nil && !os.IsNotExist(err) {
|
||||||
return nil, fmt.Errorf("check file permissions %q: %w", path, err)
|
return nil, fmt.Errorf("check file permissions '%s': %w", path, err)
|
||||||
}
|
}
|
||||||
c := &Config{
|
c := &Config{
|
||||||
Clusters: map[string]*Cluster{},
|
Contexts: map[string]*Context{},
|
||||||
path: path,
|
path: path,
|
||||||
}
|
}
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
@@ -34,30 +35,37 @@ func NewFromFile(path string) (*Config, error) {
|
|||||||
return c, nil
|
return c, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Config) Path() string {
|
||||||
|
return c.path
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Config) Read() error {
|
func (c *Config) Read() error {
|
||||||
_, err := toml.DecodeFile(c.path, c)
|
data, err := os.ReadFile(c.path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("read config file %q: %w", c.path, err)
|
return fmt.Errorf("read config file '%s': %w", c.path, err)
|
||||||
}
|
}
|
||||||
|
if err = yaml.Unmarshal(data, c); err != nil {
|
||||||
|
return fmt.Errorf("parse config file '%s': %s", c.path, yaml.FormatError(err, true, true))
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Config) Save() error {
|
func (c *Config) Save() error {
|
||||||
dir, _ := filepath.Split(c.path)
|
dir, _ := filepath.Split(c.path)
|
||||||
if err := os.MkdirAll(dir, 0700); err != nil {
|
if err := os.MkdirAll(dir, 0700); err != nil {
|
||||||
return fmt.Errorf("create config directory %q: %w", dir, err)
|
return fmt.Errorf("create config directory '%s': %w", dir, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
f, err := os.OpenFile(c.path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
|
f, err := os.OpenFile(c.path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("write config file %q: %w", c.path, err)
|
return fmt.Errorf("write config file '%s': %w", c.path, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
encoder := toml.NewEncoder(f)
|
encoder := yaml.NewEncoder(f, yaml.Indent(2), yaml.IndentSequence(true))
|
||||||
encoder.Indent = ""
|
|
||||||
if err = encoder.Encode(c); err != nil {
|
if err = encoder.Encode(c); err != nil {
|
||||||
_ = f.Close()
|
_ = f.Close()
|
||||||
return fmt.Errorf("encode config file %q: %w", c.path, err)
|
return fmt.Errorf("encode config file '%s': %w", c.path, err)
|
||||||
}
|
}
|
||||||
return f.Close()
|
return f.Close()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,11 @@ package config
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"net"
|
"net"
|
||||||
|
"net/netip"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"uncloud/internal/secret"
|
|
||||||
|
"github.com/psviderski/uncloud/internal/secret"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -13,9 +15,13 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type MachineConnection struct {
|
type MachineConnection struct {
|
||||||
SSH SSHDestination `toml:"ssh,omitempty"`
|
SSH SSHDestination `yaml:"ssh,omitempty"`
|
||||||
Host string `toml:"host,omitempty"`
|
SSHKeyFile string `yaml:"ssh_key_file,omitempty"`
|
||||||
PublicKey secret.Secret `toml:"public_key,omitempty"`
|
// TCP is the address and port of the machine's API server.
|
||||||
|
// The pointer is used to omit the field when not set. Otherwise, yaml marshalling includes an empty object.
|
||||||
|
TCP *netip.AddrPort `yaml:"tcp,omitempty"`
|
||||||
|
Host string `yaml:"host,omitempty"`
|
||||||
|
PublicKey secret.Secret `yaml:"public_key,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// SSHDestination represents an SSH destination string in the canonical form of "user@host:port".
|
// SSHDestination represents an SSH destination string in the canonical form of "user@host:port".
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
type Context struct {
|
||||||
|
Name string `yaml:"-"`
|
||||||
|
Connections []MachineConnection `yaml:"connections"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ExpandCommaSeparatedValues takes a slice of strings and expands any comma-separated values into individual elements.
|
||||||
|
func ExpandCommaSeparatedValues(values []string) []string {
|
||||||
|
if len(values) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var expanded []string
|
||||||
|
for _, value := range values {
|
||||||
|
for _, v := range strings.Split(value, ",") {
|
||||||
|
if v = strings.TrimSpace(v); v != "" {
|
||||||
|
expanded = append(expanded, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return expanded
|
||||||
|
}
|
||||||
+28
-12
@@ -4,7 +4,9 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"uncloud/internal/sshexec"
|
"strings"
|
||||||
|
|
||||||
|
"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 +19,38 @@ type RemoteMachine struct {
|
|||||||
KeyPath string
|
KeyPath string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func installCmd(user string, version string) string {
|
||||||
|
sudoPrefix := ""
|
||||||
|
var env []string
|
||||||
|
|
||||||
|
// Add the SSH user (non-root) to the uncloud group to allow access to the Uncloud daemon unix socket.
|
||||||
|
if user != "root" {
|
||||||
|
sudoPrefix = "sudo"
|
||||||
|
env = append(env, "UNCLOUD_GROUP_ADD_USER="+sshexec.Quote(user))
|
||||||
|
}
|
||||||
|
if version != "" {
|
||||||
|
env = append(env, "UNCLOUD_VERSION="+sshexec.Quote(version))
|
||||||
|
}
|
||||||
|
|
||||||
|
envCmd := strings.Join(env, " ")
|
||||||
|
curlBashCmd := fmt.Sprintf("curl -fsSL %s | %s %s bash", sshexec.Quote(installScriptURL), sudoPrefix, envCmd)
|
||||||
|
|
||||||
|
return curlBashCmd
|
||||||
|
}
|
||||||
|
|
||||||
// provisionMachine provisions the remote machine by downloading the Uncloud install script from GitHub and running it.
|
// provisionMachine provisions the remote machine by downloading the Uncloud install script from GitHub and running it.
|
||||||
func provisionMachine(ctx context.Context, exec sshexec.Executor) error {
|
// If version is specified, it will be passed to the install script as UNCLOUD_VERSION environment variable.
|
||||||
|
func provisionMachine(ctx context.Context, exec sshexec.Executor, version string) error {
|
||||||
user, err := exec.Run(ctx, "whoami")
|
user, err := exec.Run(ctx, "whoami")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("run whoami: %w", err)
|
return fmt.Errorf("run whoami: %w", err)
|
||||||
}
|
}
|
||||||
sudoPrefix, env := "", ""
|
|
||||||
if user != "root" {
|
cmd := installCmd(user, version)
|
||||||
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; "+cmd)
|
||||||
)
|
|
||||||
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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
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")
|
||||||
|
assert.NotContains(t, cmd, "UNCLOUD_GROUP_ADD_USER")
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test with version
|
||||||
|
t.Run("root with version", func(t *testing.T) {
|
||||||
|
cmd := installCmd("root", "v1.2.3")
|
||||||
|
assert.NotContains(t, cmd, "sudo")
|
||||||
|
assert.NotContains(t, cmd, "UNCLOUD_GROUP_ADD_USER")
|
||||||
|
assert.Contains(t, cmd, "UNCLOUD_VERSION=v1.2.3")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("nonroot", func(t *testing.T) {
|
||||||
|
cmd := installCmd("nonroot", "")
|
||||||
|
assert.Contains(t, cmd, "sudo")
|
||||||
|
assert.Contains(t, cmd, "UNCLOUD_GROUP_ADD_USER=nonroot")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("nonroot with version", func(t *testing.T) {
|
||||||
|
cmd := installCmd("nonroot", "v1.2.3")
|
||||||
|
assert.Contains(t, cmd, "sudo")
|
||||||
|
assert.Contains(t, cmd, "UNCLOUD_GROUP_ADD_USER=nonroot")
|
||||||
|
assert.Contains(t, cmd, "UNCLOUD_VERSION=v1.2.3")
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import "github.com/charmbracelet/huh"
|
||||||
|
|
||||||
|
func Confirm() (bool, error) {
|
||||||
|
var confirmed bool
|
||||||
|
form := huh.NewForm(
|
||||||
|
huh.NewGroup(
|
||||||
|
huh.NewConfirm().
|
||||||
|
Title(
|
||||||
|
"Do you want to continue?",
|
||||||
|
).
|
||||||
|
Affirmative("Yes!").
|
||||||
|
Negative("No").
|
||||||
|
Value(&confirmed),
|
||||||
|
),
|
||||||
|
).WithAccessible(true)
|
||||||
|
if err := form.Run(); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return confirmed, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,258 @@
|
|||||||
|
package corrosion
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"net/netip"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AdminClient is a client for the Corrosion admin API.
|
||||||
|
type AdminClient struct {
|
||||||
|
sockPath string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAdminClient(sockPath string) (*AdminClient, error) {
|
||||||
|
return &AdminClient{sockPath: sockPath}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type Response struct {
|
||||||
|
JSON map[string]any
|
||||||
|
// Err is set if the response is an error or if an error occurred while processing the response.
|
||||||
|
Err error
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendCommand sends a command to the Corrosion admin API and returns a channel that will receive responses.
|
||||||
|
// The channel will be closed after sending the last or error response. The caller must read from the channel until
|
||||||
|
// it is closed.
|
||||||
|
func (c *AdminClient) SendCommand(cmd []byte) (<-chan Response, error) {
|
||||||
|
conn, err := net.Dial("unix", c.sockPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("connect to admin socket: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err = conn.Write(encodeFrame(cmd)); err != nil {
|
||||||
|
conn.Close()
|
||||||
|
return nil, fmt.Errorf("send command: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ch := make(chan Response)
|
||||||
|
go func() {
|
||||||
|
defer close(ch)
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
for {
|
||||||
|
r := Response{}
|
||||||
|
|
||||||
|
data, err := readFrame(conn)
|
||||||
|
if err != nil {
|
||||||
|
r.Err = err
|
||||||
|
ch <- r
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var decoded any
|
||||||
|
if err = json.Unmarshal(data, &decoded); err != nil {
|
||||||
|
r.Err = fmt.Errorf("unmarshal response: %w", err)
|
||||||
|
ch <- r
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch v := decoded.(type) {
|
||||||
|
case string:
|
||||||
|
if v == "Success" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Ignore other strings.
|
||||||
|
case map[string]any:
|
||||||
|
if errData, ok := v["Error"].(map[string]any); ok {
|
||||||
|
if errMsg, ok := errData["msg"].(string); ok {
|
||||||
|
r.Err = errors.New(errMsg)
|
||||||
|
} else {
|
||||||
|
r.Err = fmt.Errorf("invalid error response: %v", errData)
|
||||||
|
}
|
||||||
|
ch <- r
|
||||||
|
return
|
||||||
|
} else if jsonData, ok := v["Json"].(map[string]any); ok {
|
||||||
|
r.JSON = jsonData
|
||||||
|
ch <- r
|
||||||
|
}
|
||||||
|
// Ignore other maps.
|
||||||
|
default:
|
||||||
|
// Ignore other types.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return ch, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// encodeFrame encodes a length_delimited Tokio frame by prefacing frame data with a frame head that specifies
|
||||||
|
// the length of the frame.
|
||||||
|
func encodeFrame(data []byte) []byte {
|
||||||
|
encoded := make([]byte, 4+len(data))
|
||||||
|
binary.BigEndian.PutUint32(encoded, uint32(len(data)))
|
||||||
|
copy(encoded[4:], data)
|
||||||
|
return encoded
|
||||||
|
}
|
||||||
|
|
||||||
|
// readFrame reads a length_delimited Tokio frame from the connection by extracting the frame data that follows
|
||||||
|
// the frame head.
|
||||||
|
func readFrame(conn net.Conn) ([]byte, error) {
|
||||||
|
// Read the frame head (4 bytes).
|
||||||
|
head := make([]byte, 4)
|
||||||
|
if _, err := io.ReadFull(conn, head); err != nil {
|
||||||
|
return nil, fmt.Errorf("read frame head: %w", err)
|
||||||
|
}
|
||||||
|
// Read the frame data (length specified in the frame head).
|
||||||
|
length := binary.BigEndian.Uint32(head)
|
||||||
|
data := make([]byte, length)
|
||||||
|
if _, err := io.ReadFull(conn, data); err != nil {
|
||||||
|
return nil, fmt.Errorf("read frame data: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type ClusterMembershipState struct {
|
||||||
|
ID string
|
||||||
|
Addr netip.AddrPort
|
||||||
|
State string
|
||||||
|
Timestamp time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
// MembershipStateAlive indicates that the member is active.
|
||||||
|
MembershipStateAlive string = "Alive"
|
||||||
|
// MembershipStateSuspect indicates that the member is active, but at least one cluster member suspects its down.
|
||||||
|
// For all purposes, a Suspect member is treated as if it were Alive until either it refutes the suspicion
|
||||||
|
// (becoming Alive) or fails to do so (being declared Down).
|
||||||
|
MembershipStateSuspect string = "Suspect"
|
||||||
|
// MembershipStateDown indicates that the member is confirmed Down. A member that reaches this state can't join
|
||||||
|
// the cluster with the same identity until the cluster forgets this knowledge.
|
||||||
|
MembershipStateDown string = "Down"
|
||||||
|
)
|
||||||
|
|
||||||
|
func parseClusterMembershipState(json map[string]any) (ClusterMembershipState, error) {
|
||||||
|
// Example JSON:
|
||||||
|
// {
|
||||||
|
// "id": {
|
||||||
|
// "addr": "[fdcc:1d51:6bae:6bb2:53c0:8796:1be0:b783]:51001",
|
||||||
|
// "cluster_id": 0,
|
||||||
|
// "id": "10d69d6f-6578-4dcf-a285-e860e40c5f06",
|
||||||
|
// "ts": 7435936225798880256
|
||||||
|
// },
|
||||||
|
// "incarnation": 0,
|
||||||
|
// "state": "Down"
|
||||||
|
// }
|
||||||
|
|
||||||
|
var state ClusterMembershipState
|
||||||
|
var err error
|
||||||
|
|
||||||
|
idObj, ok := json["id"].(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
return state, fmt.Errorf("missing or invalid 'id' field")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ID
|
||||||
|
if id, ok := idObj["id"].(string); ok {
|
||||||
|
state.ID = id
|
||||||
|
} else {
|
||||||
|
return state, fmt.Errorf("missing or invalid 'id' field")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Addr
|
||||||
|
if addr, ok := idObj["addr"].(string); ok {
|
||||||
|
state.Addr, err = netip.ParseAddrPort(addr)
|
||||||
|
if err != nil {
|
||||||
|
return state, fmt.Errorf("parse 'addr' field: %w", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return state, fmt.Errorf("missing or invalid 'addr' field")
|
||||||
|
}
|
||||||
|
|
||||||
|
// State
|
||||||
|
if stateStr, ok := json["state"].(string); ok {
|
||||||
|
switch stateStr {
|
||||||
|
case MembershipStateAlive, MembershipStateSuspect, MembershipStateDown:
|
||||||
|
state.State = stateStr
|
||||||
|
default:
|
||||||
|
return state, fmt.Errorf("invalid 'state' field: %s", stateStr)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return state, fmt.Errorf("missing or invalid 'state' field")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Timestamp
|
||||||
|
if ts, ok := idObj["ts"].(float64); ok {
|
||||||
|
state.Timestamp = ntp64ToTime(uint64(ts))
|
||||||
|
} else {
|
||||||
|
return state, fmt.Errorf("missing or invalid 'ts' field")
|
||||||
|
}
|
||||||
|
|
||||||
|
return state, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// nt64ToTime converts a 64-bit NTP timestamp relative to the Unix epoch (1st Jan 1970) to time.Time.
|
||||||
|
// See for more details: https://datatracker.ietf.org/doc/html/rfc5905#section-6
|
||||||
|
func ntp64ToTime(ntp uint64) time.Time {
|
||||||
|
// The NTP timestamp returned from Corrosion is relative to the Unix epoch (1st Jan 1970)
|
||||||
|
// so no need to subtract the 70 years offset.
|
||||||
|
secs := uint32(ntp >> 32)
|
||||||
|
frac := uint32(ntp)
|
||||||
|
// Convert the fraction to nanoseconds: frac * 1e9 / 2^32
|
||||||
|
nsecs := (uint64(frac) * 1000_000_000) >> 32
|
||||||
|
|
||||||
|
return time.Unix(int64(secs), int64(nsecs))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClusterMembershipStates returns the current membership SWIM states of all cluster members.
|
||||||
|
// If latest is true, only the latest state of each member is returned.
|
||||||
|
func (c *AdminClient) ClusterMembershipStates(latest bool) ([]ClusterMembershipState, error) {
|
||||||
|
respCh, err := c.SendCommand([]byte("{\"Cluster\":\"MembershipStates\"}"))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
states []ClusterMembershipState
|
||||||
|
latestStates map[string]ClusterMembershipState
|
||||||
|
parseErr error
|
||||||
|
)
|
||||||
|
if latest {
|
||||||
|
latestStates = make(map[string]ClusterMembershipState)
|
||||||
|
}
|
||||||
|
|
||||||
|
for r := range respCh {
|
||||||
|
if r.Err != nil {
|
||||||
|
// It's safe to return here because the channel is closed after the first error response.
|
||||||
|
return nil, r.Err
|
||||||
|
}
|
||||||
|
|
||||||
|
s, err := parseClusterMembershipState(r.JSON)
|
||||||
|
if err != nil {
|
||||||
|
// Do not return early to drain the channel.
|
||||||
|
parseErr = errors.Join(parseErr, err)
|
||||||
|
} else {
|
||||||
|
if latest {
|
||||||
|
if existing, ok := latestStates[s.ID]; !ok || existing.Timestamp.Before(s.Timestamp) {
|
||||||
|
latestStates[s.ID] = s
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
states = append(states, s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if latest {
|
||||||
|
states = make([]ClusterMembershipState, 0, len(latestStates))
|
||||||
|
for _, s := range latestStates {
|
||||||
|
states = append(states, s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return states, parseErr
|
||||||
|
}
|
||||||
@@ -0,0 +1,233 @@
|
|||||||
|
package corrosion
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"github.com/cenkalti/backoff/v4"
|
||||||
|
"golang.org/x/net/http2"
|
||||||
|
"log/slog"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/netip"
|
||||||
|
"net/url"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// http2ConnectTimeout is the maximum amount of time an HTTP2 client will wait for a connection to be established.
|
||||||
|
http2ConnectTimeout = 3 * time.Second
|
||||||
|
// http2MaxRetryTime is the maximum amount of time an HTTP2 client will retry a request.
|
||||||
|
http2MaxRetryTime = 10 * time.Second
|
||||||
|
// resubscribeMaxRetryTime is the maximum amount of time an API client will retry resubscribing to a query after
|
||||||
|
// an error occurs.
|
||||||
|
resubscribeMaxRetryTime = 60 * time.Second
|
||||||
|
)
|
||||||
|
|
||||||
|
// APIClient is a client for the Corrosion API.
|
||||||
|
type APIClient struct {
|
||||||
|
baseURL *url.URL
|
||||||
|
client *http.Client
|
||||||
|
newResubBackoff func() backoff.BackOff
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewAPIClient creates a new Corrosion API client. The client retries on network errors using an exponential backoff
|
||||||
|
// policy with a maximum interval of 1 second and a maximum elapsed time of 10 seconds.
|
||||||
|
// It automatically resubscribes to active subscriptions if an error occurs using an exponential backoff policy with a
|
||||||
|
// maximum interval of 1 second and a maximum elapsed time of 60 seconds.
|
||||||
|
// Use the WithHTTP2Client option to provide a custom HTTP client and the WithResubscribeBackoff option to change the
|
||||||
|
// backoff policy for resubscribing to a query.
|
||||||
|
func NewAPIClient(addr netip.AddrPort, opts ...APIClientOption) (*APIClient, error) {
|
||||||
|
baseURL, err := url.Parse(fmt.Sprintf("http://%s", addr))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid URL: %w", err)
|
||||||
|
}
|
||||||
|
c := &APIClient{
|
||||||
|
baseURL: baseURL,
|
||||||
|
client: &http.Client{
|
||||||
|
Transport: &RetryRoundTripper{
|
||||||
|
Base: &http2.Transport{
|
||||||
|
AllowHTTP: true,
|
||||||
|
DialTLSContext: func(ctx context.Context, network, addr string, _ *tls.Config) (net.Conn, error) {
|
||||||
|
dialer := &net.Dialer{
|
||||||
|
Timeout: http2ConnectTimeout,
|
||||||
|
}
|
||||||
|
return dialer.DialContext(ctx, network, addr)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
NewBackoff: func() backoff.BackOff {
|
||||||
|
return backoff.NewExponentialBackOff(
|
||||||
|
backoff.WithInitialInterval(100*time.Millisecond),
|
||||||
|
backoff.WithMaxInterval(1*time.Second),
|
||||||
|
backoff.WithMaxElapsedTime(http2MaxRetryTime),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
newResubBackoff: func() backoff.BackOff {
|
||||||
|
return backoff.NewExponentialBackOff(
|
||||||
|
backoff.WithInitialInterval(100*time.Millisecond),
|
||||||
|
backoff.WithMaxInterval(1*time.Second),
|
||||||
|
backoff.WithMaxElapsedTime(resubscribeMaxRetryTime),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, opt := range opts {
|
||||||
|
opt(c)
|
||||||
|
}
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type APIClientOption func(*APIClient)
|
||||||
|
|
||||||
|
func WithHTTP2Client(client *http.Client) APIClientOption {
|
||||||
|
return func(c *APIClient) {
|
||||||
|
c.client = client
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithResubscribeBackoff sets the backoff policy for resubscribing to a query if an error occurs.
|
||||||
|
// Pass nil to disable resubscribing.
|
||||||
|
func WithResubscribeBackoff(newBackoff func() backoff.BackOff) APIClientOption {
|
||||||
|
return func(c *APIClient) {
|
||||||
|
c.newResubBackoff = newBackoff
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type RetryRoundTripper struct {
|
||||||
|
Base http.RoundTripper
|
||||||
|
// NewBackoff creates a new backoff policy for each request.
|
||||||
|
NewBackoff func() backoff.BackOff
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rt *RetryRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||||
|
roundTrip := func() (*http.Response, error) {
|
||||||
|
resp, err := rt.Base.RoundTrip(req)
|
||||||
|
if err != nil {
|
||||||
|
var opErr *net.OpError
|
||||||
|
if errors.As(err, &opErr) {
|
||||||
|
// Not certain, but I expect operational errors should generally be retryable.
|
||||||
|
slog.Debug("Retrying corrosion API request due to network error.", "error", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
// Don't retry on other errors.
|
||||||
|
return nil, backoff.Permanent(err)
|
||||||
|
}
|
||||||
|
// Success, don't retry.
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
boff := backoff.WithContext(rt.NewBackoff(), req.Context())
|
||||||
|
return backoff.RetryWithData(roundTrip, boff)
|
||||||
|
}
|
||||||
|
|
||||||
|
type RetrySubscription struct {
|
||||||
|
ctx context.Context
|
||||||
|
cancel context.CancelFunc
|
||||||
|
|
||||||
|
client *APIClient
|
||||||
|
sub *Subscription
|
||||||
|
changes chan *ChangeEvent
|
||||||
|
lastChangeID uint64
|
||||||
|
err error
|
||||||
|
backoff backoff.BackOff
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRetrySubscription(ctx context.Context, client *APIClient, sub *Subscription, boff backoff.BackOff) *RetrySubscription {
|
||||||
|
ctx, cancel := context.WithCancel(ctx)
|
||||||
|
if boff == nil {
|
||||||
|
boff = backoff.NewExponentialBackOff(
|
||||||
|
backoff.WithInitialInterval(100*time.Millisecond),
|
||||||
|
backoff.WithMaxInterval(1*time.Second),
|
||||||
|
backoff.WithMaxElapsedTime(60*time.Second),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return &RetrySubscription{
|
||||||
|
ctx: ctx,
|
||||||
|
cancel: cancel,
|
||||||
|
client: client,
|
||||||
|
sub: sub,
|
||||||
|
backoff: backoff.WithContext(boff, ctx),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rs *RetrySubscription) ID() string {
|
||||||
|
return rs.sub.ID()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rs *RetrySubscription) Changes() (<-chan *ChangeEvent, error) {
|
||||||
|
if rs.changes != nil {
|
||||||
|
return rs.changes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure the rows are consumed if they have been requested.
|
||||||
|
changes, err := rs.sub.Changes()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
rs.lastChangeID = rs.sub.lastChangeID
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
defer close(rs.changes)
|
||||||
|
|
||||||
|
var change *ChangeEvent
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case change = <-changes:
|
||||||
|
case <-rs.ctx.Done():
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if change != nil {
|
||||||
|
select {
|
||||||
|
case rs.changes <- change:
|
||||||
|
case <-rs.ctx.Done():
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rs.lastChangeID = change.ChangeID
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// The underlying subscription has been closed due to an error or context cancellation.
|
||||||
|
// Return if the context is done or try to resubscribe otherwise.
|
||||||
|
if rs.ctx.Err() != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = rs.resubscribe(); err != nil {
|
||||||
|
// resubscribe returns a permanent error after unsuccessful retries.
|
||||||
|
rs.err = fmt.Errorf("resubscribe to query: %w", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
changes, err = rs.sub.Changes()
|
||||||
|
if err != nil {
|
||||||
|
// Unexpected error but report it anyway.
|
||||||
|
rs.err = err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return rs.changes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rs *RetrySubscription) resubscribe() error {
|
||||||
|
return backoff.Retry(func() error {
|
||||||
|
slog.Debug("Resubscribing to Corrosion query.", "id", rs.ID(), "from_change", rs.lastChangeID)
|
||||||
|
sub, err := rs.client.ResubscribeContext(rs.ctx, rs.ID(), rs.lastChangeID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
rs.sub = sub
|
||||||
|
return nil
|
||||||
|
}, rs.backoff)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rs *RetrySubscription) Err() error {
|
||||||
|
return rs.err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rs *RetrySubscription) Close() {
|
||||||
|
rs.cancel()
|
||||||
|
}
|
||||||
@@ -0,0 +1,327 @@
|
|||||||
|
package corrosion
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Statement struct {
|
||||||
|
Query string `json:"query"`
|
||||||
|
Params []any `json:"params"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExecResponse struct {
|
||||||
|
Results []ExecResult `json:"results"`
|
||||||
|
Time float64 `json:"time"`
|
||||||
|
Version *uint `json:"version"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExecResult struct {
|
||||||
|
RowsAffected uint `json:"rows_affected"`
|
||||||
|
Time float64 `json:"time"`
|
||||||
|
Error *string `json:"error"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExecContext writes changes to the Corrosion database for propagation through the cluster. The args are for any
|
||||||
|
// placeholder parameters in the query. Corrosion does not sync schema changes made using this method. Use Corrosion's
|
||||||
|
// schema_files to create and update the cluster's database schema.
|
||||||
|
func (c *APIClient) ExecContext(ctx context.Context, query string, args ...any) (*ExecResult, error) {
|
||||||
|
statements := []Statement{
|
||||||
|
{
|
||||||
|
Query: query,
|
||||||
|
Params: args,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
resp, err := c.ExecMultiContext(ctx, statements...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(resp.Results) == 0 {
|
||||||
|
return nil, fmt.Errorf("no results: %+v", resp)
|
||||||
|
}
|
||||||
|
return &resp.Results[0], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExecMultiContext writes changes to the Corrosion database for propagation through the cluster.
|
||||||
|
// Unlike ExecContext, this method allows multiple statements to be executed in a single transaction.
|
||||||
|
func (c *APIClient) ExecMultiContext(ctx context.Context, statements ...Statement) (*ExecResponse, error) {
|
||||||
|
body, err := json.Marshal(statements)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("marshal queries: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
transactionsURL := c.baseURL.JoinPath("/v1/transactions").String()
|
||||||
|
req, err := http.NewRequestWithContext(ctx, "POST", transactionsURL, bytes.NewReader(body))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("create request: %w", err)
|
||||||
|
}
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
req.Header.Set("Accept", "application/json")
|
||||||
|
|
||||||
|
resp, err := c.client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("send request: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
var execResp ExecResponse
|
||||||
|
if resp.StatusCode == http.StatusOK {
|
||||||
|
if err = json.NewDecoder(resp.Body).Decode(&execResp); err != nil {
|
||||||
|
return nil, fmt.Errorf("decode response: %w", err)
|
||||||
|
}
|
||||||
|
// The response may still contain DB errors even if the status code is OK. Return them along with the response.
|
||||||
|
var errs []error
|
||||||
|
for _, result := range execResp.Results {
|
||||||
|
if result.Error != nil {
|
||||||
|
errs = append(errs, errors.New(*result.Error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &execResp, errors.Join(errs...)
|
||||||
|
} else if resp.StatusCode == http.StatusInternalServerError {
|
||||||
|
// If the response is an Internal Server Error, the response body may contain the error encoded as ExecResponse.
|
||||||
|
respBody, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("read response body: %w", err)
|
||||||
|
}
|
||||||
|
if err = json.Unmarshal(respBody, &execResp); err != nil {
|
||||||
|
return nil, fmt.Errorf("internal server error: %s", respBody)
|
||||||
|
}
|
||||||
|
if len(execResp.Results) > 0 && execResp.Results[0].Error != nil {
|
||||||
|
return nil, errors.New(*execResp.Results[0].Error)
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("internal server error: %s", respBody)
|
||||||
|
}
|
||||||
|
|
||||||
|
respBody, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("read response body: %w", err)
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("unexpected status code %d: %s", resp.StatusCode, respBody)
|
||||||
|
}
|
||||||
|
|
||||||
|
type QueryEvent struct {
|
||||||
|
Columns []string `json:"columns"`
|
||||||
|
Row *RowEvent `json:"row"`
|
||||||
|
EOQ *EndOfQuery `json:"eoq"`
|
||||||
|
Change *ChangeEvent `json:"change"`
|
||||||
|
// Error is a server-side error that occurred during query execution. It's considered fatal for the client
|
||||||
|
// as it cannot be recovered from server-side.
|
||||||
|
Error *string `json:"error"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type EndOfQuery struct {
|
||||||
|
Time float64 `json:"time"`
|
||||||
|
ChangeID *uint64 `json:"change_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RowEvent struct {
|
||||||
|
RowID uint64
|
||||||
|
Values []json.RawMessage
|
||||||
|
}
|
||||||
|
|
||||||
|
func (re *RowEvent) UnmarshalJSON(data []byte) error {
|
||||||
|
var raw []json.RawMessage
|
||||||
|
if err := json.Unmarshal(data, &raw); err != nil {
|
||||||
|
return fmt.Errorf("invalid row event: %w", err)
|
||||||
|
}
|
||||||
|
if len(raw) != 2 {
|
||||||
|
return fmt.Errorf("invalid row event: expected an array of 2 elements")
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(raw[0], &re.RowID); err != nil {
|
||||||
|
return fmt.Errorf("invalid row event: %w", err)
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(raw[1], &re.Values); err != nil {
|
||||||
|
return fmt.Errorf("invalid row event: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (re *RowEvent) MarshalJSON() ([]byte, error) {
|
||||||
|
return json.Marshal([]any{re.RowID, re.Values})
|
||||||
|
}
|
||||||
|
|
||||||
|
// QueryContext executes a query that returns rows, typically a SELECT.
|
||||||
|
// The args are for any placeholder parameters in the query.
|
||||||
|
func (c *APIClient) QueryContext(ctx context.Context, query string, args ...any) (*Rows, error) {
|
||||||
|
statement := Statement{
|
||||||
|
Query: query,
|
||||||
|
Params: args,
|
||||||
|
}
|
||||||
|
body, err := json.Marshal(statement)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("marshal query: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
queriesURL := c.baseURL.JoinPath("/v1/queries").String()
|
||||||
|
req, err := http.NewRequestWithContext(ctx, "POST", queriesURL, bytes.NewReader(body))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("create request: %w", err)
|
||||||
|
}
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
req.Header.Set("Accept", "application/json")
|
||||||
|
|
||||||
|
resp, err := c.client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("send request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
respBody, err := io.ReadAll(resp.Body)
|
||||||
|
resp.Body.Close()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("read response body: %w", err)
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("unexpected status code %d: %s", resp.StatusCode, respBody)
|
||||||
|
}
|
||||||
|
|
||||||
|
rows, err := newRows(ctx, resp.Body, true)
|
||||||
|
if err != nil {
|
||||||
|
resp.Body.Close()
|
||||||
|
return nil, fmt.Errorf("parse query response: %w", err)
|
||||||
|
}
|
||||||
|
return rows, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rows is the result of a query. Its cursor starts before the first row of the result set.
|
||||||
|
// Use [Rows.Next] to advance from row to row.
|
||||||
|
type Rows struct {
|
||||||
|
ctx context.Context
|
||||||
|
body io.ReadCloser
|
||||||
|
decoder *json.Decoder
|
||||||
|
eoq *EndOfQuery
|
||||||
|
closeOnEOQ bool
|
||||||
|
|
||||||
|
columns []string
|
||||||
|
row RowEvent
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func newRows(ctx context.Context, body io.ReadCloser, closeOnEOQ bool) (*Rows, error) {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return nil, ctx.Err()
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
decoder := json.NewDecoder(body)
|
||||||
|
var e QueryEvent
|
||||||
|
if err := decoder.Decode(&e); err != nil {
|
||||||
|
return nil, fmt.Errorf("decode query event: %w", err)
|
||||||
|
}
|
||||||
|
if e.Columns == nil {
|
||||||
|
return nil, fmt.Errorf("expected columns event, got: %+v", e)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Rows{
|
||||||
|
ctx: ctx,
|
||||||
|
body: body,
|
||||||
|
decoder: decoder,
|
||||||
|
closeOnEOQ: closeOnEOQ,
|
||||||
|
columns: e.Columns,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Columns returns the column names.
|
||||||
|
func (rs *Rows) Columns() []string {
|
||||||
|
return rs.columns
|
||||||
|
}
|
||||||
|
|
||||||
|
// Next prepares the next result row for reading with the [Rows.Scan] method. It returns true on success, or false
|
||||||
|
// if there is no next result row or an error happened while preparing it. [Rows.Err] should be consulted to distinguish
|
||||||
|
// between the two cases.
|
||||||
|
//
|
||||||
|
// Every call to [Rows.Scan], even the first one, must be preceded by a call to [Rows.Next].
|
||||||
|
func (rs *Rows) Next() bool {
|
||||||
|
select {
|
||||||
|
case <-rs.ctx.Done():
|
||||||
|
rs.err = rs.ctx.Err()
|
||||||
|
_ = rs.Close()
|
||||||
|
return false
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
var e QueryEvent
|
||||||
|
if err := rs.decoder.Decode(&e); err != nil {
|
||||||
|
rs.err = fmt.Errorf("decode query event: %w", err)
|
||||||
|
_ = rs.Close()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// Server-side query error.
|
||||||
|
if e.Error != nil {
|
||||||
|
rs.err = fmt.Errorf("query error: %s", *e.Error)
|
||||||
|
_ = rs.Close()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if e.Row != nil {
|
||||||
|
if len(e.Row.Values) != len(rs.columns) {
|
||||||
|
rs.err = fmt.Errorf("expected %d column values, got %d", len(rs.columns), len(e.Row.Values))
|
||||||
|
_ = rs.Close()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
rs.row = *e.Row
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if e.EOQ != nil {
|
||||||
|
rs.eoq = e.EOQ
|
||||||
|
// Rows could be used as part of a subscription, so don't close the body if so.
|
||||||
|
if rs.closeOnEOQ {
|
||||||
|
_ = rs.Close()
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
rs.err = fmt.Errorf("expected row or eof event, got: %+v", e)
|
||||||
|
_ = rs.Close()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Err returns the error, if any, that was encountered during iteration.
|
||||||
|
// Err may be called after an explicit or implicit [Rows.Close].
|
||||||
|
func (rs *Rows) Err() error {
|
||||||
|
return rs.err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scan copies the columns in the current row into the values pointed at by dest.
|
||||||
|
// The number of values in dest must be the same as the number of columns in [Rows].
|
||||||
|
// Scan converts JSON-encoded column values to the provided Go types using [json.Unmarshal].
|
||||||
|
func (rs *Rows) Scan(dest ...any) error {
|
||||||
|
if rs.err != nil {
|
||||||
|
return rs.err
|
||||||
|
}
|
||||||
|
if len(dest) != len(rs.columns) {
|
||||||
|
return fmt.Errorf("expected %d values, got %d", len(rs.columns), len(dest))
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, v := range rs.row.Values {
|
||||||
|
if err := json.Unmarshal(v, dest[i]); err != nil {
|
||||||
|
return fmt.Errorf("unmarshal column value #%d: %w", i, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Time returns the time taken to execute the query in seconds. It's only available after all rows have been consumed.
|
||||||
|
// It doesn't include the time to send the query, receive the response, or iterate over the rows.
|
||||||
|
func (rs *Rows) Time() (float64, error) {
|
||||||
|
if rs.eoq == nil {
|
||||||
|
if rs.Err() != nil {
|
||||||
|
return 0, fmt.Errorf("time is not available: %w", rs.Err())
|
||||||
|
}
|
||||||
|
return 0, errors.New("time is not available until all rows are consumed")
|
||||||
|
}
|
||||||
|
return rs.eoq.Time, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close closes the [Rows], preventing further enumeration. If [Rows.Next] is called and returns false,
|
||||||
|
// the [Rows] are closed automatically and it will suffice to check the result of [Rows.Err].
|
||||||
|
// Close is idempotent and does not affect the result of [Rows.Err].
|
||||||
|
func (rs *Rows) Close() error {
|
||||||
|
return rs.body.Close()
|
||||||
|
}
|
||||||
@@ -0,0 +1,323 @@
|
|||||||
|
package corrosion
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"github.com/cenkalti/backoff/v4"
|
||||||
|
"io"
|
||||||
|
"log/slog"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ChangeType string
|
||||||
|
|
||||||
|
var (
|
||||||
|
ChangeTypeInsert ChangeType = "insert"
|
||||||
|
ChangeTypeUpdate ChangeType = "update"
|
||||||
|
ChangeTypeDelete ChangeType = "delete"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ChangeEvent struct {
|
||||||
|
Type ChangeType
|
||||||
|
RowID uint64
|
||||||
|
Values []json.RawMessage
|
||||||
|
ChangeID uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ce *ChangeEvent) UnmarshalJSON(data []byte) error {
|
||||||
|
var raw []json.RawMessage
|
||||||
|
if err := json.Unmarshal(data, &raw); err != nil {
|
||||||
|
return fmt.Errorf("invalid change event: %w", err)
|
||||||
|
}
|
||||||
|
if len(raw) != 4 {
|
||||||
|
return fmt.Errorf("invalid change event: expected an array of 4 elements")
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(raw[0], &ce.Type); err != nil {
|
||||||
|
return fmt.Errorf("invalid change event type: %w", err)
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(raw[1], &ce.RowID); err != nil {
|
||||||
|
return fmt.Errorf("invalid change event row ID: %w", err)
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(raw[2], &ce.Values); err != nil {
|
||||||
|
return fmt.Errorf("invalid change event values: %w", err)
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(raw[3], &ce.ChangeID); err != nil {
|
||||||
|
return fmt.Errorf("invalid change event change ID: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ce *ChangeEvent) MarshalJSON() ([]byte, error) {
|
||||||
|
return json.Marshal([]any{ce.Type, ce.RowID, ce.Values, ce.ChangeID})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scan copies the column values in the change event into the values pointed at by dest.
|
||||||
|
// The number of values in dest must be the same as the number of columns in the change.
|
||||||
|
// Scan converts JSON-encoded column values to the provided Go types using [json.Unmarshal].
|
||||||
|
func (ce *ChangeEvent) Scan(dest ...any) error {
|
||||||
|
if len(dest) != len(ce.Values) {
|
||||||
|
return fmt.Errorf("expected %d values, got %d", len(ce.Values), len(dest))
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, v := range ce.Values {
|
||||||
|
if err := json.Unmarshal(v, dest[i]); err != nil {
|
||||||
|
return fmt.Errorf("unmarshal column value #%d: %w", i, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subscription receives updates from the Corrosion database for a desired SQL query.
|
||||||
|
type Subscription struct {
|
||||||
|
ctx context.Context
|
||||||
|
cancel context.CancelFunc
|
||||||
|
|
||||||
|
id string
|
||||||
|
rows *Rows
|
||||||
|
body io.ReadCloser
|
||||||
|
decoder *json.Decoder
|
||||||
|
resubscribe func(ctx context.Context, fromChange uint64) (*Subscription, error)
|
||||||
|
changes chan *ChangeEvent
|
||||||
|
lastChangeID uint64
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func newSubscription(
|
||||||
|
ctx context.Context,
|
||||||
|
id string,
|
||||||
|
rows *Rows,
|
||||||
|
body io.ReadCloser,
|
||||||
|
decoder *json.Decoder,
|
||||||
|
resubscribe func(ctx context.Context, fromChange uint64) (*Subscription, error),
|
||||||
|
) *Subscription {
|
||||||
|
ctx, cancel := context.WithCancel(ctx)
|
||||||
|
if decoder == nil {
|
||||||
|
decoder = json.NewDecoder(body)
|
||||||
|
}
|
||||||
|
return &Subscription{
|
||||||
|
ctx: ctx,
|
||||||
|
cancel: cancel,
|
||||||
|
id: id,
|
||||||
|
rows: rows,
|
||||||
|
body: body,
|
||||||
|
decoder: decoder,
|
||||||
|
resubscribe: resubscribe,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ID returns the subscription ID.
|
||||||
|
func (s *Subscription) ID() string {
|
||||||
|
return s.id
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rows returns the rows of the query or nil if skipRows was true when creating the subscription or if the subscription
|
||||||
|
// was created with [APIClient.ResubscribeContext].
|
||||||
|
func (s *Subscription) Rows() *Rows {
|
||||||
|
return s.rows
|
||||||
|
}
|
||||||
|
|
||||||
|
// Changes returns a channel that receives change events for the query. Changes are not available until all rows
|
||||||
|
// are consumed. The channel is closed when the context is done, or an error occurs while reading the changes,
|
||||||
|
// or when the subscription is closed explicitly. If it's closed due to an error, [Subscription.Err] will return
|
||||||
|
// the error.
|
||||||
|
func (s *Subscription) Changes() (<-chan *ChangeEvent, error) {
|
||||||
|
if s.changes != nil {
|
||||||
|
return s.changes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.rows != nil {
|
||||||
|
if s.rows.eoq == nil {
|
||||||
|
return nil, errors.New("changes are not available until all rows are consumed")
|
||||||
|
}
|
||||||
|
s.lastChangeID = *s.rows.eoq.ChangeID
|
||||||
|
}
|
||||||
|
s.changes = make(chan *ChangeEvent)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
// Close the body when the context is done to unblock the decoder in the following goroutine.
|
||||||
|
<-s.ctx.Done()
|
||||||
|
s.body.Close()
|
||||||
|
}()
|
||||||
|
go s.handleChangeEvents()
|
||||||
|
|
||||||
|
return s.changes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Subscription) handleChangeEvents() {
|
||||||
|
defer s.cancel()
|
||||||
|
defer close(s.changes)
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-s.ctx.Done():
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
var e QueryEvent
|
||||||
|
var err error
|
||||||
|
if err = s.decoder.Decode(&e); err != nil {
|
||||||
|
// Do not report an error that occurred due to context cancellation, just return.
|
||||||
|
if s.ctx.Err() != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = fmt.Errorf("decode query event: %w", err)
|
||||||
|
} else if e.Error != nil {
|
||||||
|
err = fmt.Errorf("query error: %s", *e.Error)
|
||||||
|
} else if e.Change == nil {
|
||||||
|
err = fmt.Errorf("expected change event, got: %+v", e)
|
||||||
|
} else if s.lastChangeID != 0 && e.Change.ChangeID != s.lastChangeID+1 {
|
||||||
|
// If skipRows is true, the last change ID is unknown.
|
||||||
|
err = fmt.Errorf("missed a change: expected change ID %d, got %d",
|
||||||
|
s.lastChangeID+1, e.Change.ChangeID)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
s.lastChangeID = e.Change.ChangeID
|
||||||
|
select {
|
||||||
|
case s.changes <- e.Change:
|
||||||
|
case <-s.ctx.Done():
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Report the error if resubscribing is disabled.
|
||||||
|
if s.resubscribe == nil {
|
||||||
|
s.err = err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
slog.Info("Resubscribing to Corrosion query due to an error.",
|
||||||
|
"err", err, "id", s.id, "from_change", s.lastChangeID)
|
||||||
|
sub, sErr := s.resubscribe(s.ctx, s.lastChangeID)
|
||||||
|
if sErr != nil {
|
||||||
|
// resubscribe returns a permanent error after unsuccessful retries.
|
||||||
|
s.err = fmt.Errorf("resubscribe to query with backoff: %w", sErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Reset the subscription to the new one.
|
||||||
|
s.rows = nil
|
||||||
|
s.body = sub.body
|
||||||
|
s.decoder = sub.decoder
|
||||||
|
// Do not close the sub to not close the body.
|
||||||
|
sub.cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Err returns the error, if any, that was encountered during fetching changes.
|
||||||
|
// Err may be called after an explicit or implicit [Subscription.Close].
|
||||||
|
func (s *Subscription) Err() error {
|
||||||
|
return s.err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Subscription) Close() error {
|
||||||
|
s.cancel()
|
||||||
|
return s.body.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// SubscribeContext creates a subscription to receive updates for a desired SQL query. If skipRows is false,
|
||||||
|
// Subscription.Rows must be consumed before Subscription.Changes can be called. If skipRows is true, Subscription.Rows
|
||||||
|
// will return nil.
|
||||||
|
func (c *APIClient) SubscribeContext(
|
||||||
|
ctx context.Context, query string, args []any, skipRows bool,
|
||||||
|
) (*Subscription, error) {
|
||||||
|
statement := Statement{
|
||||||
|
Query: query,
|
||||||
|
Params: args,
|
||||||
|
}
|
||||||
|
body, err := json.Marshal(statement)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("marshal query: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
subURL := c.baseURL.JoinPath("/v1/subscriptions")
|
||||||
|
if skipRows {
|
||||||
|
q := subURL.Query()
|
||||||
|
q.Set("skip_rows", "true")
|
||||||
|
subURL.RawQuery = q.Encode()
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, "POST", subURL.String(), bytes.NewReader(body))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("create request: %w", err)
|
||||||
|
}
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
req.Header.Set("Accept", "application/json")
|
||||||
|
|
||||||
|
resp, err := c.client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("send request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
respBody, err := io.ReadAll(resp.Body)
|
||||||
|
resp.Body.Close()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("read response body: %w", err)
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("unexpected status code %d: %s", resp.StatusCode, respBody)
|
||||||
|
}
|
||||||
|
|
||||||
|
id := resp.Header.Get("corro-query-id")
|
||||||
|
if id == "" {
|
||||||
|
resp.Body.Close()
|
||||||
|
return nil, errors.New("missing corro-query-id header in response")
|
||||||
|
}
|
||||||
|
|
||||||
|
if skipRows {
|
||||||
|
return newSubscription(ctx, id, nil, resp.Body, nil, c.resubscribeWithBackoffFn(id)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
rows, err := newRows(ctx, resp.Body, false)
|
||||||
|
if err != nil {
|
||||||
|
resp.Body.Close()
|
||||||
|
return nil, fmt.Errorf("parse query response: %w", err)
|
||||||
|
}
|
||||||
|
return newSubscription(ctx, id, rows, rows.body, rows.decoder, c.resubscribeWithBackoffFn(id)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *APIClient) resubscribeWithBackoffFn(id string) func(context.Context, uint64) (*Subscription, error) {
|
||||||
|
if c.newResubBackoff == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return func(ctx context.Context, fromChange uint64) (*Subscription, error) {
|
||||||
|
return backoff.RetryWithData(func() (*Subscription, error) {
|
||||||
|
slog.Debug("Retrying to resubscribe to Corrosion query.", "id", id, "from_change", fromChange)
|
||||||
|
return c.ResubscribeContext(ctx, id, fromChange)
|
||||||
|
}, c.newResubBackoff())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *APIClient) ResubscribeContext(ctx context.Context, id string, fromChange uint64) (*Subscription, error) {
|
||||||
|
subURL := c.baseURL.JoinPath("/v1/subscriptions", id)
|
||||||
|
q := subURL.Query()
|
||||||
|
q.Set("from", strconv.FormatUint(fromChange, 10))
|
||||||
|
subURL.RawQuery = q.Encode()
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, "GET", subURL.String(), nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("create request: %w", err)
|
||||||
|
}
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
req.Header.Set("Accept", "application/json")
|
||||||
|
|
||||||
|
resp, err := c.client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("send request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
respBody, err := io.ReadAll(resp.Body)
|
||||||
|
resp.Body.Close()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("read response body: %w", err)
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("unexpected status code %d: %s", resp.StatusCode, respBody)
|
||||||
|
}
|
||||||
|
|
||||||
|
return newSubscription(ctx, id, nil, resp.Body, nil, c.resubscribeWithBackoffFn(id)), nil
|
||||||
|
}
|
||||||
@@ -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 {
|
||||||
@@ -35,7 +35,7 @@ func (d *Daemon) Run(ctx context.Context) error {
|
|||||||
case <-d.machine.Started():
|
case <-d.machine.Started():
|
||||||
_, err := systemd.SdNotify(false, systemd.SdNotifyReady)
|
_, err := systemd.SdNotify(false, systemd.SdNotifyReady)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("Failed to notify systemd that the daemon is ready.", "error", err)
|
slog.Error("Failed to notify systemd that the daemon is ready.", "err", err)
|
||||||
}
|
}
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"`
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package docker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"github.com/cenkalti/backoff/v4"
|
||||||
|
"github.com/docker/docker/client"
|
||||||
|
"log/slog"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// WaitDaemonReady waits for the Docker daemon to start and be ready to serve requests.
|
||||||
|
func WaitDaemonReady(ctx context.Context, cli *client.Client) error {
|
||||||
|
// Retry to ping the Docker daemon until it's ready or the context is canceled.
|
||||||
|
boff := backoff.WithContext(backoff.NewExponentialBackOff(
|
||||||
|
backoff.WithInitialInterval(100*time.Millisecond),
|
||||||
|
backoff.WithMaxInterval(1*time.Second),
|
||||||
|
backoff.WithMaxElapsedTime(0),
|
||||||
|
), ctx)
|
||||||
|
|
||||||
|
waitingLogged := false
|
||||||
|
ping := func() error {
|
||||||
|
_, err := cli.Ping(ctx)
|
||||||
|
if err == nil {
|
||||||
|
if waitingLogged {
|
||||||
|
slog.Info("Docker daemon is ready.")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if !client.IsErrConnectionFailed(err) {
|
||||||
|
return backoff.Permanent(fmt.Errorf("connect to Docker daemon: %w", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
if !waitingLogged {
|
||||||
|
slog.Info("Waiting for Docker daemon to start and be ready.")
|
||||||
|
waitingLogged = true
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := backoff.Retry(ping, boff); err != nil {
|
||||||
|
if errors.Is(err, context.Canceled) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return fmt.Errorf("ping Docker: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
package fs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/user"
|
||||||
|
"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.
|
||||||
|
func LookupUIDGID(username string) (uid, gid int, err error) {
|
||||||
|
usr, err := user.Lookup(username)
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("lookup user %q: %w", username, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
uid, err = strconv.Atoi(usr.Uid)
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("parse %q user ID (UID) %q: %w", username, usr.Uid, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
gid, err = strconv.Atoi(usr.Gid)
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("parse %q user group ID (GID) %q: %w", username, usr.Gid, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func Chown(path, username, group string) error {
|
||||||
|
uid, gid := -1, -1
|
||||||
|
if username != "" {
|
||||||
|
usr, err := user.Lookup(username)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("lookup user %q: %w", username, err)
|
||||||
|
}
|
||||||
|
uid, err = strconv.Atoi(usr.Uid)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("parse %q user ID (UID) %q: %w", username, usr.Uid, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if group != "" {
|
||||||
|
grp, err := user.LookupGroup(group)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("lookup group %q: %w", group, err)
|
||||||
|
}
|
||||||
|
gid, err = strconv.Atoi(grp.Gid)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("parse %q group ID (GID) %q: %w", group, grp.Gid, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.Chown(path, uid, gid); err != nil {
|
||||||
|
return fmt.Errorf("chown %q: %w", path, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -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"))
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
package log
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log/slog"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SlogTextHandler extends the standard [slog.TextHandler] to provide custom formatting that is very similar
|
||||||
|
// to the default slog handler. It prefixes each log entry with a right-padded level indicator and message,
|
||||||
|
// while excluding the default time, level, and message attributes from the structured output.
|
||||||
|
//
|
||||||
|
// The output format is:
|
||||||
|
// LEVEL MESSAGE key1=value1 key2=value2
|
||||||
|
type SlogTextHandler struct {
|
||||||
|
*slog.TextHandler
|
||||||
|
opts slog.HandlerOptions
|
||||||
|
mu sync.Mutex
|
||||||
|
w io.Writer
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSlogTextHandler(w io.Writer, opts *slog.HandlerOptions) *SlogTextHandler {
|
||||||
|
if opts == nil {
|
||||||
|
opts = &slog.HandlerOptions{}
|
||||||
|
}
|
||||||
|
// Remove time, level, and message from the default attributes.
|
||||||
|
opts.ReplaceAttr = func(groups []string, a slog.Attr) slog.Attr {
|
||||||
|
if a.Key == slog.TimeKey || a.Key == slog.LevelKey || a.Key == slog.MessageKey {
|
||||||
|
return slog.Attr{}
|
||||||
|
}
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
|
||||||
|
return &SlogTextHandler{
|
||||||
|
TextHandler: slog.NewTextHandler(w, opts),
|
||||||
|
opts: *opts,
|
||||||
|
w: w,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *SlogTextHandler) Enabled(ctx context.Context, level slog.Level) bool {
|
||||||
|
return h.TextHandler.Enabled(ctx, level)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *SlogTextHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
|
||||||
|
return &SlogTextHandler{
|
||||||
|
TextHandler: h.TextHandler.WithAttrs(attrs).(*slog.TextHandler),
|
||||||
|
opts: h.opts,
|
||||||
|
w: h.w,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *SlogTextHandler) WithGroup(name string) slog.Handler {
|
||||||
|
return &SlogTextHandler{
|
||||||
|
TextHandler: h.TextHandler.WithGroup(name).(*slog.TextHandler),
|
||||||
|
opts: h.opts,
|
||||||
|
w: h.w,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *SlogTextHandler) Handle(ctx context.Context, r slog.Record) error {
|
||||||
|
// Only log entries that are at or above the minimum level (INFO by default).
|
||||||
|
minLevel := slog.LevelInfo
|
||||||
|
if h.opts.Level != nil {
|
||||||
|
minLevel = h.opts.Level.Level()
|
||||||
|
}
|
||||||
|
if r.Level < minLevel {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
h.mu.Lock()
|
||||||
|
defer h.mu.Unlock()
|
||||||
|
|
||||||
|
levelMsg := fmt.Sprintf("%-5s %s ", r.Level.String(), r.Message)
|
||||||
|
if _, err := h.w.Write([]byte(levelMsg)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return h.TextHandler.Handle(ctx, r)
|
||||||
|
}
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
package pb
|
package pb
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"google.golang.org/grpc/codes"
|
"google.golang.org/grpc/codes"
|
||||||
"google.golang.org/grpc/status"
|
"google.golang.org/grpc/status"
|
||||||
)
|
)
|
||||||
@@ -40,10 +39,3 @@ func (c *NetworkConfig) Validate() error {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *AddMachineRequest) Validate() error {
|
|
||||||
if r.Network == nil {
|
|
||||||
return fmt.Errorf("network not set")
|
|
||||||
}
|
|
||||||
return r.Network.Validate()
|
|
||||||
}
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -6,62 +6,73 @@ 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/common.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 ListMachineEndpoints(ListMachineEndpointsRequest) returns (ListMachineEndpointsResponse);
|
|
||||||
|
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 {
|
||||||
MachineInfo machine = 1;
|
MachineInfo machine = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message MachineMember {
|
||||||
|
MachineInfo machine = 1;
|
||||||
|
|
||||||
|
enum MembershipState {
|
||||||
|
UNKNOWN = 0;
|
||||||
|
// The member is active.
|
||||||
|
UP = 1;
|
||||||
|
// The member is active, but at least one cluster member suspects its down. For all purposes,
|
||||||
|
// a SUSPECT member is treated as if it were UP until either it refutes the suspicion (becoming UP)
|
||||||
|
// or fails to do so (being declared DOWN).
|
||||||
|
SUSPECT = 2;
|
||||||
|
// The member is confirmed DOWN.
|
||||||
|
DOWN = 3;
|
||||||
|
}
|
||||||
|
MembershipState state = 2;
|
||||||
|
}
|
||||||
|
|
||||||
message ListMachinesResponse {
|
message ListMachinesResponse {
|
||||||
repeated MachineInfo machines = 1;
|
repeated MachineMember machines = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
message ListMachineEndpointsRequest {
|
message Domain {
|
||||||
string id = 1;
|
string name = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
message ListMachineEndpointsResponse {
|
message ReserveDomainRequest {
|
||||||
MachineEndpoints endpoints = 1;
|
string endpoint = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
message MachineInfo {
|
message CreateDomainRecordsRequest {
|
||||||
string id = 1;
|
repeated DNSRecord records = 1;
|
||||||
string name = 2;
|
|
||||||
NetworkConfig network = 3;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
message NetworkConfig {
|
message CreateDomainRecordsResponse {
|
||||||
IPPrefix subnet = 1;
|
repeated DNSRecord records = 1;
|
||||||
IP management_ip = 2;
|
|
||||||
repeated IPPort endpoints = 3;
|
|
||||||
bytes publicKey = 4;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
message MachineEndpoints {
|
message DNSRecord {
|
||||||
string id = 1;
|
string name = 1;
|
||||||
repeated IPPort endpoints = 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
message User {
|
enum RecordType {
|
||||||
NetworkConfig network = 1;
|
UNSPECIFIED = 0;
|
||||||
|
A = 1;
|
||||||
|
AAAA = 2;
|
||||||
|
}
|
||||||
|
RecordType type = 2;
|
||||||
|
repeated string values = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
message State {
|
|
||||||
IPPrefix Network = 1;
|
|
||||||
// The machine configuration in the state is the source of truth set by the administrator.
|
|
||||||
// The machine itself can't update it.
|
|
||||||
map<string, MachineInfo> machines = 2;
|
|
||||||
map<string, MachineEndpoints> endpoints = 3;
|
|
||||||
repeated User users = 4;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|||||||
@@ -20,9 +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_ListMachineEndpoints_FullMethodName = "/api.Cluster/ListMachineEndpoints"
|
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.
|
||||||
@@ -31,7 +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)
|
||||||
ListMachineEndpoints(ctx context.Context, in *ListMachineEndpointsRequest, opts ...grpc.CallOption) (*ListMachineEndpointsResponse, 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 {
|
||||||
@@ -62,10 +68,40 @@ func (c *clusterClient) ListMachines(ctx context.Context, in *emptypb.Empty, opt
|
|||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *clusterClient) ListMachineEndpoints(ctx context.Context, in *ListMachineEndpointsRequest, opts ...grpc.CallOption) (*ListMachineEndpointsResponse, error) {
|
func (c *clusterClient) ReserveDomain(ctx context.Context, in *ReserveDomainRequest, opts ...grpc.CallOption) (*Domain, error) {
|
||||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
out := new(ListMachineEndpointsResponse)
|
out := new(Domain)
|
||||||
err := c.cc.Invoke(ctx, Cluster_ListMachineEndpoints_FullMethodName, in, out, cOpts...)
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -78,7 +114,10 @@ func (c *clusterClient) ListMachineEndpoints(ctx context.Context, in *ListMachin
|
|||||||
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)
|
||||||
ListMachineEndpoints(context.Context, *ListMachineEndpointsRequest) (*ListMachineEndpointsResponse, 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()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,8 +134,17 @@ 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) ListMachineEndpoints(context.Context, *ListMachineEndpointsRequest) (*ListMachineEndpointsResponse, error) {
|
func (UnimplementedClusterServer) ReserveDomain(context.Context, *ReserveDomainRequest) (*Domain, error) {
|
||||||
return nil, status.Errorf(codes.Unimplemented, "method ListMachineEndpoints not implemented")
|
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() {}
|
||||||
@@ -155,20 +203,74 @@ 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_ListMachineEndpoints_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
func _Cluster_ReserveDomain_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
in := new(ListMachineEndpointsRequest)
|
in := new(ReserveDomainRequest)
|
||||||
if err := dec(in); err != nil {
|
if err := dec(in); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if interceptor == nil {
|
if interceptor == nil {
|
||||||
return srv.(ClusterServer).ListMachineEndpoints(ctx, in)
|
return srv.(ClusterServer).ReserveDomain(ctx, in)
|
||||||
}
|
}
|
||||||
info := &grpc.UnaryServerInfo{
|
info := &grpc.UnaryServerInfo{
|
||||||
Server: srv,
|
Server: srv,
|
||||||
FullMethod: Cluster_ListMachineEndpoints_FullMethodName,
|
FullMethod: Cluster_ReserveDomain_FullMethodName,
|
||||||
}
|
}
|
||||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
return srv.(ClusterServer).ListMachineEndpoints(ctx, req.(*ListMachineEndpointsRequest))
|
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)
|
return interceptor(ctx, in, info, handler)
|
||||||
}
|
}
|
||||||
@@ -189,8 +291,20 @@ var Cluster_ServiceDesc = grpc.ServiceDesc{
|
|||||||
Handler: _Cluster_ListMachines_Handler,
|
Handler: _Cluster_ListMachines_Handler,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
MethodName: "ListMachineEndpoints",
|
MethodName: "ReserveDomain",
|
||||||
Handler: _Cluster_ListMachineEndpoints_Handler,
|
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{},
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
package pb
|
package pb
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
status "google.golang.org/genproto/googleapis/rpc/status"
|
||||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||||
reflect "reflect"
|
reflect "reflect"
|
||||||
@@ -20,6 +21,171 @@ const (
|
|||||||
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Common metadata message nested in all reply message types, injected by the gRPC proxy to provide information
|
||||||
|
// about the machine that responded to the request.
|
||||||
|
type Metadata struct {
|
||||||
|
state protoimpl.MessageState
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
|
||||||
|
// Address of the machine the response came from.
|
||||||
|
Machine string `protobuf:"bytes,1,opt,name=machine,proto3" json:"machine,omitempty"`
|
||||||
|
// error is set if the request to upstream failed. The rest of the response is undefined.
|
||||||
|
Error string `protobuf:"bytes,2,opt,name=error,proto3" json:"error,omitempty"`
|
||||||
|
// error as a gRPC Status message.
|
||||||
|
Status *status.Status `protobuf:"bytes,3,opt,name=status,proto3" json:"status,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Metadata) Reset() {
|
||||||
|
*x = Metadata{}
|
||||||
|
if protoimpl.UnsafeEnabled {
|
||||||
|
mi := &file_internal_machine_api_pb_common_proto_msgTypes[0]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Metadata) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*Metadata) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *Metadata) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_internal_machine_api_pb_common_proto_msgTypes[0]
|
||||||
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use Metadata.ProtoReflect.Descriptor instead.
|
||||||
|
func (*Metadata) Descriptor() ([]byte, []int) {
|
||||||
|
return file_internal_machine_api_pb_common_proto_rawDescGZIP(), []int{0}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Metadata) GetMachine() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Machine
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Metadata) GetError() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Error
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Metadata) GetStatus() *status.Status {
|
||||||
|
if x != nil {
|
||||||
|
return x.Status
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// A helper message for marshalling the metadata field and injecting it into the response by the gRPC proxy.
|
||||||
|
type Empty struct {
|
||||||
|
state protoimpl.MessageState
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
|
||||||
|
Metadata *Metadata `protobuf:"bytes,1,opt,name=metadata,proto3" json:"metadata,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Empty) Reset() {
|
||||||
|
*x = Empty{}
|
||||||
|
if protoimpl.UnsafeEnabled {
|
||||||
|
mi := &file_internal_machine_api_pb_common_proto_msgTypes[1]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Empty) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*Empty) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *Empty) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_internal_machine_api_pb_common_proto_msgTypes[1]
|
||||||
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use Empty.ProtoReflect.Descriptor instead.
|
||||||
|
func (*Empty) Descriptor() ([]byte, []int) {
|
||||||
|
return file_internal_machine_api_pb_common_proto_rawDescGZIP(), []int{1}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Empty) GetMetadata() *Metadata {
|
||||||
|
if x != nil {
|
||||||
|
return x.Metadata
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// EmptyResponse is a response message to be returned by the gRPC proxy when a request to the upstream failed.
|
||||||
|
// The nested Empty.Metadata message should contain an error.
|
||||||
|
type EmptyResponse struct {
|
||||||
|
state protoimpl.MessageState
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
|
||||||
|
Messages []*Empty `protobuf:"bytes,1,rep,name=messages,proto3" json:"messages,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *EmptyResponse) Reset() {
|
||||||
|
*x = EmptyResponse{}
|
||||||
|
if protoimpl.UnsafeEnabled {
|
||||||
|
mi := &file_internal_machine_api_pb_common_proto_msgTypes[2]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *EmptyResponse) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*EmptyResponse) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *EmptyResponse) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_internal_machine_api_pb_common_proto_msgTypes[2]
|
||||||
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use EmptyResponse.ProtoReflect.Descriptor instead.
|
||||||
|
func (*EmptyResponse) Descriptor() ([]byte, []int) {
|
||||||
|
return file_internal_machine_api_pb_common_proto_rawDescGZIP(), []int{2}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *EmptyResponse) GetMessages() []*Empty {
|
||||||
|
if x != nil {
|
||||||
|
return x.Messages
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
type IP struct {
|
type IP struct {
|
||||||
state protoimpl.MessageState
|
state protoimpl.MessageState
|
||||||
sizeCache protoimpl.SizeCache
|
sizeCache protoimpl.SizeCache
|
||||||
@@ -31,7 +197,7 @@ type IP struct {
|
|||||||
func (x *IP) Reset() {
|
func (x *IP) Reset() {
|
||||||
*x = IP{}
|
*x = IP{}
|
||||||
if protoimpl.UnsafeEnabled {
|
if protoimpl.UnsafeEnabled {
|
||||||
mi := &file_internal_machine_api_pb_common_proto_msgTypes[0]
|
mi := &file_internal_machine_api_pb_common_proto_msgTypes[3]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@@ -44,7 +210,7 @@ func (x *IP) String() string {
|
|||||||
func (*IP) ProtoMessage() {}
|
func (*IP) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *IP) ProtoReflect() protoreflect.Message {
|
func (x *IP) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_internal_machine_api_pb_common_proto_msgTypes[0]
|
mi := &file_internal_machine_api_pb_common_proto_msgTypes[3]
|
||||||
if protoimpl.UnsafeEnabled && x != nil {
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
@@ -57,7 +223,7 @@ func (x *IP) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use IP.ProtoReflect.Descriptor instead.
|
// Deprecated: Use IP.ProtoReflect.Descriptor instead.
|
||||||
func (*IP) Descriptor() ([]byte, []int) {
|
func (*IP) Descriptor() ([]byte, []int) {
|
||||||
return file_internal_machine_api_pb_common_proto_rawDescGZIP(), []int{0}
|
return file_internal_machine_api_pb_common_proto_rawDescGZIP(), []int{3}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *IP) GetIp() []byte {
|
func (x *IP) GetIp() []byte {
|
||||||
@@ -79,7 +245,7 @@ type IPPort struct {
|
|||||||
func (x *IPPort) Reset() {
|
func (x *IPPort) Reset() {
|
||||||
*x = IPPort{}
|
*x = IPPort{}
|
||||||
if protoimpl.UnsafeEnabled {
|
if protoimpl.UnsafeEnabled {
|
||||||
mi := &file_internal_machine_api_pb_common_proto_msgTypes[1]
|
mi := &file_internal_machine_api_pb_common_proto_msgTypes[4]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@@ -92,7 +258,7 @@ func (x *IPPort) String() string {
|
|||||||
func (*IPPort) ProtoMessage() {}
|
func (*IPPort) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *IPPort) ProtoReflect() protoreflect.Message {
|
func (x *IPPort) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_internal_machine_api_pb_common_proto_msgTypes[1]
|
mi := &file_internal_machine_api_pb_common_proto_msgTypes[4]
|
||||||
if protoimpl.UnsafeEnabled && x != nil {
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
@@ -105,7 +271,7 @@ func (x *IPPort) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use IPPort.ProtoReflect.Descriptor instead.
|
// Deprecated: Use IPPort.ProtoReflect.Descriptor instead.
|
||||||
func (*IPPort) Descriptor() ([]byte, []int) {
|
func (*IPPort) Descriptor() ([]byte, []int) {
|
||||||
return file_internal_machine_api_pb_common_proto_rawDescGZIP(), []int{1}
|
return file_internal_machine_api_pb_common_proto_rawDescGZIP(), []int{4}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *IPPort) GetIp() *IP {
|
func (x *IPPort) GetIp() *IP {
|
||||||
@@ -134,7 +300,7 @@ type IPPrefix struct {
|
|||||||
func (x *IPPrefix) Reset() {
|
func (x *IPPrefix) Reset() {
|
||||||
*x = IPPrefix{}
|
*x = IPPrefix{}
|
||||||
if protoimpl.UnsafeEnabled {
|
if protoimpl.UnsafeEnabled {
|
||||||
mi := &file_internal_machine_api_pb_common_proto_msgTypes[2]
|
mi := &file_internal_machine_api_pb_common_proto_msgTypes[5]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@@ -147,7 +313,7 @@ func (x *IPPrefix) String() string {
|
|||||||
func (*IPPrefix) ProtoMessage() {}
|
func (*IPPrefix) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *IPPrefix) ProtoReflect() protoreflect.Message {
|
func (x *IPPrefix) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_internal_machine_api_pb_common_proto_msgTypes[2]
|
mi := &file_internal_machine_api_pb_common_proto_msgTypes[5]
|
||||||
if protoimpl.UnsafeEnabled && x != nil {
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
@@ -160,7 +326,7 @@ func (x *IPPrefix) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use IPPrefix.ProtoReflect.Descriptor instead.
|
// Deprecated: Use IPPrefix.ProtoReflect.Descriptor instead.
|
||||||
func (*IPPrefix) Descriptor() ([]byte, []int) {
|
func (*IPPrefix) Descriptor() ([]byte, []int) {
|
||||||
return file_internal_machine_api_pb_common_proto_rawDescGZIP(), []int{2}
|
return file_internal_machine_api_pb_common_proto_rawDescGZIP(), []int{5}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *IPPrefix) GetIp() *IP {
|
func (x *IPPrefix) GetIp() *IP {
|
||||||
@@ -182,20 +348,34 @@ var File_internal_machine_api_pb_common_proto protoreflect.FileDescriptor
|
|||||||
var file_internal_machine_api_pb_common_proto_rawDesc = []byte{
|
var file_internal_machine_api_pb_common_proto_rawDesc = []byte{
|
||||||
0x0a, 0x24, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69,
|
0x0a, 0x24, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69,
|
||||||
0x6e, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x62, 0x2f, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e,
|
0x6e, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x62, 0x2f, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e,
|
||||||
0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x03, 0x61, 0x70, 0x69, 0x22, 0x14, 0x0a, 0x02, 0x49,
|
0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x03, 0x61, 0x70, 0x69, 0x1a, 0x17, 0x67, 0x6f, 0x6f,
|
||||||
0x50, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x02, 0x69,
|
0x67, 0x6c, 0x65, 0x2f, 0x72, 0x70, 0x63, 0x2f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x2e, 0x70,
|
||||||
0x70, 0x22, 0x35, 0x0a, 0x06, 0x49, 0x50, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x17, 0x0a, 0x02, 0x69,
|
0x72, 0x6f, 0x74, 0x6f, 0x22, 0x66, 0x0a, 0x08, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61,
|
||||||
0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x07, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x50,
|
0x12, 0x18, 0x0a, 0x07, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28,
|
||||||
0x52, 0x02, 0x69, 0x70, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x02, 0x20, 0x01,
|
0x09, 0x52, 0x07, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72,
|
||||||
0x28, 0x0d, 0x52, 0x04, 0x70, 0x6f, 0x72, 0x74, 0x22, 0x37, 0x0a, 0x08, 0x49, 0x50, 0x50, 0x72,
|
0x72, 0x6f, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72,
|
||||||
0x65, 0x66, 0x69, 0x78, 0x12, 0x17, 0x0a, 0x02, 0x69, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b,
|
0x12, 0x2a, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b,
|
||||||
0x32, 0x07, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x50, 0x52, 0x02, 0x69, 0x70, 0x12, 0x12, 0x0a,
|
0x32, 0x12, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x74,
|
||||||
0x04, 0x62, 0x69, 0x74, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x04, 0x62, 0x69, 0x74,
|
0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x32, 0x0a, 0x05,
|
||||||
0x73, 0x42, 0x37, 0x5a, 0x35, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f,
|
0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x29, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74,
|
||||||
0x70, 0x73, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x6b, 0x69, 0x2f, 0x75, 0x6e, 0x63, 0x6c, 0x6f,
|
0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d, 0x65,
|
||||||
0x75, 0x64, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x6d, 0x61, 0x63, 0x68,
|
0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61,
|
||||||
0x69, 0x6e, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74,
|
0x22, 0x37, 0x0a, 0x0d, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
|
||||||
0x6f, 0x33,
|
0x65, 0x12, 0x26, 0x0a, 0x08, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x18, 0x01, 0x20,
|
||||||
|
0x03, 0x28, 0x0b, 0x32, 0x0a, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x52,
|
||||||
|
0x08, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x22, 0x14, 0x0a, 0x02, 0x49, 0x50, 0x12,
|
||||||
|
0x0e, 0x0a, 0x02, 0x69, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x02, 0x69, 0x70, 0x22,
|
||||||
|
0x35, 0x0a, 0x06, 0x49, 0x50, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x17, 0x0a, 0x02, 0x69, 0x70, 0x18,
|
||||||
|
0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x07, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x50, 0x52, 0x02,
|
||||||
|
0x69, 0x70, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d,
|
||||||
|
0x52, 0x04, 0x70, 0x6f, 0x72, 0x74, 0x22, 0x37, 0x0a, 0x08, 0x49, 0x50, 0x50, 0x72, 0x65, 0x66,
|
||||||
|
0x69, 0x78, 0x12, 0x17, 0x0a, 0x02, 0x69, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x07,
|
||||||
|
0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x50, 0x52, 0x02, 0x69, 0x70, 0x12, 0x12, 0x0a, 0x04, 0x62,
|
||||||
|
0x69, 0x74, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x04, 0x62, 0x69, 0x74, 0x73, 0x42,
|
||||||
|
0x37, 0x5a, 0x35, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x70, 0x73,
|
||||||
|
0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x6b, 0x69, 0x2f, 0x75, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64,
|
||||||
|
0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e,
|
||||||
|
0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -210,20 +390,27 @@ func file_internal_machine_api_pb_common_proto_rawDescGZIP() []byte {
|
|||||||
return file_internal_machine_api_pb_common_proto_rawDescData
|
return file_internal_machine_api_pb_common_proto_rawDescData
|
||||||
}
|
}
|
||||||
|
|
||||||
var file_internal_machine_api_pb_common_proto_msgTypes = make([]protoimpl.MessageInfo, 3)
|
var file_internal_machine_api_pb_common_proto_msgTypes = make([]protoimpl.MessageInfo, 6)
|
||||||
var file_internal_machine_api_pb_common_proto_goTypes = []any{
|
var file_internal_machine_api_pb_common_proto_goTypes = []any{
|
||||||
(*IP)(nil), // 0: api.IP
|
(*Metadata)(nil), // 0: api.Metadata
|
||||||
(*IPPort)(nil), // 1: api.IPPort
|
(*Empty)(nil), // 1: api.Empty
|
||||||
(*IPPrefix)(nil), // 2: api.IPPrefix
|
(*EmptyResponse)(nil), // 2: api.EmptyResponse
|
||||||
|
(*IP)(nil), // 3: api.IP
|
||||||
|
(*IPPort)(nil), // 4: api.IPPort
|
||||||
|
(*IPPrefix)(nil), // 5: api.IPPrefix
|
||||||
|
(*status.Status)(nil), // 6: google.rpc.Status
|
||||||
}
|
}
|
||||||
var file_internal_machine_api_pb_common_proto_depIdxs = []int32{
|
var file_internal_machine_api_pb_common_proto_depIdxs = []int32{
|
||||||
0, // 0: api.IPPort.ip:type_name -> api.IP
|
6, // 0: api.Metadata.status:type_name -> google.rpc.Status
|
||||||
0, // 1: api.IPPrefix.ip:type_name -> api.IP
|
0, // 1: api.Empty.metadata:type_name -> api.Metadata
|
||||||
2, // [2:2] is the sub-list for method output_type
|
1, // 2: api.EmptyResponse.messages:type_name -> api.Empty
|
||||||
2, // [2:2] is the sub-list for method input_type
|
3, // 3: api.IPPort.ip:type_name -> api.IP
|
||||||
2, // [2:2] is the sub-list for extension type_name
|
3, // 4: api.IPPrefix.ip:type_name -> api.IP
|
||||||
2, // [2:2] is the sub-list for extension extendee
|
5, // [5:5] is the sub-list for method output_type
|
||||||
0, // [0:2] is the sub-list for field type_name
|
5, // [5:5] is the sub-list for method input_type
|
||||||
|
5, // [5:5] is the sub-list for extension type_name
|
||||||
|
5, // [5:5] is the sub-list for extension extendee
|
||||||
|
0, // [0:5] is the sub-list for field type_name
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() { file_internal_machine_api_pb_common_proto_init() }
|
func init() { file_internal_machine_api_pb_common_proto_init() }
|
||||||
@@ -233,7 +420,7 @@ func file_internal_machine_api_pb_common_proto_init() {
|
|||||||
}
|
}
|
||||||
if !protoimpl.UnsafeEnabled {
|
if !protoimpl.UnsafeEnabled {
|
||||||
file_internal_machine_api_pb_common_proto_msgTypes[0].Exporter = func(v any, i int) any {
|
file_internal_machine_api_pb_common_proto_msgTypes[0].Exporter = func(v any, i int) any {
|
||||||
switch v := v.(*IP); i {
|
switch v := v.(*Metadata); i {
|
||||||
case 0:
|
case 0:
|
||||||
return &v.state
|
return &v.state
|
||||||
case 1:
|
case 1:
|
||||||
@@ -245,7 +432,7 @@ func file_internal_machine_api_pb_common_proto_init() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
file_internal_machine_api_pb_common_proto_msgTypes[1].Exporter = func(v any, i int) any {
|
file_internal_machine_api_pb_common_proto_msgTypes[1].Exporter = func(v any, i int) any {
|
||||||
switch v := v.(*IPPort); i {
|
switch v := v.(*Empty); i {
|
||||||
case 0:
|
case 0:
|
||||||
return &v.state
|
return &v.state
|
||||||
case 1:
|
case 1:
|
||||||
@@ -257,6 +444,42 @@ func file_internal_machine_api_pb_common_proto_init() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
file_internal_machine_api_pb_common_proto_msgTypes[2].Exporter = func(v any, i int) any {
|
file_internal_machine_api_pb_common_proto_msgTypes[2].Exporter = func(v any, i int) any {
|
||||||
|
switch v := v.(*EmptyResponse); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file_internal_machine_api_pb_common_proto_msgTypes[3].Exporter = func(v any, i int) any {
|
||||||
|
switch v := v.(*IP); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file_internal_machine_api_pb_common_proto_msgTypes[4].Exporter = func(v any, i int) any {
|
||||||
|
switch v := v.(*IPPort); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file_internal_machine_api_pb_common_proto_msgTypes[5].Exporter = func(v any, i int) any {
|
||||||
switch v := v.(*IPPrefix); i {
|
switch v := v.(*IPPrefix); i {
|
||||||
case 0:
|
case 0:
|
||||||
return &v.state
|
return &v.state
|
||||||
@@ -275,7 +498,7 @@ func file_internal_machine_api_pb_common_proto_init() {
|
|||||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||||
RawDescriptor: file_internal_machine_api_pb_common_proto_rawDesc,
|
RawDescriptor: file_internal_machine_api_pb_common_proto_rawDesc,
|
||||||
NumEnums: 0,
|
NumEnums: 0,
|
||||||
NumMessages: 3,
|
NumMessages: 6,
|
||||||
NumExtensions: 0,
|
NumExtensions: 0,
|
||||||
NumServices: 0,
|
NumServices: 0,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -4,6 +4,31 @@ package api;
|
|||||||
|
|
||||||
option go_package = "github.com/psviderski/uncloud/internal/machine/api/pb";
|
option go_package = "github.com/psviderski/uncloud/internal/machine/api/pb";
|
||||||
|
|
||||||
|
// Vendored at internal/machine/api/vendor/google/rpc/status.proto.
|
||||||
|
import "google/rpc/status.proto";
|
||||||
|
|
||||||
|
// Common metadata message nested in all reply message types, injected by the gRPC proxy to provide information
|
||||||
|
// about the machine that responded to the request.
|
||||||
|
message Metadata {
|
||||||
|
// Address of the machine the response came from.
|
||||||
|
string machine = 1;
|
||||||
|
// error is set if the request to upstream failed. The rest of the response is undefined.
|
||||||
|
string error = 2;
|
||||||
|
// error as a gRPC Status message.
|
||||||
|
google.rpc.Status status = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A helper message for marshalling the metadata field and injecting it into the response by the gRPC proxy.
|
||||||
|
message Empty {
|
||||||
|
Metadata metadata = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// EmptyResponse is a response message to be returned by the gRPC proxy when a request to the upstream failed.
|
||||||
|
// The nested Empty.Metadata message should contain an error.
|
||||||
|
message EmptyResponse {
|
||||||
|
repeated Empty messages = 1;
|
||||||
|
}
|
||||||
|
|
||||||
message IP {
|
message IP {
|
||||||
bytes ip = 1;
|
bytes ip = 1;
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,197 @@
|
|||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
package api;
|
||||||
|
|
||||||
|
option go_package = "github.com/psviderski/uncloud/internal/machine/api/pb";
|
||||||
|
|
||||||
|
import "google/protobuf/empty.proto";
|
||||||
|
import "internal/machine/api/pb/common.proto";
|
||||||
|
|
||||||
|
service Docker {
|
||||||
|
rpc CreateContainer(CreateContainerRequest) returns (CreateContainerResponse);
|
||||||
|
rpc InspectContainer(InspectContainerRequest) returns (InspectContainerResponse);
|
||||||
|
rpc StartContainer(StartContainerRequest) returns (google.protobuf.Empty);
|
||||||
|
rpc StopContainer(StopContainerRequest) returns (google.protobuf.Empty);
|
||||||
|
rpc ListContainers(ListContainersRequest) returns (ListContainersResponse);
|
||||||
|
rpc RemoveContainer(RemoveContainerRequest) returns (google.protobuf.Empty);
|
||||||
|
|
||||||
|
rpc PullImage(PullImageRequest) returns (stream JSONMessage);
|
||||||
|
rpc InspectImage(InspectImageRequest) returns (InspectImageResponse);
|
||||||
|
// InspectRemoteImage returns the image metadata for an image in a remote registry using the machine's
|
||||||
|
// Docker auth credentials if necessary.
|
||||||
|
rpc InspectRemoteImage(InspectRemoteImageRequest) returns (InspectRemoteImageResponse);
|
||||||
|
|
||||||
|
rpc CreateVolume(CreateVolumeRequest) returns (CreateVolumeResponse);
|
||||||
|
rpc ListVolumes(ListVolumesRequest) returns (ListVolumesResponse);
|
||||||
|
rpc RemoveVolume(RemoveVolumeRequest) returns (google.protobuf.Empty);
|
||||||
|
|
||||||
|
rpc CreateServiceContainer(CreateServiceContainerRequest) returns (CreateContainerResponse);
|
||||||
|
rpc InspectServiceContainer(InspectContainerRequest) returns (ServiceContainer);
|
||||||
|
rpc ListServiceContainers(ListServiceContainersRequest) returns (ListServiceContainersResponse);
|
||||||
|
rpc RemoveServiceContainer(RemoveContainerRequest) returns (google.protobuf.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
message CreateContainerRequest {
|
||||||
|
// JSON serialised container.Config.
|
||||||
|
bytes config = 1;
|
||||||
|
// JSON serialised container.HostConfig.
|
||||||
|
bytes host_config = 2;
|
||||||
|
// JSON serialised network.NetworkingConfig.
|
||||||
|
bytes network_config = 3;
|
||||||
|
// JSON serialised ocispec.Platform.
|
||||||
|
bytes platform = 4;
|
||||||
|
string name = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
message CreateContainerResponse {
|
||||||
|
// JSON serialised container.CreateResponse.
|
||||||
|
bytes response = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message InspectContainerRequest {
|
||||||
|
string id = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message InspectContainerResponse {
|
||||||
|
// JSON serialised container.InspectResponse.
|
||||||
|
bytes response = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message StartContainerRequest {
|
||||||
|
string id = 1;
|
||||||
|
// JSON serialised container.StartOptions.
|
||||||
|
bytes options = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message StopContainerRequest {
|
||||||
|
string id = 1;
|
||||||
|
// JSON serialised container.StopOptions.
|
||||||
|
bytes options = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ListContainersRequest {
|
||||||
|
// JSON serialised container.ListOptions.
|
||||||
|
bytes options = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ListContainersResponse {
|
||||||
|
// Must contain only one repeated messages field to allow broadcasting ListContainers requests to multiple machines.
|
||||||
|
repeated MachineContainers messages = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message MachineContainers {
|
||||||
|
Metadata metadata = 1;
|
||||||
|
// JSON serialised []container.InspectResponse.
|
||||||
|
bytes containers = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message RemoveContainerRequest {
|
||||||
|
string id = 1;
|
||||||
|
// JSON serialised container.RemoveOptions.
|
||||||
|
bytes options = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message PullImageRequest {
|
||||||
|
string image = 1;
|
||||||
|
// JSON serialised image.PullOptions.
|
||||||
|
bytes options = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message JSONMessage {
|
||||||
|
// JSON serialised jsonmessage.JSONMessage.
|
||||||
|
bytes message = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message InspectImageRequest {
|
||||||
|
string id = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message InspectImageResponse {
|
||||||
|
// Must contain only one repeated messages field to allow broadcasting InspectImage requests to multiple machines.
|
||||||
|
repeated Image messages = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message Image {
|
||||||
|
Metadata metadata = 1;
|
||||||
|
// JSON serialised types.ImageInspect.
|
||||||
|
bytes image = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message InspectRemoteImageRequest {
|
||||||
|
string id = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message InspectRemoteImageResponse {
|
||||||
|
// Must contain only one repeated messages field to allow broadcasting InspectRemoteImage requests to multiple machines.
|
||||||
|
repeated RemoteImage messages = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message RemoteImage {
|
||||||
|
Metadata metadata = 1;
|
||||||
|
// Image reference in the canonical form with the digest.
|
||||||
|
string reference = 2;
|
||||||
|
// Raw JSON manifest from the registry.
|
||||||
|
bytes manifest = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message CreateVolumeRequest {
|
||||||
|
// JSON serialised volume.CreateOptions.
|
||||||
|
bytes options = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message CreateVolumeResponse {
|
||||||
|
// JSON serialised volume.Volume.
|
||||||
|
bytes volume = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ListVolumesRequest {
|
||||||
|
// JSON serialised volume.ListOptions.
|
||||||
|
bytes options = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ListVolumesResponse {
|
||||||
|
// Must contain only one repeated messages field to allow broadcasting ListVolumes requests to multiple machines.
|
||||||
|
repeated MachineVolumes messages = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message MachineVolumes {
|
||||||
|
Metadata metadata = 1;
|
||||||
|
// JSON serialised volume.ListResponse.
|
||||||
|
bytes response = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message RemoveVolumeRequest {
|
||||||
|
string id = 1;
|
||||||
|
bool force = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message CreateServiceContainerRequest {
|
||||||
|
string service_id = 1;
|
||||||
|
// JSON serialised api.ServiceSpec.
|
||||||
|
bytes service_spec = 2;
|
||||||
|
string container_name = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ServiceContainer {
|
||||||
|
// JSON serialised container.InspectResponse.
|
||||||
|
bytes container = 1;
|
||||||
|
// JSON serialised api.ServiceSpec.
|
||||||
|
bytes service_spec = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ListServiceContainersRequest {
|
||||||
|
string service_id = 1;
|
||||||
|
// JSON serialised container.ListOptions.
|
||||||
|
bytes options = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ListServiceContainersResponse {
|
||||||
|
// Must contain only one repeated messages field to allow broadcasting ListServiceContainers requests
|
||||||
|
// to multiple machines.
|
||||||
|
repeated MachineServiceContainers messages = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message MachineServiceContainers {
|
||||||
|
Metadata metadata = 1;
|
||||||
|
repeated ServiceContainer containers = 2;
|
||||||
|
}
|
||||||
@@ -0,0 +1,700 @@
|
|||||||
|
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// - protoc-gen-go-grpc v1.5.1
|
||||||
|
// - protoc v5.27.3
|
||||||
|
// source: internal/machine/api/pb/docker.proto
|
||||||
|
|
||||||
|
package pb
|
||||||
|
|
||||||
|
import (
|
||||||
|
context "context"
|
||||||
|
grpc "google.golang.org/grpc"
|
||||||
|
codes "google.golang.org/grpc/codes"
|
||||||
|
status "google.golang.org/grpc/status"
|
||||||
|
emptypb "google.golang.org/protobuf/types/known/emptypb"
|
||||||
|
)
|
||||||
|
|
||||||
|
// This is a compile-time assertion to ensure that this generated file
|
||||||
|
// is compatible with the grpc package it is being compiled against.
|
||||||
|
// Requires gRPC-Go v1.64.0 or later.
|
||||||
|
const _ = grpc.SupportPackageIsVersion9
|
||||||
|
|
||||||
|
const (
|
||||||
|
Docker_CreateContainer_FullMethodName = "/api.Docker/CreateContainer"
|
||||||
|
Docker_InspectContainer_FullMethodName = "/api.Docker/InspectContainer"
|
||||||
|
Docker_StartContainer_FullMethodName = "/api.Docker/StartContainer"
|
||||||
|
Docker_StopContainer_FullMethodName = "/api.Docker/StopContainer"
|
||||||
|
Docker_ListContainers_FullMethodName = "/api.Docker/ListContainers"
|
||||||
|
Docker_RemoveContainer_FullMethodName = "/api.Docker/RemoveContainer"
|
||||||
|
Docker_PullImage_FullMethodName = "/api.Docker/PullImage"
|
||||||
|
Docker_InspectImage_FullMethodName = "/api.Docker/InspectImage"
|
||||||
|
Docker_InspectRemoteImage_FullMethodName = "/api.Docker/InspectRemoteImage"
|
||||||
|
Docker_CreateVolume_FullMethodName = "/api.Docker/CreateVolume"
|
||||||
|
Docker_ListVolumes_FullMethodName = "/api.Docker/ListVolumes"
|
||||||
|
Docker_RemoveVolume_FullMethodName = "/api.Docker/RemoveVolume"
|
||||||
|
Docker_CreateServiceContainer_FullMethodName = "/api.Docker/CreateServiceContainer"
|
||||||
|
Docker_InspectServiceContainer_FullMethodName = "/api.Docker/InspectServiceContainer"
|
||||||
|
Docker_ListServiceContainers_FullMethodName = "/api.Docker/ListServiceContainers"
|
||||||
|
Docker_RemoveServiceContainer_FullMethodName = "/api.Docker/RemoveServiceContainer"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DockerClient is the client API for Docker service.
|
||||||
|
//
|
||||||
|
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
|
||||||
|
type DockerClient interface {
|
||||||
|
CreateContainer(ctx context.Context, in *CreateContainerRequest, opts ...grpc.CallOption) (*CreateContainerResponse, error)
|
||||||
|
InspectContainer(ctx context.Context, in *InspectContainerRequest, opts ...grpc.CallOption) (*InspectContainerResponse, 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)
|
||||||
|
RemoveContainer(ctx context.Context, in *RemoveContainerRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
|
||||||
|
PullImage(ctx context.Context, in *PullImageRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[JSONMessage], error)
|
||||||
|
InspectImage(ctx context.Context, in *InspectImageRequest, opts ...grpc.CallOption) (*InspectImageResponse, error)
|
||||||
|
// InspectRemoteImage returns the image metadata for an image in a remote registry using the machine's
|
||||||
|
// Docker auth credentials if necessary.
|
||||||
|
InspectRemoteImage(ctx context.Context, in *InspectRemoteImageRequest, opts ...grpc.CallOption) (*InspectRemoteImageResponse, error)
|
||||||
|
CreateVolume(ctx context.Context, in *CreateVolumeRequest, opts ...grpc.CallOption) (*CreateVolumeResponse, error)
|
||||||
|
ListVolumes(ctx context.Context, in *ListVolumesRequest, opts ...grpc.CallOption) (*ListVolumesResponse, error)
|
||||||
|
RemoveVolume(ctx context.Context, in *RemoveVolumeRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
|
||||||
|
CreateServiceContainer(ctx context.Context, in *CreateServiceContainerRequest, opts ...grpc.CallOption) (*CreateContainerResponse, error)
|
||||||
|
InspectServiceContainer(ctx context.Context, in *InspectContainerRequest, opts ...grpc.CallOption) (*ServiceContainer, error)
|
||||||
|
ListServiceContainers(ctx context.Context, in *ListServiceContainersRequest, opts ...grpc.CallOption) (*ListServiceContainersResponse, error)
|
||||||
|
RemoveServiceContainer(ctx context.Context, in *RemoveContainerRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type dockerClient struct {
|
||||||
|
cc grpc.ClientConnInterface
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDockerClient(cc grpc.ClientConnInterface) DockerClient {
|
||||||
|
return &dockerClient{cc}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *dockerClient) CreateContainer(ctx context.Context, in *CreateContainerRequest, opts ...grpc.CallOption) (*CreateContainerResponse, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(CreateContainerResponse)
|
||||||
|
err := c.cc.Invoke(ctx, Docker_CreateContainer_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *dockerClient) 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) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(emptypb.Empty)
|
||||||
|
err := c.cc.Invoke(ctx, Docker_StartContainer_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *dockerClient) 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) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(ListContainersResponse)
|
||||||
|
err := c.cc.Invoke(ctx, Docker_ListContainers_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *dockerClient) RemoveContainer(ctx context.Context, in *RemoveContainerRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(emptypb.Empty)
|
||||||
|
err := c.cc.Invoke(ctx, Docker_RemoveContainer_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *dockerClient) PullImage(ctx context.Context, in *PullImageRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[JSONMessage], error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
stream, err := c.cc.NewStream(ctx, &Docker_ServiceDesc.Streams[0], Docker_PullImage_FullMethodName, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
x := &grpc.GenericClientStream[PullImageRequest, JSONMessage]{ClientStream: stream}
|
||||||
|
if err := x.ClientStream.SendMsg(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := x.ClientStream.CloseSend(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return x, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
|
||||||
|
type Docker_PullImageClient = grpc.ServerStreamingClient[JSONMessage]
|
||||||
|
|
||||||
|
func (c *dockerClient) InspectImage(ctx context.Context, in *InspectImageRequest, opts ...grpc.CallOption) (*InspectImageResponse, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(InspectImageResponse)
|
||||||
|
err := c.cc.Invoke(ctx, Docker_InspectImage_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *dockerClient) InspectRemoteImage(ctx context.Context, in *InspectRemoteImageRequest, opts ...grpc.CallOption) (*InspectRemoteImageResponse, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(InspectRemoteImageResponse)
|
||||||
|
err := c.cc.Invoke(ctx, Docker_InspectRemoteImage_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *dockerClient) CreateVolume(ctx context.Context, in *CreateVolumeRequest, opts ...grpc.CallOption) (*CreateVolumeResponse, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(CreateVolumeResponse)
|
||||||
|
err := c.cc.Invoke(ctx, Docker_CreateVolume_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *dockerClient) ListVolumes(ctx context.Context, in *ListVolumesRequest, opts ...grpc.CallOption) (*ListVolumesResponse, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(ListVolumesResponse)
|
||||||
|
err := c.cc.Invoke(ctx, Docker_ListVolumes_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *dockerClient) RemoveVolume(ctx context.Context, in *RemoveVolumeRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(emptypb.Empty)
|
||||||
|
err := c.cc.Invoke(ctx, Docker_RemoveVolume_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *dockerClient) CreateServiceContainer(ctx context.Context, in *CreateServiceContainerRequest, opts ...grpc.CallOption) (*CreateContainerResponse, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(CreateContainerResponse)
|
||||||
|
err := c.cc.Invoke(ctx, Docker_CreateServiceContainer_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *dockerClient) InspectServiceContainer(ctx context.Context, in *InspectContainerRequest, opts ...grpc.CallOption) (*ServiceContainer, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(ServiceContainer)
|
||||||
|
err := c.cc.Invoke(ctx, Docker_InspectServiceContainer_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *dockerClient) ListServiceContainers(ctx context.Context, in *ListServiceContainersRequest, opts ...grpc.CallOption) (*ListServiceContainersResponse, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(ListServiceContainersResponse)
|
||||||
|
err := c.cc.Invoke(ctx, Docker_ListServiceContainers_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *dockerClient) RemoveServiceContainer(ctx context.Context, in *RemoveContainerRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(emptypb.Empty)
|
||||||
|
err := c.cc.Invoke(ctx, Docker_RemoveServiceContainer_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DockerServer is the server API for Docker service.
|
||||||
|
// All implementations must embed UnimplementedDockerServer
|
||||||
|
// for forward compatibility.
|
||||||
|
type DockerServer interface {
|
||||||
|
CreateContainer(context.Context, *CreateContainerRequest) (*CreateContainerResponse, error)
|
||||||
|
InspectContainer(context.Context, *InspectContainerRequest) (*InspectContainerResponse, error)
|
||||||
|
StartContainer(context.Context, *StartContainerRequest) (*emptypb.Empty, error)
|
||||||
|
StopContainer(context.Context, *StopContainerRequest) (*emptypb.Empty, error)
|
||||||
|
ListContainers(context.Context, *ListContainersRequest) (*ListContainersResponse, error)
|
||||||
|
RemoveContainer(context.Context, *RemoveContainerRequest) (*emptypb.Empty, error)
|
||||||
|
PullImage(*PullImageRequest, grpc.ServerStreamingServer[JSONMessage]) error
|
||||||
|
InspectImage(context.Context, *InspectImageRequest) (*InspectImageResponse, error)
|
||||||
|
// InspectRemoteImage returns the image metadata for an image in a remote registry using the machine's
|
||||||
|
// Docker auth credentials if necessary.
|
||||||
|
InspectRemoteImage(context.Context, *InspectRemoteImageRequest) (*InspectRemoteImageResponse, error)
|
||||||
|
CreateVolume(context.Context, *CreateVolumeRequest) (*CreateVolumeResponse, error)
|
||||||
|
ListVolumes(context.Context, *ListVolumesRequest) (*ListVolumesResponse, error)
|
||||||
|
RemoveVolume(context.Context, *RemoveVolumeRequest) (*emptypb.Empty, error)
|
||||||
|
CreateServiceContainer(context.Context, *CreateServiceContainerRequest) (*CreateContainerResponse, error)
|
||||||
|
InspectServiceContainer(context.Context, *InspectContainerRequest) (*ServiceContainer, error)
|
||||||
|
ListServiceContainers(context.Context, *ListServiceContainersRequest) (*ListServiceContainersResponse, error)
|
||||||
|
RemoveServiceContainer(context.Context, *RemoveContainerRequest) (*emptypb.Empty, error)
|
||||||
|
mustEmbedUnimplementedDockerServer()
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnimplementedDockerServer must be embedded to have
|
||||||
|
// forward compatible implementations.
|
||||||
|
//
|
||||||
|
// NOTE: this should be embedded by value instead of pointer to avoid a nil
|
||||||
|
// pointer dereference when methods are called.
|
||||||
|
type UnimplementedDockerServer struct{}
|
||||||
|
|
||||||
|
func (UnimplementedDockerServer) CreateContainer(context.Context, *CreateContainerRequest) (*CreateContainerResponse, error) {
|
||||||
|
return nil, status.Errorf(codes.Unimplemented, "method CreateContainer not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedDockerServer) 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) {
|
||||||
|
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) {
|
||||||
|
return nil, status.Errorf(codes.Unimplemented, "method ListContainers not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedDockerServer) RemoveContainer(context.Context, *RemoveContainerRequest) (*emptypb.Empty, error) {
|
||||||
|
return nil, status.Errorf(codes.Unimplemented, "method RemoveContainer not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedDockerServer) PullImage(*PullImageRequest, grpc.ServerStreamingServer[JSONMessage]) error {
|
||||||
|
return status.Errorf(codes.Unimplemented, "method PullImage not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedDockerServer) InspectImage(context.Context, *InspectImageRequest) (*InspectImageResponse, error) {
|
||||||
|
return nil, status.Errorf(codes.Unimplemented, "method InspectImage not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedDockerServer) InspectRemoteImage(context.Context, *InspectRemoteImageRequest) (*InspectRemoteImageResponse, error) {
|
||||||
|
return nil, status.Errorf(codes.Unimplemented, "method InspectRemoteImage not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedDockerServer) CreateVolume(context.Context, *CreateVolumeRequest) (*CreateVolumeResponse, error) {
|
||||||
|
return nil, status.Errorf(codes.Unimplemented, "method CreateVolume not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedDockerServer) ListVolumes(context.Context, *ListVolumesRequest) (*ListVolumesResponse, error) {
|
||||||
|
return nil, status.Errorf(codes.Unimplemented, "method ListVolumes not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedDockerServer) RemoveVolume(context.Context, *RemoveVolumeRequest) (*emptypb.Empty, error) {
|
||||||
|
return nil, status.Errorf(codes.Unimplemented, "method RemoveVolume not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedDockerServer) CreateServiceContainer(context.Context, *CreateServiceContainerRequest) (*CreateContainerResponse, error) {
|
||||||
|
return nil, status.Errorf(codes.Unimplemented, "method CreateServiceContainer not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedDockerServer) InspectServiceContainer(context.Context, *InspectContainerRequest) (*ServiceContainer, error) {
|
||||||
|
return nil, status.Errorf(codes.Unimplemented, "method InspectServiceContainer not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedDockerServer) ListServiceContainers(context.Context, *ListServiceContainersRequest) (*ListServiceContainersResponse, error) {
|
||||||
|
return nil, status.Errorf(codes.Unimplemented, "method ListServiceContainers not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedDockerServer) RemoveServiceContainer(context.Context, *RemoveContainerRequest) (*emptypb.Empty, error) {
|
||||||
|
return nil, status.Errorf(codes.Unimplemented, "method RemoveServiceContainer not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedDockerServer) mustEmbedUnimplementedDockerServer() {}
|
||||||
|
func (UnimplementedDockerServer) testEmbeddedByValue() {}
|
||||||
|
|
||||||
|
// UnsafeDockerServer may be embedded to opt out of forward compatibility for this service.
|
||||||
|
// Use of this interface is not recommended, as added methods to DockerServer will
|
||||||
|
// result in compilation errors.
|
||||||
|
type UnsafeDockerServer interface {
|
||||||
|
mustEmbedUnimplementedDockerServer()
|
||||||
|
}
|
||||||
|
|
||||||
|
func RegisterDockerServer(s grpc.ServiceRegistrar, srv DockerServer) {
|
||||||
|
// If the following call pancis, it indicates UnimplementedDockerServer was
|
||||||
|
// embedded by pointer and is nil. This will cause panics if an
|
||||||
|
// unimplemented method is ever invoked, so we test this at initialization
|
||||||
|
// time to prevent it from happening at runtime later due to I/O.
|
||||||
|
if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
|
||||||
|
t.testEmbeddedByValue()
|
||||||
|
}
|
||||||
|
s.RegisterService(&Docker_ServiceDesc, srv)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _Docker_CreateContainer_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(CreateContainerRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(DockerServer).CreateContainer(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: Docker_CreateContainer_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(DockerServer).CreateContainer(ctx, req.(*CreateContainerRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _Docker_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) {
|
||||||
|
in := new(StartContainerRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(DockerServer).StartContainer(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: Docker_StartContainer_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(DockerServer).StartContainer(ctx, req.(*StartContainerRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _Docker_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) {
|
||||||
|
in := new(ListContainersRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(DockerServer).ListContainers(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: Docker_ListContainers_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(DockerServer).ListContainers(ctx, req.(*ListContainersRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _Docker_RemoveContainer_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(RemoveContainerRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(DockerServer).RemoveContainer(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: Docker_RemoveContainer_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(DockerServer).RemoveContainer(ctx, req.(*RemoveContainerRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _Docker_PullImage_Handler(srv interface{}, stream grpc.ServerStream) error {
|
||||||
|
m := new(PullImageRequest)
|
||||||
|
if err := stream.RecvMsg(m); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return srv.(DockerServer).PullImage(m, &grpc.GenericServerStream[PullImageRequest, JSONMessage]{ServerStream: stream})
|
||||||
|
}
|
||||||
|
|
||||||
|
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
|
||||||
|
type Docker_PullImageServer = grpc.ServerStreamingServer[JSONMessage]
|
||||||
|
|
||||||
|
func _Docker_InspectImage_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(InspectImageRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(DockerServer).InspectImage(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: Docker_InspectImage_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(DockerServer).InspectImage(ctx, req.(*InspectImageRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _Docker_InspectRemoteImage_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(InspectRemoteImageRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(DockerServer).InspectRemoteImage(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: Docker_InspectRemoteImage_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(DockerServer).InspectRemoteImage(ctx, req.(*InspectRemoteImageRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _Docker_CreateVolume_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(CreateVolumeRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(DockerServer).CreateVolume(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: Docker_CreateVolume_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(DockerServer).CreateVolume(ctx, req.(*CreateVolumeRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _Docker_ListVolumes_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(ListVolumesRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(DockerServer).ListVolumes(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: Docker_ListVolumes_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(DockerServer).ListVolumes(ctx, req.(*ListVolumesRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _Docker_RemoveVolume_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(RemoveVolumeRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(DockerServer).RemoveVolume(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: Docker_RemoveVolume_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(DockerServer).RemoveVolume(ctx, req.(*RemoveVolumeRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _Docker_CreateServiceContainer_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(CreateServiceContainerRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(DockerServer).CreateServiceContainer(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: Docker_CreateServiceContainer_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(DockerServer).CreateServiceContainer(ctx, req.(*CreateServiceContainerRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _Docker_InspectServiceContainer_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(InspectContainerRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(DockerServer).InspectServiceContainer(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: Docker_InspectServiceContainer_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(DockerServer).InspectServiceContainer(ctx, req.(*InspectContainerRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _Docker_ListServiceContainers_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(ListServiceContainersRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(DockerServer).ListServiceContainers(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: Docker_ListServiceContainers_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(DockerServer).ListServiceContainers(ctx, req.(*ListServiceContainersRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _Docker_RemoveServiceContainer_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(RemoveContainerRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(DockerServer).RemoveServiceContainer(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: Docker_RemoveServiceContainer_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(DockerServer).RemoveServiceContainer(ctx, req.(*RemoveContainerRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Docker_ServiceDesc is the grpc.ServiceDesc for Docker service.
|
||||||
|
// It's only intended for direct use with grpc.RegisterService,
|
||||||
|
// and not to be introspected or modified (even as a copy)
|
||||||
|
var Docker_ServiceDesc = grpc.ServiceDesc{
|
||||||
|
ServiceName: "api.Docker",
|
||||||
|
HandlerType: (*DockerServer)(nil),
|
||||||
|
Methods: []grpc.MethodDesc{
|
||||||
|
{
|
||||||
|
MethodName: "CreateContainer",
|
||||||
|
Handler: _Docker_CreateContainer_Handler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MethodName: "InspectContainer",
|
||||||
|
Handler: _Docker_InspectContainer_Handler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MethodName: "StartContainer",
|
||||||
|
Handler: _Docker_StartContainer_Handler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MethodName: "StopContainer",
|
||||||
|
Handler: _Docker_StopContainer_Handler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MethodName: "ListContainers",
|
||||||
|
Handler: _Docker_ListContainers_Handler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MethodName: "RemoveContainer",
|
||||||
|
Handler: _Docker_RemoveContainer_Handler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MethodName: "InspectImage",
|
||||||
|
Handler: _Docker_InspectImage_Handler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MethodName: "InspectRemoteImage",
|
||||||
|
Handler: _Docker_InspectRemoteImage_Handler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MethodName: "CreateVolume",
|
||||||
|
Handler: _Docker_CreateVolume_Handler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MethodName: "ListVolumes",
|
||||||
|
Handler: _Docker_ListVolumes_Handler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MethodName: "RemoveVolume",
|
||||||
|
Handler: _Docker_RemoveVolume_Handler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MethodName: "CreateServiceContainer",
|
||||||
|
Handler: _Docker_CreateServiceContainer_Handler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MethodName: "InspectServiceContainer",
|
||||||
|
Handler: _Docker_InspectServiceContainer_Handler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MethodName: "ListServiceContainers",
|
||||||
|
Handler: _Docker_ListServiceContainers_Handler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MethodName: "RemoveServiceContainer",
|
||||||
|
Handler: _Docker_RemoveServiceContainer_Handler,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Streams: []grpc.StreamDesc{
|
||||||
|
{
|
||||||
|
StreamName: "PullImage",
|
||||||
|
Handler: _Docker_PullImage_Handler,
|
||||||
|
ServerStreams: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Metadata: "internal/machine/api/pb/docker.proto",
|
||||||
|
}
|
||||||
@@ -21,6 +21,205 @@ const (
|
|||||||
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type CheckPrerequisitesResponse struct {
|
||||||
|
state protoimpl.MessageState
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
|
||||||
|
// Overall status of the checks.
|
||||||
|
Satisfied bool `protobuf:"varint,1,opt,name=satisfied,proto3" json:"satisfied,omitempty"`
|
||||||
|
// Error message if not satisfied (empty if all checks pass).
|
||||||
|
Error string `protobuf:"bytes,2,opt,name=error,proto3" json:"error,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *CheckPrerequisitesResponse) Reset() {
|
||||||
|
*x = CheckPrerequisitesResponse{}
|
||||||
|
if protoimpl.UnsafeEnabled {
|
||||||
|
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[0]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *CheckPrerequisitesResponse) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*CheckPrerequisitesResponse) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *CheckPrerequisitesResponse) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[0]
|
||||||
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use CheckPrerequisitesResponse.ProtoReflect.Descriptor instead.
|
||||||
|
func (*CheckPrerequisitesResponse) Descriptor() ([]byte, []int) {
|
||||||
|
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{0}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *CheckPrerequisitesResponse) GetSatisfied() bool {
|
||||||
|
if x != nil {
|
||||||
|
return x.Satisfied
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *CheckPrerequisitesResponse) GetError() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Error
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
type MachineInfo struct {
|
||||||
|
state protoimpl.MessageState
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
|
||||||
|
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
|
||||||
|
Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`
|
||||||
|
Network *NetworkConfig `protobuf:"bytes,3,opt,name=network,proto3" json:"network,omitempty"`
|
||||||
|
PublicIp *IP `protobuf:"bytes,4,opt,name=public_ip,json=publicIp,proto3" json:"public_ip,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *MachineInfo) Reset() {
|
||||||
|
*x = MachineInfo{}
|
||||||
|
if protoimpl.UnsafeEnabled {
|
||||||
|
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[1]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *MachineInfo) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*MachineInfo) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *MachineInfo) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[1]
|
||||||
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use MachineInfo.ProtoReflect.Descriptor instead.
|
||||||
|
func (*MachineInfo) Descriptor() ([]byte, []int) {
|
||||||
|
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{1}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *MachineInfo) GetId() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Id
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *MachineInfo) GetName() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Name
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *MachineInfo) GetNetwork() *NetworkConfig {
|
||||||
|
if x != nil {
|
||||||
|
return x.Network
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *MachineInfo) GetPublicIp() *IP {
|
||||||
|
if x != nil {
|
||||||
|
return x.PublicIp
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type NetworkConfig struct {
|
||||||
|
state protoimpl.MessageState
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
|
||||||
|
Subnet *IPPrefix `protobuf:"bytes,1,opt,name=subnet,proto3" json:"subnet,omitempty"`
|
||||||
|
ManagementIp *IP `protobuf:"bytes,2,opt,name=management_ip,json=managementIp,proto3" json:"management_ip,omitempty"`
|
||||||
|
Endpoints []*IPPort `protobuf:"bytes,3,rep,name=endpoints,proto3" json:"endpoints,omitempty"`
|
||||||
|
PublicKey []byte `protobuf:"bytes,4,opt,name=public_key,json=publicKey,proto3" json:"public_key,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *NetworkConfig) Reset() {
|
||||||
|
*x = NetworkConfig{}
|
||||||
|
if protoimpl.UnsafeEnabled {
|
||||||
|
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[2]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *NetworkConfig) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*NetworkConfig) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *NetworkConfig) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[2]
|
||||||
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use NetworkConfig.ProtoReflect.Descriptor instead.
|
||||||
|
func (*NetworkConfig) Descriptor() ([]byte, []int) {
|
||||||
|
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{2}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *NetworkConfig) GetSubnet() *IPPrefix {
|
||||||
|
if x != nil {
|
||||||
|
return x.Subnet
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *NetworkConfig) GetManagementIp() *IP {
|
||||||
|
if x != nil {
|
||||||
|
return x.ManagementIp
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *NetworkConfig) GetEndpoints() []*IPPort {
|
||||||
|
if x != nil {
|
||||||
|
return x.Endpoints
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *NetworkConfig) GetPublicKey() []byte {
|
||||||
|
if x != nil {
|
||||||
|
return x.PublicKey
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
type InitClusterRequest struct {
|
type InitClusterRequest struct {
|
||||||
state protoimpl.MessageState
|
state protoimpl.MessageState
|
||||||
sizeCache protoimpl.SizeCache
|
sizeCache protoimpl.SizeCache
|
||||||
@@ -28,13 +227,17 @@ 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"`
|
||||||
User *User `protobuf:"bytes,3,opt,name=user,proto3" json:"user,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() {
|
||||||
*x = InitClusterRequest{}
|
*x = InitClusterRequest{}
|
||||||
if protoimpl.UnsafeEnabled {
|
if protoimpl.UnsafeEnabled {
|
||||||
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[0]
|
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[3]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@@ -47,7 +250,7 @@ func (x *InitClusterRequest) String() string {
|
|||||||
func (*InitClusterRequest) ProtoMessage() {}
|
func (*InitClusterRequest) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *InitClusterRequest) ProtoReflect() protoreflect.Message {
|
func (x *InitClusterRequest) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[0]
|
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[3]
|
||||||
if protoimpl.UnsafeEnabled && x != nil {
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
@@ -60,7 +263,7 @@ func (x *InitClusterRequest) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use InitClusterRequest.ProtoReflect.Descriptor instead.
|
// Deprecated: Use InitClusterRequest.ProtoReflect.Descriptor instead.
|
||||||
func (*InitClusterRequest) Descriptor() ([]byte, []int) {
|
func (*InitClusterRequest) Descriptor() ([]byte, []int) {
|
||||||
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{0}
|
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{3}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *InitClusterRequest) GetMachineName() string {
|
func (x *InitClusterRequest) GetMachineName() string {
|
||||||
@@ -77,13 +280,43 @@ func (x *InitClusterRequest) GetNetwork() *IPPrefix {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *InitClusterRequest) GetUser() *User {
|
func (m *InitClusterRequest) GetPublicIpConfig() isInitClusterRequest_PublicIpConfig {
|
||||||
if x != nil {
|
if m != nil {
|
||||||
return x.User
|
return m.PublicIpConfig
|
||||||
}
|
}
|
||||||
return nil
|
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
|
||||||
@@ -95,7 +328,7 @@ type InitClusterResponse struct {
|
|||||||
func (x *InitClusterResponse) Reset() {
|
func (x *InitClusterResponse) Reset() {
|
||||||
*x = InitClusterResponse{}
|
*x = InitClusterResponse{}
|
||||||
if protoimpl.UnsafeEnabled {
|
if protoimpl.UnsafeEnabled {
|
||||||
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[1]
|
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[4]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@@ -108,7 +341,7 @@ func (x *InitClusterResponse) String() string {
|
|||||||
func (*InitClusterResponse) ProtoMessage() {}
|
func (*InitClusterResponse) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *InitClusterResponse) ProtoReflect() protoreflect.Message {
|
func (x *InitClusterResponse) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[1]
|
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[4]
|
||||||
if protoimpl.UnsafeEnabled && x != nil {
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
@@ -121,7 +354,7 @@ func (x *InitClusterResponse) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use InitClusterResponse.ProtoReflect.Descriptor instead.
|
// Deprecated: Use InitClusterResponse.ProtoReflect.Descriptor instead.
|
||||||
func (*InitClusterResponse) Descriptor() ([]byte, []int) {
|
func (*InitClusterResponse) Descriptor() ([]byte, []int) {
|
||||||
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{1}
|
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{4}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *InitClusterResponse) GetMachine() *MachineInfo {
|
func (x *InitClusterResponse) GetMachine() *MachineInfo {
|
||||||
@@ -143,7 +376,7 @@ type JoinClusterRequest struct {
|
|||||||
func (x *JoinClusterRequest) Reset() {
|
func (x *JoinClusterRequest) Reset() {
|
||||||
*x = JoinClusterRequest{}
|
*x = JoinClusterRequest{}
|
||||||
if protoimpl.UnsafeEnabled {
|
if protoimpl.UnsafeEnabled {
|
||||||
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[2]
|
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[5]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@@ -156,7 +389,7 @@ func (x *JoinClusterRequest) String() string {
|
|||||||
func (*JoinClusterRequest) ProtoMessage() {}
|
func (*JoinClusterRequest) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *JoinClusterRequest) ProtoReflect() protoreflect.Message {
|
func (x *JoinClusterRequest) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[2]
|
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[5]
|
||||||
if protoimpl.UnsafeEnabled && x != nil {
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
@@ -169,7 +402,7 @@ func (x *JoinClusterRequest) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use JoinClusterRequest.ProtoReflect.Descriptor instead.
|
// Deprecated: Use JoinClusterRequest.ProtoReflect.Descriptor instead.
|
||||||
func (*JoinClusterRequest) Descriptor() ([]byte, []int) {
|
func (*JoinClusterRequest) Descriptor() ([]byte, []int) {
|
||||||
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{2}
|
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{5}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *JoinClusterRequest) GetMachine() *MachineInfo {
|
func (x *JoinClusterRequest) GetMachine() *MachineInfo {
|
||||||
@@ -197,7 +430,7 @@ type TokenResponse struct {
|
|||||||
func (x *TokenResponse) Reset() {
|
func (x *TokenResponse) Reset() {
|
||||||
*x = TokenResponse{}
|
*x = TokenResponse{}
|
||||||
if protoimpl.UnsafeEnabled {
|
if protoimpl.UnsafeEnabled {
|
||||||
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[3]
|
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[6]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@@ -210,7 +443,7 @@ func (x *TokenResponse) String() string {
|
|||||||
func (*TokenResponse) ProtoMessage() {}
|
func (*TokenResponse) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *TokenResponse) ProtoReflect() protoreflect.Message {
|
func (x *TokenResponse) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[3]
|
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[6]
|
||||||
if protoimpl.UnsafeEnabled && x != nil {
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
@@ -223,7 +456,7 @@ func (x *TokenResponse) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use TokenResponse.ProtoReflect.Descriptor instead.
|
// Deprecated: Use TokenResponse.ProtoReflect.Descriptor instead.
|
||||||
func (*TokenResponse) Descriptor() ([]byte, []int) {
|
func (*TokenResponse) Descriptor() ([]byte, []int) {
|
||||||
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{3}
|
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{6}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *TokenResponse) GetToken() string {
|
func (x *TokenResponse) GetToken() string {
|
||||||
@@ -233,6 +466,227 @@ func (x *TokenResponse) GetToken() string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Service struct {
|
||||||
|
state protoimpl.MessageState
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
|
||||||
|
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
|
||||||
|
Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`
|
||||||
|
Mode string `protobuf:"bytes,3,opt,name=mode,proto3" json:"mode,omitempty"`
|
||||||
|
Containers []*Service_Container `protobuf:"bytes,4,rep,name=containers,proto3" json:"containers,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Service) Reset() {
|
||||||
|
*x = Service{}
|
||||||
|
if protoimpl.UnsafeEnabled {
|
||||||
|
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[7]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Service) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*Service) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *Service) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[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 Service.ProtoReflect.Descriptor instead.
|
||||||
|
func (*Service) Descriptor() ([]byte, []int) {
|
||||||
|
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{7}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Service) GetId() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Id
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Service) GetName() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Name
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Service) GetMode() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Mode
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Service) GetContainers() []*Service_Container {
|
||||||
|
if x != nil {
|
||||||
|
return x.Containers
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type InspectServiceRequest struct {
|
||||||
|
state protoimpl.MessageState
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
|
||||||
|
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *InspectServiceRequest) Reset() {
|
||||||
|
*x = InspectServiceRequest{}
|
||||||
|
if protoimpl.UnsafeEnabled {
|
||||||
|
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[8]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *InspectServiceRequest) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*InspectServiceRequest) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *InspectServiceRequest) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[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 InspectServiceRequest.ProtoReflect.Descriptor instead.
|
||||||
|
func (*InspectServiceRequest) Descriptor() ([]byte, []int) {
|
||||||
|
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{8}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *InspectServiceRequest) GetId() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Id
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
type InspectServiceResponse struct {
|
||||||
|
state protoimpl.MessageState
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
|
||||||
|
Service *Service `protobuf:"bytes,1,opt,name=service,proto3" json:"service,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *InspectServiceResponse) Reset() {
|
||||||
|
*x = InspectServiceResponse{}
|
||||||
|
if protoimpl.UnsafeEnabled {
|
||||||
|
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[9]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *InspectServiceResponse) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*InspectServiceResponse) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *InspectServiceResponse) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[9]
|
||||||
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use InspectServiceResponse.ProtoReflect.Descriptor instead.
|
||||||
|
func (*InspectServiceResponse) Descriptor() ([]byte, []int) {
|
||||||
|
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{9}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *InspectServiceResponse) GetService() *Service {
|
||||||
|
if x != nil {
|
||||||
|
return x.Service
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type Service_Container struct {
|
||||||
|
state protoimpl.MessageState
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
|
||||||
|
MachineId string `protobuf:"bytes,1,opt,name=machine_id,json=machineId,proto3" json:"machine_id,omitempty"`
|
||||||
|
// JSON encoded Docker types.Container.
|
||||||
|
Container []byte `protobuf:"bytes,2,opt,name=container,proto3" json:"container,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Service_Container) Reset() {
|
||||||
|
*x = Service_Container{}
|
||||||
|
if protoimpl.UnsafeEnabled {
|
||||||
|
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[10]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Service_Container) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*Service_Container) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *Service_Container) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[10]
|
||||||
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use Service_Container.ProtoReflect.Descriptor instead.
|
||||||
|
func (*Service_Container) Descriptor() ([]byte, []int) {
|
||||||
|
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{7, 0}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Service_Container) GetMachineId() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.MachineId
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Service_Container) GetContainer() []byte {
|
||||||
|
if x != nil {
|
||||||
|
return x.Container
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
var File_internal_machine_api_pb_machine_proto protoreflect.FileDescriptor
|
var File_internal_machine_api_pb_machine_proto protoreflect.FileDescriptor
|
||||||
|
|
||||||
var file_internal_machine_api_pb_machine_proto_rawDesc = []byte{
|
var file_internal_machine_api_pb_machine_proto_rawDesc = []byte{
|
||||||
@@ -242,52 +696,107 @@ var file_internal_machine_api_pb_machine_proto_rawDesc = []byte{
|
|||||||
0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x65, 0x6d,
|
0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x65, 0x6d,
|
||||||
0x70, 0x74, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x24, 0x69, 0x6e, 0x74, 0x65, 0x72,
|
0x70, 0x74, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 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, 0x1a,
|
0x70, 0x62, 0x2f, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22,
|
||||||
0x25, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e,
|
0x50, 0x0a, 0x1a, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x50, 0x72, 0x65, 0x72, 0x65, 0x71, 0x75, 0x69,
|
||||||
0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x62, 0x2f, 0x63, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72,
|
0x73, 0x69, 0x74, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1c, 0x0a,
|
||||||
0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x7e, 0x0a, 0x12, 0x49, 0x6e, 0x69, 0x74, 0x43, 0x6c,
|
0x09, 0x73, 0x61, 0x74, 0x69, 0x73, 0x66, 0x69, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08,
|
||||||
0x75, 0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x20, 0x0a, 0x0b,
|
0x52, 0x09, 0x73, 0x61, 0x74, 0x69, 0x73, 0x66, 0x69, 0x65, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x65,
|
||||||
0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28,
|
0x72, 0x72, 0x6f, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f,
|
||||||
0x09, 0x52, 0x0b, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x27,
|
0x72, 0x22, 0x85, 0x01, 0x0a, 0x0b, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66,
|
||||||
0x0a, 0x07, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32,
|
0x6f, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69,
|
||||||
0x0d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x50, 0x50, 0x72, 0x65, 0x66, 0x69, 0x78, 0x52, 0x07,
|
0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52,
|
||||||
0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x12, 0x1d, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18,
|
0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x2c, 0x0a, 0x07, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b,
|
||||||
0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x09, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x55, 0x73, 0x65, 0x72,
|
0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4e, 0x65, 0x74,
|
||||||
0x52, 0x04, 0x75, 0x73, 0x65, 0x72, 0x22, 0x41, 0x0a, 0x13, 0x49, 0x6e, 0x69, 0x74, 0x43, 0x6c,
|
0x77, 0x6f, 0x72, 0x6b, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x07, 0x6e, 0x65, 0x74, 0x77,
|
||||||
0x75, 0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2a, 0x0a,
|
0x6f, 0x72, 0x6b, 0x12, 0x24, 0x0a, 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x69, 0x70,
|
||||||
0x07, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10,
|
0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x07, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x50, 0x52,
|
||||||
0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f,
|
0x08, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x49, 0x70, 0x22, 0xae, 0x01, 0x0a, 0x0d, 0x4e, 0x65,
|
||||||
0x52, 0x07, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x22, 0x79, 0x0a, 0x12, 0x4a, 0x6f, 0x69,
|
0x74, 0x77, 0x6f, 0x72, 0x6b, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x25, 0x0a, 0x06, 0x73,
|
||||||
0x6e, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12,
|
0x75, 0x62, 0x6e, 0x65, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x61, 0x70,
|
||||||
0x2a, 0x0a, 0x07, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b,
|
0x69, 0x2e, 0x49, 0x50, 0x50, 0x72, 0x65, 0x66, 0x69, 0x78, 0x52, 0x06, 0x73, 0x75, 0x62, 0x6e,
|
||||||
0x32, 0x10, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e,
|
0x65, 0x74, 0x12, 0x2c, 0x0a, 0x0d, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74,
|
||||||
0x66, 0x6f, 0x52, 0x07, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x12, 0x37, 0x0a, 0x0e, 0x6f,
|
0x5f, 0x69, 0x70, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x07, 0x2e, 0x61, 0x70, 0x69, 0x2e,
|
||||||
0x74, 0x68, 0x65, 0x72, 0x5f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x18, 0x03, 0x20,
|
0x49, 0x50, 0x52, 0x0c, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x49, 0x70,
|
||||||
0x03, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e,
|
0x12, 0x29, 0x0a, 0x09, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x18, 0x03, 0x20,
|
||||||
0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x0d, 0x6f, 0x74, 0x68, 0x65, 0x72, 0x4d, 0x61, 0x63, 0x68,
|
0x03, 0x28, 0x0b, 0x32, 0x0b, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x50, 0x50, 0x6f, 0x72, 0x74,
|
||||||
0x69, 0x6e, 0x65, 0x73, 0x22, 0x25, 0x0a, 0x0d, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x73,
|
0x52, 0x09, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x70,
|
||||||
0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01,
|
0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52,
|
||||||
0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x32, 0xf5, 0x01, 0x0a, 0x07,
|
0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x22, 0xc3, 0x01, 0x0a, 0x12, 0x49,
|
||||||
0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x12, 0x40, 0x0a, 0x0b, 0x49, 0x6e, 0x69, 0x74, 0x43,
|
0x6e, 0x69, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
|
||||||
0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x12, 0x17, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x69,
|
0x74, 0x12, 0x20, 0x0a, 0x0b, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x4e, 0x61, 0x6d, 0x65,
|
||||||
0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a,
|
0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x4e,
|
||||||
0x18, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x69, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65,
|
0x61, 0x6d, 0x65, 0x12, 0x27, 0x0a, 0x07, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x18, 0x02,
|
||||||
0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3e, 0x0a, 0x0b, 0x4a, 0x6f, 0x69,
|
0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x50, 0x50, 0x72, 0x65,
|
||||||
0x6e, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x12, 0x17, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4a,
|
0x66, 0x69, 0x78, 0x52, 0x07, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x12, 0x26, 0x0a, 0x09,
|
||||||
0x6f, 0x69, 0x6e, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
|
0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x69, 0x70, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32,
|
||||||
0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f,
|
0x07, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x50, 0x48, 0x00, 0x52, 0x08, 0x70, 0x75, 0x62, 0x6c,
|
||||||
0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x33, 0x0a, 0x05, 0x54, 0x6f, 0x6b,
|
0x69, 0x63, 0x49, 0x70, 0x12, 0x26, 0x0a, 0x0e, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x69,
|
||||||
0x65, 0x6e, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74,
|
0x70, 0x5f, 0x61, 0x75, 0x74, 0x6f, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x48, 0x00, 0x52, 0x0c,
|
||||||
0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x12, 0x2e, 0x61, 0x70, 0x69,
|
0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x49, 0x70, 0x41, 0x75, 0x74, 0x6f, 0x42, 0x12, 0x0a, 0x10,
|
||||||
0x2e, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x33,
|
0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x69, 0x70, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67,
|
||||||
0x0a, 0x07, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67,
|
0x22, 0x41, 0x0a, 0x13, 0x49, 0x6e, 0x69, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x52,
|
||||||
0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74,
|
0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2a, 0x0a, 0x07, 0x6d, 0x61, 0x63, 0x68, 0x69,
|
||||||
0x79, 0x1a, 0x10, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49,
|
0x6e, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d,
|
||||||
0x6e, 0x66, 0x6f, 0x42, 0x37, 0x5a, 0x35, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f,
|
0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x07, 0x6d, 0x61, 0x63, 0x68,
|
||||||
0x6d, 0x2f, 0x70, 0x73, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x6b, 0x69, 0x2f, 0x75, 0x6e, 0x63,
|
0x69, 0x6e, 0x65, 0x22, 0x79, 0x0a, 0x12, 0x4a, 0x6f, 0x69, 0x6e, 0x43, 0x6c, 0x75, 0x73, 0x74,
|
||||||
0x6c, 0x6f, 0x75, 0x64, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x6d, 0x61,
|
0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2a, 0x0a, 0x07, 0x6d, 0x61, 0x63,
|
||||||
0x63, 0x68, 0x69, 0x6e, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72,
|
0x68, 0x69, 0x6e, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x61, 0x70, 0x69,
|
||||||
0x6f, 0x74, 0x6f, 0x33,
|
0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x07, 0x6d, 0x61,
|
||||||
|
0x63, 0x68, 0x69, 0x6e, 0x65, 0x12, 0x37, 0x0a, 0x0e, 0x6f, 0x74, 0x68, 0x65, 0x72, 0x5f, 0x6d,
|
||||||
|
0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x10, 0x2e,
|
||||||
|
0x61, 0x70, 0x69, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52,
|
||||||
|
0x0d, 0x6f, 0x74, 0x68, 0x65, 0x72, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x22, 0x25,
|
||||||
|
0x0a, 0x0d, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12,
|
||||||
|
0x14, 0x0a, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05,
|
||||||
|
0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0xc3, 0x01, 0x0a, 0x07, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63,
|
||||||
|
0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69,
|
||||||
|
0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52,
|
||||||
|
0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x18, 0x03, 0x20,
|
||||||
|
0x01, 0x28, 0x09, 0x52, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x12, 0x36, 0x0a, 0x0a, 0x63, 0x6f, 0x6e,
|
||||||
|
0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e,
|
||||||
|
0x61, 0x70, 0x69, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x43, 0x6f, 0x6e, 0x74,
|
||||||
|
0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x0a, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72,
|
||||||
|
0x73, 0x1a, 0x48, 0x0a, 0x09, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x1d,
|
||||||
|
0x0a, 0x0a, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01,
|
||||||
|
0x28, 0x09, 0x52, 0x09, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x64, 0x12, 0x1c, 0x0a,
|
||||||
|
0x09, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c,
|
||||||
|
0x52, 0x09, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x22, 0x27, 0x0a, 0x15, 0x49,
|
||||||
|
0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x71,
|
||||||
|
0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
|
||||||
|
0x52, 0x02, 0x69, 0x64, 0x22, 0x40, 0x0a, 0x16, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53,
|
||||||
|
0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x26,
|
||||||
|
0x0a, 0x07, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32,
|
||||||
|
0x0c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x07, 0x73,
|
||||||
|
0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x32, 0x8f, 0x03, 0x0a, 0x07, 0x4d, 0x61, 0x63, 0x68, 0x69,
|
||||||
|
0x6e, 0x65, 0x12, 0x4d, 0x0a, 0x12, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x50, 0x72, 0x65, 0x72, 0x65,
|
||||||
|
0x71, 0x75, 0x69, 0x73, 0x69, 0x74, 0x65, 0x73, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c,
|
||||||
|
0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79,
|
||||||
|
0x1a, 0x1f, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x50, 0x72, 0x65, 0x72,
|
||||||
|
0x65, 0x71, 0x75, 0x69, 0x73, 0x69, 0x74, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
|
||||||
|
0x65, 0x12, 0x40, 0x0a, 0x0b, 0x49, 0x6e, 0x69, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72,
|
||||||
|
0x12, 0x17, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x69, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74,
|
||||||
|
0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x61, 0x70, 0x69, 0x2e,
|
||||||
|
0x49, 0x6e, 0x69, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f,
|
||||||
|
0x6e, 0x73, 0x65, 0x12, 0x3e, 0x0a, 0x0b, 0x4a, 0x6f, 0x69, 0x6e, 0x43, 0x6c, 0x75, 0x73, 0x74,
|
||||||
|
0x65, 0x72, 0x12, 0x17, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4a, 0x6f, 0x69, 0x6e, 0x43, 0x6c, 0x75,
|
||||||
|
0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f,
|
||||||
|
0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d,
|
||||||
|
0x70, 0x74, 0x79, 0x12, 0x33, 0x0a, 0x05, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x16, 0x2e, 0x67,
|
||||||
|
0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45,
|
||||||
|
0x6d, 0x70, 0x74, 0x79, 0x1a, 0x12, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x54, 0x6f, 0x6b, 0x65, 0x6e,
|
||||||
|
0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x33, 0x0a, 0x07, 0x49, 0x6e, 0x73, 0x70,
|
||||||
|
0x65, 0x63, 0x74, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f,
|
||||||
|
0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x10, 0x2e, 0x61, 0x70,
|
||||||
|
0x69, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x49, 0x0a,
|
||||||
|
0x0e, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12,
|
||||||
|
0x1a, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72,
|
||||||
|
0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x61, 0x70,
|
||||||
|
0x69, 0x2e, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65,
|
||||||
|
0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x37, 0x5a, 0x35, 0x67, 0x69, 0x74, 0x68,
|
||||||
|
0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x70, 0x73, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x6b,
|
||||||
|
0x69, 0x2f, 0x75, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e,
|
||||||
|
0x61, 0x6c, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70,
|
||||||
|
0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -302,36 +811,54 @@ func file_internal_machine_api_pb_machine_proto_rawDescGZIP() []byte {
|
|||||||
return file_internal_machine_api_pb_machine_proto_rawDescData
|
return file_internal_machine_api_pb_machine_proto_rawDescData
|
||||||
}
|
}
|
||||||
|
|
||||||
var file_internal_machine_api_pb_machine_proto_msgTypes = make([]protoimpl.MessageInfo, 4)
|
var file_internal_machine_api_pb_machine_proto_msgTypes = make([]protoimpl.MessageInfo, 11)
|
||||||
var file_internal_machine_api_pb_machine_proto_goTypes = []any{
|
var file_internal_machine_api_pb_machine_proto_goTypes = []any{
|
||||||
(*InitClusterRequest)(nil), // 0: api.InitClusterRequest
|
(*CheckPrerequisitesResponse)(nil), // 0: api.CheckPrerequisitesResponse
|
||||||
(*InitClusterResponse)(nil), // 1: api.InitClusterResponse
|
(*MachineInfo)(nil), // 1: api.MachineInfo
|
||||||
(*JoinClusterRequest)(nil), // 2: api.JoinClusterRequest
|
(*NetworkConfig)(nil), // 2: api.NetworkConfig
|
||||||
(*TokenResponse)(nil), // 3: api.TokenResponse
|
(*InitClusterRequest)(nil), // 3: api.InitClusterRequest
|
||||||
(*IPPrefix)(nil), // 4: api.IPPrefix
|
(*InitClusterResponse)(nil), // 4: api.InitClusterResponse
|
||||||
(*User)(nil), // 5: api.User
|
(*JoinClusterRequest)(nil), // 5: api.JoinClusterRequest
|
||||||
(*MachineInfo)(nil), // 6: api.MachineInfo
|
(*TokenResponse)(nil), // 6: api.TokenResponse
|
||||||
(*emptypb.Empty)(nil), // 7: google.protobuf.Empty
|
(*Service)(nil), // 7: api.Service
|
||||||
|
(*InspectServiceRequest)(nil), // 8: api.InspectServiceRequest
|
||||||
|
(*InspectServiceResponse)(nil), // 9: api.InspectServiceResponse
|
||||||
|
(*Service_Container)(nil), // 10: api.Service.Container
|
||||||
|
(*IP)(nil), // 11: api.IP
|
||||||
|
(*IPPrefix)(nil), // 12: api.IPPrefix
|
||||||
|
(*IPPort)(nil), // 13: api.IPPort
|
||||||
|
(*emptypb.Empty)(nil), // 14: google.protobuf.Empty
|
||||||
}
|
}
|
||||||
var file_internal_machine_api_pb_machine_proto_depIdxs = []int32{
|
var file_internal_machine_api_pb_machine_proto_depIdxs = []int32{
|
||||||
4, // 0: api.InitClusterRequest.network:type_name -> api.IPPrefix
|
2, // 0: api.MachineInfo.network:type_name -> api.NetworkConfig
|
||||||
5, // 1: api.InitClusterRequest.user:type_name -> api.User
|
11, // 1: api.MachineInfo.public_ip:type_name -> api.IP
|
||||||
6, // 2: api.InitClusterResponse.machine:type_name -> api.MachineInfo
|
12, // 2: api.NetworkConfig.subnet:type_name -> api.IPPrefix
|
||||||
6, // 3: api.JoinClusterRequest.machine:type_name -> api.MachineInfo
|
11, // 3: api.NetworkConfig.management_ip:type_name -> api.IP
|
||||||
6, // 4: api.JoinClusterRequest.other_machines:type_name -> api.MachineInfo
|
13, // 4: api.NetworkConfig.endpoints:type_name -> api.IPPort
|
||||||
0, // 5: api.Machine.InitCluster:input_type -> api.InitClusterRequest
|
12, // 5: api.InitClusterRequest.network:type_name -> api.IPPrefix
|
||||||
2, // 6: api.Machine.JoinCluster:input_type -> api.JoinClusterRequest
|
11, // 6: api.InitClusterRequest.public_ip:type_name -> api.IP
|
||||||
7, // 7: api.Machine.Token:input_type -> google.protobuf.Empty
|
1, // 7: api.InitClusterResponse.machine:type_name -> api.MachineInfo
|
||||||
7, // 8: api.Machine.Inspect:input_type -> google.protobuf.Empty
|
1, // 8: api.JoinClusterRequest.machine:type_name -> api.MachineInfo
|
||||||
1, // 9: api.Machine.InitCluster:output_type -> api.InitClusterResponse
|
1, // 9: api.JoinClusterRequest.other_machines:type_name -> api.MachineInfo
|
||||||
7, // 10: api.Machine.JoinCluster:output_type -> google.protobuf.Empty
|
10, // 10: api.Service.containers:type_name -> api.Service.Container
|
||||||
3, // 11: api.Machine.Token:output_type -> api.TokenResponse
|
7, // 11: api.InspectServiceResponse.service:type_name -> api.Service
|
||||||
6, // 12: api.Machine.Inspect:output_type -> api.MachineInfo
|
14, // 12: api.Machine.CheckPrerequisites:input_type -> google.protobuf.Empty
|
||||||
9, // [9:13] is the sub-list for method output_type
|
3, // 13: api.Machine.InitCluster:input_type -> api.InitClusterRequest
|
||||||
5, // [5:9] is the sub-list for method input_type
|
5, // 14: api.Machine.JoinCluster:input_type -> api.JoinClusterRequest
|
||||||
5, // [5:5] is the sub-list for extension type_name
|
14, // 15: api.Machine.Token:input_type -> google.protobuf.Empty
|
||||||
5, // [5:5] is the sub-list for extension extendee
|
14, // 16: api.Machine.Inspect:input_type -> google.protobuf.Empty
|
||||||
0, // [0:5] is the sub-list for field type_name
|
8, // 17: api.Machine.InspectService:input_type -> api.InspectServiceRequest
|
||||||
|
0, // 18: api.Machine.CheckPrerequisites:output_type -> api.CheckPrerequisitesResponse
|
||||||
|
4, // 19: api.Machine.InitCluster:output_type -> api.InitClusterResponse
|
||||||
|
14, // 20: api.Machine.JoinCluster:output_type -> google.protobuf.Empty
|
||||||
|
6, // 21: api.Machine.Token:output_type -> api.TokenResponse
|
||||||
|
1, // 22: api.Machine.Inspect:output_type -> api.MachineInfo
|
||||||
|
9, // 23: api.Machine.InspectService:output_type -> api.InspectServiceResponse
|
||||||
|
18, // [18:24] is the sub-list for method output_type
|
||||||
|
12, // [12:18] is the sub-list for method input_type
|
||||||
|
12, // [12:12] is the sub-list for extension type_name
|
||||||
|
12, // [12:12] is the sub-list for extension extendee
|
||||||
|
0, // [0:12] is the sub-list for field type_name
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() { file_internal_machine_api_pb_machine_proto_init() }
|
func init() { file_internal_machine_api_pb_machine_proto_init() }
|
||||||
@@ -340,10 +867,9 @@ func file_internal_machine_api_pb_machine_proto_init() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
file_internal_machine_api_pb_common_proto_init()
|
file_internal_machine_api_pb_common_proto_init()
|
||||||
file_internal_machine_api_pb_cluster_proto_init()
|
|
||||||
if !protoimpl.UnsafeEnabled {
|
if !protoimpl.UnsafeEnabled {
|
||||||
file_internal_machine_api_pb_machine_proto_msgTypes[0].Exporter = func(v any, i int) any {
|
file_internal_machine_api_pb_machine_proto_msgTypes[0].Exporter = func(v any, i int) any {
|
||||||
switch v := v.(*InitClusterRequest); i {
|
switch v := v.(*CheckPrerequisitesResponse); i {
|
||||||
case 0:
|
case 0:
|
||||||
return &v.state
|
return &v.state
|
||||||
case 1:
|
case 1:
|
||||||
@@ -355,7 +881,7 @@ func file_internal_machine_api_pb_machine_proto_init() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
file_internal_machine_api_pb_machine_proto_msgTypes[1].Exporter = func(v any, i int) any {
|
file_internal_machine_api_pb_machine_proto_msgTypes[1].Exporter = func(v any, i int) any {
|
||||||
switch v := v.(*InitClusterResponse); i {
|
switch v := v.(*MachineInfo); i {
|
||||||
case 0:
|
case 0:
|
||||||
return &v.state
|
return &v.state
|
||||||
case 1:
|
case 1:
|
||||||
@@ -367,7 +893,7 @@ func file_internal_machine_api_pb_machine_proto_init() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
file_internal_machine_api_pb_machine_proto_msgTypes[2].Exporter = func(v any, i int) any {
|
file_internal_machine_api_pb_machine_proto_msgTypes[2].Exporter = func(v any, i int) any {
|
||||||
switch v := v.(*JoinClusterRequest); i {
|
switch v := v.(*NetworkConfig); i {
|
||||||
case 0:
|
case 0:
|
||||||
return &v.state
|
return &v.state
|
||||||
case 1:
|
case 1:
|
||||||
@@ -379,6 +905,42 @@ func file_internal_machine_api_pb_machine_proto_init() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
file_internal_machine_api_pb_machine_proto_msgTypes[3].Exporter = func(v any, i int) any {
|
file_internal_machine_api_pb_machine_proto_msgTypes[3].Exporter = func(v any, i int) any {
|
||||||
|
switch v := v.(*InitClusterRequest); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file_internal_machine_api_pb_machine_proto_msgTypes[4].Exporter = func(v any, i int) any {
|
||||||
|
switch v := v.(*InitClusterResponse); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file_internal_machine_api_pb_machine_proto_msgTypes[5].Exporter = func(v any, i int) any {
|
||||||
|
switch v := v.(*JoinClusterRequest); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file_internal_machine_api_pb_machine_proto_msgTypes[6].Exporter = func(v any, i int) any {
|
||||||
switch v := v.(*TokenResponse); i {
|
switch v := v.(*TokenResponse); i {
|
||||||
case 0:
|
case 0:
|
||||||
return &v.state
|
return &v.state
|
||||||
@@ -390,6 +952,58 @@ func file_internal_machine_api_pb_machine_proto_init() {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
file_internal_machine_api_pb_machine_proto_msgTypes[7].Exporter = func(v any, i int) any {
|
||||||
|
switch v := v.(*Service); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file_internal_machine_api_pb_machine_proto_msgTypes[8].Exporter = func(v any, i int) any {
|
||||||
|
switch v := v.(*InspectServiceRequest); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file_internal_machine_api_pb_machine_proto_msgTypes[9].Exporter = func(v any, i int) any {
|
||||||
|
switch v := v.(*InspectServiceResponse); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file_internal_machine_api_pb_machine_proto_msgTypes[10].Exporter = func(v any, i int) any {
|
||||||
|
switch v := v.(*Service_Container); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file_internal_machine_api_pb_machine_proto_msgTypes[3].OneofWrappers = []any{
|
||||||
|
(*InitClusterRequest_PublicIp)(nil),
|
||||||
|
(*InitClusterRequest_PublicIpAuto)(nil),
|
||||||
}
|
}
|
||||||
type x struct{}
|
type x struct{}
|
||||||
out := protoimpl.TypeBuilder{
|
out := protoimpl.TypeBuilder{
|
||||||
@@ -397,7 +1011,7 @@ func file_internal_machine_api_pb_machine_proto_init() {
|
|||||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||||
RawDescriptor: file_internal_machine_api_pb_machine_proto_rawDesc,
|
RawDescriptor: file_internal_machine_api_pb_machine_proto_rawDesc,
|
||||||
NumEnums: 0,
|
NumEnums: 0,
|
||||||
NumMessages: 4,
|
NumMessages: 11,
|
||||||
NumExtensions: 0,
|
NumExtensions: 0,
|
||||||
NumServices: 1,
|
NumServices: 1,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -6,19 +6,46 @@ 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/common.proto";
|
||||||
import "internal/machine/api/pb/cluster.proto";
|
|
||||||
|
|
||||||
service Machine {
|
service Machine {
|
||||||
|
// CheckPrerequisites verifies if the machine meets all necessary system requirements to participate in the cluster.
|
||||||
|
rpc CheckPrerequisites(google.protobuf.Empty) returns (CheckPrerequisitesResponse);
|
||||||
rpc InitCluster(InitClusterRequest) returns (InitClusterResponse);
|
rpc InitCluster(InitClusterRequest) returns (InitClusterResponse);
|
||||||
rpc JoinCluster(JoinClusterRequest) returns (google.protobuf.Empty);
|
rpc JoinCluster(JoinClusterRequest) returns (google.protobuf.Empty);
|
||||||
rpc Token(google.protobuf.Empty) returns (TokenResponse);
|
rpc Token(google.protobuf.Empty) returns (TokenResponse);
|
||||||
rpc Inspect(google.protobuf.Empty) returns (MachineInfo);
|
rpc Inspect(google.protobuf.Empty) returns (MachineInfo);
|
||||||
|
rpc InspectService(InspectServiceRequest) returns (InspectServiceResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
message MachineInfo {
|
||||||
|
string id = 1;
|
||||||
|
string name = 2;
|
||||||
|
NetworkConfig network = 3;
|
||||||
|
IP public_ip = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message NetworkConfig {
|
||||||
|
IPPrefix subnet = 1;
|
||||||
|
IP management_ip = 2;
|
||||||
|
repeated IPPort endpoints = 3;
|
||||||
|
bytes public_key = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message CheckPrerequisitesResponse {
|
||||||
|
// Overall status of the checks.
|
||||||
|
bool satisfied = 1;
|
||||||
|
// Error message if not satisfied (empty if all checks pass).
|
||||||
|
string error = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
message InitClusterRequest {
|
message InitClusterRequest {
|
||||||
string machineName = 1;
|
string machineName = 1;
|
||||||
IPPrefix network = 2;
|
IPPrefix network = 2;
|
||||||
User user = 3;
|
|
||||||
|
oneof public_ip_config {
|
||||||
|
IP public_ip = 3;
|
||||||
|
bool public_ip_auto = 4;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
message InitClusterResponse {
|
message InitClusterResponse {
|
||||||
@@ -33,3 +60,25 @@ message JoinClusterRequest {
|
|||||||
message TokenResponse {
|
message TokenResponse {
|
||||||
string token = 1;
|
string token = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message Service {
|
||||||
|
string id = 1;
|
||||||
|
string name = 2;
|
||||||
|
string mode = 3;
|
||||||
|
|
||||||
|
message Container {
|
||||||
|
string machine_id = 1;
|
||||||
|
// JSON encoded Docker types.Container.
|
||||||
|
bytes container = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
repeated Container containers = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message InspectServiceRequest {
|
||||||
|
string id = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message InspectServiceResponse {
|
||||||
|
Service service = 1;
|
||||||
|
}
|
||||||
|
|||||||
@@ -20,20 +20,24 @@ import (
|
|||||||
const _ = grpc.SupportPackageIsVersion9
|
const _ = grpc.SupportPackageIsVersion9
|
||||||
|
|
||||||
const (
|
const (
|
||||||
Machine_InitCluster_FullMethodName = "/api.Machine/InitCluster"
|
Machine_CheckPrerequisites_FullMethodName = "/api.Machine/CheckPrerequisites"
|
||||||
Machine_JoinCluster_FullMethodName = "/api.Machine/JoinCluster"
|
Machine_InitCluster_FullMethodName = "/api.Machine/InitCluster"
|
||||||
Machine_Token_FullMethodName = "/api.Machine/Token"
|
Machine_JoinCluster_FullMethodName = "/api.Machine/JoinCluster"
|
||||||
Machine_Inspect_FullMethodName = "/api.Machine/Inspect"
|
Machine_Token_FullMethodName = "/api.Machine/Token"
|
||||||
|
Machine_Inspect_FullMethodName = "/api.Machine/Inspect"
|
||||||
|
Machine_InspectService_FullMethodName = "/api.Machine/InspectService"
|
||||||
)
|
)
|
||||||
|
|
||||||
// MachineClient is the client API for Machine service.
|
// MachineClient is the client API for Machine service.
|
||||||
//
|
//
|
||||||
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
|
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
|
||||||
type MachineClient interface {
|
type MachineClient interface {
|
||||||
|
CheckPrerequisites(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*CheckPrerequisitesResponse, error)
|
||||||
InitCluster(ctx context.Context, in *InitClusterRequest, opts ...grpc.CallOption) (*InitClusterResponse, error)
|
InitCluster(ctx context.Context, in *InitClusterRequest, opts ...grpc.CallOption) (*InitClusterResponse, error)
|
||||||
JoinCluster(ctx context.Context, in *JoinClusterRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
|
JoinCluster(ctx context.Context, in *JoinClusterRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
|
||||||
Token(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*TokenResponse, error)
|
Token(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*TokenResponse, error)
|
||||||
Inspect(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*MachineInfo, error)
|
Inspect(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*MachineInfo, error)
|
||||||
|
InspectService(ctx context.Context, in *InspectServiceRequest, opts ...grpc.CallOption) (*InspectServiceResponse, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type machineClient struct {
|
type machineClient struct {
|
||||||
@@ -44,6 +48,16 @@ func NewMachineClient(cc grpc.ClientConnInterface) MachineClient {
|
|||||||
return &machineClient{cc}
|
return &machineClient{cc}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *machineClient) CheckPrerequisites(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*CheckPrerequisitesResponse, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(CheckPrerequisitesResponse)
|
||||||
|
err := c.cc.Invoke(ctx, Machine_CheckPrerequisites_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (c *machineClient) InitCluster(ctx context.Context, in *InitClusterRequest, opts ...grpc.CallOption) (*InitClusterResponse, error) {
|
func (c *machineClient) InitCluster(ctx context.Context, in *InitClusterRequest, opts ...grpc.CallOption) (*InitClusterResponse, error) {
|
||||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
out := new(InitClusterResponse)
|
out := new(InitClusterResponse)
|
||||||
@@ -84,14 +98,26 @@ func (c *machineClient) Inspect(ctx context.Context, in *emptypb.Empty, opts ...
|
|||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *machineClient) InspectService(ctx context.Context, in *InspectServiceRequest, opts ...grpc.CallOption) (*InspectServiceResponse, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(InspectServiceResponse)
|
||||||
|
err := c.cc.Invoke(ctx, Machine_InspectService_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
// MachineServer is the server API for Machine service.
|
// MachineServer is the server API for Machine service.
|
||||||
// All implementations must embed UnimplementedMachineServer
|
// All implementations must embed UnimplementedMachineServer
|
||||||
// for forward compatibility.
|
// for forward compatibility.
|
||||||
type MachineServer interface {
|
type MachineServer interface {
|
||||||
|
CheckPrerequisites(context.Context, *emptypb.Empty) (*CheckPrerequisitesResponse, error)
|
||||||
InitCluster(context.Context, *InitClusterRequest) (*InitClusterResponse, error)
|
InitCluster(context.Context, *InitClusterRequest) (*InitClusterResponse, error)
|
||||||
JoinCluster(context.Context, *JoinClusterRequest) (*emptypb.Empty, error)
|
JoinCluster(context.Context, *JoinClusterRequest) (*emptypb.Empty, error)
|
||||||
Token(context.Context, *emptypb.Empty) (*TokenResponse, error)
|
Token(context.Context, *emptypb.Empty) (*TokenResponse, error)
|
||||||
Inspect(context.Context, *emptypb.Empty) (*MachineInfo, error)
|
Inspect(context.Context, *emptypb.Empty) (*MachineInfo, error)
|
||||||
|
InspectService(context.Context, *InspectServiceRequest) (*InspectServiceResponse, error)
|
||||||
mustEmbedUnimplementedMachineServer()
|
mustEmbedUnimplementedMachineServer()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,6 +128,9 @@ type MachineServer interface {
|
|||||||
// pointer dereference when methods are called.
|
// pointer dereference when methods are called.
|
||||||
type UnimplementedMachineServer struct{}
|
type UnimplementedMachineServer struct{}
|
||||||
|
|
||||||
|
func (UnimplementedMachineServer) CheckPrerequisites(context.Context, *emptypb.Empty) (*CheckPrerequisitesResponse, error) {
|
||||||
|
return nil, status.Errorf(codes.Unimplemented, "method CheckPrerequisites not implemented")
|
||||||
|
}
|
||||||
func (UnimplementedMachineServer) InitCluster(context.Context, *InitClusterRequest) (*InitClusterResponse, error) {
|
func (UnimplementedMachineServer) InitCluster(context.Context, *InitClusterRequest) (*InitClusterResponse, error) {
|
||||||
return nil, status.Errorf(codes.Unimplemented, "method InitCluster not implemented")
|
return nil, status.Errorf(codes.Unimplemented, "method InitCluster not implemented")
|
||||||
}
|
}
|
||||||
@@ -114,6 +143,9 @@ func (UnimplementedMachineServer) Token(context.Context, *emptypb.Empty) (*Token
|
|||||||
func (UnimplementedMachineServer) Inspect(context.Context, *emptypb.Empty) (*MachineInfo, error) {
|
func (UnimplementedMachineServer) Inspect(context.Context, *emptypb.Empty) (*MachineInfo, error) {
|
||||||
return nil, status.Errorf(codes.Unimplemented, "method Inspect not implemented")
|
return nil, status.Errorf(codes.Unimplemented, "method Inspect not implemented")
|
||||||
}
|
}
|
||||||
|
func (UnimplementedMachineServer) InspectService(context.Context, *InspectServiceRequest) (*InspectServiceResponse, error) {
|
||||||
|
return nil, status.Errorf(codes.Unimplemented, "method InspectService not implemented")
|
||||||
|
}
|
||||||
func (UnimplementedMachineServer) mustEmbedUnimplementedMachineServer() {}
|
func (UnimplementedMachineServer) mustEmbedUnimplementedMachineServer() {}
|
||||||
func (UnimplementedMachineServer) testEmbeddedByValue() {}
|
func (UnimplementedMachineServer) testEmbeddedByValue() {}
|
||||||
|
|
||||||
@@ -135,6 +167,24 @@ func RegisterMachineServer(s grpc.ServiceRegistrar, srv MachineServer) {
|
|||||||
s.RegisterService(&Machine_ServiceDesc, srv)
|
s.RegisterService(&Machine_ServiceDesc, srv)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func _Machine_CheckPrerequisites_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(emptypb.Empty)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(MachineServer).CheckPrerequisites(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: Machine_CheckPrerequisites_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(MachineServer).CheckPrerequisites(ctx, req.(*emptypb.Empty))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
func _Machine_InitCluster_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
func _Machine_InitCluster_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
in := new(InitClusterRequest)
|
in := new(InitClusterRequest)
|
||||||
if err := dec(in); err != nil {
|
if err := dec(in); err != nil {
|
||||||
@@ -207,6 +257,24 @@ func _Machine_Inspect_Handler(srv interface{}, ctx context.Context, dec func(int
|
|||||||
return interceptor(ctx, in, info, handler)
|
return interceptor(ctx, in, info, handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func _Machine_InspectService_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(InspectServiceRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(MachineServer).InspectService(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: Machine_InspectService_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(MachineServer).InspectService(ctx, req.(*InspectServiceRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
// Machine_ServiceDesc is the grpc.ServiceDesc for Machine service.
|
// Machine_ServiceDesc is the grpc.ServiceDesc for Machine 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)
|
||||||
@@ -214,6 +282,10 @@ var Machine_ServiceDesc = grpc.ServiceDesc{
|
|||||||
ServiceName: "api.Machine",
|
ServiceName: "api.Machine",
|
||||||
HandlerType: (*MachineServer)(nil),
|
HandlerType: (*MachineServer)(nil),
|
||||||
Methods: []grpc.MethodDesc{
|
Methods: []grpc.MethodDesc{
|
||||||
|
{
|
||||||
|
MethodName: "CheckPrerequisites",
|
||||||
|
Handler: _Machine_CheckPrerequisites_Handler,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
MethodName: "InitCluster",
|
MethodName: "InitCluster",
|
||||||
Handler: _Machine_InitCluster_Handler,
|
Handler: _Machine_InitCluster_Handler,
|
||||||
@@ -230,6 +302,10 @@ var Machine_ServiceDesc = grpc.ServiceDesc{
|
|||||||
MethodName: "Inspect",
|
MethodName: "Inspect",
|
||||||
Handler: _Machine_Inspect_Handler,
|
Handler: _Machine_Inspect_Handler,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
MethodName: "InspectService",
|
||||||
|
Handler: _Machine_InspectService_Handler,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
Streams: []grpc.StreamDesc{},
|
Streams: []grpc.StreamDesc{},
|
||||||
Metadata: "internal/machine/api/pb/machine.proto",
|
Metadata: "internal/machine/api/pb/machine.proto",
|
||||||
|
|||||||
@@ -0,0 +1,144 @@
|
|||||||
|
package proxy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"google.golang.org/grpc/status"
|
||||||
|
"google.golang.org/protobuf/encoding/protowire"
|
||||||
|
"google.golang.org/protobuf/proto"
|
||||||
|
"github.com/psviderski/uncloud/internal/machine/api/pb"
|
||||||
|
)
|
||||||
|
|
||||||
|
// One2ManyResponder converts upstream responses into messages from upstreams, so that multiple
|
||||||
|
// successful and failure responses might be returned in One2Many mode.
|
||||||
|
type One2ManyResponder struct {
|
||||||
|
machine string
|
||||||
|
}
|
||||||
|
|
||||||
|
// AppendInfo is called to enhance response from the backend with additional data.
|
||||||
|
//
|
||||||
|
// AppendInfo enhances upstream response with machine metadata (target).
|
||||||
|
//
|
||||||
|
// This method depends on grpc protobuf response structure, each response should
|
||||||
|
// look like:
|
||||||
|
//
|
||||||
|
// message SomeResponse {
|
||||||
|
// repeated SomeReply messages = 1; // please note field ID == 1
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// message SomeReply {
|
||||||
|
// common.Metadata metadata = 1;
|
||||||
|
// <other fields go here ...>
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// As 'SomeReply' is repeated in 'SomeResponse', if we concatenate protobuf representation
|
||||||
|
// of several 'SomeResponse' messages, we still get valid 'SomeResponse' representation but with more
|
||||||
|
// entries (feature of protobuf binary representation).
|
||||||
|
//
|
||||||
|
// If we look at binary representation of any unary 'SomeResponse' message, it will always contain one
|
||||||
|
// protobuf field with field ID 1 (see above) and type 2 (embedded message SomeReply is encoded
|
||||||
|
// as string with length). So if we want to add fields to 'SomeReply', we can simply read field
|
||||||
|
// header, adjust length for new 'SomeReply' representation, and prepend new field header.
|
||||||
|
//
|
||||||
|
// At the same time, we can add 'common.Metadata' structure to 'SomeReply' by simply
|
||||||
|
// appending or prepending 'common.Metadata' as a single field. This requires 'metadata'
|
||||||
|
// field to be not defined in original response. (This is due to the fact that protobuf message
|
||||||
|
// representation is concatenation of each field representation).
|
||||||
|
//
|
||||||
|
// To build only single field (Metadata) we use helper message which contains exactly this
|
||||||
|
// field with same field ID as in every other 'SomeReply':
|
||||||
|
//
|
||||||
|
// message Empty {
|
||||||
|
// common.Metadata metadata = 1;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// As streaming replies are not wrapped into 'SomeResponse' with 'repeated', handling is simpler: we just
|
||||||
|
// need to append Empty with details.
|
||||||
|
//
|
||||||
|
// So AppendInfo does the following: validates that response contains field ID 1 encoded as string,
|
||||||
|
// cuts field header, rest is representation of some reply. Marshal 'Empty' as protobuf,
|
||||||
|
// which builds 'common.Metadata' field, append it to original response message, build new header
|
||||||
|
// for new length of some response, and add back new field header.
|
||||||
|
func (b *One2ManyResponder) AppendInfo(streaming bool, resp []byte) ([]byte, error) {
|
||||||
|
payload, err := proto.Marshal(&pb.Empty{
|
||||||
|
Metadata: &pb.Metadata{
|
||||||
|
Machine: b.machine,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if streaming {
|
||||||
|
return append(resp, payload...), err
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
metadataField = 1 // field number in proto definition for repeated response
|
||||||
|
metadataType = 2 // "string" for embedded messages
|
||||||
|
)
|
||||||
|
|
||||||
|
// decode protobuf embedded header
|
||||||
|
|
||||||
|
typ, n1 := protowire.ConsumeVarint(resp)
|
||||||
|
if n1 < 0 {
|
||||||
|
return nil, protowire.ParseError(n1)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, n2 := protowire.ConsumeVarint(resp[n1:]) // length
|
||||||
|
if n2 < 0 {
|
||||||
|
return nil, protowire.ParseError(n2)
|
||||||
|
}
|
||||||
|
|
||||||
|
if typ != (metadataField<<3)|metadataType {
|
||||||
|
return nil, fmt.Errorf("unexpected message format: %d", typ)
|
||||||
|
}
|
||||||
|
|
||||||
|
if n1+n2 > len(resp) {
|
||||||
|
return nil, fmt.Errorf("unexpected message size: %d", len(resp))
|
||||||
|
}
|
||||||
|
|
||||||
|
// cut off embedded message header
|
||||||
|
resp = resp[n1+n2:]
|
||||||
|
// build new embedded message header
|
||||||
|
prefix := protowire.AppendVarint(
|
||||||
|
protowire.AppendVarint(nil, (metadataField<<3)|metadataType),
|
||||||
|
uint64(len(resp)+len(payload)),
|
||||||
|
)
|
||||||
|
resp = append(prefix, resp...)
|
||||||
|
|
||||||
|
return append(resp, payload...), err
|
||||||
|
}
|
||||||
|
|
||||||
|
// BuildError converts upstream error into message from upstream, so that multiple
|
||||||
|
// successful and failure responses might be returned.
|
||||||
|
//
|
||||||
|
// This simply relies on the fact that any response contains 'Empty' message.
|
||||||
|
// So if 'Empty' is unmarshalled into any other reply message, all the fields
|
||||||
|
// are undefined but 'Metadata':
|
||||||
|
//
|
||||||
|
// message Empty {
|
||||||
|
// common.Metadata metadata = 1;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// message EmptyResponse {
|
||||||
|
// repeated Empty messages = 1;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// Streaming responses are not wrapped into Empty, so we simply marshall EmptyResponse
|
||||||
|
// message.
|
||||||
|
func (b *One2ManyResponder) BuildError(streaming bool, err error) ([]byte, error) {
|
||||||
|
var resp proto.Message = &pb.Empty{
|
||||||
|
Metadata: &pb.Metadata{
|
||||||
|
Machine: b.machine,
|
||||||
|
Error: err.Error(),
|
||||||
|
Status: status.Convert(err).Proto(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if !streaming {
|
||||||
|
resp = &pb.EmptyResponse{
|
||||||
|
Messages: []*pb.Empty{
|
||||||
|
resp.(*pb.Empty),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return proto.Marshal(resp)
|
||||||
|
}
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
package proxy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"github.com/siderolabs/grpc-proxy/proxy"
|
||||||
|
"google.golang.org/grpc/codes"
|
||||||
|
"google.golang.org/grpc/metadata"
|
||||||
|
"google.golang.org/grpc/status"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Director manages routing of gRPC requests between local and remote backends.
|
||||||
|
type Director struct {
|
||||||
|
localBackend *LocalBackend
|
||||||
|
remotePort uint16
|
||||||
|
remoteBackends sync.Map
|
||||||
|
// mu synchronizes access to localAddress.
|
||||||
|
mu sync.RWMutex
|
||||||
|
localAddress string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDirector(localSockPath string, remotePort uint16) *Director {
|
||||||
|
return &Director{
|
||||||
|
localBackend: NewLocalBackend(localSockPath, ""),
|
||||||
|
remotePort: remotePort,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateLocalAddress updates the local machine address used to identify which requests should be proxied
|
||||||
|
// to the local gRPC server.
|
||||||
|
func (d *Director) UpdateLocalAddress(addr string) {
|
||||||
|
d.mu.Lock()
|
||||||
|
defer d.mu.Unlock()
|
||||||
|
|
||||||
|
d.localAddress = addr
|
||||||
|
// Replace the local backend with the one that has local address set.
|
||||||
|
d.localBackend = NewLocalBackend(d.localBackend.sockPath, addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Director implements proxy.StreamDirector for grpc-proxy, routing requests to local or remote backends based
|
||||||
|
// on gRPC metadata in the context. Each machine metadata is injected into the response messages by the proxy
|
||||||
|
// if the request is proxied to multiple backends.
|
||||||
|
func (d *Director) Director(ctx context.Context, fullMethodName string) (proxy.Mode, []proxy.Backend, error) {
|
||||||
|
md, ok := metadata.FromIncomingContext(ctx)
|
||||||
|
if !ok {
|
||||||
|
return proxy.One2One, []proxy.Backend{d.localBackend}, nil
|
||||||
|
}
|
||||||
|
// If the request is already proxied, send it to the local backend.
|
||||||
|
if _, ok = md["proxy-authority"]; ok {
|
||||||
|
return proxy.One2One, []proxy.Backend{d.localBackend}, nil
|
||||||
|
}
|
||||||
|
// If the request metadata doesn't contain machines to proxy to, send it to the local backend.
|
||||||
|
machines, ok := md["machines"]
|
||||||
|
if !ok {
|
||||||
|
return proxy.One2One, []proxy.Backend{d.localBackend}, nil
|
||||||
|
}
|
||||||
|
if len(machines) == 0 {
|
||||||
|
return proxy.One2One, nil, status.Error(codes.InvalidArgument, "no machines specified")
|
||||||
|
}
|
||||||
|
|
||||||
|
d.mu.RLock()
|
||||||
|
localAddress := d.localAddress
|
||||||
|
localBackend := d.localBackend
|
||||||
|
d.mu.RUnlock()
|
||||||
|
|
||||||
|
backends := make([]proxy.Backend, len(machines))
|
||||||
|
for i, addr := range machines {
|
||||||
|
if addr == localAddress {
|
||||||
|
backends[i] = localBackend
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
backend, err := d.remoteBackend(addr)
|
||||||
|
if err != nil {
|
||||||
|
return proxy.One2One, nil, status.Error(codes.Internal, err.Error())
|
||||||
|
}
|
||||||
|
backends[i] = backend
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(backends) == 1 {
|
||||||
|
return proxy.One2One, backends, nil
|
||||||
|
}
|
||||||
|
return proxy.One2Many, backends, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// remoteBackend returns a RemoteBackend for the given address from the cache or creates a new one.
|
||||||
|
func (d *Director) remoteBackend(addr string) (*RemoteBackend, error) {
|
||||||
|
b, ok := d.remoteBackends.Load(addr)
|
||||||
|
if ok {
|
||||||
|
return b.(*RemoteBackend), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
backend, err := NewRemoteBackend(addr, d.remotePort)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
existing, loaded := d.remoteBackends.LoadOrStore(addr, backend)
|
||||||
|
if loaded {
|
||||||
|
// A concurrent remoteBackend call built a different backend.
|
||||||
|
backend.Close()
|
||||||
|
return existing.(*RemoteBackend), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return backend, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// FlushRemoteBackends closes all remote backend connections and removes them from the cache.
|
||||||
|
func (d *Director) FlushRemoteBackends() {
|
||||||
|
d.remoteBackends.Range(func(key, value interface{}) bool {
|
||||||
|
backend, ok := value.(*RemoteBackend)
|
||||||
|
if !ok {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
backend.Close()
|
||||||
|
d.remoteBackends.Delete(key)
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close closes all backend connections.
|
||||||
|
func (d *Director) Close() {
|
||||||
|
d.localBackend.Close()
|
||||||
|
d.FlushRemoteBackends()
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
package proxy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"github.com/siderolabs/grpc-proxy/proxy"
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
"google.golang.org/grpc/credentials/insecure"
|
||||||
|
"google.golang.org/grpc/metadata"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LocalBackend is a proxy.One2ManyResponder implementation that proxies to a local gRPC server listening on a Unix socket.
|
||||||
|
type LocalBackend struct {
|
||||||
|
One2ManyResponder
|
||||||
|
sockPath string
|
||||||
|
|
||||||
|
mu sync.RWMutex
|
||||||
|
conn *grpc.ClientConn
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ proxy.Backend = (*LocalBackend)(nil)
|
||||||
|
|
||||||
|
// NewLocalBackend returns a new LocalBackend for the given Unix socket path. The addr parameter is the local address
|
||||||
|
// of the current machine which could be empty if it's not known. The address is used to populate response metadata
|
||||||
|
// in one2many mode.
|
||||||
|
func NewLocalBackend(sockPath, addr string) *LocalBackend {
|
||||||
|
return &LocalBackend{
|
||||||
|
One2ManyResponder: One2ManyResponder{
|
||||||
|
machine: addr,
|
||||||
|
},
|
||||||
|
sockPath: sockPath,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *LocalBackend) String() string {
|
||||||
|
return b.machine
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetConnection returns a gRPC connection to the local server listening on the Unix socket.
|
||||||
|
func (b *LocalBackend) GetConnection(ctx context.Context, _ string) (context.Context, *grpc.ClientConn, error) {
|
||||||
|
md, _ := metadata.FromIncomingContext(ctx)
|
||||||
|
outCtx := metadata.NewOutgoingContext(ctx, md)
|
||||||
|
|
||||||
|
b.mu.RLock()
|
||||||
|
if b.conn != nil {
|
||||||
|
defer b.mu.RUnlock()
|
||||||
|
return outCtx, b.conn, nil
|
||||||
|
}
|
||||||
|
b.mu.RUnlock()
|
||||||
|
|
||||||
|
b.mu.Lock()
|
||||||
|
defer b.mu.Unlock()
|
||||||
|
|
||||||
|
var err error
|
||||||
|
b.conn, err = grpc.NewClient(
|
||||||
|
"unix://"+b.sockPath,
|
||||||
|
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
||||||
|
grpc.WithDefaultCallOptions(
|
||||||
|
grpc.ForceCodecV2(proxy.Codec()),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
return outCtx, b.conn, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close closes the upstream gRPC connection.
|
||||||
|
func (b *LocalBackend) Close() {
|
||||||
|
b.mu.Lock()
|
||||||
|
defer b.mu.Unlock()
|
||||||
|
|
||||||
|
if b.conn != nil {
|
||||||
|
b.conn.Close()
|
||||||
|
b.conn = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
package proxy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"github.com/siderolabs/grpc-proxy/proxy"
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
"google.golang.org/grpc/backoff"
|
||||||
|
"google.golang.org/grpc/credentials/insecure"
|
||||||
|
"google.golang.org/grpc/metadata"
|
||||||
|
"net/netip"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RemoteBackend is a proxy.One2ManyResponder implementation that proxies to a remote gRPC server, injecting machine metadata
|
||||||
|
// into the response.
|
||||||
|
//
|
||||||
|
// Based on the Talos apid implementation:
|
||||||
|
// https://github.com/siderolabs/talos/blob/59a78da42cdea8fbccc35d0851f9b0eef928261b/internal/app/apid/pkg/backend/apid.go
|
||||||
|
type RemoteBackend struct {
|
||||||
|
One2ManyResponder
|
||||||
|
target string
|
||||||
|
|
||||||
|
mu sync.RWMutex
|
||||||
|
conn *grpc.ClientConn
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ proxy.Backend = (*RemoteBackend)(nil)
|
||||||
|
|
||||||
|
// NewRemoteBackend creates a new instance of RemoteBackend for the given IPv6 address and port.
|
||||||
|
func NewRemoteBackend(addr string, port uint16) (*RemoteBackend, error) {
|
||||||
|
ip, err := netip.ParseAddr(addr)
|
||||||
|
if err != nil || !ip.Is6() {
|
||||||
|
return nil, fmt.Errorf("address must be a valid IPv6 address: %s", addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &RemoteBackend{
|
||||||
|
One2ManyResponder: One2ManyResponder{
|
||||||
|
machine: addr,
|
||||||
|
},
|
||||||
|
target: netip.AddrPortFrom(ip, port).String(),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *RemoteBackend) String() string {
|
||||||
|
return b.machine
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetConnection returns a gRPC connection to the remote server.
|
||||||
|
func (b *RemoteBackend) GetConnection(ctx context.Context, _ string) (context.Context, *grpc.ClientConn, error) {
|
||||||
|
md, _ := metadata.FromIncomingContext(ctx)
|
||||||
|
if authority := md[":authority"]; len(authority) > 0 {
|
||||||
|
md.Set("proxy-authority", authority...)
|
||||||
|
} else {
|
||||||
|
md.Set("proxy-authority", "unknown")
|
||||||
|
}
|
||||||
|
delete(md, ":authority")
|
||||||
|
delete(md, "machines")
|
||||||
|
|
||||||
|
outCtx := metadata.NewOutgoingContext(ctx, md)
|
||||||
|
|
||||||
|
b.mu.RLock()
|
||||||
|
if b.conn != nil {
|
||||||
|
defer b.mu.RUnlock()
|
||||||
|
return outCtx, b.conn, nil
|
||||||
|
}
|
||||||
|
b.mu.RUnlock()
|
||||||
|
|
||||||
|
b.mu.Lock()
|
||||||
|
defer b.mu.Unlock()
|
||||||
|
|
||||||
|
// Override the max delay to avoid excessive backoff when the another node is unavailable, e.g. rebooted
|
||||||
|
// or the WireGuard connection is temporary down.
|
||||||
|
//
|
||||||
|
// Default max delay is 2 minutes, which is too long for our use case.
|
||||||
|
backoffConfig := backoff.DefaultConfig
|
||||||
|
// The maximum wait time between attempts.
|
||||||
|
backoffConfig.MaxDelay = 15 * time.Second
|
||||||
|
|
||||||
|
var err error
|
||||||
|
b.conn, err = grpc.NewClient(
|
||||||
|
b.target,
|
||||||
|
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
||||||
|
grpc.WithConnectParams(grpc.ConnectParams{
|
||||||
|
Backoff: backoffConfig,
|
||||||
|
// Not published as a constant in gRPC library.
|
||||||
|
// See: https://github.com/grpc/grpc-go/blob/d5dee5fdbdeb52f6ea10b37b2cc7ce37814642d7/clientconn.go#L55-L56
|
||||||
|
// Each connection attempt can take up to MinConnectTimeout.
|
||||||
|
MinConnectTimeout: 20 * time.Second,
|
||||||
|
}),
|
||||||
|
grpc.WithDefaultCallOptions(
|
||||||
|
grpc.ForceCodecV2(proxy.Codec()),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
return outCtx, b.conn, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close closes the upstream gRPC connection.
|
||||||
|
func (b *RemoteBackend) Close() {
|
||||||
|
b.mu.Lock()
|
||||||
|
defer b.mu.Unlock()
|
||||||
|
|
||||||
|
if b.conn != nil {
|
||||||
|
b.conn.Close()
|
||||||
|
b.conn = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
// Copyright 2024 Google LLC
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
package google.rpc;
|
||||||
|
|
||||||
|
import "google/protobuf/any.proto";
|
||||||
|
|
||||||
|
option cc_enable_arenas = true;
|
||||||
|
option go_package = "google.golang.org/genproto/googleapis/rpc/status;status";
|
||||||
|
option java_multiple_files = true;
|
||||||
|
option java_outer_classname = "StatusProto";
|
||||||
|
option java_package = "com.google.rpc";
|
||||||
|
option objc_class_prefix = "RPC";
|
||||||
|
|
||||||
|
// The `Status` type defines a logical error model that is suitable for
|
||||||
|
// different programming environments, including REST APIs and RPC APIs. It is
|
||||||
|
// used by [gRPC](https://github.com/grpc). Each `Status` message contains
|
||||||
|
// three pieces of data: error code, error message, and error details.
|
||||||
|
//
|
||||||
|
// You can find out more about this error model and how to work with it in the
|
||||||
|
// [API Design Guide](https://cloud.google.com/apis/design/errors).
|
||||||
|
message Status {
|
||||||
|
// The status code, which should be an enum value of
|
||||||
|
// [google.rpc.Code][google.rpc.Code].
|
||||||
|
int32 code = 1;
|
||||||
|
|
||||||
|
// A developer-facing error message, which should be in English. Any
|
||||||
|
// user-facing error message should be localized and sent in the
|
||||||
|
// [google.rpc.Status.details][google.rpc.Status.details] field, or localized
|
||||||
|
// by the client.
|
||||||
|
string message = 2;
|
||||||
|
|
||||||
|
// A list of messages that carry the error details. There is a common set of
|
||||||
|
// message types for APIs to use.
|
||||||
|
repeated google.protobuf.Any details = 3;
|
||||||
|
}
|
||||||
@@ -0,0 +1,167 @@
|
|||||||
|
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/pkg/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GenerateConfig(containers []api.ServiceContainer, verifyResponse string) (*caddy.Config, error) {
|
||||||
|
// Maps hostnames to lists of upstreams (container IP:port pairs).
|
||||||
|
httpHostUpstreams := make(map[string][]string)
|
||||||
|
httpsHostUpstreams := make(map[string][]string)
|
||||||
|
for _, ctr := range containers {
|
||||||
|
if !ctr.Healthy() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
ip := ctr.UncloudNetworkIP()
|
||||||
|
if !ip.IsValid() {
|
||||||
|
// Container is not connected to the uncloud Docker network (could be host network).
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
log := slog.With("container", ctr.ID)
|
||||||
|
|
||||||
|
ports, err := ctr.ServicePorts()
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Failed to parse service ports for container.", "err", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, port := range ports {
|
||||||
|
if port.Mode != api.PortModeIngress {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
switch port.Protocol {
|
||||||
|
case api.ProtocolHTTP:
|
||||||
|
upstream := net.JoinHostPort(ip.String(), strconv.Itoa(int(port.ContainerPort)))
|
||||||
|
httpHostUpstreams[port.Hostname] = append(httpHostUpstreams[port.Hostname], upstream)
|
||||||
|
case api.ProtocolHTTPS:
|
||||||
|
upstream := net.JoinHostPort(ip.String(), strconv.Itoa(int(port.ContainerPort)))
|
||||||
|
httpsHostUpstreams[port.Hostname] = append(httpsHostUpstreams[port.Hostname], upstream)
|
||||||
|
default:
|
||||||
|
// TODO: implement L4 ingress routing for TCP and UDP.
|
||||||
|
log.Error("Unsupported protocol for ingress port.", "port", port)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var warnings []caddyconfig.Warning
|
||||||
|
servers := make(map[string]*caddyhttp.Server)
|
||||||
|
servers["http"] = &caddyhttp.Server{
|
||||||
|
Listen: []string{fmt.Sprintf(":%d", caddyhttp.DefaultHTTPPort)},
|
||||||
|
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),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,385 @@
|
|||||||
|
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) {
|
||||||
|
configWithoutServices := `{
|
||||||
|
"servers": {
|
||||||
|
"http": {
|
||||||
|
"listen": [":80"],
|
||||||
|
"routes": [{
|
||||||
|
"match": [{"path": ["/.uncloud-verify"]}],
|
||||||
|
"handle": [{
|
||||||
|
"body": "verification-response-body",
|
||||||
|
"handler": "static_response",
|
||||||
|
"status_code": 200
|
||||||
|
}]
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
"https": {
|
||||||
|
"listen": [":443"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
containers []api.ServiceContainer
|
||||||
|
want string
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "empty containers",
|
||||||
|
containers: []api.ServiceContainer{},
|
||||||
|
want: configWithoutServices,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: "HTTP container",
|
||||||
|
containers: []api.ServiceContainer{
|
||||||
|
newContainer("10.210.0.2", "app.example.com:8080/http"),
|
||||||
|
},
|
||||||
|
want: `{
|
||||||
|
"servers": {
|
||||||
|
"http": {
|
||||||
|
"listen": [":80"],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"match": [{"host": ["app.example.com"]}],
|
||||||
|
"handle": [{
|
||||||
|
"handler": "reverse_proxy",
|
||||||
|
"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: configWithoutServices,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "container with invalid port ignored",
|
||||||
|
containers: []api.ServiceContainer{
|
||||||
|
newContainer("10.210.0.2", "invalid-port"),
|
||||||
|
},
|
||||||
|
want: configWithoutServices,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "containers with unsupported protocols and host mode ignored",
|
||||||
|
containers: []api.ServiceContainer{
|
||||||
|
newContainer("10.210.0.2", "5000/tcp"),
|
||||||
|
newContainer("10.210.0.3", "5000/udp"),
|
||||||
|
newContainer("10.210.0.4", "80:8080/tcp@host"),
|
||||||
|
},
|
||||||
|
want: configWithoutServices,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "restarting container ignored",
|
||||||
|
containers: []api.ServiceContainer{
|
||||||
|
newRestartingContainer("10.210.0.2", "app.example.com:8080/http"),
|
||||||
|
},
|
||||||
|
want: configWithoutServices,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "stopped container ignored",
|
||||||
|
containers: []api.ServiceContainer{
|
||||||
|
newStoppedContainer("10.210.0.2", "app.example.com:8080/http"),
|
||||||
|
},
|
||||||
|
want: configWithoutServices,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "mix of running, restarting, and stopped containers",
|
||||||
|
containers: []api.ServiceContainer{
|
||||||
|
newContainer("10.210.0.2", "app.example.com:8080/http"),
|
||||||
|
newRestartingContainer("10.210.0.3", "app.example.com:8080/http"),
|
||||||
|
newStoppedContainer("10.210.0.4", "app.example.com:8080/http"),
|
||||||
|
},
|
||||||
|
want: `{
|
||||||
|
"servers": {
|
||||||
|
"http": {
|
||||||
|
"listen": [":80"],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"match": [{"host": ["app.example.com"]}],
|
||||||
|
"handle": [{
|
||||||
|
"handler": "reverse_proxy",
|
||||||
|
"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,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
config, err := GenerateConfig(tt.containers, "verification-response-body")
|
||||||
|
|
||||||
|
if tt.wantErr {
|
||||||
|
assert.Error(t, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.Len(t, config.AppsRaw, 1, "Expected one http app")
|
||||||
|
require.Contains(t, config.AppsRaw, "http", "Expected http app")
|
||||||
|
|
||||||
|
assert.JSONEq(t, tt.want, string(config.AppsRaw["http"]), "Generated Caddy app config doesn't match")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newContainer(ip string, ports ...string) api.ServiceContainer {
|
||||||
|
portsLabel := strings.Join(ports, ",")
|
||||||
|
return api.ServiceContainer{Container: api.Container{ContainerJSON: types.ContainerJSON{
|
||||||
|
ContainerJSONBase: &types.ContainerJSONBase{
|
||||||
|
State: &types.ContainerState{
|
||||||
|
Running: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
NetworkSettings: &types.NetworkSettings{
|
||||||
|
Networks: map[string]*network.EndpointSettings{
|
||||||
|
docker.NetworkName: {
|
||||||
|
IPAddress: ip,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Config: &container.Config{
|
||||||
|
Labels: map[string]string{
|
||||||
|
api.LabelServicePorts: portsLabel,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newContainerWithoutNetwork(ports ...string) api.ServiceContainer {
|
||||||
|
portsLabel := strings.Join(ports, ",")
|
||||||
|
return api.ServiceContainer{Container: api.Container{ContainerJSON: types.ContainerJSON{
|
||||||
|
ContainerJSONBase: &types.ContainerJSONBase{
|
||||||
|
State: &types.ContainerState{
|
||||||
|
Running: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
NetworkSettings: &types.NetworkSettings{
|
||||||
|
Networks: map[string]*network.EndpointSettings{
|
||||||
|
"other-network": {
|
||||||
|
IPAddress: "172.17.0.2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Config: &container.Config{
|
||||||
|
Labels: map[string]string{
|
||||||
|
api.LabelServicePorts: portsLabel,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newRestartingContainer(ip string, ports ...string) api.ServiceContainer {
|
||||||
|
ctr := newContainer(ip, ports...)
|
||||||
|
ctr.Container.State.Restarting = true
|
||||||
|
return ctr
|
||||||
|
}
|
||||||
|
|
||||||
|
func newStoppedContainer(ip string, ports ...string) api.ServiceContainer {
|
||||||
|
ctr := newContainer(ip, ports...)
|
||||||
|
ctr.Container.State.Running = false
|
||||||
|
return ctr
|
||||||
|
}
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
package caddyfile
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/psviderski/uncloud/internal/fs"
|
||||||
|
"github.com/psviderski/uncloud/internal/machine/store"
|
||||||
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
CaddyGroup = "uncloud"
|
||||||
|
VerifyPath = "/.uncloud-verify"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Controller monitors container changes in the cluster store and generates a configuration file for Caddy reverse
|
||||||
|
// proxy. The generated Caddyfile allows Caddy to route external traffic to service containers across the internal
|
||||||
|
// network.
|
||||||
|
type Controller struct {
|
||||||
|
store *store.Store
|
||||||
|
path string
|
||||||
|
verifyResponse string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewController(store *store.Store, path string, verifyResponse string) (*Controller, error) {
|
||||||
|
dir := filepath.Dir(path)
|
||||||
|
if err := os.MkdirAll(dir, 0750); err != nil {
|
||||||
|
return nil, fmt.Errorf("create parent directory for Caddy configuration '%s': %w", dir, err)
|
||||||
|
}
|
||||||
|
if err := fs.Chown(dir, "", CaddyGroup); err != nil {
|
||||||
|
return nil, fmt.Errorf("change owner of parent directory for Caddy configuration '%s': %w", dir, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Controller{
|
||||||
|
store: store,
|
||||||
|
path: path,
|
||||||
|
verifyResponse: verifyResponse,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Controller) Run(ctx context.Context) error {
|
||||||
|
containerRecords, changes, err := c.store.SubscribeContainers(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("subscribe to container changes: %w", err)
|
||||||
|
}
|
||||||
|
slog.Info("Subscribed to container changes in the cluster to generate Caddy configuration.")
|
||||||
|
|
||||||
|
containers, err := c.filterAvailableContainers(containerRecords)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("filter available containers: %w", err)
|
||||||
|
}
|
||||||
|
if err = c.generateConfig(containers); err != nil {
|
||||||
|
return fmt.Errorf("generate Caddy configuration: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case _, ok := <-changes:
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("containers subscription failed")
|
||||||
|
}
|
||||||
|
slog.Debug("Cluster containers changed, updating Caddy configuration.")
|
||||||
|
|
||||||
|
containerRecords, err = c.store.ListContainers(ctx, store.ListOptions{})
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("Failed to list containers.", "err", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
containers, err = c.filterAvailableContainers(containerRecords)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("Failed to filter available containers.", "err", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err = c.generateConfig(containers); err != nil {
|
||||||
|
slog.Error("Failed to generate Caddy configuration.", "err", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
slog.Debug("Updated Caddy configuration.", "path", c.path)
|
||||||
|
case <-ctx.Done():
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// filterAvailableContainers filters out containers from this machine that are likely unavailable. The availability
|
||||||
|
// is determined by the cluster membership state of the machine that the container is running on.
|
||||||
|
// TODO: implement machine membership check using Corrossion Admin client.
|
||||||
|
func (c *Controller) filterAvailableContainers(
|
||||||
|
containerRecords []store.ContainerRecord,
|
||||||
|
) ([]api.ServiceContainer, error) {
|
||||||
|
containers := make([]api.ServiceContainer, len(containerRecords))
|
||||||
|
for i, cr := range containerRecords {
|
||||||
|
containers[i] = api.ServiceContainer{
|
||||||
|
Container: cr.Container,
|
||||||
|
// TODO: restore ServiceSpec from the container record once it's saved in the store.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return containers, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Controller) generateConfig(containers []api.ServiceContainer) error {
|
||||||
|
config, err := GenerateConfig(containers, c.verifyResponse)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
configBytes, err := json.MarshalIndent(config, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("marshal Caddy configuration: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = os.WriteFile(c.path, configBytes, 0640); err != nil {
|
||||||
|
return fmt.Errorf("write Caddy configuration to file '%s': %w", c.path, err)
|
||||||
|
}
|
||||||
|
if err = fs.Chown(c.path, "", CaddyGroup); err != nil {
|
||||||
|
return fmt.Errorf("change owner of Caddy configuration file '%s': %w", c.path, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
+157
-116
@@ -3,130 +3,165 @@ package cluster
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"google.golang.org/grpc/codes"
|
"google.golang.org/grpc/codes"
|
||||||
"google.golang.org/grpc/status"
|
"google.golang.org/grpc/status"
|
||||||
"google.golang.org/protobuf/proto"
|
|
||||||
"google.golang.org/protobuf/types/known/emptypb"
|
"google.golang.org/protobuf/types/known/emptypb"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"uncloud/internal/machine/api/pb"
|
"time"
|
||||||
"uncloud/internal/machine/network"
|
"github.com/psviderski/uncloud/internal/corrosion"
|
||||||
"uncloud/internal/secret"
|
"github.com/psviderski/uncloud/internal/machine/api/pb"
|
||||||
|
"github.com/psviderski/uncloud/internal/machine/network"
|
||||||
|
"github.com/psviderski/uncloud/internal/machine/store"
|
||||||
|
"github.com/psviderski/uncloud/internal/secret"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Cluster struct {
|
type Cluster struct {
|
||||||
pb.UnimplementedClusterServer
|
pb.UnimplementedClusterServer
|
||||||
|
|
||||||
state *State
|
store *store.Store
|
||||||
|
corroAdmin *corrosion.AdminClient
|
||||||
// TODO: temporary channel until the state is replaced with networkDB.
|
// machineID is the ID of the current machine that is running the cluster service.
|
||||||
newMachinesCh chan *pb.MachineInfo
|
machineID string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewCluster(state *State) *Cluster {
|
func NewCluster(store *store.Store, corroAdmin *corrosion.AdminClient) *Cluster {
|
||||||
return &Cluster{
|
return &Cluster{
|
||||||
state: state,
|
store: store,
|
||||||
newMachinesCh: make(chan *pb.MachineInfo, 1),
|
corroAdmin: corroAdmin,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Cluster) SetState(state *State) {
|
// UpdateMachineID updates the current machine ID that is running the cluster service.
|
||||||
c.state = state
|
func (c *Cluster) UpdateMachineID(mid string) {
|
||||||
|
c.machineID = mid
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Cluster) Network() (netip.Prefix, error) {
|
func (c *Cluster) Init(ctx context.Context, network netip.Prefix) error {
|
||||||
if c.state == nil {
|
initialised, err := c.Initialised(ctx)
|
||||||
return netip.Prefix{}, status.Error(codes.FailedPrecondition, "cluster is not initialized")
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if initialised {
|
||||||
|
return fmt.Errorf("cluster is already initialised")
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.state.State.Network == nil {
|
if err = c.store.Put(ctx, "network", network.String()); err != nil {
|
||||||
return netip.Prefix{}, fmt.Errorf("network not set")
|
return fmt.Errorf("put network to store: %w", err)
|
||||||
}
|
}
|
||||||
return c.state.State.Network.ToPrefix()
|
if err = c.store.Put(ctx, "created_at", time.Now().UTC().Format(time.RFC3339)); err != nil {
|
||||||
}
|
return fmt.Errorf("put created_at to store: %w", err)
|
||||||
|
|
||||||
func (c *Cluster) SetNetwork(network *pb.IPPrefix) error {
|
|
||||||
if c.state == nil {
|
|
||||||
return status.Error(codes.FailedPrecondition, "cluster is not initialized")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.state.State.Network != nil {
|
|
||||||
return fmt.Errorf("network already set and cannot be changed")
|
|
||||||
}
|
|
||||||
if network == nil {
|
|
||||||
return fmt.Errorf("network not set")
|
|
||||||
}
|
|
||||||
c.state.State.Network = network
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: this is a temporary watcher for PoC until the state is state is replaced with networkDB.
|
func (c *Cluster) Initialised(ctx context.Context) (bool, error) {
|
||||||
func (c *Cluster) WatchNewMachines() <-chan *pb.MachineInfo {
|
var createdAt string
|
||||||
return c.newMachinesCh
|
if err := c.store.Get(ctx, "created_at", &createdAt); err != nil {
|
||||||
|
if errors.Is(err, store.ErrKeyNotFound) {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
return false, status.Errorf(codes.Internal, "get created_at from store: %v", err)
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cluster) checkInitialised(ctx context.Context) error {
|
||||||
|
initialised, err := c.Initialised(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !initialised {
|
||||||
|
return status.Error(codes.FailedPrecondition, "cluster is not initialised")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cluster) Network(ctx context.Context) (netip.Prefix, error) {
|
||||||
|
if err := c.checkInitialised(ctx); err != nil {
|
||||||
|
return netip.Prefix{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var net string
|
||||||
|
if err := c.store.Get(ctx, "network", &net); err != nil {
|
||||||
|
return netip.Prefix{}, status.Errorf(codes.Internal, "get network from store: %v", err)
|
||||||
|
}
|
||||||
|
prefix, err := netip.ParsePrefix(net)
|
||||||
|
if err != nil {
|
||||||
|
return netip.Prefix{}, status.Errorf(codes.Internal, "parse network prefix: %v", err)
|
||||||
|
}
|
||||||
|
return prefix, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddMachine adds a machine to the cluster.
|
// AddMachine adds a machine to the cluster.
|
||||||
func (c *Cluster) AddMachine(ctx context.Context, req *pb.AddMachineRequest) (*pb.AddMachineResponse, error) {
|
func (c *Cluster) AddMachine(ctx context.Context, req *pb.AddMachineRequest) (*pb.AddMachineResponse, error) {
|
||||||
if c.state == nil {
|
if err := c.checkInitialised(ctx); err != nil {
|
||||||
return nil, status.Error(codes.FailedPrecondition, "cluster is not initialized")
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := req.Validate(); err != nil {
|
if req.Network == nil {
|
||||||
|
return nil, status.Error(codes.InvalidArgument, "network not set")
|
||||||
|
}
|
||||||
|
if err := req.Network.Validate(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
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 := c.state.State.Machines
|
machines, err := c.store.ListMachines(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, status.Errorf(codes.Internal, "list machines: %v", err)
|
||||||
|
}
|
||||||
allocatedSubnets := make([]netip.Prefix, len(machines))
|
allocatedSubnets := make([]netip.Prefix, len(machines))
|
||||||
var err error
|
for i, m := range machines {
|
||||||
i := 0
|
|
||||||
for _, m := range machines {
|
|
||||||
if req.Name != "" && m.Name == req.Name {
|
if req.Name != "" && m.Name == req.Name {
|
||||||
return nil, status.Errorf(codes.AlreadyExists, "machine with name %q already exists", req.Name)
|
return nil, status.Errorf(codes.AlreadyExists, "machine with name %q already exists", req.Name)
|
||||||
}
|
}
|
||||||
if m.Network.ManagementIp != nil && req.Network.ManagementIp != nil &&
|
if req.Network.ManagementIp != nil && req.Network.ManagementIp.Equal(m.Network.ManagementIp) {
|
||||||
m.Network.ManagementIp.Equal(req.Network.ManagementIp) {
|
|
||||||
manageIP, _ := req.Network.ManagementIp.ToAddr()
|
manageIP, _ := req.Network.ManagementIp.ToAddr()
|
||||||
return nil, status.Errorf(codes.AlreadyExists, "machine with management IP %q already exists", manageIP)
|
return nil, status.Errorf(
|
||||||
|
codes.AlreadyExists, "machine with management IP %q already exists under the name %q",
|
||||||
|
manageIP, m.Name,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
if bytes.Equal(m.Network.PublicKey, req.Network.PublicKey) {
|
if bytes.Equal(m.Network.PublicKey, req.Network.PublicKey) {
|
||||||
publicKey := secret.Secret(m.Network.PublicKey)
|
publicKey := secret.Secret(m.Network.PublicKey)
|
||||||
return nil, status.Errorf(codes.AlreadyExists, "machine with public key %q already exists", publicKey)
|
return nil, status.Errorf(
|
||||||
|
codes.AlreadyExists, "machine with public key %q already exists under the name %q",
|
||||||
|
publicKey, m.Name,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
allocatedSubnets[i], _ = m.Network.Subnet.ToPrefix()
|
||||||
if allocatedSubnets[i], err = m.Network.Subnet.ToPrefix(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
i++
|
|
||||||
}
|
}
|
||||||
|
|
||||||
mid, err := NewMachineID()
|
mid, err := NewMachineID()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, status.Errorf(codes.Internal, "generate machine ID: %v", err)
|
return nil, status.Errorf(codes.Internal, "generate machine ID: %v", err)
|
||||||
}
|
}
|
||||||
|
name := req.Name
|
||||||
|
if name == "" {
|
||||||
|
if name, err = NewRandomMachineName(); err != nil {
|
||||||
|
return nil, status.Errorf(codes.Internal, "generate machine name: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
manageIP := req.Network.ManagementIp
|
manageIP := req.Network.ManagementIp
|
||||||
if manageIP == nil {
|
if manageIP == nil {
|
||||||
manageIP = pb.NewIP(network.ManagementIP(req.Network.PublicKey))
|
manageIP = pb.NewIP(network.ManagementIP(req.Network.PublicKey))
|
||||||
}
|
}
|
||||||
m := &pb.MachineInfo{
|
// Allocate a subnet for the machine from the cluster network.
|
||||||
Id: mid,
|
clusterNetwork, err := c.Network(ctx)
|
||||||
Name: req.Name,
|
|
||||||
Network: &pb.NetworkConfig{
|
|
||||||
ManagementIp: manageIP,
|
|
||||||
PublicKey: req.Network.PublicKey,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
if m.Name == "" {
|
|
||||||
m.Name, err = NewRandomMachineName()
|
|
||||||
if err != nil {
|
|
||||||
return nil, status.Errorf(codes.Internal, "generate machine name: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
clusterNetwork, err := c.Network()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, status.Errorf(codes.Internal, "get cluster network: %v", err)
|
return nil, status.Errorf(codes.Internal, "get cluster network: %v", err)
|
||||||
}
|
}
|
||||||
@@ -134,70 +169,76 @@ func (c *Cluster) AddMachine(ctx context.Context, req *pb.AddMachineRequest) (*p
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, status.Errorf(codes.Internal, "create IPAM manager: %v", err)
|
return nil, status.Errorf(codes.Internal, "create IPAM manager: %v", err)
|
||||||
}
|
}
|
||||||
subnet, err := ipam.AllocateSubnetLen(network.DefaultSubnetBits)
|
subnet, err := ipam.AllocateSubnetLen(DefaultSubnetBits)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, status.Errorf(codes.Internal, "allocate subnet for machine: %v", err)
|
return nil, status.Errorf(codes.Internal, "allocate subnet for machine: %v", err)
|
||||||
}
|
}
|
||||||
m.Network.Subnet = pb.NewIPPrefix(subnet)
|
|
||||||
|
|
||||||
// TODO: announce the new machine to the cluster network using Serf and achieve consensus.
|
m := &pb.MachineInfo{
|
||||||
mCopy := proto.Clone(m).(*pb.MachineInfo)
|
Id: mid,
|
||||||
c.state.State.Machines[mCopy.Id] = mCopy
|
Name: name,
|
||||||
// Store machine endpoints in a separate collection to allow modifying them with limited permissions.
|
Network: &pb.NetworkConfig{
|
||||||
c.state.State.Endpoints[mCopy.Id] = &pb.MachineEndpoints{
|
Subnet: pb.NewIPPrefix(subnet),
|
||||||
Id: mCopy.Id,
|
ManagementIp: manageIP,
|
||||||
Endpoints: req.Network.Endpoints,
|
Endpoints: req.Network.Endpoints,
|
||||||
|
PublicKey: req.Network.PublicKey,
|
||||||
|
},
|
||||||
|
PublicIp: req.PublicIp,
|
||||||
}
|
}
|
||||||
if err = c.state.Save(); err != nil {
|
// TODO: announce the new machine to the cluster members and achieve consensus.
|
||||||
return nil, status.Errorf(codes.Internal, "save state: %v", err)
|
// We should perhaps not proceed if this machine is in a minority partition.
|
||||||
|
if err = c.store.CreateMachine(ctx, m); err != nil {
|
||||||
|
return nil, status.Errorf(codes.Internal, "create machine: %v", err)
|
||||||
}
|
}
|
||||||
slog.Info("Machine added to the cluster.", "id", m.Id, "name", m.Name)
|
slog.Info("Machine added to the cluster.",
|
||||||
|
"id", m.Id, "name", m.Name, "subnet", subnet, "public_key", secret.Secret(m.Network.PublicKey))
|
||||||
// Include the machine endpoints in the response.
|
|
||||||
m.Network.Endpoints = req.Network.Endpoints
|
|
||||||
|
|
||||||
// TODO: notify all cluster machines about the new machine so they can update their peers config.
|
|
||||||
// In PoC we just notify the local machine.
|
|
||||||
c.newMachinesCh <- m
|
|
||||||
|
|
||||||
resp := &pb.AddMachineResponse{Machine: m}
|
resp := &pb.AddMachineResponse{Machine: m}
|
||||||
return resp, nil
|
return resp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ListMachines lists all machines in the cluster including their membership states.
|
||||||
func (c *Cluster) ListMachines(ctx context.Context, _ *emptypb.Empty) (*pb.ListMachinesResponse, error) {
|
func (c *Cluster) ListMachines(ctx context.Context, _ *emptypb.Empty) (*pb.ListMachinesResponse, error) {
|
||||||
if c.state == nil {
|
if err := c.checkInitialised(ctx); err != nil {
|
||||||
return nil, status.Error(codes.FailedPrecondition, "cluster is not initialized")
|
return nil, err
|
||||||
}
|
|
||||||
// TODO: consider creating MachineInfo type that pb.MachineInfo is mapped to always carry valid data to reduce
|
|
||||||
// error handling caused by conversions and nil pointers.
|
|
||||||
machines := make([]*pb.MachineInfo, 0, len(c.state.State.Machines))
|
|
||||||
for _, sm := range c.state.State.Machines {
|
|
||||||
m := proto.Clone(sm).(*pb.MachineInfo)
|
|
||||||
m.Network.Endpoints = c.state.State.Endpoints[m.Id].Endpoints
|
|
||||||
machines = append(machines, m)
|
|
||||||
}
|
|
||||||
return &pb.ListMachinesResponse{Machines: machines}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Cluster) ListMachineEndpoints(
|
|
||||||
ctx context.Context, req *pb.ListMachineEndpointsRequest,
|
|
||||||
) (*pb.ListMachineEndpointsResponse, error) {
|
|
||||||
if c.state == nil {
|
|
||||||
return nil, status.Error(codes.FailedPrecondition, "cluster is not initialized")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
endpoints, ok := c.state.State.Endpoints[req.Id]
|
machines, err := c.store.ListMachines(ctx)
|
||||||
if !ok {
|
if err != nil {
|
||||||
return nil, status.Errorf(codes.NotFound, "machine %q not found", req.Id)
|
return nil, status.Error(codes.Internal, err.Error())
|
||||||
}
|
}
|
||||||
return &pb.ListMachineEndpointsResponse{Endpoints: endpoints}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Cluster) AddUser(user *pb.User) error {
|
states, err := c.corroAdmin.ClusterMembershipStates(true)
|
||||||
c.state.State.Users = append(c.state.State.Users, user)
|
if err != nil {
|
||||||
return c.state.Save()
|
return nil, status.Errorf(codes.Internal, "get cluster membership states: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Cluster) ListUsers() []*pb.User {
|
members := make([]*pb.MachineMember, len(machines))
|
||||||
return c.state.State.Users
|
for i, m := range machines {
|
||||||
|
// If the machine is not in the cluster membership states or its state is not ALIVE or SUSPECT, it is DOWN.
|
||||||
|
// The exception is the current machine which is always UP as it is serving this request.
|
||||||
|
state := pb.MachineMember_DOWN
|
||||||
|
addr, _ := m.Network.ManagementIp.ToAddr()
|
||||||
|
for _, s := range states {
|
||||||
|
if s.Addr.Addr().Compare(addr) == 0 {
|
||||||
|
switch s.State {
|
||||||
|
case corrosion.MembershipStateAlive:
|
||||||
|
state = pb.MachineMember_UP
|
||||||
|
case corrosion.MembershipStateSuspect:
|
||||||
|
state = pb.MachineMember_SUSPECT
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If the machine is the current machine, it is UP.
|
||||||
|
if m.Id == c.machineID {
|
||||||
|
state = pb.MachineMember_UP
|
||||||
|
}
|
||||||
|
members[i] = &pb.MachineMember{
|
||||||
|
Machine: m,
|
||||||
|
State: state,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &pb.ListMachinesResponse{Machines: members}, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -7,6 +7,10 @@ import (
|
|||||||
"net/netip"
|
"net/netip"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const DefaultSubnetBits = 24
|
||||||
|
|
||||||
|
var DefaultNetwork = netip.MustParsePrefix("10.210.0.0/16")
|
||||||
|
|
||||||
// IPAM is an in-memory IP address manager for allocating and releasing subnets for machines from a cluster network.
|
// IPAM is an in-memory IP address manager for allocating and releasing subnets for machines from a cluster network.
|
||||||
type IPAM struct {
|
type IPAM struct {
|
||||||
network netip.Prefix
|
network netip.Prefix
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
package cluster
|
package cluster
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/rand"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"math/big"
|
"github.com/psviderski/uncloud/internal/secret"
|
||||||
"uncloud/internal/secret"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewMachineID generates a new unique machine ID.
|
// NewMachineID generates a new unique machine ID.
|
||||||
@@ -14,14 +12,9 @@ func NewMachineID() (string, error) {
|
|||||||
|
|
||||||
// NewRandomMachineName generates a random machine name in the format "machine-xxxx".
|
// NewRandomMachineName generates a random machine name in the format "machine-xxxx".
|
||||||
func NewRandomMachineName() (string, error) {
|
func NewRandomMachineName() (string, error) {
|
||||||
const charset = "abcdefghijklmnopqrstuvwxyz0123456789"
|
suffix, err := secret.RandomAlphaNumeric(4)
|
||||||
suffix := make([]byte, 4)
|
if err != nil {
|
||||||
for i := range suffix {
|
return "", fmt.Errorf("generate random suffix: %w", err)
|
||||||
randIdx, err := rand.Int(rand.Reader, big.NewInt(int64(len(charset))))
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("get random number: %w", err)
|
|
||||||
}
|
|
||||||
suffix[i] = charset[randIdx.Int64()]
|
|
||||||
}
|
}
|
||||||
return "machine-" + string(suffix), nil
|
return "machine-" + suffix, nil
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user