mirror of
https://github.com/psviderski/uncloud.git
synced 2026-08-27 11:33:33 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7b88b8b810 | ||
|
|
97bdb8eae1 | ||
|
|
5baa8087e5 | ||
|
|
d25864e52f | ||
|
|
ec2787c99c | ||
|
|
9963f9df2d | ||
|
|
835834322b | ||
|
|
51ba3c7df8 | ||
|
|
047f661462 | ||
|
|
5a1e61ccff | ||
|
|
db60a81b2d | ||
|
|
7c19323ea1 | ||
|
|
290e6db98e | ||
|
|
df29d9ba43 | ||
|
|
64769081d9 | ||
|
|
0107363d41 | ||
|
|
48dc1dd624 | ||
|
|
3a6eef410a | ||
|
|
9f5ca9a33d | ||
|
|
b046b78398 | ||
|
|
ff213e71d3 | ||
|
|
3cda5cc564 | ||
|
|
75fdbaf2f4 | ||
|
|
81f4e3a67a | ||
|
|
e99e769455 | ||
|
|
11949eeb3b | ||
|
|
b437659678 | ||
|
|
c01365b416 | ||
|
|
813c397644 | ||
|
|
5cc005a423 | ||
|
|
066d411367 | ||
|
|
03970862ab | ||
|
|
8bf9fc0c9c | ||
|
|
93fef88fac | ||
|
|
455174ccb0 | ||
|
|
1ce3e62dbb | ||
|
|
4be8339c51 | ||
|
|
8dd69b46da | ||
|
|
4cc1e556dd | ||
|
|
9186d31d12 | ||
|
|
dd7bc6c982 | ||
|
|
12c07812a2 | ||
|
|
ec73f9ecd8 | ||
|
|
879c7c1876 | ||
|
|
c67127f83f | ||
|
|
5d3f1fe225 | ||
|
|
2e585d0183 | ||
|
|
ae9f943404 | ||
|
|
8805178a58 | ||
|
|
ec3de3a099 | ||
|
|
2c02139369 | ||
|
|
6c244bb8f9 | ||
|
|
fc0bf4a91b | ||
|
|
bc577fe405 | ||
|
|
6cc0611d75 | ||
|
|
8beb9e2679 | ||
|
|
0c2ff499c6 | ||
|
|
b39585df4c | ||
|
|
35d0a90125 | ||
|
|
a7273c8c96 | ||
|
|
9b48003a3f | ||
|
|
1e750cc7e1 | ||
|
|
2d73b541c5 | ||
|
|
cd9c0a1d2c | ||
|
|
37a76809ec | ||
|
|
f6062cf343 | ||
|
|
4fe360c573 | ||
|
|
cac1550b7d | ||
|
|
0bd42b2fca | ||
|
|
ad83d942e1 | ||
|
|
65f5a714dd | ||
|
|
2714587ec5 | ||
|
|
4166474ee8 | ||
|
|
3faaac4da7 | ||
|
|
1b6130447f | ||
|
|
614212a24c | ||
|
|
3ab708a437 | ||
|
|
7a6c5bf6d7 | ||
|
|
2acfafe218 | ||
|
|
b24b55c4c7 | ||
|
|
9a88e914f4 | ||
|
|
f613d3ce6d | ||
|
|
2c3bea64e5 | ||
|
|
da3634b690 | ||
|
|
6fb07db4b2 | ||
|
|
fea7edcbc5 | ||
|
|
a54555cd13 | ||
|
|
87c7889c2e | ||
|
|
05d0078451 | ||
|
|
053d73048c | ||
|
|
4ec3e97a54 | ||
|
|
10bbe9fbc5 | ||
|
|
31cd4c77e9 | ||
|
|
492a0af2b2 | ||
|
|
ff7bb25f2c | ||
|
|
a73cbfd691 | ||
|
|
55773e92ce | ||
|
|
37f543b905 | ||
|
|
e5f96509bf | ||
|
|
2f35ac6498 | ||
|
|
f75afa0cec | ||
|
|
0f38b128fa | ||
|
|
535b91fe52 | ||
|
|
5c8beb8bd3 | ||
|
|
2b4da1e6fe | ||
|
|
be8b4f079f | ||
|
|
59b96cc01f | ||
|
|
4c77fe2cfd | ||
|
|
3b89af4a11 | ||
|
|
1abadaef50 | ||
|
|
3b1ce42dd7 | ||
|
|
1640b4c433 | ||
|
|
44549c00fd | ||
|
|
26f34df3e6 | ||
|
|
89fb9efcfa | ||
|
|
638f320959 | ||
|
|
3f2e60ea74 | ||
|
|
8e0a24e781 | ||
|
|
e3e39dde32 | ||
|
|
fc61e2fc07 | ||
|
|
2a0d31cec0 | ||
|
|
2f55af0cab | ||
|
|
971b21d6c1 | ||
|
|
b651d95183 | ||
|
|
d45c1b6869 | ||
|
|
a5fe9fbfb2 | ||
|
|
4590a516ad | ||
|
|
5f9ed6dacf | ||
|
|
e97497d737 | ||
|
|
b4bf1d17d0 | ||
|
|
cfb4693286 | ||
|
|
fd3facb6b0 | ||
|
|
bd03bb9bfa | ||
|
|
eec728620b | ||
|
|
d2dd5e3c89 | ||
|
|
2bfcce7803 | ||
|
|
1ec5184f21 | ||
|
|
23dfe45b2f | ||
|
|
d9e906a77e | ||
|
|
51f7ebf3bf | ||
|
|
ae04dea808 | ||
|
|
009c0e1b4c | ||
|
|
18d38e96b4 | ||
|
|
12c86f1dd4 | ||
|
|
4df1820f96 | ||
|
|
6f9ac1fcc7 | ||
|
|
6a98acbe79 | ||
|
|
b0059af9b1 | ||
|
|
7edd8f2976 | ||
|
|
735e36012a | ||
|
|
69ebce57e9 | ||
|
|
365a5fc0b5 | ||
|
|
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 |
@@ -4,6 +4,7 @@
|
|||||||
# Allow files and directories.
|
# Allow files and directories.
|
||||||
!cmd/
|
!cmd/
|
||||||
!internal/
|
!internal/
|
||||||
|
!pkg/
|
||||||
!scripts/docker/
|
!scripts/docker/
|
||||||
!go.*
|
!go.*
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# EditorConfig is awesome: https://editorconfig.org
|
||||||
|
|
||||||
|
root = true
|
||||||
|
|
||||||
|
# Default settings for all files
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
end_of_line = lf
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
# Settings for Go files
|
||||||
|
[*.go]
|
||||||
|
indent_style = tab
|
||||||
|
indent_size = 4
|
||||||
|
|
||||||
|
# Settings for Bash scripts
|
||||||
|
[*.sh]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 4
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
github: [psviderski]
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
---
|
||||||
|
name: Bug Report
|
||||||
|
about: Create a report to help us improve
|
||||||
|
title: "[BUG] "
|
||||||
|
labels: bug
|
||||||
|
assignees: ""
|
||||||
|
---
|
||||||
|
|
||||||
|
**Describe the bug**
|
||||||
|
|
||||||
|
<!-- A clear and concise description of what the bug is. -->
|
||||||
|
|
||||||
|
**How to reproduce**
|
||||||
|
|
||||||
|
<!-- Steps to reproduce the behavior:
|
||||||
|
|
||||||
|
1. Run ...
|
||||||
|
2. Do ...
|
||||||
|
-->
|
||||||
|
|
||||||
|
**Expected behavior**
|
||||||
|
|
||||||
|
<!-- A clear and concise description of what you expected to happen. -->
|
||||||
|
|
||||||
|
**Environment:**
|
||||||
|
|
||||||
|
- Uncloud versions:
|
||||||
|
- Control (client) node (`uc --version`):
|
||||||
|
- Uncloud daemon (from the server) (`uncloudd --version`):
|
||||||
|
- OS version (`uname -a`):
|
||||||
|
- Client (control node):
|
||||||
|
- Server:
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
|
||||||
|
<!-- Add any other context about the problem here. -->
|
||||||
Symlink
+1
@@ -0,0 +1 @@
|
|||||||
|
../AI.md
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 525 KiB |
@@ -0,0 +1,69 @@
|
|||||||
|
name: Go Tests
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- "main"
|
||||||
|
- "test/**"
|
||||||
|
- "release/**"
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
paths:
|
||||||
|
- ".github/**"
|
||||||
|
- "**.go"
|
||||||
|
- "**.proto"
|
||||||
|
- "go.*"
|
||||||
|
- "Makefile"
|
||||||
|
- "scripts/**"
|
||||||
|
- "test/**"
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
|
|
||||||
|
- name: Set up Go
|
||||||
|
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
|
||||||
|
with:
|
||||||
|
go-version: "1.23.2"
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
go mod tidy
|
||||||
|
git diff --exit-code ||
|
||||||
|
(echo "go.mod or go.sum has changed. Please run 'go mod tidy' and commit the changes." && exit 1)
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
run: |
|
||||||
|
make ucind-image
|
||||||
|
make test
|
||||||
|
timeout-minutes: 10
|
||||||
|
|
||||||
|
check-protobuf:
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
timeout-minutes: 10
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
# Run on more platforms so that mise lockfile checksums are checked/generated for all platforms.
|
||||||
|
# See https://github.com/jdx/mise/issues/4276 for details.
|
||||||
|
os: [ubuntu-latest, macos-latest]
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
|
|
||||||
|
- name: Install Mise
|
||||||
|
uses: jdx/mise-action@13abe502c30c1559a5c37dff303831bab82c9402 # v2.2.3
|
||||||
|
with:
|
||||||
|
version: "2025.6.5"
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ github.token }}
|
||||||
|
|
||||||
|
- name: Generate Protobuf files
|
||||||
|
run: |
|
||||||
|
make proto
|
||||||
|
# check if the generated files are up to date
|
||||||
|
git diff --exit-code ||
|
||||||
|
(echo "Generated protobuf files are not up to date. Please run 'make proto' and commit the changes." && exit 1)
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
name: Lint
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- "main"
|
||||||
|
- "test/**"
|
||||||
|
- "release/**"
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
paths:
|
||||||
|
- "**.go"
|
||||||
|
- "go.*"
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
jobs:
|
||||||
|
lint:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
|
|
||||||
|
- name: Set up Go
|
||||||
|
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
|
||||||
|
with:
|
||||||
|
go-version: "1.23.2"
|
||||||
|
|
||||||
|
- name: golangci-lint
|
||||||
|
uses: golangci/golangci-lint-action@4afd733a84b1f43292c63897423277bb7f4313a9 # v8.0.0
|
||||||
|
with:
|
||||||
|
version: v2.2.2
|
||||||
|
|
||||||
|
- name: Format code
|
||||||
|
run: |
|
||||||
|
make format
|
||||||
|
git diff --exit-code ||
|
||||||
|
(echo "Code is not formatted. Please run 'make format' and commit the changes." && exit 1)
|
||||||
|
|
||||||
|
timeout-minutes: 10
|
||||||
+7
-2
@@ -27,8 +27,13 @@ go.work.sum
|
|||||||
.env
|
.env
|
||||||
.idea/
|
.idea/
|
||||||
|
|
||||||
data
|
/data/
|
||||||
dist/
|
/dist/
|
||||||
|
/uc
|
||||||
|
/tmp
|
||||||
|
|
||||||
# Web
|
# Web
|
||||||
node_modules/
|
node_modules/
|
||||||
|
|
||||||
|
# VS Code
|
||||||
|
.vscode
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
version: "2"
|
||||||
|
|
||||||
|
run:
|
||||||
|
concurrency: 4
|
||||||
|
tests: true
|
||||||
|
timeout: 5m
|
||||||
|
|
||||||
|
linters:
|
||||||
|
default: none
|
||||||
|
enable:
|
||||||
|
- bodyclose
|
||||||
|
# - dogsled
|
||||||
|
- dupl
|
||||||
|
# - errcheck
|
||||||
|
- gochecknoinits
|
||||||
|
- goconst
|
||||||
|
# - gocritic
|
||||||
|
# - gocyclo
|
||||||
|
# - godot
|
||||||
|
# - gosec
|
||||||
|
# - govet
|
||||||
|
- ineffassign
|
||||||
|
- misspell
|
||||||
|
- nakedret
|
||||||
|
# - prealloc
|
||||||
|
# - revive
|
||||||
|
# - staticcheck
|
||||||
|
- unconvert
|
||||||
|
# - unparam
|
||||||
|
# - unused
|
||||||
|
- whitespace
|
||||||
|
exclusions:
|
||||||
|
rules:
|
||||||
|
- path: ^test/e2e
|
||||||
|
linters:
|
||||||
|
- goconst # constants here add no value, so we skip goconst only for test/e2e.
|
||||||
|
|
||||||
|
formatters:
|
||||||
|
enable:
|
||||||
|
- gofumpt
|
||||||
|
- goimports
|
||||||
|
|
||||||
+26
-3
@@ -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,12 +37,16 @@ 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
|
||||||
@@ -49,14 +55,16 @@ archives:
|
|||||||
# use zip for windows archives
|
# use zip for windows archives
|
||||||
format_overrides:
|
format_overrides:
|
||||||
- goos: windows
|
- goos: windows
|
||||||
format: zip
|
formats:
|
||||||
|
- zip
|
||||||
files:
|
files:
|
||||||
- none*
|
- none*
|
||||||
|
|
||||||
- id: uncloudd
|
- id: uncloudd
|
||||||
builds:
|
builds:
|
||||||
- uncloudd
|
- uncloudd
|
||||||
format: gz
|
formats:
|
||||||
|
- tar.gz
|
||||||
name_template: "{{ .Binary }}_{{ .Os }}_{{ .Arch }}"
|
name_template: "{{ .Binary }}_{{ .Os }}_{{ .Arch }}"
|
||||||
files:
|
files:
|
||||||
- none*
|
- none*
|
||||||
@@ -70,3 +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 }}"
|
||||||
|
|||||||
+47
@@ -0,0 +1,47 @@
|
|||||||
|
[tools."aqua:vektra/mockery"]
|
||||||
|
version = "3.5.3"
|
||||||
|
backend = "aqua:vektra/mockery"
|
||||||
|
|
||||||
|
[tools."aqua:vektra/mockery".checksums]
|
||||||
|
"mockery_3.5.3_Darwin_arm64.tar.gz" = "sha256:a3a94b14c7414e148f2252199ffc4a0108d311358f3d336cbe05bb73cb203704"
|
||||||
|
"mockery_3.5.3_Linux_x86_64.tar.gz" = "sha256:ebce416b0175338525246c376885a1579ca6cd4d4015140ba0c70e6b5339a39c"
|
||||||
|
|
||||||
|
[tools.go]
|
||||||
|
version = "1.23.10"
|
||||||
|
backend = "core:go"
|
||||||
|
|
||||||
|
[tools.go.checksums]
|
||||||
|
"go1.23.10.darwin-arm64.tar.gz" = "sha256:25c64bfa8a8fd8e7f62fb54afa4354af8409a4bb2358c2699a1003b733e6fce5"
|
||||||
|
"go1.23.10.linux-amd64.tar.gz" = "sha256:535f9f81802499f2a7dbfa70abb8fda3793725fcc29460f719815f6e10b5fd60"
|
||||||
|
|
||||||
|
[tools.golangci-lint]
|
||||||
|
version = "2.2.2"
|
||||||
|
backend = "aqua:golangci/golangci-lint"
|
||||||
|
|
||||||
|
[tools.golangci-lint.checksums]
|
||||||
|
"golangci-lint-2.2.2-darwin-arm64.tar.gz" = "sha256:d84d94d042c0d495fd1746f3d18948a75de163b17a14e8de3ef840928dd2df74"
|
||||||
|
"golangci-lint-2.2.2-linux-amd64.tar.gz" = "sha256:c27fbde948a87d326feacd21df2f61a9c54dbd2e3bfa185c0a1cd6917a6f964f"
|
||||||
|
|
||||||
|
[tools.protoc]
|
||||||
|
version = "27.3"
|
||||||
|
backend = "aqua:protocolbuffers/protobuf/protoc"
|
||||||
|
|
||||||
|
[tools.protoc.checksums]
|
||||||
|
"protoc-27.3-linux-x86_64.zip" = "sha256:6dab2adab83f915126cab53540d48957c40e9e9023969c3e84d44bfb936c7741"
|
||||||
|
"protoc-27.3-osx-aarch_64.zip" = "sha256:b22116bd97cdbd7ea25346abe635a9df268515fe5ef5afa93cd9a68fc2513f84"
|
||||||
|
|
||||||
|
[tools.protoc-gen-go]
|
||||||
|
version = "1.34.2"
|
||||||
|
backend = "aqua:protocolbuffers/protobuf-go/protoc-gen-go"
|
||||||
|
|
||||||
|
[tools.protoc-gen-go.checksums]
|
||||||
|
"protoc-gen-go.v1.34.2.darwin.arm64.tar.gz" = "sha256:17aca7f948dbb624049030cf841e35895cf34183ba006e721247fdeb95ff2780"
|
||||||
|
"protoc-gen-go.v1.34.2.linux.amd64.tar.gz" = "sha256:b87bc134dee55576a842141bf0ed27761c635d746780fce5dee038c6dd16554f"
|
||||||
|
|
||||||
|
[tools.protoc-gen-go-grpc]
|
||||||
|
version = "1.5.1"
|
||||||
|
backend = "aqua:grpc/grpc-go/protoc-gen-go-grpc"
|
||||||
|
|
||||||
|
[tools.protoc-gen-go-grpc.checksums]
|
||||||
|
"protoc-gen-go-grpc.v1.5.1.darwin.arm64.tar.gz" = "sha256:d6083feb51dcfe59f26793e99ef01ef5eac68b64294ec2546711f614ac5878f3"
|
||||||
|
"protoc-gen-go-grpc.v1.5.1.linux.amd64.tar.gz" = "sha256:a6cac4ea731e54aea304ad44d704a69d1cdc82997084b33637e21a89dc9229d6"
|
||||||
@@ -1,5 +1,13 @@
|
|||||||
|
[settings]
|
||||||
|
experimental = true
|
||||||
|
|
||||||
[tools]
|
[tools]
|
||||||
|
"aqua:vektra/mockery" = "3.5.3"
|
||||||
go = "1.23"
|
go = "1.23"
|
||||||
|
golangci-lint = "2.2.2"
|
||||||
|
protoc = "27.3"
|
||||||
|
protoc-gen-go = "1.34.2"
|
||||||
|
protoc-gen-go-grpc = "1.5.1"
|
||||||
|
|
||||||
[env]
|
[env]
|
||||||
_.file = ".env"
|
_.file = ".env"
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
packages:
|
||||||
|
github.com/psviderski/uncloud/internal/machine/caddyconfig:
|
||||||
|
interfaces:
|
||||||
|
CaddyfileValidator:
|
||||||
@@ -0,0 +1,244 @@
|
|||||||
|
# AI.md - Uncloud Project Guide
|
||||||
|
|
||||||
|
This document provides comprehensive information about the Uncloud project for AI assistants to understand the codebase, architecture, and development practices.
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
**Uncloud** is a lightweight clustering and container orchestration tool that enables deployment and management of web applications across cloud VMs and bare metal servers. It creates a secure WireGuard mesh network between Docker hosts and provides automatic service discovery, load balancing, HTTPS ingress, and simple CLI commands for application management.
|
||||||
|
|
||||||
|
### Key Characteristics
|
||||||
|
|
||||||
|
- **Language**: Go
|
||||||
|
- **Architecture**: Decentralized, no control plane
|
||||||
|
- **Target**: Self-hosted infrastructure without Kubernetes complexity
|
||||||
|
- **License**: View LICENSE file for details
|
||||||
|
- **Status**: Active development, not yet ready for production
|
||||||
|
|
||||||
|
## Core Features
|
||||||
|
|
||||||
|
### 🏗️ Infrastructure
|
||||||
|
|
||||||
|
- **Multi-machine deployment**: Combine cloud VMs, dedicated servers, and bare metal
|
||||||
|
- **Zero-config networking**: Automatic WireGuard mesh with NAT traversal
|
||||||
|
- **Decentralized design**: No central control plane, all machines are equal
|
||||||
|
- **Service discovery**: Built-in DNS server resolves service names to container IPs
|
||||||
|
|
||||||
|
### 🚀 Application Management
|
||||||
|
|
||||||
|
- **Docker Compose compatibility**: Uses familiar Docker Compose format
|
||||||
|
- **Zero-downtime deployments**: Rolling updates without service interruption
|
||||||
|
- **Automatic HTTPS**: Caddy reverse proxy with Let's Encrypt integration
|
||||||
|
- **Managed DNS**: Free `*.cluster.uncloud.run` subdomains via Uncloud DNS service
|
||||||
|
- **Cross-machine scaling**: Run containers across multiple machines
|
||||||
|
|
||||||
|
### 🔧 Developer Experience
|
||||||
|
|
||||||
|
- **Docker-like CLI**: Familiar commands (`uc` binary)
|
||||||
|
- **Imperative operations**: Direct commands vs. declarative state reconciliation
|
||||||
|
- **Remote management**: Control entire infrastructure via SSH to any machine
|
||||||
|
- **Minimal overhead**: ~150MB RAM footprint per machine
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Core Components
|
||||||
|
|
||||||
|
1. **CLI (`uc`)** - Main user interface for cluster management
|
||||||
|
2. **Daemon (`uncloudd`)** - Machine daemon running on each node
|
||||||
|
3. **Corrosion** - Distributed SQLite database for cluster state (Fly.io project)
|
||||||
|
4. **Caddy** - Reverse proxy for HTTPS termination and routing
|
||||||
|
5. **WireGuard** - Secure mesh networking between machines
|
||||||
|
|
||||||
|
### Network Architecture
|
||||||
|
|
||||||
|
- Each machine gets unique subnet (e.g., `10.210.0.0/24`, `10.210.1.0/24`)
|
||||||
|
- Containers get cluster-unique IPs for direct communication
|
||||||
|
- Automatic peer discovery and key management
|
||||||
|
- NAT traversal for machines behind firewalls
|
||||||
|
|
||||||
|
### State Management
|
||||||
|
|
||||||
|
- **CRDT-based distributed storage** using Corrosion
|
||||||
|
- **Eventually consistent** state across all machines
|
||||||
|
- **Gossip protocol** (Serf) for state propagation
|
||||||
|
- **No quorum requirements** - partial network splits remain functional
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
### Key Directories
|
||||||
|
|
||||||
|
- **`cmd/`**: Contains main applications
|
||||||
|
|
||||||
|
- `uncloud/`: CLI tool with subcommands for machine, service, volume management
|
||||||
|
- `uncloudd/`: Daemon that runs on each machine
|
||||||
|
- `ucind/`: Development cluster management for testing
|
||||||
|
|
||||||
|
- **`internal/`**: Internal implementation packages
|
||||||
|
|
||||||
|
- `cli/`: Command-line interface logic
|
||||||
|
- `machine/`: Machine lifecycle and state management
|
||||||
|
- `daemon/`: Daemon implementation and gRPC services
|
||||||
|
- `dns/`: Internal DNS server for service discovery
|
||||||
|
|
||||||
|
- **`pkg/`**: Public API packages for external use
|
||||||
|
|
||||||
|
- `api/`: Core API types and definitions
|
||||||
|
- `client/`: Client libraries for interacting with Uncloud
|
||||||
|
|
||||||
|
- **`experiment/`**: Experimental features and prototypes
|
||||||
|
- **`scripts/`**: Installation and utility scripts
|
||||||
|
- **`test/`**: Test suites and test infrastructure
|
||||||
|
- **`website/`**: Documentation website (Docusaurus)
|
||||||
|
- **`misc/`**: Design documents and guides
|
||||||
|
|
||||||
|
## Key Technologies
|
||||||
|
|
||||||
|
### Core Dependencies
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Networking and orchestration
|
||||||
|
github.com/docker/docker // Docker API client
|
||||||
|
github.com/docker/compose/v2 // Docker Compose integration
|
||||||
|
golang.zx2c4.com/wireguard // WireGuard implementation
|
||||||
|
github.com/hashicorp/serf // Gossip protocol
|
||||||
|
|
||||||
|
// State management
|
||||||
|
github.com/ipfs/go-ds-crdt // CRDT distributed storage
|
||||||
|
github.com/dgraph-io/badger/v3 // Embedded database
|
||||||
|
|
||||||
|
// Web proxy
|
||||||
|
github.com/caddyserver/caddy/v2 // HTTP server and reverse proxy
|
||||||
|
|
||||||
|
// CLI and UX
|
||||||
|
github.com/spf13/cobra // CLI framework
|
||||||
|
github.com/charmbracelet/huh // Interactive forms
|
||||||
|
|
||||||
|
// gRPC and networking
|
||||||
|
google.golang.org/grpc // gRPC framework
|
||||||
|
github.com/siderolabs/grpc-proxy // gRPC proxy for forwarding
|
||||||
|
```
|
||||||
|
|
||||||
|
## Development Workflow
|
||||||
|
|
||||||
|
### Build and Development
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build binaries
|
||||||
|
go build -o uncloud ./cmd/uncloud
|
||||||
|
go build -o uncloudd ./cmd/uncloudd
|
||||||
|
```
|
||||||
|
|
||||||
|
### Key Make Targets
|
||||||
|
|
||||||
|
- `proto`: Generate protobuf code
|
||||||
|
- `ucind-cluster`: Create development cluster
|
||||||
|
- `update-dev`: Deploy to development machines
|
||||||
|
- `demo-reset`: Reset demo environment
|
||||||
|
- `fmt`: Format code
|
||||||
|
- `test`: Run all tests
|
||||||
|
- `lint`: Lint the code using golangci-lint
|
||||||
|
- `lint-and-fix`: Lint the code and fix issues whenever possible
|
||||||
|
|
||||||
|
## CLI Commands Structure
|
||||||
|
|
||||||
|
The `uc` CLI provides these main command groups:
|
||||||
|
|
||||||
|
### Machine Management
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uc machine init <user@host> # Initialize new cluster
|
||||||
|
uc machine add <user@host> # Add machine to cluster
|
||||||
|
uc machine ls # List machines
|
||||||
|
uc machine rm <name> # Remove machine
|
||||||
|
```
|
||||||
|
|
||||||
|
### Service Management
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uc run <image> # Run container from image
|
||||||
|
uc deploy # Deploy from compose.yaml
|
||||||
|
uc scale <service> <count> # Scale service replicas
|
||||||
|
uc ls # List services
|
||||||
|
uc rm <service> # Remove service
|
||||||
|
```
|
||||||
|
|
||||||
|
### Context and Connectivity
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uc context ls # List available contexts
|
||||||
|
uc context use <name> # Switch context
|
||||||
|
```
|
||||||
|
|
||||||
|
### Global Flags
|
||||||
|
|
||||||
|
- `--connect`: Connect to remote machine directly, without a config file
|
||||||
|
- `--uncloud-config`: Override config file path
|
||||||
|
|
||||||
|
## Development Guidelines
|
||||||
|
|
||||||
|
### Code Organization
|
||||||
|
|
||||||
|
- **Package naming**: Use clear, descriptive names
|
||||||
|
- **Error handling**: Wrap errors with context using `fmt.Errorf`
|
||||||
|
- **Logging**: Use structured logging with levels
|
||||||
|
- **gRPC**: Services defined in `internal/machine/api/pb/`
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
|
||||||
|
- Unit tests alongside source files (`*_test.go`)
|
||||||
|
- Integration tests in `test/e2e/`
|
||||||
|
- Test fixtures in `test/fixtures/`
|
||||||
|
|
||||||
|
### Dependencies
|
||||||
|
|
||||||
|
- Prefer standard library when possible
|
||||||
|
- Pin versions in `go.mod`
|
||||||
|
- Document rationale for external dependencies
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
|
||||||
|
- Support environment variables for key settings
|
||||||
|
- Validate configuration early
|
||||||
|
- Provide sensible defaults
|
||||||
|
|
||||||
|
## Troubleshooting and Debugging
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
- **Networking**: Check WireGuard status, iptables rules
|
||||||
|
- **DNS**: Verify service discovery resolution
|
||||||
|
- **Containers**: Use standard Docker debugging tools
|
||||||
|
- **State sync**: Check Corrosion logs for replication issues
|
||||||
|
|
||||||
|
### Debugging Tools
|
||||||
|
|
||||||
|
- Standard Linux networking tools (`ping`, `traceroute`, `wireshark`)
|
||||||
|
- Docker commands (`docker ps`, `docker logs`)
|
||||||
|
- SSH access to machines for direct inspection
|
||||||
|
- gRPC debugging tools
|
||||||
|
|
||||||
|
### Logs and Monitoring
|
||||||
|
|
||||||
|
- Systemd services (getting logs via `journalctl -u SERVICE_NAME`)
|
||||||
|
- `uncloud` -- Uncloud daemon
|
||||||
|
- `uncloud-corrosion` -- Corrosion process
|
||||||
|
- Machine daemon logs
|
||||||
|
- Container logs via Docker
|
||||||
|
|
||||||
|
## File Patterns and Conventions
|
||||||
|
|
||||||
|
### Important Files to Understand
|
||||||
|
|
||||||
|
- `cmd/uncloud/main.go`: CLI entry point and command structure
|
||||||
|
- `internal/cli/cli.go`: CLI implementation and configuration
|
||||||
|
- `internal/machine/machine.go`: Core machine management
|
||||||
|
- `pkg/api/`: Public API definitions
|
||||||
|
- `misc/design.md`: Architecture and design philosophy
|
||||||
|
- `README.md`: User-facing documentation
|
||||||
|
|
||||||
|
### Configuration Files
|
||||||
|
|
||||||
|
- `go.mod/go.sum`: Go dependency management
|
||||||
|
- `Makefile`: Build and development tasks
|
||||||
|
- `Dockerfile`: Container build instructions forUncloud-in-Docker (used for testing)
|
||||||
|
|
||||||
|
This document should help AI assistants understand the project structure, make informed suggestions, and contribute effectively to the Uncloud codebase.
|
||||||
+12
-3
@@ -17,13 +17,22 @@ RUN GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -o uncloudd cmd/uncloudd/main
|
|||||||
|
|
||||||
|
|
||||||
FROM alpine:${ALPINE_VERSION} AS corrosion-download
|
FROM alpine:${ALPINE_VERSION} AS corrosion-download
|
||||||
RUN wget -q -O /tmp/corrosion.tar.gz \
|
ARG TARGETARCH
|
||||||
https://github.com/psviderski/corrosion/releases/latest/download/corrosion-aarch64-unknown-linux-gnu.tar.gz \
|
|
||||||
|
RUN CORROSION_ARCH=$(case "${TARGETARCH}" in \
|
||||||
|
"amd64") echo "x86_64" ;; \
|
||||||
|
"arm64") echo "aarch64" ;; \
|
||||||
|
*) echo "Architecture '${TARGETARCH}' not supported" >&2 && exit 1 ;; \
|
||||||
|
esac) \
|
||||||
|
&& wget -q -O /tmp/corrosion.tar.gz \
|
||||||
|
"https://github.com/psviderski/corrosion/releases/latest/download/corrosion-${CORROSION_ARCH}-unknown-linux-gnu.tar.gz" \
|
||||||
&& tar -xzf /tmp/corrosion.tar.gz -C /tmp \
|
&& tar -xzf /tmp/corrosion.tar.gz -C /tmp \
|
||||||
&& install /tmp/corrosion /usr/local/bin/corrosion \
|
&& install /tmp/corrosion /usr/local/bin/corrosion \
|
||||||
&& rm /tmp/corrosion.tar.gz /tmp/corrosion
|
&& rm /tmp/corrosion.tar.gz /tmp/corrosion
|
||||||
|
|
||||||
FROM chainguard/wolfi-base:latest AS corrosion
|
# Beware that more modern images like chainguard/wolfi-base build glibc with flags that require newer CPU features,
|
||||||
|
# e.g. x86-64-v2. So such image may fail with "Fatal glibc error: CPU does not support x86-64-v2" on older CPUs.
|
||||||
|
FROM gcr.io/distroless/cc-debian12:latest AS corrosion
|
||||||
COPY --from=corrosion-download /usr/local/bin/corrosion /usr/local/bin/corrosion
|
COPY --from=corrosion-download /usr/local/bin/corrosion /usr/local/bin/corrosion
|
||||||
CMD ["corrosion", "agent"]
|
CMD ["corrosion", "agent"]
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,10 @@ update-dev:
|
|||||||
scp uncloudd-linux-amd64 spy@192.168.40.176:~/ && \
|
scp uncloudd-linux-amd64 spy@192.168.40.176:~/ && \
|
||||||
ssh spy@192.168.40.176 sudo install ./uncloudd-linux-amd64 /usr/local/bin/uncloudd && \
|
ssh spy@192.168.40.176 sudo install ./uncloudd-linux-amd64 /usr/local/bin/uncloudd && \
|
||||||
rm uncloudd-linux-amd64
|
rm uncloudd-linux-amd64
|
||||||
|
# GOOS=linux GOARCH=arm64 go build -o uncloudd-linux-arm64 ./cmd/uncloudd && \
|
||||||
|
# scp uncloudd-linux-arm64 ubuntu@152.67.101.197:~/ && \
|
||||||
|
# ssh ubuntu@152.67.101.197 sudo install ./uncloudd-linux-arm64 /usr/local/bin/uncloudd && \
|
||||||
|
# rm uncloudd-linux-arm64
|
||||||
|
|
||||||
update-restart-dev:
|
update-restart-dev:
|
||||||
GOOS=linux GOARCH=amd64 go build -o uncloudd-linux-amd64 ./cmd/uncloudd && \
|
GOOS=linux GOARCH=amd64 go build -o uncloudd-linux-amd64 ./cmd/uncloudd && \
|
||||||
@@ -16,29 +20,101 @@ update-restart-dev:
|
|||||||
scp uncloudd-linux-amd64 spy@192.168.40.176:~/ && \
|
scp uncloudd-linux-amd64 spy@192.168.40.176:~/ && \
|
||||||
ssh spy@192.168.40.176 "sudo install ./uncloudd-linux-amd64 /usr/local/bin/uncloudd && sudo systemctl restart uncloud" && \
|
ssh spy@192.168.40.176 "sudo install ./uncloudd-linux-amd64 /usr/local/bin/uncloudd && sudo systemctl restart uncloud" && \
|
||||||
rm uncloudd-linux-amd64
|
rm uncloudd-linux-amd64
|
||||||
|
# GOOS=linux GOARCH=arm64 go build -o uncloudd-linux-arm64 ./cmd/uncloudd && \
|
||||||
|
# scp uncloudd-linux-arm64 ubuntu@152.67.101.197:~/ && \
|
||||||
|
# ssh ubuntu@152.67.101.197 "sudo install ./uncloudd-linux-arm64 /usr/local/bin/uncloudd && sudo systemctl restart uncloud" && \
|
||||||
|
# rm uncloudd-linux-arm64
|
||||||
|
|
||||||
reset-dev:
|
reset-dev:
|
||||||
ssh spy@192.168.40.243 "sudo systemctl stop uncloud && sudo rm -rf /var/lib/uncloud"
|
ssh spy@192.168.40.243 "sudo systemctl stop uncloud && sudo rm -rf /var/lib/uncloud"
|
||||||
ssh spy@192.168.40.176 "sudo systemctl stop uncloud && sudo rm -rf /var/lib/uncloud"
|
ssh spy@192.168.40.176 "sudo systemctl stop uncloud && sudo rm -rf /var/lib/uncloud"
|
||||||
ssh ubuntu@152.67.101.197 "sudo systemctl stop uncloud && sudo rm -rf /var/lib/uncloud"
|
ssh ubuntu@152.67.101.197 "sudo systemctl stop uncloud && sudo rm -rf /var/lib/uncloud"
|
||||||
|
|
||||||
|
demo-reset:
|
||||||
|
rm -fv ~/.config/uncloud/config.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 \
|
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
|
--proto_path=. --proto_path=internal/machine/api/vendor internal/machine/api/pb/*.proto
|
||||||
|
|
||||||
|
.PHONY: proto-mise
|
||||||
|
proto-mise:
|
||||||
|
mise exec -- 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
|
.PHONY: corrosion-image
|
||||||
corrosion-image:
|
corrosion-image:
|
||||||
docker build -t "$(CORROSION_IMAGE)" --target corrosion .
|
docker build -t "$(CORROSION_IMAGE)" --target corrosion .
|
||||||
|
|
||||||
.PHONY: corrosion-image-push
|
.PHONY: corrosion-multiarch-image-push
|
||||||
corrosion-image-push: corrosion-image
|
corrosion-multiarch-image-push:
|
||||||
docker push "$(CORROSION_IMAGE)"
|
docker buildx build --push --platform linux/amd64,linux/arm64 -t "$(CORROSION_IMAGE)" --target corrosion .
|
||||||
|
|
||||||
.PHONY: ucind-image
|
.PHONY: ucind-image
|
||||||
ucind-image:
|
ucind-image:
|
||||||
docker build -t "$(UCIND_IMAGE)" --target ucind .
|
docker build -t "$(UCIND_IMAGE)" --target ucind .
|
||||||
|
|
||||||
.PHONY: ucind-image-push
|
.PHONY: ucind-multiarch-image-push
|
||||||
ucind-image-push: ucind-image
|
ucind-multiarch-image-push:
|
||||||
docker push "$(UCIND_IMAGE)"
|
docker buildx build --push --platform linux/amd64,linux/arm64 -t "$(UCIND_IMAGE)" --target ucind .
|
||||||
|
|
||||||
|
.PHONY: mocks
|
||||||
|
mocks:
|
||||||
|
@mockery
|
||||||
|
|
||||||
|
.PHONY: test
|
||||||
|
test:
|
||||||
|
ifeq ($(TEST_NAME),)
|
||||||
|
go test -count=1 -v ./...
|
||||||
|
else
|
||||||
|
go test -race -count=1 -v -run ^$(TEST_NAME)$$ ./...
|
||||||
|
endif
|
||||||
|
|
||||||
|
.PHONY: test-e2e
|
||||||
|
test-e2e:
|
||||||
|
go test -race -count=1 -v ./test/e2e
|
||||||
|
|
||||||
|
.PHONY: test-clean
|
||||||
|
test-clean:
|
||||||
|
@CONTAINERS=$$(docker ps --filter "name=ucind-test" -q); \
|
||||||
|
if [ -n "$$CONTAINERS" ]; then \
|
||||||
|
echo "Killing containers..."; \
|
||||||
|
docker kill $$CONTAINERS; \
|
||||||
|
fi; \
|
||||||
|
CONTAINERS_STOPPED=$$(docker ps -a --filter "name=ucind-test" -q); \
|
||||||
|
if [ -n "$$CONTAINERS_STOPPED" ]; then \
|
||||||
|
echo "Removing stopped containers..."; \
|
||||||
|
docker rm $$CONTAINERS_STOPPED; \
|
||||||
|
else \
|
||||||
|
echo "Nothing to clean."; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
.PHONY: vet
|
||||||
|
vet:
|
||||||
|
go vet ./...
|
||||||
|
|
||||||
|
.PHONY: format fmt
|
||||||
|
format fmt:
|
||||||
|
GOOS=linux golangci-lint fmt
|
||||||
|
|
||||||
|
LINT_TARGETS := lint lint-and-fix
|
||||||
|
.PHONY: $(LINT_TARGETS) _lint
|
||||||
|
$(LINT_TARGETS): _lint
|
||||||
|
lint: ARGS=
|
||||||
|
lint-and-fix: ARGS=--fix
|
||||||
|
_lint:
|
||||||
|
# Explicitly set OS to Linux to not skip *_linux.go files when running on macOS.
|
||||||
|
# Uncloud daemon won't likely support OS other than Linux anytime soon, so for now we can rely on that.
|
||||||
|
GOOS=linux golangci-lint run $(ARGS)
|
||||||
|
|
||||||
|
.PHONY: cli-docs
|
||||||
|
cli-docs:
|
||||||
|
go run ./cmd/uncloud docs
|
||||||
|
|||||||
@@ -1,44 +1,341 @@
|
|||||||
<div align="center">
|
<div align="center">
|
||||||
<img src="./website/images/logo.svg" height="100" alt="Uncloud logo"/>
|
<img src="./website/landing/images/logo.svg" height="100" width="100" alt="Uncloud logo"/>
|
||||||
<h1>Uncloud</h2>
|
<h1>Uncloud</h1>
|
||||||
A lightweight Docker clustering tool for running web apps on your own servers — from cloud VMs to bare metal.<br>
|
<p><strong>Docker simplicity. Multi-machine power.</strong></p>
|
||||||
Replace Heroku and Render, no Kubernetes required.
|
|
||||||
|
<p>
|
||||||
|
<a href="https://uncloud.run/docs"><img src="https://img.shields.io/badge/Docs-blue.svg?style=for-the-badge&logo=gitbook&logoColor=white" alt="Documentation"></a>
|
||||||
|
<a href="https://discord.gg/eR35KQJhPu"><img src="https://img.shields.io/badge/discord-5865F2.svg?style=for-the-badge&logo=discord&logoColor=white" alt="Join Discord"></a>
|
||||||
|
<a href="https://x.com/psviderski"><img src="https://img.shields.io/badge/follow-black?style=for-the-badge&logo=X&logoColor=while" alt="Follow on X"></a>
|
||||||
|
<a href="https://github.com/sponsors/psviderski"><img src="https://img.shields.io/badge/Donate-EA4AAA.svg?style=for-the-badge&logo=githubsponsors&logoColor=white" alt="Donate"></a>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
## Quick start
|
Uncloud is a lightweight clustering and container orchestration tool that lets you deploy and manage web apps across
|
||||||
|
cloud VMs and bare metal with minimised cluster management overhead. It creates a secure WireGuard mesh network between
|
||||||
|
your Docker hosts and provides automatic service discovery, load balancing, ingress with HTTPS, and simple CLI commands
|
||||||
|
to manage your apps.
|
||||||
|
|
||||||
|
Unlike traditional orchestrators, there's no central control plane and quorum to maintain. Each machine maintains a
|
||||||
|
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.
|
||||||
|
|
||||||
|
### 🚀 Coming soon
|
||||||
|
|
||||||
|
* **[Unregistry](https://github.com/psviderski/unregistry) integration**: Push your Docker images directly to your
|
||||||
|
machines without an external registry. It will transfer only the missing layers, making it fast and efficient.
|
||||||
|
|
||||||
|
## 🎬 Quick demo
|
||||||
|
|
||||||
|
The screenshot below demonstrates how I use Uncloud to deploy https://uncloud.run website to 2 remote machines from
|
||||||
|
the [`compose.yaml`](website/compose.yaml) file on my local machine.
|
||||||
|
|
||||||
|
It exposes the container port `8000/tcp` as HTTPS on the domain `uncloud.run`, served by the Caddy reverse proxy on the
|
||||||
|
remote machines. All managed by Uncloud.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Here is a more advanced use case. Deploy a highly available web app with automatic HTTPS across multiple regions and
|
||||||
|
on-premises in just a couple minutes.
|
||||||
|
|
||||||
|
<a href="https://uncloud.wistia.com/medias/k47uwt9uau?wvideo=k47uwt9uau">
|
||||||
|
<img src="https://embed-ssl.wistia.com/deliveries/3cf7014a48b93afc556444bed3e39a8c.jpg?image_crop_resized=900x526&image_play_button_rounded=true&image_play_button_size=2x&image_play_button_color=18181Be0" alt="Uncloud demo" width="450" height="263" />
|
||||||
|
</a>
|
||||||
|
|
||||||
|
## 💫 Why Uncloud?
|
||||||
|
|
||||||
|
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:
|
1. Install Uncloud CLI:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl -fsSL https://raw.githubusercontent.com/psviderski/uncloud/main/scripts/install_cli.sh | sh
|
brew install psviderski/tap/uncloud
|
||||||
|
|
||||||
|
# or using curl (macOS/Linux)
|
||||||
|
curl -fsS https://get.uncloud.run/install.sh | sh
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Initialize your first machine:
|
See [Installation](https://uncloud.run/docs/getting-started/install-cli) for more options.
|
||||||
|
|
||||||
|
2. Initialise your first machine:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
uc machine init root@your-server-ip
|
uc machine init root@your-server-ip
|
||||||
```
|
```
|
||||||
|
|
||||||
3. Create a DNS A record pointing `app.example.com` to your server's IP address, then deploy your app with
|
3. Deploy your app from a Docker image and publish its container port 8000 as HTTPS using `app.example.com` domain:
|
||||||
automatic HTTPS:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
uc run --name my-app -p app.example.com:8000/https registry/app
|
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 ✨
|
That's it! Your app is now running and accessible at https://app.example.com ✨
|
||||||
|
|
||||||
## Project status
|
5. Clean up when you're done:
|
||||||
|
|
||||||
Uncloud is an open source project I'm ([@psviderski](https://github.com/psviderski)) actively developing. I'd love to
|
```bash
|
||||||
share this journey with you. [Subscribe](https://uncloud.run/) to follow the progress, get early insights into new
|
uc ls
|
||||||
|
# Copy the service name from the output and run the rm command:
|
||||||
|
uc rm my-app-name
|
||||||
|
```
|
||||||
|
|
||||||
|
If you want to fully uninstall Uncloud on a machine, run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uncloud-uninstall
|
||||||
|
```
|
||||||
|
|
||||||
|
View the [Documentation](https://uncloud.run/docs) for more information.
|
||||||
|
|
||||||
|
## ⚙️ How it works
|
||||||
|
|
||||||
|
Check out the [design document](misc/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 questions, ideas, or need help?
|
||||||
|
* Start a discussion or join an existing one in
|
||||||
|
the [Discussions](https://github.com/psviderski/uncloud/discussions).
|
||||||
|
* Join our [Discord community](https://discord.gg/eR35KQJhPu) where we discuss features, roadmap, implementation
|
||||||
|
details, and help each other out.
|
||||||
|
|
||||||
|
## 🙏 Inspiration & Acknowledgements
|
||||||
|
|
||||||
|
I'm grateful to the following projects that inspired Uncloud's design and implementation:
|
||||||
|
|
||||||
|
* [Kamal](https://kamal-deploy.org/) — for proving that even in the declarative era of Kubernetes there is a place for
|
||||||
|
simple deployment tools that use imperative commands without complex orchestration. Kamal powers the multi-billion
|
||||||
|
dollar company [37signals](https://37signals.com/) where it was created, and that's truly inspiring!
|
||||||
|
* [Fly.io](https://fly.io/) — for inspiring my vision for what self-hosted infrastructure should feel like, proving that
|
||||||
|
developer experience and powerful infrastructure can coexist beautifully.
|
||||||
|
* [Tailscale](https://tailscale.com/) — for pioneering the vision of decentralised flat mesh networking with an amazing
|
||||||
|
user experience that feels like magic.
|
||||||
|
* [Talos Linux](https://github.com/siderolabs/talos)
|
||||||
|
and [KubeSpan](https://www.talos.dev/v1.10/talos-guides/network/kubespan/) — for the machine API design using
|
||||||
|
[grpc-proxy](https://github.com/siderolabs/grpc-proxy) and for its elegant approach to secure WireGuard-based overlay
|
||||||
|
networking with zero configuration.
|
||||||
|
* [Docker Swarm Classic](https://github.com/docker-archive/classicswarm) and
|
||||||
|
[Rancher 1.x](http://rancher-com-website-main-elb-elb-1798790864.us-west-2.elb.amazonaws.com/docs/rancher/v1.6/en/)
|
||||||
|
— for showing the power of simplicity and pragmatism in container orchestration and that not every problem needs the
|
||||||
|
complexity of Kubernetes.
|
||||||
|
|
||||||
|
Special thanks to the [Corrosion](https://github.com/superfly/corrosion) project by Fly.io for providing the distributed
|
||||||
|
SQLite database used to share Uncloud's cluster state.
|
||||||
|
|
||||||
|
## 📫 Stay updated
|
||||||
|
|
||||||
|
* Join our [Discord server](https://discord.gg/eR35KQJhPu) for real-time discussions, support, and updates.
|
||||||
|
* Follow [@psviderski](https://x.com/psviderski) on X/Twitter.
|
||||||
|
* Subscribe to [my newsletter](https://uncloud.run/#subscribe) to follow the progress, get early insights into new
|
||||||
features, and be the first to know when it's ready for production use.
|
features, and be the first to know when it's ready for production use.
|
||||||
|
* Watch this repository for releases.
|
||||||
|
|
||||||
I'd also love your input:
|
## 💖 Sponsors
|
||||||
|
|
||||||
- 🐛 Found a bug? [Open an issue](https://github.com/psviderski/uncloud/issues)
|
These companies and projects are helping Uncloud with their generous sponsorship and/or services:
|
||||||
- 💡 Have ideas? [Join the discussion](https://github.com/psviderski/uncloud/discussions)
|
|
||||||
|
|
||||||
## Motivation
|
<!-- Sentry -->
|
||||||
|
<a href="https://sentry.io/welcome/">
|
||||||
|
<img height="100" alt="Sentry" src="https://github.com/user-attachments/assets/6c1439c0-d20d-40dc-a669-c9aa94651dfa" />
|
||||||
|
</a>
|
||||||
|
|
||||||
TBD
|
## ❤️ 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>
|
||||||
|
|||||||
@@ -2,8 +2,9 @@ package cluster
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/psviderski/uncloud/internal/ucind"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"uncloud/internal/ucind"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewCreateCommand() *cobra.Command {
|
func NewCreateCommand() *cobra.Command {
|
||||||
|
|||||||
@@ -2,8 +2,9 @@ package cluster
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/psviderski/uncloud/internal/ucind"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"uncloud/internal/ucind"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewRemoveCommand() *cobra.Command {
|
func NewRemoveCommand() *cobra.Command {
|
||||||
|
|||||||
+7
-6
@@ -3,12 +3,13 @@ package main
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/docker/docker/client"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"uncloud/cmd/ucind/cluster"
|
|
||||||
"uncloud/internal/ucind"
|
"github.com/docker/docker/client"
|
||||||
|
"github.com/psviderski/uncloud/cmd/ucind/cluster"
|
||||||
|
"github.com/psviderski/uncloud/internal/ucind"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
@@ -44,9 +45,9 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO: allow to override using UNCLOUD_CONFIG env var.
|
// TODO: allow to override using UNCLOUD_CONFIG env var.
|
||||||
cmd.PersistentFlags().StringVar(&configPath, "uncloud-config", "~/.config/uncloud/config.toml",
|
cmd.PersistentFlags().StringVar(&configPath, "uncloud-config", "~/.config/uncloud/config.yaml",
|
||||||
"path to the Uncloud configuration file.")
|
"path to the Uncloud configuration file.")
|
||||||
_ = cmd.MarkPersistentFlagFilename("uncloud-config", "toml")
|
_ = cmd.MarkPersistentFlagFilename("uncloud-config", "yaml", "yml")
|
||||||
|
|
||||||
cmd.AddCommand(
|
cmd.AddCommand(
|
||||||
cluster.NewRootCommand(),
|
cluster.NewRootCommand(),
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
composecli "github.com/compose-spec/compose-go/v2/cli"
|
||||||
|
"github.com/psviderski/uncloud/internal/cli"
|
||||||
|
"github.com/psviderski/uncloud/pkg/client/compose"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewBuildCommand creates a new command to build services from a Compose file.
|
||||||
|
func NewBuildCommand() *cobra.Command {
|
||||||
|
opts := cli.BuildOptions{}
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "build [FLAGS] [SERVICE...]",
|
||||||
|
Short: "Build services from a Compose file.",
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
uncli := cmd.Context().Value("cli").(*cli.CLI)
|
||||||
|
|
||||||
|
if len(args) > 0 {
|
||||||
|
opts.Services = args
|
||||||
|
}
|
||||||
|
|
||||||
|
return runBuild(cmd.Context(), uncli, opts)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Flags().StringSliceVarP(&opts.Files, "file", "f", nil,
|
||||||
|
"One or more Compose files to build (default compose.yaml)")
|
||||||
|
cmd.Flags().StringSliceVarP(&opts.Profiles, "profile", "p", nil,
|
||||||
|
"One or more Compose profiles to enable.")
|
||||||
|
cmd.Flags().BoolVarP(&opts.Push, "push", "P", false,
|
||||||
|
"Push built images to the registry after building. (default false)")
|
||||||
|
cmd.Flags().BoolVarP(&opts.NoCache, "no-cache", "n", false,
|
||||||
|
"Do not use cache when building images. (default false)")
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: deduplicate with a similar functino for deploy options
|
||||||
|
// projectOpts returns the project options for the Compose file(s).
|
||||||
|
func projectOptsFromBuildOpts(opts cli.BuildOptions) []composecli.ProjectOptionsFn {
|
||||||
|
projectOpts := []composecli.ProjectOptionsFn{}
|
||||||
|
|
||||||
|
if len(opts.Profiles) > 0 {
|
||||||
|
projectOpts = append(projectOpts, composecli.WithDefaultProfiles(opts.Profiles...))
|
||||||
|
}
|
||||||
|
|
||||||
|
return projectOpts
|
||||||
|
}
|
||||||
|
|
||||||
|
// runBuild parses the Compose file(s), builds the services, and pushes them if requested.
|
||||||
|
func runBuild(ctx context.Context, uncli *cli.CLI, opts cli.BuildOptions) error {
|
||||||
|
projectOpts := projectOptsFromBuildOpts(opts)
|
||||||
|
project, err := compose.LoadProject(ctx, opts.Files, projectOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("load compose file(s): %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(opts.Services) > 0 {
|
||||||
|
project, err = project.WithSelectedServices(opts.Services)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("select services: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
servicesToBuild := cli.GetServicesThatNeedBuild(project)
|
||||||
|
|
||||||
|
if len(servicesToBuild) == 0 {
|
||||||
|
fmt.Println("No services to build.")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return cli.BuildServices(ctx, servicesToBuild, opts)
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
package caddy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/alecthomas/chroma/v2/quick"
|
||||||
|
"github.com/psviderski/uncloud/internal/cli"
|
||||||
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
type configOptions struct {
|
||||||
|
machine string
|
||||||
|
noColor bool
|
||||||
|
context string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewConfigCommand() *cobra.Command {
|
||||||
|
opts := configOptions{}
|
||||||
|
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "config",
|
||||||
|
Short: "Show the current Caddy configuration (Caddyfile).",
|
||||||
|
Long: "Display the current Caddy configuration (Caddyfile) from the connected machine or a specified one.",
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
uncli := cmd.Context().Value("cli").(*cli.CLI)
|
||||||
|
return runConfig(cmd.Context(), uncli, opts)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Flags().StringVarP(&opts.machine, "machine", "m", "",
|
||||||
|
"Name or ID of the machine to get the configuration from. (default is connected machine)")
|
||||||
|
cmd.Flags().BoolVar(&opts.noColor, "no-color", false,
|
||||||
|
"Disable syntax highlighting for the output.")
|
||||||
|
cmd.Flags().StringVarP(
|
||||||
|
&opts.context, "context", "c", "",
|
||||||
|
"Name of the cluster context. (default is the current context)",
|
||||||
|
)
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func runConfig(ctx context.Context, uncli *cli.CLI, opts configOptions) error {
|
||||||
|
clusterClient, err := uncli.ConnectCluster(ctx, opts.context)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("connect to cluster: %w", err)
|
||||||
|
}
|
||||||
|
defer clusterClient.Close()
|
||||||
|
|
||||||
|
if opts.machine != "" {
|
||||||
|
// If a specific machine is requested, use it to get the Caddy configuration.
|
||||||
|
ctx, _, err = api.ProxyMachinesContext(ctx, clusterClient, []string{opts.machine})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
config, err := clusterClient.Caddy.GetConfig(ctx, nil)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("get Caddy config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print the Caddyfile with syntax highlighting.
|
||||||
|
if opts.noColor {
|
||||||
|
fmt.Print(config.Caddyfile)
|
||||||
|
} else {
|
||||||
|
if err = quick.Highlight(os.Stdout, config.Caddyfile, "caddy", "terminal256", "monokai"); err != nil {
|
||||||
|
// If highlighting fails, fall back to plain output.
|
||||||
|
fmt.Print(config.Caddyfile)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,213 @@
|
|||||||
|
package caddy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"maps"
|
||||||
|
"os"
|
||||||
|
"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 {
|
||||||
|
caddyfile string
|
||||||
|
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.caddyfile, "caddyfile", "",
|
||||||
|
"Path to a custom global Caddy config (Caddyfile) that will be prepended to the auto-generated Caddy config.")
|
||||||
|
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 {
|
||||||
|
caddyfile := ""
|
||||||
|
if opts.caddyfile != "" {
|
||||||
|
data, err := os.ReadFile(opts.caddyfile)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("read Caddyfile: %w", err)
|
||||||
|
}
|
||||||
|
caddyfile = strings.TrimSpace(string(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
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, caddyfile, 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,17 @@
|
|||||||
|
package caddy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewRootCommand() *cobra.Command {
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "caddy",
|
||||||
|
Short: "Manage Caddy reverse proxy service.",
|
||||||
|
}
|
||||||
|
cmd.AddCommand(
|
||||||
|
NewConfigCommand(),
|
||||||
|
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,187 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
composecli "github.com/compose-spec/compose-go/v2/cli"
|
||||||
|
"github.com/docker/compose/v2/pkg/progress"
|
||||||
|
"github.com/psviderski/uncloud/internal/cli"
|
||||||
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
|
"github.com/psviderski/uncloud/pkg/client"
|
||||||
|
"github.com/psviderski/uncloud/pkg/client/compose"
|
||||||
|
"github.com/psviderski/uncloud/pkg/client/deploy"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
type deployOptions struct {
|
||||||
|
files []string
|
||||||
|
profiles []string
|
||||||
|
services []string
|
||||||
|
noBuild bool
|
||||||
|
recreate bool
|
||||||
|
|
||||||
|
context string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDeployCommand creates a new command to deploy services from a Compose file.
|
||||||
|
func NewDeployCommand() *cobra.Command {
|
||||||
|
opts := deployOptions{}
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "deploy [FLAGS] [SERVICE...]",
|
||||||
|
Short: "Deploy services from a Compose file.",
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
uncli := cmd.Context().Value("cli").(*cli.CLI)
|
||||||
|
|
||||||
|
if len(args) > 0 {
|
||||||
|
opts.services = args
|
||||||
|
}
|
||||||
|
|
||||||
|
return runDeploy(cmd.Context(), uncli, opts)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Flags().StringSliceVarP(&opts.files, "file", "f", nil,
|
||||||
|
"One or more Compose files to deploy services from. (default compose.yaml)")
|
||||||
|
cmd.Flags().StringSliceVarP(&opts.profiles, "profile", "p", nil,
|
||||||
|
"One or more Compose profiles to enable.")
|
||||||
|
cmd.Flags().StringVarP(&opts.context, "context", "c", "",
|
||||||
|
"Name of the cluster context to deploy to (default is the current context)")
|
||||||
|
cmd.Flags().BoolVarP(&opts.noBuild, "no-build", "n", false,
|
||||||
|
"Do not build images before deploying services. (default false)")
|
||||||
|
cmd.Flags().BoolVar(&opts.recreate, "recreate", false,
|
||||||
|
"Recreate containers even if their configuration and image haven't changed.")
|
||||||
|
|
||||||
|
// TODO: Consider adding a filter flag to specify which machines to deploy to but keep the rest running.
|
||||||
|
// Could be useful to test a new version on a subset of machines before rolling out to all.
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
// projectOpts returns the project options for the Compose file(s).
|
||||||
|
func projectOpts(opts deployOptions) []composecli.ProjectOptionsFn {
|
||||||
|
projectOpts := []composecli.ProjectOptionsFn{}
|
||||||
|
|
||||||
|
if len(opts.profiles) > 0 {
|
||||||
|
projectOpts = append(projectOpts, composecli.WithDefaultProfiles(opts.profiles...))
|
||||||
|
}
|
||||||
|
|
||||||
|
return projectOpts
|
||||||
|
}
|
||||||
|
|
||||||
|
// runDeploy parses the Compose file(s) and deploys the services.
|
||||||
|
func runDeploy(ctx context.Context, uncli *cli.CLI, opts deployOptions) error {
|
||||||
|
projectOpts := projectOpts(opts)
|
||||||
|
|
||||||
|
project, err := compose.LoadProject(ctx, opts.files, projectOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("load compose file(s): %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(opts.services) > 0 {
|
||||||
|
// Includes service dependencies by default. This is the default docker compose behavior.
|
||||||
|
project, err = project.WithSelectedServices(opts.services)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("select services: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
servicesToBuild := cli.GetServicesThatNeedBuild(project)
|
||||||
|
|
||||||
|
if len(servicesToBuild) > 0 {
|
||||||
|
if opts.noBuild {
|
||||||
|
fmt.Println("Not building services as requested.")
|
||||||
|
} else {
|
||||||
|
buildOpts := cli.BuildOptions{
|
||||||
|
Push: true,
|
||||||
|
NoCache: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := cli.BuildServices(ctx, servicesToBuild, buildOpts); err != nil {
|
||||||
|
return fmt.Errorf("build services: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clusterClient, err := uncli.ConnectCluster(ctx, opts.context)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("connect to cluster: %w", err)
|
||||||
|
}
|
||||||
|
defer clusterClient.Close()
|
||||||
|
|
||||||
|
var strategy deploy.Strategy
|
||||||
|
if opts.recreate {
|
||||||
|
strategy = &deploy.RollingStrategy{ForceRecreate: true}
|
||||||
|
}
|
||||||
|
composeDeploy, err := compose.NewDeploymentWithStrategy(ctx, clusterClient, project, strategy)
|
||||||
|
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
|
||||||
|
}
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/spf13/cobra/doc"
|
||||||
|
)
|
||||||
|
|
||||||
|
const docsDir = "website/docs/9-cli-reference"
|
||||||
|
|
||||||
|
type cmdWrapper struct {
|
||||||
|
cmd *cobra.Command
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDocsCommand creates a new hidden command to generate CLI reference docs.
|
||||||
|
func NewDocsCommand() *cobra.Command {
|
||||||
|
wrapper := &cmdWrapper{}
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "docs",
|
||||||
|
Short: "Generate Uncloud CLI reference docs",
|
||||||
|
SilenceUsage: true,
|
||||||
|
DisableFlagsInUseLine: true,
|
||||||
|
Hidden: true,
|
||||||
|
Args: cobra.NoArgs,
|
||||||
|
ValidArgsFunction: cobra.NoFileCompletions,
|
||||||
|
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||||
|
// Remove existing markdown files.
|
||||||
|
mdFiles, err := filepath.Glob(filepath.Join(docsDir, "*.md"))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("list existing CLI docs: %w", err)
|
||||||
|
}
|
||||||
|
for _, f := range mdFiles {
|
||||||
|
if err = os.Remove(f); err != nil {
|
||||||
|
return fmt.Errorf("remove '%s': %w", f, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate new CLI reference docs.
|
||||||
|
wrapper.cmd.Root().DisableAutoGenTag = true
|
||||||
|
if err := doc.GenMarkdownTree(cmd.Root(), docsDir); err != nil {
|
||||||
|
return fmt.Errorf("generate CLI docs: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove *completion*.md files that contain malformatted code blocks that break Docusaurus.
|
||||||
|
mdFiles, err = filepath.Glob(filepath.Join(docsDir, "*completion*.md"))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("list generated CLI docs: %w", err)
|
||||||
|
}
|
||||||
|
for _, f := range mdFiles {
|
||||||
|
if err = os.Remove(f); err != nil {
|
||||||
|
return fmt.Errorf("remove '%s': %w", f, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Post-process generated markdown files.
|
||||||
|
mdFiles, err = filepath.Glob(filepath.Join(docsDir, "*.md"))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("list generated CLI docs: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, f := range mdFiles {
|
||||||
|
if err = postProcessMarkdown(f); err != nil {
|
||||||
|
return fmt.Errorf("post-process '%s': %w", f, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
wrapper.cmd = cmd
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
// postProcessMarkdown applies transformations to generated markdown files.
|
||||||
|
func postProcessMarkdown(filename string) error {
|
||||||
|
data, err := os.ReadFile(filename)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
content := string(data)
|
||||||
|
|
||||||
|
// Replace "SEE ALSO" with "See also".
|
||||||
|
content = strings.ReplaceAll(content, "SEE ALSO", "See also")
|
||||||
|
// Escape <id> to avoid Docusaurus treating it as an HTML tag.
|
||||||
|
content = strings.ReplaceAll(content, "<id>", "\\<id>")
|
||||||
|
|
||||||
|
// Remove broken links to completion docs.
|
||||||
|
if strings.Contains(content, "[uc completion") {
|
||||||
|
lines := strings.Split(content, "\n")
|
||||||
|
var filteredLines []string
|
||||||
|
for _, line := range lines {
|
||||||
|
if !strings.Contains(line, "[uc completion") {
|
||||||
|
filteredLines = append(filteredLines, line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
content = strings.Join(filteredLines, "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adjust heading levels. Process from shortest to longest to avoid double replacements.
|
||||||
|
replacements := []struct {
|
||||||
|
old, new string
|
||||||
|
}{
|
||||||
|
{`(?m)^## `, `# `},
|
||||||
|
{`(?m)^### `, `## `},
|
||||||
|
{`(?m)^#### `, `### `},
|
||||||
|
{`(?m)^##### `, `#### `},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, r := range replacements {
|
||||||
|
re := regexp.MustCompile(r.old)
|
||||||
|
content = re.ReplaceAllString(content, r.new)
|
||||||
|
}
|
||||||
|
|
||||||
|
return os.WriteFile(filename, []byte(content), 0o644)
|
||||||
|
}
|
||||||
+147
-8
@@ -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
|
||||||
|
noCaddy bool
|
||||||
|
publicIP string
|
||||||
sshKey string
|
sshKey string
|
||||||
cluster string
|
context string
|
||||||
|
version string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAddCommand() *cobra.Command {
|
func NewAddCommand() *cobra.Command {
|
||||||
@@ -26,24 +41,148 @@ func NewAddCommand() *cobra.Command {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("parse remote machine: %w", err)
|
return fmt.Errorf("parse remote machine: %w", err)
|
||||||
}
|
}
|
||||||
remoteMachine := cli.RemoteMachine{
|
remoteMachine := &cli.RemoteMachine{
|
||||||
User: user,
|
User: user,
|
||||||
Host: host,
|
Host: host,
|
||||||
Port: port,
|
Port: port,
|
||||||
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, "+
|
||||||
|
fmt.Sprintf("blank '' or '%s' to disable ingress on this machine, or specify an IP address.", PublicIPNone),
|
||||||
|
)
|
||||||
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_*)",
|
fmt.Sprintf("Path to SSH private key for remote login (if not already added to SSH agent). (default %q)",
|
||||||
|
cli.DefaultSSHKeyPath),
|
||||||
|
)
|
||||||
|
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 "", PublicIPNone:
|
||||||
|
publicIP = nil
|
||||||
|
default:
|
||||||
|
ip, err := netip.ParseAddr(opts.publicIP)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("parse public IP: %w", err)
|
||||||
|
}
|
||||||
|
publicIP = &ip
|
||||||
|
}
|
||||||
|
|
||||||
|
clusterClient, machineClient, err := uncli.AddMachine(ctx, cli.AddMachineOptions{
|
||||||
|
Context: opts.context,
|
||||||
|
MachineName: opts.name,
|
||||||
|
PublicIP: publicIP,
|
||||||
|
RemoteMachine: remoteMachine,
|
||||||
|
Version: opts.version,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer clusterClient.Close()
|
||||||
|
defer machineClient.Close()
|
||||||
|
|
||||||
|
if opts.noCaddy {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for the cluster to be initialised to be able to deploy the Caddy service.
|
||||||
|
fmt.Println("Waiting for the machine to be ready...")
|
||||||
|
fmt.Println()
|
||||||
|
if err = waitClusterInitialised(ctx, machineClient); err != nil {
|
||||||
|
return fmt.Errorf("wait for cluster to be initialised on machine: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deploy a Caddy service container to the added machine. If caddy service is already deployed on other machines,
|
||||||
|
// use the deployed image version. Otherwise, use the latest version.
|
||||||
|
// NOTE: We use the cluster client to inspect and scale the Caddy service because the newly added machine may have
|
||||||
|
// issues accessing the Machine API of existing machines in the cluster.
|
||||||
|
// See the issue for more details: https://github.com/psviderski/uncloud/issues/65.
|
||||||
|
caddyImage := ""
|
||||||
|
caddySvc, err := clusterClient.InspectService(ctx, client.CaddyServiceName)
|
||||||
|
if err != nil {
|
||||||
|
if !errors.Is(err, api.ErrNotFound) {
|
||||||
|
return fmt.Errorf("inspect caddy service: %w", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
caddyImage = caddySvc.Containers[0].Container.Config.Image
|
||||||
|
// Find the latest created container and use its image.
|
||||||
|
var latestCreated time.Time
|
||||||
|
for _, c := range caddySvc.Containers[1:] {
|
||||||
|
created, err := time.Parse(time.RFC3339Nano, c.Container.Created)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if created.After(latestCreated) {
|
||||||
|
latestCreated = created
|
||||||
|
caddyImage = c.Container.Config.Image
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: scale the existing Caddy service to the new machine instead of running a new deployment
|
||||||
|
// that may cause a small downtime.
|
||||||
|
d, err := clusterClient.NewCaddyDeployment(caddyImage, "", api.Placement{})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("create caddy deployment: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = progress.RunWithTitle(ctx, func(ctx context.Context) error {
|
||||||
|
if _, err = d.Run(ctx); err != nil {
|
||||||
|
return fmt.Errorf("deploy caddy: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}, uncli.ProgressOut(), fmt.Sprintf("Deploying service %s", d.Spec.Name))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println()
|
||||||
|
return caddy.UpdateDomainRecords(ctx, machineClient, uncli.ProgressOut())
|
||||||
|
}
|
||||||
|
|
||||||
|
func waitClusterInitialised(ctx context.Context, client *client.Client) error {
|
||||||
|
boff := backoff.WithContext(backoff.NewExponentialBackOff(
|
||||||
|
backoff.WithMaxInterval(1*time.Second),
|
||||||
|
backoff.WithMaxElapsedTime(5*time.Minute),
|
||||||
|
), ctx)
|
||||||
|
|
||||||
|
check := func() error {
|
||||||
|
_, err := client.ListMachines(ctx, nil)
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
statusErr := status.Convert(err)
|
||||||
|
if statusErr.Code() == codes.FailedPrecondition {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return backoff.Permanent(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return backoff.Retry(check, boff)
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package machine
|
||||||
|
|
||||||
|
const (
|
||||||
|
// PublicIPNone is the value used to indicate removal of public IP
|
||||||
|
PublicIPNone = "none"
|
||||||
|
)
|
||||||
+128
-23
@@ -1,27 +1,40 @@
|
|||||||
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/cluster"
|
"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 {
|
||||||
|
dnsEndpoint string
|
||||||
name string
|
name string
|
||||||
network string
|
network string
|
||||||
|
noCaddy bool
|
||||||
|
noDNS bool
|
||||||
|
publicIP string
|
||||||
sshKey string
|
sshKey string
|
||||||
cluster string
|
version string
|
||||||
|
context string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewInitCommand() *cobra.Command {
|
func NewInitCommand() *cobra.Command {
|
||||||
opts := initOptions{}
|
opts := initOptions{}
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "init [USER@HOST:PORT]",
|
Use: "init [USER@HOST:PORT]",
|
||||||
Short: "Initialise a new cluster that consists of the local or remote machine.",
|
Short: "Initialise a new cluster with a remote machine as the first member.",
|
||||||
// TODO: include usage examples of initialising a local and remote machine.
|
// TODO: include usage examples of initialising a remote machine.
|
||||||
|
// TODO: support initialising a cluster on the local machine.
|
||||||
Args: cobra.MaximumNArgs(1),
|
Args: cobra.MaximumNArgs(1),
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
uncli := cmd.Context().Value("cli").(*cli.CLI)
|
uncli := cmd.Context().Value("cli").(*cli.CLI)
|
||||||
@@ -39,27 +52,119 @@ func NewInitCommand() *cobra.Command {
|
|||||||
KeyPath: opts.sshKey,
|
KeyPath: opts.sshKey,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return initCluster(cmd.Context(), uncli, remoteMachine, opts)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
cmd.Flags().StringVar(&opts.dnsEndpoint, "dns-endpoint", dns.DefaultUncloudDNSAPIEndpoint,
|
||||||
|
"API endpoint for the Uncloud DNS service.")
|
||||||
|
cmd.Flags().StringVarP(
|
||||||
|
&opts.name, "name", "n", "",
|
||||||
|
"Assign a name to the machine.",
|
||||||
|
)
|
||||||
|
cmd.Flags().StringVar(
|
||||||
|
&opts.network, "network", cluster.DefaultNetwork.String(),
|
||||||
|
"IPv4 network CIDR to use for machines and services.",
|
||||||
|
)
|
||||||
|
cmd.Flags().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, "+
|
||||||
|
fmt.Sprintf("blank '' or '%s' to disable ingress on this machine, or specify an IP address.", PublicIPNone),
|
||||||
|
)
|
||||||
|
cmd.Flags().StringVarP(
|
||||||
|
&opts.sshKey, "ssh-key", "i", "",
|
||||||
|
fmt.Sprintf("Path to SSH private key for remote login (if not already added to SSH agent). (default %q)",
|
||||||
|
cli.DefaultSSHKeyPath),
|
||||||
|
)
|
||||||
|
cmd.Flags().StringVar(
|
||||||
|
&opts.version, "version", "latest",
|
||||||
|
"Version of the Uncloud daemon to install on the machine.",
|
||||||
|
)
|
||||||
|
cmd.Flags().StringVarP(
|
||||||
|
&opts.context, "context", "c", cli.DefaultContextName,
|
||||||
|
"Name of the created context for the initialised cluster in the Uncloud config.",
|
||||||
|
)
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func initCluster(ctx context.Context, uncli *cli.CLI, remoteMachine *cli.RemoteMachine, opts initOptions) error {
|
||||||
netPrefix, err := netip.ParsePrefix(opts.network)
|
netPrefix, err := netip.ParsePrefix(opts.network)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("parse network CIDR: %w", err)
|
return fmt.Errorf("parse network CIDR: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return uncli.InitCluster(cmd.Context(), remoteMachine, opts.cluster, opts.name, netPrefix)
|
var publicIP *netip.Addr
|
||||||
},
|
switch opts.publicIP {
|
||||||
|
case "auto":
|
||||||
|
publicIP = &netip.Addr{}
|
||||||
|
case "", PublicIPNone:
|
||||||
|
publicIP = nil
|
||||||
|
default:
|
||||||
|
ip, err := netip.ParseAddr(opts.publicIP)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("parse public IP: %w", err)
|
||||||
}
|
}
|
||||||
cmd.Flags().StringVarP(&opts.name, "name", "n", "", "Assign a name to the machine.")
|
publicIP = &ip
|
||||||
cmd.Flags().StringVar(
|
}
|
||||||
&opts.network, "network", cluster.DefaultNetwork.String(),
|
client, err := uncli.InitCluster(ctx, cli.InitClusterOptions{
|
||||||
"IPv4 network CIDR to use for machines and services.",
|
Context: opts.context,
|
||||||
)
|
MachineName: opts.name,
|
||||||
cmd.Flags().StringVarP(
|
Network: netPrefix,
|
||||||
&opts.sshKey, "ssh-key", "i", "",
|
PublicIP: publicIP,
|
||||||
"path to SSH private key for SSH remote login. (default ~/.ssh/id_*)",
|
RemoteMachine: remoteMachine,
|
||||||
)
|
Version: opts.version,
|
||||||
cmd.Flags().StringVarP(
|
})
|
||||||
&opts.cluster, "cluster", "c", "",
|
if err != nil {
|
||||||
"Name of the cluster in the local config if initialising a remote machine.",
|
return err
|
||||||
)
|
}
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
return cmd
|
if opts.noCaddy && opts.noDNS {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deploy the Caddy service to the initialised machine.
|
||||||
|
// The creation of a deployment plan talks to cluster API. Since the API needs a few moments to become available
|
||||||
|
// after cluster initialisation, we keep the user informed during this wait.
|
||||||
|
fmt.Println("Waiting for the machine to be ready...")
|
||||||
|
fmt.Println()
|
||||||
|
|
||||||
|
if !opts.noDNS {
|
||||||
|
domain, err := client.ReserveDomain(ctx, &pb.ReserveDomainRequest{Endpoint: opts.dnsEndpoint})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("reserve cluster domain in Uncloud DNS: %w", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("Reserved cluster domain: %s\n", domain.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !opts.noCaddy {
|
||||||
|
d, err := client.NewCaddyDeployment("", "", api.Placement{})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("create caddy deployment: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = progress.RunWithTitle(ctx, func(ctx context.Context) error {
|
||||||
|
if _, err = d.Run(ctx); err != nil {
|
||||||
|
return fmt.Errorf("deploy caddy: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}, uncli.ProgressOut(), fmt.Sprintf("Deploying service %s", d.Spec.Name))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println()
|
||||||
|
return caddy.UpdateDomainRecords(ctx, client, uncli.ProgressOut())
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,30 +3,30 @@ package machine
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"text/tabwriter"
|
"text/tabwriter"
|
||||||
"uncloud/internal/cli"
|
|
||||||
"uncloud/internal/machine/network"
|
"github.com/psviderski/uncloud/internal/cli"
|
||||||
"uncloud/internal/secret"
|
"github.com/psviderski/uncloud/internal/machine/network"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewListCommand() *cobra.Command {
|
func NewListCommand() *cobra.Command {
|
||||||
var cluster string
|
var contextName string
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "ls",
|
Use: "ls",
|
||||||
Aliases: []string{"list"},
|
Aliases: []string{"list"},
|
||||||
Short: "List machines in a cluster.",
|
Short: "List machines in a cluster.",
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
uncli := cmd.Context().Value("cli").(*cli.CLI)
|
uncli := cmd.Context().Value("cli").(*cli.CLI)
|
||||||
return list(cmd.Context(), uncli, cluster)
|
return list(cmd.Context(), uncli, contextName)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
cmd.Flags().StringVarP(
|
cmd.Flags().StringVarP(
|
||||||
&cluster, "cluster", "c", "",
|
&contextName, "context", "c", "",
|
||||||
"Name of the cluster. (default is the current cluster)",
|
"Name of the cluster context. (default is the current context)",
|
||||||
)
|
)
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
@@ -38,7 +38,7 @@ func list(ctx context.Context, uncli *cli.CLI, clusterName string) error {
|
|||||||
}
|
}
|
||||||
defer client.Close()
|
defer client.Close()
|
||||||
|
|
||||||
machines, err := client.ListMachines(ctx)
|
machines, err := client.ListMachines(ctx, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("list machines: %w", err)
|
return fmt.Errorf("list machines: %w", err)
|
||||||
}
|
}
|
||||||
@@ -46,7 +46,7 @@ func list(ctx context.Context, uncli *cli.CLI, clusterName string) error {
|
|||||||
// Print the list of machines in a table format.
|
// Print the list of machines in a table format.
|
||||||
tw := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', 0)
|
tw := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', 0)
|
||||||
// Print header.
|
// Print header.
|
||||||
if _, err = fmt.Fprintln(tw, "NAME\tSTATE\tADDRESS\tPUBLIC KEY\tENDPOINTS"); err != nil {
|
if _, err = fmt.Fprintln(tw, "NAME\tSTATE\tADDRESS\tPUBLIC IP\tWIREGUARD ENDPOINTS"); err != nil {
|
||||||
return fmt.Errorf("write header: %w", err)
|
return fmt.Errorf("write header: %w", err)
|
||||||
}
|
}
|
||||||
// Print rows.
|
// Print rows.
|
||||||
@@ -54,14 +54,22 @@ func list(ctx context.Context, uncli *cli.CLI, clusterName string) error {
|
|||||||
m := member.Machine
|
m := member.Machine
|
||||||
subnet, _ := m.Network.Subnet.ToPrefix()
|
subnet, _ := m.Network.Subnet.ToPrefix()
|
||||||
subnet = netip.PrefixFrom(network.MachineIP(subnet), subnet.Bits())
|
subnet = netip.PrefixFrom(network.MachineIP(subnet), subnet.Bits())
|
||||||
|
|
||||||
|
publicIP := "-"
|
||||||
|
if m.PublicIp != nil {
|
||||||
|
ip, _ := m.PublicIp.ToAddr()
|
||||||
|
publicIP = ip.String()
|
||||||
|
}
|
||||||
|
|
||||||
endpoints := make([]string, len(m.Network.Endpoints))
|
endpoints := make([]string, len(m.Network.Endpoints))
|
||||||
for i, ep := range m.Network.Endpoints {
|
for i, ep := range m.Network.Endpoints {
|
||||||
addrPort, _ := ep.ToAddrPort()
|
addrPort, _ := ep.ToAddrPort()
|
||||||
endpoints[i] = addrPort.String()
|
endpoints[i] = addrPort.String()
|
||||||
}
|
}
|
||||||
publicKey := secret.Secret(m.Network.PublicKey)
|
|
||||||
if _, err = fmt.Fprintf(
|
if _, err = fmt.Fprintf(
|
||||||
tw, "%s\t%s\t%s\t%s\t%s\n", m.Name, capitalise(member.State.String()), subnet, publicKey, strings.Join(endpoints, ", "),
|
tw, "%s\t%s\t%s\t%s\t%s\n", m.Name, capitalise(member.State.String()), subnet, publicIP,
|
||||||
|
strings.Join(endpoints, ", "),
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return fmt.Errorf("write row: %w", err)
|
return fmt.Errorf("write row: %w", err)
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
package machine
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/psviderski/uncloud/internal/cli"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewRenameCommand() *cobra.Command {
|
||||||
|
var contextName string
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "rename OLD_NAME NEW_NAME",
|
||||||
|
Short: "Rename a machine in the cluster.",
|
||||||
|
Long: `Rename a machine in the cluster.
|
||||||
|
|
||||||
|
This command changes the name of an existing machine while preserving all other
|
||||||
|
configuration including network settings, public IP, and cluster membership.`,
|
||||||
|
Args: cobra.ExactArgs(2),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
uncli := cmd.Context().Value("cli").(*cli.CLI)
|
||||||
|
return rename(cmd.Context(), uncli, contextName, args[0], args[1])
|
||||||
|
},
|
||||||
|
}
|
||||||
|
cmd.Flags().StringVarP(
|
||||||
|
&contextName, "context", "c", "",
|
||||||
|
"Name of the cluster context. (default is the current context)",
|
||||||
|
)
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func rename(ctx context.Context, uncli *cli.CLI, contextName, oldName, newName string) error {
|
||||||
|
client, err := uncli.ConnectCluster(ctx, contextName)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
machine, err := client.RenameMachine(ctx, oldName, newName)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("rename machine: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Machine %q renamed to %q (ID: %s)\n", oldName, machine.Name, machine.Id)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,249 @@
|
|||||||
|
package machine
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"maps"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/lipgloss"
|
||||||
|
"github.com/charmbracelet/lipgloss/tree"
|
||||||
|
"github.com/docker/compose/v2/pkg/progress"
|
||||||
|
"github.com/docker/docker/api/types/container"
|
||||||
|
"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 removeOptions struct {
|
||||||
|
noReset bool
|
||||||
|
yes bool
|
||||||
|
context string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRmCommand() *cobra.Command {
|
||||||
|
opts := removeOptions{}
|
||||||
|
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "rm MACHINE",
|
||||||
|
Aliases: []string{"remove", "delete"},
|
||||||
|
Short: "Remove a machine from a cluster and reset it.",
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
uncli := cmd.Context().Value("cli").(*cli.CLI)
|
||||||
|
return remove(cmd.Context(), uncli, args[0], opts)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Flags().StringVarP(&opts.context, "context", "c", "",
|
||||||
|
"Name of the cluster context. (default is the current context)")
|
||||||
|
cmd.Flags().BoolVarP(&opts.yes, "yes", "y", false,
|
||||||
|
"Do not prompt for confirmation before removing the machine.")
|
||||||
|
cmd.Flags().BoolVar(&opts.noReset, "no-reset", false,
|
||||||
|
"Do not reset the machine after removing it from the cluster. This will leave all containers and data intact.")
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func remove(ctx context.Context, uncli *cli.CLI, nameOrID string, opts removeOptions) error {
|
||||||
|
// TODO: automatically choose a connection to the machine that is not being removed.
|
||||||
|
client, err := uncli.ConnectCluster(ctx, opts.context)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("connect to cluster: %w", err)
|
||||||
|
}
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
// Verify the machine exists and list all service containers on it including stopped ones.
|
||||||
|
mctx, machines, err := api.ProxyMachinesContext(ctx, client, []string{nameOrID})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(machines) == 0 {
|
||||||
|
return fmt.Errorf("machine '%s' not found in the cluster", nameOrID)
|
||||||
|
}
|
||||||
|
m := machines[0].Machine
|
||||||
|
|
||||||
|
// Verify if the machine being removed is the proxy machine we're connected to.
|
||||||
|
proxyMachine, err := client.MachineClient.Inspect(ctx, nil)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("inspect proxy machine: %w", err)
|
||||||
|
}
|
||||||
|
if proxyMachine.Id == m.Id {
|
||||||
|
allMachines, err := client.ListMachines(ctx, nil)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("list machines: %w", err)
|
||||||
|
}
|
||||||
|
if len(allMachines) > 1 {
|
||||||
|
return errors.New("cannot remove the machine you are currently connected to. " +
|
||||||
|
"Please connect to another machine in the cluster and try again. " +
|
||||||
|
"Use --connect flag or update 'connections' for the cluster context in your Uncloud config")
|
||||||
|
// It's ok to remove the proxy machine if it's the last one in the cluster.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: mark the machine as being removed and unschedulable when this is possible to prevent new containers
|
||||||
|
// from being scheduled on it while the removal is in progress.
|
||||||
|
|
||||||
|
reset := !opts.noReset
|
||||||
|
var containers []api.ServiceContainer
|
||||||
|
reachable := false
|
||||||
|
if reset {
|
||||||
|
// Check if the machine is up and has service containers.
|
||||||
|
listOpts := container.ListOptions{All: true}
|
||||||
|
machineContainers, err := client.Docker.ListServiceContainers(mctx, "", listOpts)
|
||||||
|
if err == nil {
|
||||||
|
reachable = true
|
||||||
|
containers = machineContainers[0].Containers
|
||||||
|
if len(containers) > 0 {
|
||||||
|
plural := ""
|
||||||
|
if len(containers) > 1 {
|
||||||
|
plural = "s"
|
||||||
|
}
|
||||||
|
fmt.Printf("Found %d service container%s on machine '%s':\n", len(containers), plural, m.Name)
|
||||||
|
fmt.Println(formatContainerTree(containers))
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Println("This will remove all service containers from the machine, remove it from the cluster, " +
|
||||||
|
"and reset it to the uninitialised state.")
|
||||||
|
} else {
|
||||||
|
fmt.Printf("No service containers found on machine '%s'.\n", m.Name)
|
||||||
|
fmt.Println("This will remove the machine from the cluster and reset it to the uninitialised state.")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fmt.Printf("This will remove machine '%s' from the cluster without resetting it as it's unreachable.\n",
|
||||||
|
m.Name)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fmt.Printf("This will remove machine '%s' from the cluster without resetting it.\n", m.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !opts.yes {
|
||||||
|
confirmed, err := cli.Confirm()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("confirm removal: %w", err)
|
||||||
|
}
|
||||||
|
if !confirmed {
|
||||||
|
fmt.Println("Cancelled. Machine was not removed.")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if reset && len(containers) > 0 {
|
||||||
|
err = progress.RunWithTitle(ctx, func(ctx context.Context) error {
|
||||||
|
return removeContainers(ctx, client, containers)
|
||||||
|
}, uncli.ProgressOut(), "Removing containers")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("remove containers: %w", err)
|
||||||
|
}
|
||||||
|
fmt.Println()
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err = client.RemoveMachine(ctx, &pb.RemoveMachineRequest{Id: m.Id}); err != nil {
|
||||||
|
return fmt.Errorf("remove machine from cluster: %w", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("Machine '%s' removed from the cluster.\n", m.Name)
|
||||||
|
|
||||||
|
if reset && reachable {
|
||||||
|
_, err = client.MachineClient.Reset(mctx, &pb.ResetRequest{})
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("WARNING: Failed to reset machine: %v\n", err)
|
||||||
|
} else {
|
||||||
|
fmt.Println("Machine reset initiated and will complete in the background.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: remove the connection to the machine from the uncloud config if it exists. We need a way to associate
|
||||||
|
// the machine with its connection in the config, e.g. by storing the machine name in the connection metadata.
|
||||||
|
|
||||||
|
// TODO: If Caddy was running on this machine and a cluster domain is reserved,
|
||||||
|
// let the user know that the DNS records should be updated.
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// formatContainerTree formats a list of containers grouped by service as a tree structure.
|
||||||
|
func formatContainerTree(containers []api.ServiceContainer) string {
|
||||||
|
if len(containers) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// Group containers by service.
|
||||||
|
serviceContainers := make(map[string][]api.ServiceContainer)
|
||||||
|
for _, ctr := range containers {
|
||||||
|
serviceName := ctr.ServiceName()
|
||||||
|
serviceContainers[serviceName] = append(serviceContainers[serviceName], ctr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build tree output.
|
||||||
|
var output []string
|
||||||
|
serviceNames := slices.Sorted(maps.Keys(serviceContainers))
|
||||||
|
for _, serviceName := range serviceNames {
|
||||||
|
ctrs := serviceContainers[serviceName]
|
||||||
|
mode := ctrs[0].ServiceMode()
|
||||||
|
|
||||||
|
// Format a tree for the service with its containers.
|
||||||
|
plural := ""
|
||||||
|
if len(ctrs) > 1 {
|
||||||
|
plural = "s"
|
||||||
|
}
|
||||||
|
t := tree.Root(fmt.Sprintf("• %s (%s, %d container%s)", serviceName, mode, len(ctrs), plural)).
|
||||||
|
EnumeratorStyle(lipgloss.NewStyle().MarginLeft(2).MarginRight(1))
|
||||||
|
|
||||||
|
// Add containers as children.
|
||||||
|
for _, ctr := range ctrs {
|
||||||
|
state, _ := ctr.HumanState()
|
||||||
|
info := fmt.Sprintf("%s • %s • %s", ctr.Name, ctr.Config.Image, state)
|
||||||
|
t.Child(info)
|
||||||
|
}
|
||||||
|
|
||||||
|
output = append(output, t.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Join(output, "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// removeContainers removes the given service containers from the machine.
|
||||||
|
func removeContainers(ctx context.Context, client api.Client, containers []api.ServiceContainer) error {
|
||||||
|
if len(containers) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
wg := sync.WaitGroup{}
|
||||||
|
errCh := make(chan error)
|
||||||
|
|
||||||
|
for _, ctr := range containers {
|
||||||
|
wg.Add(1)
|
||||||
|
go func(c api.ServiceContainer) {
|
||||||
|
defer wg.Done()
|
||||||
|
|
||||||
|
// Gracefully stop the container before removing it.
|
||||||
|
err := client.StopContainer(ctx, c.ServiceID(), c.ID, container.StopOptions{})
|
||||||
|
if err != nil && !errors.Is(err, api.ErrNotFound) {
|
||||||
|
errCh <- fmt.Errorf("stop container '%s': %w", c.ID, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = client.RemoveContainer(ctx, c.ServiceID(), c.ID, container.RemoveOptions{
|
||||||
|
// Remove anonymous volumes created by the container.
|
||||||
|
RemoveVolumes: true,
|
||||||
|
})
|
||||||
|
if err != nil && !errors.Is(err, api.ErrNotFound) {
|
||||||
|
errCh <- fmt.Errorf("remove container '%s': %w", c.ID, err)
|
||||||
|
}
|
||||||
|
}(ctr)
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
wg.Wait()
|
||||||
|
close(errCh)
|
||||||
|
}()
|
||||||
|
|
||||||
|
var err error
|
||||||
|
for e := range errCh {
|
||||||
|
err = errors.Join(err, e)
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
@@ -7,12 +7,16 @@ import (
|
|||||||
func NewRootCommand() *cobra.Command {
|
func NewRootCommand() *cobra.Command {
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "machine",
|
Use: "machine",
|
||||||
|
Aliases: []string{"m"},
|
||||||
Short: "Manage machines in an Uncloud cluster.",
|
Short: "Manage machines in an Uncloud cluster.",
|
||||||
}
|
}
|
||||||
cmd.AddCommand(
|
cmd.AddCommand(
|
||||||
NewAddCommand(),
|
NewAddCommand(),
|
||||||
NewInitCommand(),
|
NewInitCommand(),
|
||||||
NewListCommand(),
|
NewListCommand(),
|
||||||
|
NewRenameCommand(),
|
||||||
|
NewRmCommand(),
|
||||||
|
NewUpdateCommand(),
|
||||||
NewTokenCommand(),
|
NewTokenCommand(),
|
||||||
)
|
)
|
||||||
return cmd
|
return cmd
|
||||||
|
|||||||
@@ -2,9 +2,10 @@ package machine
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/psviderski/uncloud/internal/daemon"
|
||||||
|
"github.com/psviderski/uncloud/internal/machine"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"uncloud/internal/daemon"
|
|
||||||
"uncloud/internal/machine"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type tokenOptions struct {
|
type tokenOptions struct {
|
||||||
|
|||||||
@@ -0,0 +1,128 @@
|
|||||||
|
package machine
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/netip"
|
||||||
|
|
||||||
|
"github.com/psviderski/uncloud/internal/cli"
|
||||||
|
"github.com/psviderski/uncloud/internal/machine/api/pb"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
type updateOptions struct {
|
||||||
|
name string
|
||||||
|
publicIP string
|
||||||
|
context string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewUpdateCommand() *cobra.Command {
|
||||||
|
opts := updateOptions{}
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "update",
|
||||||
|
Short: "Update machine configuration in the cluster.",
|
||||||
|
Long: `Update machine configuration in the cluster.
|
||||||
|
|
||||||
|
This command allows setting various machine properties including:
|
||||||
|
- Machine name (--name)
|
||||||
|
- Public IP address (--public-ip)
|
||||||
|
|
||||||
|
At least one flag must be specified to perform an update operation.`,
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
uncli := cmd.Context().Value("cli").(*cli.CLI)
|
||||||
|
return update(cmd.Context(), uncli, cmd, opts, args[0])
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Flags().StringVar(
|
||||||
|
&opts.name, "name", "",
|
||||||
|
"New name for the machine",
|
||||||
|
)
|
||||||
|
cmd.Flags().StringVar(
|
||||||
|
&opts.publicIP, "public-ip", "",
|
||||||
|
fmt.Sprintf("Public IP address of the machine for ingress configuration. Use '%s' or '' to remove the public IP.", PublicIPNone),
|
||||||
|
)
|
||||||
|
cmd.Flags().StringVarP(
|
||||||
|
&opts.context, "context", "c", "",
|
||||||
|
"Name of the cluster context. (default is the current context)",
|
||||||
|
)
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func update(ctx context.Context, uncli *cli.CLI, cmd *cobra.Command, opts updateOptions, machineNameOrID string) error {
|
||||||
|
// Check if at least one flag was explicitly set
|
||||||
|
if !cmd.Flags().Changed("name") && !cmd.Flags().Changed("public-ip") {
|
||||||
|
return fmt.Errorf("at least one update flag must be specified (--name, --public-ip)")
|
||||||
|
}
|
||||||
|
|
||||||
|
client, err := uncli.ConnectCluster(ctx, opts.context)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
// First, resolve the machine to get its ID
|
||||||
|
machine, err := client.InspectMachine(ctx, machineNameOrID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("find machine: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the update request
|
||||||
|
req := &pb.UpdateMachineRequest{
|
||||||
|
MachineId: machine.Machine.Id,
|
||||||
|
}
|
||||||
|
|
||||||
|
if opts.name != "" {
|
||||||
|
req.Name = &opts.name
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if --public-ip flag was explicitly provided
|
||||||
|
if cmd.Flags().Changed("public-ip") {
|
||||||
|
if opts.publicIP == "" || opts.publicIP == PublicIPNone {
|
||||||
|
req.PublicIp = &pb.IP{} // Empty IP to signal removal
|
||||||
|
} else {
|
||||||
|
// Parse and validate the public IP
|
||||||
|
ip, err := netip.ParseAddr(opts.publicIP)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid public IP address %q: %w", opts.publicIP, err)
|
||||||
|
}
|
||||||
|
req.PublicIp = pb.NewIP(ip)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perform the update operation
|
||||||
|
updatedMachine, err := client.UpdateMachine(ctx, req)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("update machine: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Report what was changed
|
||||||
|
changes := make([]string, 0)
|
||||||
|
if opts.name != "" {
|
||||||
|
changes = append(changes, fmt.Sprintf("name: %q -> %q", machine.Machine.Name, updatedMachine.Name))
|
||||||
|
}
|
||||||
|
if cmd.Flags().Changed("public-ip") {
|
||||||
|
oldIP := PublicIPNone
|
||||||
|
if machine.Machine.PublicIp != nil {
|
||||||
|
if addr, err := machine.Machine.PublicIp.ToAddr(); err == nil {
|
||||||
|
oldIP = addr.String()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
newIP := PublicIPNone
|
||||||
|
if updatedMachine.PublicIp != nil {
|
||||||
|
if addr, err := updatedMachine.PublicIp.ToAddr(); err == nil {
|
||||||
|
newIP = addr.String()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
changes = append(changes, fmt.Sprintf("public IP: %s -> %s", oldIP, newIP))
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Machine %q (ID: %s) configuration updated:\n", updatedMachine.Name, updatedMachine.Id)
|
||||||
|
for _, change := range changes {
|
||||||
|
fmt.Printf(" %s\n", change)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
+57
-16
@@ -3,50 +3,91 @@ package main
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/spf13/cobra"
|
"net/netip"
|
||||||
"os"
|
|
||||||
"strings"
|
"strings"
|
||||||
"uncloud/cmd/uncloud/machine"
|
|
||||||
"uncloud/cmd/uncloud/service"
|
"github.com/psviderski/uncloud/cmd/uncloud/caddy"
|
||||||
"uncloud/internal/cli"
|
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: "uc",
|
||||||
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 strings.HasPrefix(opts.connect, "tcp://") {
|
||||||
|
addrPort, err := netip.ParseAddrPort(opts.connect[len("tcp://"):])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("get user home directory to resolve %q: %w", configPath, err)
|
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(),
|
||||||
|
NewDocsCommand(),
|
||||||
|
NewBuildCommand(),
|
||||||
|
caddy.NewRootCommand(),
|
||||||
|
cmdcontext.NewRootCommand(),
|
||||||
|
dns.NewRootCommand(),
|
||||||
machine.NewRootCommand(),
|
machine.NewRootCommand(),
|
||||||
service.NewRootCommand(),
|
service.NewRootCommand(),
|
||||||
service.NewInspectCommand(),
|
service.NewInspectCommand(),
|
||||||
service.NewListCommand(),
|
service.NewListCommand(),
|
||||||
service.NewRmCommand(),
|
service.NewRmCommand(),
|
||||||
service.NewRunCommand(),
|
service.NewRunCommand(),
|
||||||
|
service.NewScaleCommand(),
|
||||||
|
volume.NewRootCommand(),
|
||||||
)
|
)
|
||||||
cobra.CheckErr(cmd.Execute())
|
cobra.CheckErr(cmd.Execute())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,14 +3,14 @@ package service
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/docker/docker/pkg/stringid"
|
|
||||||
"github.com/docker/go-units"
|
|
||||||
"os"
|
"os"
|
||||||
"text/tabwriter"
|
"text/tabwriter"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/docker/docker/pkg/stringid"
|
||||||
|
"github.com/docker/go-units"
|
||||||
|
"github.com/psviderski/uncloud/internal/cli"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"uncloud/internal/cli"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type inspectOptions struct {
|
type inspectOptions struct {
|
||||||
@@ -21,7 +21,7 @@ type inspectOptions struct {
|
|||||||
func NewInspectCommand() *cobra.Command {
|
func NewInspectCommand() *cobra.Command {
|
||||||
opts := inspectOptions{}
|
opts := inspectOptions{}
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "inspect",
|
Use: "inspect SERVICE",
|
||||||
Short: "Display detailed information on a service.",
|
Short: "Display detailed information on a service.",
|
||||||
Args: cobra.ExactArgs(1),
|
Args: cobra.ExactArgs(1),
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
@@ -31,8 +31,8 @@ func NewInspectCommand() *cobra.Command {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
cmd.Flags().StringVarP(
|
cmd.Flags().StringVarP(
|
||||||
&opts.cluster, "cluster", "c", "",
|
&opts.cluster, "context", "c", "",
|
||||||
"Name of the cluster. (default is the current cluster)",
|
"Name of the cluster context. (default is the current context)",
|
||||||
)
|
)
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
@@ -49,7 +49,7 @@ func inspect(ctx context.Context, uncli *cli.CLI, opts inspectOptions) error {
|
|||||||
return fmt.Errorf("inspect service: %w", err)
|
return fmt.Errorf("inspect service: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
machines, err := client.ListMachines(ctx)
|
machines, err := client.ListMachines(ctx, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("list machines: %w", err)
|
return fmt.Errorf("list machines: %w", err)
|
||||||
}
|
}
|
||||||
@@ -70,21 +70,28 @@ func inspect(ctx context.Context, uncli *cli.CLI, opts inspectOptions) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, ctr := range svc.Containers {
|
for _, ctr := range svc.Containers {
|
||||||
createdAt := time.Unix(ctr.Container.Created, 0)
|
createdAt, err := time.Parse(time.RFC3339Nano, ctr.Container.Created)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("parse created time: %w", err)
|
||||||
|
}
|
||||||
created := units.HumanDuration(time.Now().UTC().Sub(createdAt)) + " ago"
|
created := units.HumanDuration(time.Now().UTC().Sub(createdAt)) + " ago"
|
||||||
|
|
||||||
machine := machinesNamesByID[ctr.MachineID]
|
machine := machinesNamesByID[ctr.MachineID]
|
||||||
if machine == "" {
|
if machine == "" {
|
||||||
machine = ctr.MachineID
|
machine = ctr.MachineID
|
||||||
}
|
}
|
||||||
|
state, err := ctr.Container.HumanState()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("get human state: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
_, err = fmt.Fprintf(
|
_, err = fmt.Fprintf(
|
||||||
tw,
|
tw,
|
||||||
"%s\t%s\t%s\t%s\t%s\n",
|
"%s\t%s\t%s\t%s\t%s\n",
|
||||||
stringid.TruncateID(ctr.Container.ID),
|
stringid.TruncateID(ctr.Container.ID),
|
||||||
ctr.Container.Image,
|
ctr.Container.Config.Image,
|
||||||
created,
|
created,
|
||||||
ctr.Container.Status,
|
state,
|
||||||
machine,
|
machine,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -1,53 +0,0 @@
|
|||||||
package service
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"os"
|
|
||||||
"text/tabwriter"
|
|
||||||
"uncloud/internal/cli"
|
|
||||||
)
|
|
||||||
|
|
||||||
func NewListCommand() *cobra.Command {
|
|
||||||
var cluster string
|
|
||||||
cmd := &cobra.Command{
|
|
||||||
Use: "ls",
|
|
||||||
Aliases: []string{"list"},
|
|
||||||
Short: "List services.",
|
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
|
||||||
uncli := cmd.Context().Value("cli").(*cli.CLI)
|
|
||||||
return list(cmd.Context(), uncli, cluster)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
cmd.Flags().StringVarP(
|
|
||||||
&cluster, "cluster", "c", "",
|
|
||||||
"Name of the cluster. (default is the current cluster)",
|
|
||||||
)
|
|
||||||
return cmd
|
|
||||||
}
|
|
||||||
|
|
||||||
func list(ctx context.Context, uncli *cli.CLI, clusterName string) error {
|
|
||||||
client, err := uncli.ConnectCluster(ctx, clusterName)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("connect to cluster: %w", err)
|
|
||||||
}
|
|
||||||
defer client.Close()
|
|
||||||
|
|
||||||
services, err := client.ListServices(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("list services: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Print the list of services in a table format.
|
|
||||||
tw := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', 0)
|
|
||||||
if _, err = fmt.Fprintln(tw, "SERVICE ID\tNAME\tMODE\tREPLICAS"); err != nil {
|
|
||||||
return fmt.Errorf("write header: %w", err)
|
|
||||||
}
|
|
||||||
for _, s := range services {
|
|
||||||
if _, err = fmt.Fprintf(tw, "%s\t%s\t%s\t%d\n", s.ID, s.Name, s.Mode, len(s.Containers)); err != nil {
|
|
||||||
return fmt.Errorf("write row: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return tw.Flush()
|
|
||||||
}
|
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
@@ -3,8 +3,10 @@ package service
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/docker/compose/v2/pkg/progress"
|
||||||
|
"github.com/psviderski/uncloud/internal/cli"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"uncloud/internal/cli"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type rmOptions struct {
|
type rmOptions struct {
|
||||||
@@ -26,8 +28,8 @@ func NewRmCommand() *cobra.Command {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
cmd.Flags().StringVarP(
|
cmd.Flags().StringVarP(
|
||||||
&opts.cluster, "cluster", "c", "",
|
&opts.cluster, "context", "c", "",
|
||||||
"Name of the cluster. (default is the current cluster)",
|
"Name of the cluster context. (default is the current context)",
|
||||||
)
|
)
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
@@ -40,10 +42,12 @@ func rm(ctx context.Context, uncli *cli.CLI, opts rmOptions) error {
|
|||||||
defer client.Close()
|
defer client.Close()
|
||||||
|
|
||||||
for _, s := range opts.services {
|
for _, s := range opts.services {
|
||||||
|
err = progress.RunWithTitle(ctx, func(ctx context.Context) error {
|
||||||
if err = client.RemoveService(ctx, s); err != nil {
|
if err = client.RemoveService(ctx, s); err != nil {
|
||||||
return fmt.Errorf("remove service %q: %w", s, err)
|
return fmt.Errorf("remove service '%s': %w", s, err)
|
||||||
}
|
}
|
||||||
fmt.Printf("Service %q removed.\n", s)
|
return nil
|
||||||
|
}, uncli.ProgressOut(), "Removing service "+s)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -7,12 +7,15 @@ import (
|
|||||||
func NewRootCommand() *cobra.Command {
|
func NewRootCommand() *cobra.Command {
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "service",
|
Use: "service",
|
||||||
|
Aliases: []string{"svc"},
|
||||||
Short: "Manage services in an Uncloud cluster.",
|
Short: "Manage services in an Uncloud cluster.",
|
||||||
}
|
}
|
||||||
cmd.AddCommand(
|
cmd.AddCommand(
|
||||||
|
NewInspectCommand(),
|
||||||
NewListCommand(),
|
NewListCommand(),
|
||||||
NewRmCommand(),
|
NewRmCommand(),
|
||||||
NewRunCommand(),
|
NewRunCommand(),
|
||||||
|
NewScaleCommand(),
|
||||||
)
|
)
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|||||||
+301
-34
@@ -3,18 +3,36 @@ package service
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
dockeropts "github.com/docker/cli/opts"
|
||||||
|
"github.com/docker/compose/v2/pkg/progress"
|
||||||
|
"github.com/docker/docker/daemon/names"
|
||||||
|
"github.com/psviderski/uncloud/internal/cli"
|
||||||
|
"github.com/psviderski/uncloud/internal/secret"
|
||||||
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
|
"github.com/psviderski/uncloud/pkg/client/deploy"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"uncloud/internal/api"
|
|
||||||
"uncloud/internal/cli"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type runOptions struct {
|
type runOptions struct {
|
||||||
|
caddyfile string
|
||||||
command []string
|
command []string
|
||||||
|
cpu dockeropts.NanoCPUs
|
||||||
|
entrypoint string
|
||||||
|
entrypointChanged bool
|
||||||
|
env []string
|
||||||
image string
|
image string
|
||||||
machine string
|
machines []string
|
||||||
|
memory dockeropts.MemBytes
|
||||||
mode string
|
mode string
|
||||||
name string
|
name string
|
||||||
|
privileged bool
|
||||||
publish []string
|
publish []string
|
||||||
|
pull string
|
||||||
|
replicas uint
|
||||||
|
user string
|
||||||
volumes []string
|
volumes []string
|
||||||
|
|
||||||
cluster string
|
cluster string
|
||||||
@@ -30,6 +48,7 @@ func NewRunCommand() *cobra.Command {
|
|||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
uncli := cmd.Context().Value("cli").(*cli.CLI)
|
uncli := cmd.Context().Value("cli").(*cli.CLI)
|
||||||
|
|
||||||
|
opts.entrypointChanged = cmd.Flag("entrypoint").Changed
|
||||||
opts.image = args[0]
|
opts.image = args[0]
|
||||||
if len(args) > 1 {
|
if len(args) > 1 {
|
||||||
opts.command = args[1:]
|
opts.command = args[1:]
|
||||||
@@ -39,77 +58,325 @@ func NewRunCommand() *cobra.Command {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: implement placement constraints and translate --machine to a constraint.
|
cmd.Flags().StringVar(&opts.caddyfile, "caddyfile", "",
|
||||||
//cmd.Flags().StringVarP(
|
"Path to a custom Caddy config (Caddyfile) for the service. "+
|
||||||
// &opts.machine, "machine", "m", "",
|
"Cannot be used together with non-@host published ports.")
|
||||||
// "Name or ID of the machine to run the service on. (default is first available)",
|
cmd.Flags().VarP(&opts.cpu, "cpu", "",
|
||||||
//)
|
"Maximum number of CPU cores a service container can use. Fractional values are allowed: "+
|
||||||
|
"0.5 for half a core or 2.25 for two and a quarter cores.")
|
||||||
|
cmd.Flags().StringVar(&opts.entrypoint, "entrypoint", "",
|
||||||
|
"Overwrite the default ENTRYPOINT of the image. Pass an empty string \"\" to reset it.")
|
||||||
|
cmd.Flags().StringSliceVarP(&opts.env, "env", "e", nil,
|
||||||
|
"Set an environment variable for service containers. Can be specified multiple times.\n"+
|
||||||
|
"Format: VAR=value or just VAR to use the value from the local environment.")
|
||||||
cmd.Flags().StringVar(&opts.mode, "mode", api.ServiceModeReplicated,
|
cmd.Flags().StringVar(&opts.mode, "mode", api.ServiceModeReplicated,
|
||||||
fmt.Sprintf("Replication mode of the service: either %q (a specified number of containers across "+
|
fmt.Sprintf("Replication mode of the service: either '%s' (a specified number of containers across "+
|
||||||
"the machines) or %q (one container on every machine).",
|
"the machines) or '%s' (one container on every machine).",
|
||||||
api.ServiceModeReplicated, api.ServiceModeGlobal))
|
api.ServiceModeReplicated, api.ServiceModeGlobal))
|
||||||
|
cmd.Flags().StringSliceVarP(&opts.machines, "machine", "m", nil,
|
||||||
|
"Placement constraint by machine 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", "",
|
cmd.Flags().StringVarP(&opts.name, "name", "n", "",
|
||||||
"Assign a name to the service. A random name is generated if not specified.")
|
"Assign a name to the service. A random name is generated if not specified.")
|
||||||
|
cmd.Flags().BoolVar(&opts.privileged, "privileged", false,
|
||||||
|
"Give extended privileges to service containers. This is a security risk and should be used with caution.")
|
||||||
cmd.Flags().StringSliceVarP(&opts.publish, "publish", "p", nil,
|
cmd.Flags().StringSliceVarP(&opts.publish, "publish", "p", nil,
|
||||||
"Publish a service port to make it accessible outside the cluster. Can be specified multiple times.\n"+
|
"Publish a service port to make it accessible outside the cluster. Can be specified multiple times.\n"+
|
||||||
"Format: [hostname:][load_balancer_port:]container_port[/protocol] or [host_ip:]:host_port:container_port[/protocol]@host\n"+
|
"Format: [hostname:]container_port[/protocol] or [host_ip:]host_port:container_port[/protocol]@host\n"+
|
||||||
"Supported protocols: tcp, udp, http, https (default is tcp). If a hostname for http(s) port is not specified, a random hostname is generated.\n"+
|
"Supported protocols: tcp, udp, http, https (default is tcp). If a hostname for http(s) port is not specified\n"+
|
||||||
|
"and a cluster domain is reserved, service-name.cluster-domain will be used as the hostname.\n"+
|
||||||
"Examples:\n"+
|
"Examples:\n"+
|
||||||
" -p app.example.com:8080/https Publish port 8080 as HTTPS via load balancer with custom hostname\n"+
|
" -p 8080/https Publish port 8080 as HTTPS via reverse proxy with default service-name.cluster-domain hostname\n"+
|
||||||
" -p 9000:8080 Publish port 8080 as TCP port 9000 via load balancer\n"+
|
" -p app.example.com:8080/https Publish port 8080 as HTTPS via reverse proxy with custom hostname\n"+
|
||||||
|
// TODO: add support for publishing L4 tcp/udp ports.
|
||||||
|
//" -p 9000:8080 Publish port 8080 as TCP port 9000 via reverse proxy\n"+
|
||||||
" -p 53:5353/udp@host Bind UDP port 5353 to host port 53")
|
" -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,
|
cmd.Flags().StringSliceVarP(&opts.volumes, "volume", "v", nil,
|
||||||
"Bind mount a host file or directory into a service container using the format "+
|
"Mount a data volume or host path into service containers. Service containers will be scheduled on the machine(s) where\n"+
|
||||||
"/host/path:/container/path[:ro]. Can be specified multiple times.")
|
"the volume is located. Can be specified multiple times.\n"+
|
||||||
|
"Format: volume_name:/container/path[:ro|volume-nocopy] or /host/path:/container/path[:ro]\n"+
|
||||||
|
"Examples:\n"+
|
||||||
|
" -v postgres-data:/var/lib/postgresql/data Mount volume 'postgres-data' to /var/lib/postgresql/data in container\n"+
|
||||||
|
" -v /data/uploads:/app/uploads Bind mount /data/uploads host directory to /app/uploads in container\n"+
|
||||||
|
" -v /host/path:/container/path:ro Bind mount a host directory or file as read-only")
|
||||||
|
|
||||||
cmd.Flags().StringVarP(
|
cmd.Flags().StringVarP(
|
||||||
&opts.cluster, "cluster", "c", "",
|
&opts.cluster, "context", "c", "",
|
||||||
"Name of the cluster to run the service in. (default is the current cluster)",
|
"Name of the cluster context to run the service in. (default is the current context)",
|
||||||
)
|
)
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
func run(ctx context.Context, uncli *cli.CLI, opts runOptions) error {
|
func run(ctx context.Context, uncli *cli.CLI, opts runOptions) error {
|
||||||
|
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
|
||||||
|
|
||||||
|
caddyfile := ""
|
||||||
|
if opts.caddyfile != "" {
|
||||||
|
data, err := os.ReadFile(opts.caddyfile)
|
||||||
|
if err != nil {
|
||||||
|
return spec, fmt.Errorf("read Caddyfile: %w", err)
|
||||||
|
}
|
||||||
|
caddyfile = strings.TrimSpace(string(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
env, err := parseEnv(opts.env)
|
||||||
|
if err != nil {
|
||||||
|
return spec, err
|
||||||
|
}
|
||||||
|
|
||||||
switch opts.mode {
|
switch opts.mode {
|
||||||
case "", api.ServiceModeReplicated, api.ServiceModeGlobal:
|
case api.ServiceModeReplicated, api.ServiceModeGlobal:
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("invalid replication mode: %q", opts.mode)
|
return 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))
|
ports := make([]api.PortSpec, len(opts.publish))
|
||||||
for i, publishPort := range opts.publish {
|
for i, publishPort := range opts.publish {
|
||||||
port, err := api.ParsePortSpec(publishPort)
|
port, err := api.ParsePortSpec(publishPort)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("invalid service port '%s': %w", publishPort, err)
|
return spec, fmt.Errorf("invalid service port '%s': %w", publishPort, err)
|
||||||
}
|
}
|
||||||
ports[i] = port
|
ports[i] = port
|
||||||
}
|
}
|
||||||
// TODO: parse and validate opts.volumes to fail fast if invalid.
|
|
||||||
|
|
||||||
spec := api.ServiceSpec{
|
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{
|
Container: api.ContainerSpec{
|
||||||
Command: opts.command,
|
Command: opts.command,
|
||||||
|
Env: env,
|
||||||
Image: opts.image,
|
Image: opts.image,
|
||||||
Volumes: opts.volumes,
|
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,
|
Mode: opts.mode,
|
||||||
Name: opts.name,
|
Name: opts.name,
|
||||||
|
Placement: placement,
|
||||||
Ports: ports,
|
Ports: ports,
|
||||||
}
|
Replicas: opts.replicas,
|
||||||
if err := spec.Validate(); err != nil {
|
Volumes: volumes,
|
||||||
return fmt.Errorf("invalid service configuration: %w", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
client, err := uncli.ConnectCluster(ctx, opts.cluster)
|
if caddyfile != "" {
|
||||||
|
spec.Caddy = &api.CaddySpec{
|
||||||
|
Config: caddyfile,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 {
|
if err != nil {
|
||||||
return fmt.Errorf("connect to cluster: %w", err)
|
return spec, fmt.Errorf("generate service name: %w", err)
|
||||||
}
|
}
|
||||||
defer client.Close()
|
|
||||||
|
|
||||||
if _, err = client.RunService(ctx, spec); err != nil {
|
|
||||||
return fmt.Errorf("run service: %w", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
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
|
||||||
|
}
|
||||||
@@ -2,14 +2,16 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"syscall"
|
"syscall"
|
||||||
"uncloud/internal/daemon"
|
|
||||||
"uncloud/internal/log"
|
"github.com/psviderski/uncloud/internal/daemon"
|
||||||
"uncloud/internal/machine"
|
"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() {
|
||||||
@@ -22,6 +24,7 @@ func main() {
|
|||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "uncloudd",
|
Use: "uncloudd",
|
||||||
Short: "Uncloud machine daemon.",
|
Short: "Uncloud machine daemon.",
|
||||||
|
Version: version.String(),
|
||||||
SilenceUsage: true,
|
SilenceUsage: true,
|
||||||
SilenceErrors: true,
|
SilenceErrors: true,
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
@@ -52,5 +55,4 @@ func main() {
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
cobra.CheckErr(cmd.ExecuteContext(ctx))
|
cobra.CheckErr(cmd.ExecuteContext(ctx))
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,10 +3,11 @@ package main
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/hashicorp/serf/serf"
|
|
||||||
crdt "github.com/ipfs/go-ds-crdt"
|
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/hashicorp/serf/serf"
|
||||||
|
crdt "github.com/ipfs/go-ds-crdt"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Implements the Broadcaster interface.
|
// Implements the Broadcaster interface.
|
||||||
|
|||||||
@@ -2,9 +2,10 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/dgraph-io/badger/v3"
|
|
||||||
"log"
|
"log"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/dgraph-io/badger/v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
func customTimeEncoder(t time.Time) string {
|
func customTimeEncoder(t time.Time) string {
|
||||||
|
|||||||
@@ -2,9 +2,10 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/ipfs/go-log/v2"
|
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
"github.com/ipfs/go-log/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ipfsLogger is an slog logger that implements the IPFS go-log StandardLogger interface.
|
// ipfsLogger is an slog logger that implements the IPFS go-log StandardLogger interface.
|
||||||
|
|||||||
@@ -2,12 +2,13 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/docker/docker/libnetwork/networkdb"
|
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/docker/docker/libnetwork/networkdb"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|||||||
@@ -4,6 +4,13 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/hashicorp/memberlist"
|
"github.com/hashicorp/memberlist"
|
||||||
"github.com/hashicorp/serf/cmd/serf/command/agent"
|
"github.com/hashicorp/serf/cmd/serf/command/agent"
|
||||||
"github.com/hashicorp/serf/serf"
|
"github.com/hashicorp/serf/serf"
|
||||||
@@ -11,12 +18,6 @@ import (
|
|||||||
badger "github.com/ipfs/go-ds-badger3"
|
badger "github.com/ipfs/go-ds-badger3"
|
||||||
crdt "github.com/ipfs/go-ds-crdt"
|
crdt "github.com/ipfs/go-ds-crdt"
|
||||||
"github.com/lmittmann/tint"
|
"github.com/lmittmann/tint"
|
||||||
"log/slog"
|
|
||||||
"net"
|
|
||||||
"os"
|
|
||||||
"os/signal"
|
|
||||||
"syscall"
|
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func createSerfAgentConfig(name, bindAddr, rpcAddr, profile string) *agent.Config {
|
func createSerfAgentConfig(name, bindAddr, rpcAddr, profile string) *agent.Config {
|
||||||
|
|||||||
@@ -4,18 +4,19 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
|
||||||
"github.com/hashicorp/serf/serf"
|
"github.com/hashicorp/serf/serf"
|
||||||
"github.com/ipfs/boxo/datastore/dshelp"
|
"github.com/ipfs/boxo/datastore/dshelp"
|
||||||
dag "github.com/ipfs/boxo/ipld/merkledag"
|
dag "github.com/ipfs/boxo/ipld/merkledag"
|
||||||
"github.com/ipfs/go-cid"
|
"github.com/ipfs/go-cid"
|
||||||
ds "github.com/ipfs/go-datastore"
|
ds "github.com/ipfs/go-datastore"
|
||||||
ipld "github.com/ipfs/go-ipld-format"
|
ipld "github.com/ipfs/go-ipld-format"
|
||||||
"log/slog"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Implements the DAGService interface.
|
// Implements the DAGService interface.
|
||||||
// TODO: implement SessionDAGService to optimize node fetching.
|
// TODO: implement SessionDAGService to optimize node fetching.
|
||||||
// TOOD: persistentSerfDAG?
|
// TODO: persistentSerfDAG?
|
||||||
type dagSyncer struct {
|
type dagSyncer struct {
|
||||||
// Persistent storage for the nodes.
|
// Persistent storage for the nodes.
|
||||||
store ds.Datastore
|
store ds.Datastore
|
||||||
@@ -50,8 +51,7 @@ func (d *dagSyncer) Get(ctx context.Context, cid cid.Cid) (ipld.Node, error) {
|
|||||||
}
|
}
|
||||||
slog.Debug("Queried node from peers", "cid", cid, "deadline", query.Deadline())
|
slog.Debug("Queried node from peers", "cid", cid, "deadline", query.Deadline())
|
||||||
|
|
||||||
responded := false
|
for {
|
||||||
for !responded {
|
|
||||||
select {
|
select {
|
||||||
case resp, ok := <-query.ResponseCh():
|
case resp, ok := <-query.ResponseCh():
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -63,7 +63,6 @@ func (d *dagSyncer) Get(ctx context.Context, cid cid.Cid) (ipld.Node, error) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
slog.Debug("Received node from peer", "cid", cid, "peer", resp.From)
|
slog.Debug("Received node from peer", "cid", cid, "peer", resp.From)
|
||||||
responded = true
|
|
||||||
query.Close()
|
query.Close()
|
||||||
|
|
||||||
node, err = nodeFromBytes(resp.Payload)
|
node, err = nodeFromBytes(resp.Payload)
|
||||||
|
|||||||
@@ -6,12 +6,13 @@ import (
|
|||||||
"crypto/cipher"
|
"crypto/cipher"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/netip"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/psviderski/uncloud/internal/machine/network"
|
||||||
"github.com/siderolabs/discovery-api/api/v1alpha1/client/pb"
|
"github.com/siderolabs/discovery-api/api/v1alpha1/client/pb"
|
||||||
discovery "github.com/siderolabs/discovery-client/pkg/client"
|
discovery "github.com/siderolabs/discovery-client/pkg/client"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
"net/netip"
|
|
||||||
"time"
|
|
||||||
"uncloud/internal/machine/network"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|||||||
@@ -1,23 +1,29 @@
|
|||||||
module uncloud
|
module github.com/psviderski/uncloud
|
||||||
|
|
||||||
go 1.23
|
go 1.23.0
|
||||||
|
|
||||||
toolchain go1.23.2
|
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/BurntSushi/toml v1.4.0
|
github.com/BurntSushi/toml v1.4.0
|
||||||
|
github.com/Masterminds/semver v1.5.0
|
||||||
github.com/Masterminds/squirrel v1.5.4
|
github.com/Masterminds/squirrel v1.5.4
|
||||||
|
github.com/alecthomas/chroma/v2 v2.20.0
|
||||||
github.com/caddyserver/caddy/v2 v2.8.4
|
github.com/caddyserver/caddy/v2 v2.8.4
|
||||||
github.com/cenkalti/backoff/v4 v4.3.0
|
github.com/cenkalti/backoff/v4 v4.3.0
|
||||||
github.com/charmbracelet/huh v0.6.0
|
github.com/charmbracelet/huh v0.6.0
|
||||||
|
github.com/charmbracelet/lipgloss v0.13.0
|
||||||
|
github.com/compose-spec/compose-go/v2 v2.4.5
|
||||||
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf
|
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf
|
||||||
|
github.com/deckarep/golang-set/v2 v2.8.0
|
||||||
github.com/dgraph-io/badger/v3 v3.2103.5
|
github.com/dgraph-io/badger/v3 v3.2103.5
|
||||||
github.com/distribution/reference v0.6.0
|
github.com/distribution/reference v0.6.0
|
||||||
github.com/docker/cli v27.4.0-rc.4+incompatible
|
github.com/docker/cli v27.5.0+incompatible
|
||||||
github.com/docker/compose/v2 v2.31.0
|
github.com/docker/compose/v2 v2.31.0
|
||||||
github.com/docker/docker v27.4.0-rc.2+incompatible
|
github.com/docker/docker v27.4.0-rc.2+incompatible
|
||||||
github.com/docker/go-connections v0.5.0
|
github.com/docker/go-connections v0.5.0
|
||||||
github.com/docker/go-units v0.5.0
|
github.com/docker/go-units v0.5.0
|
||||||
|
github.com/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
|
||||||
@@ -27,7 +33,12 @@ 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/mitchellh/mapstructure v1.5.0
|
||||||
|
github.com/moby/term v0.5.0
|
||||||
|
github.com/opencontainers/go-digest v1.0.0
|
||||||
github.com/opencontainers/image-spec v1.1.0
|
github.com/opencontainers/image-spec v1.1.0
|
||||||
github.com/siderolabs/discovery-api v0.1.4
|
github.com/siderolabs/discovery-api v0.1.4
|
||||||
github.com/siderolabs/discovery-client v0.1.9
|
github.com/siderolabs/discovery-client v0.1.9
|
||||||
@@ -37,15 +48,16 @@ require (
|
|||||||
github.com/vishvananda/netlink v1.3.0
|
github.com/vishvananda/netlink v1.3.0
|
||||||
go.uber.org/zap v1.27.0
|
go.uber.org/zap v1.27.0
|
||||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba
|
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba
|
||||||
golang.org/x/crypto v0.31.0
|
golang.org/x/crypto v0.33.0
|
||||||
golang.org/x/net v0.32.0
|
golang.org/x/net v0.35.0
|
||||||
golang.org/x/sync v0.10.0
|
golang.org/x/sync v0.11.0
|
||||||
golang.org/x/sys v0.28.0
|
golang.org/x/sys v0.31.0
|
||||||
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173
|
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173
|
||||||
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6
|
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20241007155032-5fefd90f89a9
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576
|
||||||
google.golang.org/grpc v1.68.0
|
google.golang.org/grpc v1.68.1
|
||||||
google.golang.org/protobuf v1.36.0
|
google.golang.org/protobuf v1.36.3
|
||||||
|
modernc.org/sqlite v1.36.3
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
@@ -57,6 +69,7 @@ require (
|
|||||||
github.com/Masterminds/semver/v3 v3.2.1 // indirect
|
github.com/Masterminds/semver/v3 v3.2.1 // indirect
|
||||||
github.com/Masterminds/sprig/v3 v3.2.3 // indirect
|
github.com/Masterminds/sprig/v3 v3.2.3 // indirect
|
||||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||||
|
github.com/OneOfOne/xxhash v1.2.8 // indirect
|
||||||
github.com/antlr4-go/antlr/v4 v4.13.0 // indirect
|
github.com/antlr4-go/antlr/v4 v4.13.0 // indirect
|
||||||
github.com/armon/circbuf v0.0.0-20190214190532-5111143e8da2 // indirect
|
github.com/armon/circbuf v0.0.0-20190214190532-5111143e8da2 // indirect
|
||||||
github.com/armon/go-metrics v0.4.1 // indirect
|
github.com/armon/go-metrics v0.4.1 // indirect
|
||||||
@@ -74,12 +87,11 @@ require (
|
|||||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
github.com/charmbracelet/bubbles v0.20.0 // indirect
|
github.com/charmbracelet/bubbles v0.20.0 // indirect
|
||||||
github.com/charmbracelet/bubbletea v1.1.1 // indirect
|
github.com/charmbracelet/bubbletea v1.1.1 // indirect
|
||||||
github.com/charmbracelet/lipgloss v0.13.0 // indirect
|
|
||||||
github.com/charmbracelet/x/ansi v0.3.2 // indirect
|
github.com/charmbracelet/x/ansi v0.3.2 // indirect
|
||||||
github.com/charmbracelet/x/exp/strings v0.0.0-20240919170804-a4978c8e603a // indirect
|
github.com/charmbracelet/x/exp/strings v0.0.0-20240919170804-a4978c8e603a // indirect
|
||||||
github.com/charmbracelet/x/term v0.2.0 // indirect
|
github.com/charmbracelet/x/term v0.2.0 // indirect
|
||||||
github.com/chzyer/readline v1.5.1 // indirect
|
github.com/chzyer/readline v1.5.1 // indirect
|
||||||
github.com/compose-spec/compose-go/v2 v2.4.5 // indirect
|
github.com/cloudflare/cfssl v1.6.4 // indirect
|
||||||
github.com/containerd/console v1.0.4 // indirect
|
github.com/containerd/console v1.0.4 // indirect
|
||||||
github.com/containerd/containerd v1.7.24 // indirect
|
github.com/containerd/containerd v1.7.24 // indirect
|
||||||
github.com/containerd/containerd/api v1.7.19 // indirect
|
github.com/containerd/containerd/api v1.7.19 // indirect
|
||||||
@@ -87,15 +99,17 @@ require (
|
|||||||
github.com/containerd/errdefs v0.3.0 // indirect
|
github.com/containerd/errdefs v0.3.0 // indirect
|
||||||
github.com/containerd/log v0.1.0 // indirect
|
github.com/containerd/log v0.1.0 // indirect
|
||||||
github.com/containerd/platforms v0.2.1 // indirect
|
github.com/containerd/platforms v0.2.1 // indirect
|
||||||
|
github.com/containerd/stargz-snapshotter/estargz v0.16.3 // indirect
|
||||||
github.com/containerd/ttrpc v1.2.5 // indirect
|
github.com/containerd/ttrpc v1.2.5 // indirect
|
||||||
github.com/containerd/typeurl/v2 v2.2.0 // indirect
|
github.com/containerd/typeurl/v2 v2.2.0 // indirect
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect
|
github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect
|
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect
|
||||||
github.com/dgraph-io/badger v1.6.2 // indirect
|
github.com/dgraph-io/badger v1.6.2 // indirect
|
||||||
github.com/dgraph-io/badger/v2 v2.2007.4 // 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/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 // indirect
|
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 // indirect
|
||||||
|
github.com/dlclark/regexp2 v1.11.5 // indirect
|
||||||
github.com/docker/buildx v0.18.0 // indirect
|
github.com/docker/buildx v0.18.0 // indirect
|
||||||
github.com/docker/distribution v2.8.3+incompatible // indirect
|
github.com/docker/distribution v2.8.3+incompatible // indirect
|
||||||
github.com/docker/docker-credential-helpers v0.8.2 // indirect
|
github.com/docker/docker-credential-helpers v0.8.2 // indirect
|
||||||
@@ -113,7 +127,7 @@ require (
|
|||||||
github.com/go-logfmt/logfmt v0.6.0 // indirect
|
github.com/go-logfmt/logfmt v0.6.0 // indirect
|
||||||
github.com/go-logr/logr v1.4.2 // indirect
|
github.com/go-logr/logr v1.4.2 // indirect
|
||||||
github.com/go-logr/stdr v1.2.2 // indirect
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
github.com/go-sql-driver/mysql v1.7.1 // indirect
|
github.com/go-sql-driver/mysql v1.8.1 // indirect
|
||||||
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
|
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
|
||||||
github.com/go-viper/mapstructure/v2 v2.0.0 // indirect
|
github.com/go-viper/mapstructure/v2 v2.0.0 // indirect
|
||||||
github.com/godbus/dbus/v5 v5.1.0 // indirect
|
github.com/godbus/dbus/v5 v5.1.0 // indirect
|
||||||
@@ -126,12 +140,11 @@ require (
|
|||||||
github.com/google/btree v1.1.2 // indirect
|
github.com/google/btree v1.1.2 // indirect
|
||||||
github.com/google/cel-go v0.20.1 // indirect
|
github.com/google/cel-go v0.20.1 // indirect
|
||||||
github.com/google/flatbuffers v24.3.25+incompatible // indirect
|
github.com/google/flatbuffers v24.3.25+incompatible // indirect
|
||||||
github.com/google/go-cmp v0.6.0 // indirect
|
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e // indirect
|
||||||
github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad // indirect
|
|
||||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
|
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
github.com/gorilla/mux v1.8.1 // indirect
|
github.com/gorilla/mux v1.8.1 // indirect
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0 // indirect
|
||||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||||
github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
|
github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
|
||||||
github.com/hashicorp/go-msgpack v0.5.5 // indirect
|
github.com/hashicorp/go-msgpack v0.5.5 // indirect
|
||||||
@@ -187,14 +200,13 @@ require (
|
|||||||
github.com/mdlayher/socket v0.5.1 // indirect
|
github.com/mdlayher/socket v0.5.1 // indirect
|
||||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
|
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
|
||||||
github.com/mholt/acmez/v2 v2.0.3 // indirect
|
github.com/mholt/acmez/v2 v2.0.3 // indirect
|
||||||
github.com/miekg/dns v1.1.62 // indirect
|
|
||||||
github.com/miekg/pkcs11 v1.1.1 // indirect
|
github.com/miekg/pkcs11 v1.1.1 // indirect
|
||||||
github.com/minio/sha256-simd v1.0.1 // indirect
|
github.com/minio/sha256-simd v1.0.1 // indirect
|
||||||
github.com/mitchellh/cli v1.1.5 // indirect
|
github.com/mitchellh/cli v1.1.5 // indirect
|
||||||
github.com/mitchellh/copystructure v1.2.0 // indirect
|
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||||
|
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||||
github.com/mitchellh/go-ps v1.0.0 // indirect
|
github.com/mitchellh/go-ps v1.0.0 // indirect
|
||||||
github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect
|
github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect
|
||||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
|
||||||
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||||
github.com/moby/buildkit v0.17.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
|
||||||
@@ -202,7 +214,8 @@ require (
|
|||||||
github.com/moby/patternmatcher v0.6.0 // indirect
|
github.com/moby/patternmatcher v0.6.0 // indirect
|
||||||
github.com/moby/sys/sequential v0.6.0 // indirect
|
github.com/moby/sys/sequential v0.6.0 // indirect
|
||||||
github.com/moby/sys/signal v0.7.1 // indirect
|
github.com/moby/sys/signal v0.7.1 // indirect
|
||||||
github.com/moby/term v0.5.0 // indirect
|
github.com/moby/sys/user v0.3.0 // indirect
|
||||||
|
github.com/moby/sys/userns v0.1.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
|
||||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
|
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
|
||||||
@@ -218,8 +231,8 @@ require (
|
|||||||
github.com/multiformats/go-multistream v0.5.0 // indirect
|
github.com/multiformats/go-multistream v0.5.0 // indirect
|
||||||
github.com/multiformats/go-varint v0.0.7 // indirect
|
github.com/multiformats/go-varint v0.0.7 // indirect
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||||
|
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||||
github.com/onsi/ginkgo/v2 v2.22.0 // indirect
|
github.com/onsi/ginkgo/v2 v2.22.0 // indirect
|
||||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
|
||||||
github.com/pelletier/go-toml v1.9.5 // indirect
|
github.com/pelletier/go-toml v1.9.5 // indirect
|
||||||
github.com/pires/go-proxyproto v0.7.0 // indirect
|
github.com/pires/go-proxyproto v0.7.0 // indirect
|
||||||
github.com/pkg/errors v0.9.1 // indirect
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
@@ -233,6 +246,7 @@ require (
|
|||||||
github.com/prometheus/procfs v0.15.1 // indirect
|
github.com/prometheus/procfs v0.15.1 // indirect
|
||||||
github.com/quic-go/qpack v0.5.1 // indirect
|
github.com/quic-go/qpack v0.5.1 // indirect
|
||||||
github.com/quic-go/quic-go v0.48.2 // indirect
|
github.com/quic-go/quic-go v0.48.2 // indirect
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
github.com/rivo/uniseg v0.4.7 // indirect
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
github.com/rs/xid v1.5.0 // indirect
|
github.com/rs/xid v1.5.0 // indirect
|
||||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||||
@@ -253,6 +267,7 @@ require (
|
|||||||
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/stoewer/go-strcase v1.2.0 // indirect
|
||||||
|
github.com/stretchr/objx v0.5.2 // indirect
|
||||||
github.com/tailscale/tscert v0.0.0-20240517230440-bbccfbf48933 // indirect
|
github.com/tailscale/tscert v0.0.0-20240517230440-bbccfbf48933 // indirect
|
||||||
github.com/theupdateframework/notary v0.7.0 // indirect
|
github.com/theupdateframework/notary v0.7.0 // indirect
|
||||||
github.com/tonistiigi/dchapes-mode v0.0.0-20241001053921-ca0759fec205 // indirect
|
github.com/tonistiigi/dchapes-mode v0.0.0-20241001053921-ca0759fec205 // indirect
|
||||||
@@ -261,22 +276,27 @@ require (
|
|||||||
github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea // indirect
|
github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea // indirect
|
||||||
github.com/tonistiigi/vt100 v0.0.0-20240514184818-90bafcd6abab // indirect
|
github.com/tonistiigi/vt100 v0.0.0-20240514184818-90bafcd6abab // indirect
|
||||||
github.com/urfave/cli v1.22.16 // indirect
|
github.com/urfave/cli v1.22.16 // indirect
|
||||||
|
github.com/vbatts/tar-split v0.11.6 // indirect
|
||||||
github.com/vishvananda/netns v0.0.4 // indirect
|
github.com/vishvananda/netns v0.0.4 // indirect
|
||||||
|
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
|
||||||
|
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
|
||||||
|
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
|
||||||
github.com/zeebo/blake3 v0.2.4 // indirect
|
github.com/zeebo/blake3 v0.2.4 // indirect
|
||||||
go.etcd.io/bbolt v1.3.11 // indirect
|
go.etcd.io/bbolt v1.3.11 // indirect
|
||||||
go.opencensus.io v0.24.0 // indirect
|
go.opencensus.io v0.24.0 // indirect
|
||||||
|
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 // indirect
|
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 // indirect
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.46.1 // indirect
|
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.46.1 // indirect
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.52.0 // indirect
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 // indirect
|
||||||
go.opentelemetry.io/otel v1.32.0 // indirect
|
go.opentelemetry.io/otel v1.33.0 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.44.0 // indirect
|
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.44.0 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.27.0 // indirect
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0 // indirect
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0 // indirect
|
||||||
go.opentelemetry.io/otel/metric v1.32.0 // indirect
|
go.opentelemetry.io/otel/metric v1.33.0 // indirect
|
||||||
go.opentelemetry.io/otel/sdk v1.32.0 // indirect
|
go.opentelemetry.io/otel/sdk v1.33.0 // indirect
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.32.0 // indirect
|
go.opentelemetry.io/otel/sdk/metric v1.32.0 // indirect
|
||||||
go.opentelemetry.io/otel/trace v1.32.0 // indirect
|
go.opentelemetry.io/otel/trace v1.33.0 // indirect
|
||||||
go.opentelemetry.io/proto/otlp v1.3.1 // indirect
|
go.opentelemetry.io/proto/otlp v1.4.0 // indirect
|
||||||
go.step.sm/cli-utils v0.9.0 // indirect
|
go.step.sm/cli-utils v0.9.0 // indirect
|
||||||
go.step.sm/crypto v0.45.0 // indirect
|
go.step.sm/crypto v0.45.0 // indirect
|
||||||
go.step.sm/linkedca v0.20.1 // indirect
|
go.step.sm/linkedca v0.20.1 // indirect
|
||||||
@@ -286,16 +306,19 @@ require (
|
|||||||
go.uber.org/zap/exp v0.2.0 // indirect
|
go.uber.org/zap/exp v0.2.0 // indirect
|
||||||
golang.org/x/crypto/x509roots/fallback v0.0.0-20240507223354-67b13616a595 // indirect
|
golang.org/x/crypto/x509roots/fallback v0.0.0-20240507223354-67b13616a595 // indirect
|
||||||
golang.org/x/exp v0.0.0-20241215155358-4a5509556b9e // indirect
|
golang.org/x/exp v0.0.0-20241215155358-4a5509556b9e // indirect
|
||||||
golang.org/x/mod v0.22.0 // indirect
|
golang.org/x/mod v0.23.0 // indirect
|
||||||
golang.org/x/term v0.27.0 // indirect
|
golang.org/x/term v0.29.0 // indirect
|
||||||
golang.org/x/text v0.21.0 // indirect
|
golang.org/x/text v0.22.0 // indirect
|
||||||
golang.org/x/time v0.8.0 // indirect
|
golang.org/x/time v0.8.0 // indirect
|
||||||
golang.org/x/tools v0.28.0 // indirect
|
golang.org/x/tools v0.30.0 // indirect
|
||||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
|
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
|
||||||
google.golang.org/genproto v0.0.0-20240401170217-c3f982113cda // indirect
|
google.golang.org/genproto v0.0.0-20240401170217-c3f982113cda // indirect
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 // indirect
|
google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259 // indirect
|
gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259 // indirect
|
||||||
howett.net/plist v1.0.0 // indirect
|
howett.net/plist v1.0.0 // indirect
|
||||||
lukechampine.com/blake3 v1.3.0 // indirect
|
lukechampine.com/blake3 v1.3.0 // indirect
|
||||||
|
modernc.org/libc v1.61.13 // indirect
|
||||||
|
modernc.org/mathutil v1.7.1 // indirect
|
||||||
|
modernc.org/memory v1.8.2 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ
|
|||||||
github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
|
github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
|
||||||
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
|
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
|
||||||
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
|
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
|
||||||
|
github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww=
|
||||||
|
github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
|
||||||
github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
|
github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
|
||||||
github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
|
github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
|
||||||
github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0=
|
github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0=
|
||||||
@@ -45,11 +47,18 @@ github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERo
|
|||||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||||
github.com/Microsoft/hcsshim v0.12.5 h1:bpTInLlDy/nDRWFVcefDZZ1+U8tS+rz3MxjKgu9boo0=
|
github.com/Microsoft/hcsshim v0.12.5 h1:bpTInLlDy/nDRWFVcefDZZ1+U8tS+rz3MxjKgu9boo0=
|
||||||
github.com/Microsoft/hcsshim v0.12.5/go.mod h1:tIUGego4G1EN5Hb6KC90aDYiUI2dqLSTTOCjVNpOgZ8=
|
github.com/Microsoft/hcsshim v0.12.5/go.mod h1:tIUGego4G1EN5Hb6KC90aDYiUI2dqLSTTOCjVNpOgZ8=
|
||||||
github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE=
|
|
||||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||||
|
github.com/OneOfOne/xxhash v1.2.8 h1:31czK/TI9sNkxIKfaUfGlU47BAxQ0ztGgd9vPyqimf8=
|
||||||
|
github.com/OneOfOne/xxhash v1.2.8/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q=
|
||||||
github.com/Shopify/logrus-bugsnag v0.0.0-20170309145241-6dbc35f2c30d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ=
|
github.com/Shopify/logrus-bugsnag v0.0.0-20170309145241-6dbc35f2c30d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ=
|
||||||
github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d h1:UrqY+r/OJnIp5u0s1SbQ8dVfLCZJsnvazdBP5hS4iRs=
|
github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d h1:UrqY+r/OJnIp5u0s1SbQ8dVfLCZJsnvazdBP5hS4iRs=
|
||||||
github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ=
|
github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ=
|
||||||
|
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
|
||||||
|
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
|
||||||
|
github.com/alecthomas/chroma/v2 v2.20.0 h1:sfIHpxPyR07/Oylvmcai3X/exDlE8+FA820NTz+9sGw=
|
||||||
|
github.com/alecthomas/chroma/v2 v2.20.0/go.mod h1:e7tViK0xh/Nf4BYHl00ycY6rV7b8iXBksI9E359yNmA=
|
||||||
|
github.com/alecthomas/repr v0.5.1 h1:E3G4t2QbHTSNpPKBgMTln5KLkZHLOcU7r37J4pXBuIg=
|
||||||
|
github.com/alecthomas/repr v0.5.1/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
|
||||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||||
@@ -102,6 +111,8 @@ github.com/aws/smithy-go v1.20.2 h1:tbp628ireGtzcHDDmLT/6ADHidqnwgF57XOXZe6tp4Q=
|
|||||||
github.com/aws/smithy-go v1.20.2/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E=
|
github.com/aws/smithy-go v1.20.2/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E=
|
||||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
||||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
||||||
|
github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8=
|
||||||
|
github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA=
|
||||||
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
||||||
github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o=
|
github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o=
|
||||||
github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
||||||
@@ -150,6 +161,8 @@ github.com/charmbracelet/lipgloss v0.13.0 h1:4X3PPeoWEDCMvzDvGmTajSyYPcZM4+y8sCA
|
|||||||
github.com/charmbracelet/lipgloss v0.13.0/go.mod h1:nw4zy0SBX/F/eAO1cWdcvy6qnkDUxr8Lw7dvFrAIbbY=
|
github.com/charmbracelet/lipgloss v0.13.0/go.mod h1:nw4zy0SBX/F/eAO1cWdcvy6qnkDUxr8Lw7dvFrAIbbY=
|
||||||
github.com/charmbracelet/x/ansi v0.3.2 h1:wsEwgAN+C9U06l9dCVMX0/L3x7ptvY1qmjMwyfE6USY=
|
github.com/charmbracelet/x/ansi v0.3.2 h1:wsEwgAN+C9U06l9dCVMX0/L3x7ptvY1qmjMwyfE6USY=
|
||||||
github.com/charmbracelet/x/ansi v0.3.2/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw=
|
github.com/charmbracelet/x/ansi v0.3.2/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw=
|
||||||
|
github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b h1:MnAMdlwSltxJyULnrYbkZpp4k58Co7Tah3ciKhSNo0Q=
|
||||||
|
github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
|
||||||
github.com/charmbracelet/x/exp/strings v0.0.0-20240919170804-a4978c8e603a h1:JMdM89Udp/cOl5tC3MuUJXTPE/nAdU1oyt9jRU44qq8=
|
github.com/charmbracelet/x/exp/strings v0.0.0-20240919170804-a4978c8e603a h1:JMdM89Udp/cOl5tC3MuUJXTPE/nAdU1oyt9jRU44qq8=
|
||||||
github.com/charmbracelet/x/exp/strings v0.0.0-20240919170804-a4978c8e603a/go.mod h1:pBhA0ybfXv6hDjQUZ7hk1lVxBiUbupdw5R31yPUViVQ=
|
github.com/charmbracelet/x/exp/strings v0.0.0-20240919170804-a4978c8e603a/go.mod h1:pBhA0ybfXv6hDjQUZ7hk1lVxBiUbupdw5R31yPUViVQ=
|
||||||
github.com/charmbracelet/x/term v0.2.0 h1:cNB9Ot9q8I711MyZ7myUR5HFWL/lc3OpU8jZ4hwm0x0=
|
github.com/charmbracelet/x/term v0.2.0 h1:cNB9Ot9q8I711MyZ7myUR5HFWL/lc3OpU8jZ4hwm0x0=
|
||||||
@@ -166,8 +179,9 @@ github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38
|
|||||||
github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag=
|
github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag=
|
||||||
github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I=
|
github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I=
|
||||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||||
github.com/cloudflare/cfssl v0.0.0-20180223231731-4e2dcbde5004 h1:lkAMpLVBDaj17e85keuznYcH5rqI438v41pKcBl4ZxQ=
|
|
||||||
github.com/cloudflare/cfssl v0.0.0-20180223231731-4e2dcbde5004/go.mod h1:yMWuSON2oQp+43nFtAV/uvKQIFpSPerB57DCt9t8sSA=
|
github.com/cloudflare/cfssl v0.0.0-20180223231731-4e2dcbde5004/go.mod h1:yMWuSON2oQp+43nFtAV/uvKQIFpSPerB57DCt9t8sSA=
|
||||||
|
github.com/cloudflare/cfssl v1.6.4 h1:NMOvfrEjFfC63K3SGXgAnFdsgkmiq4kATme5BfcqrO8=
|
||||||
|
github.com/cloudflare/cfssl v1.6.4/go.mod h1:8b3CQMxfWPAeom3zBnGJ6sd+G1NkL5TXqmDXacb+1J0=
|
||||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||||
github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I=
|
github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I=
|
||||||
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
|
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
|
||||||
@@ -197,9 +211,8 @@ github.com/containerd/nydus-snapshotter v0.14.0 h1:6/eAi6d7MjaeLLuMO8Udfe5GVsDud
|
|||||||
github.com/containerd/nydus-snapshotter v0.14.0/go.mod h1:TT4jv2SnIDxEBu4H2YOvWQHPOap031ydTaHTuvc5VQk=
|
github.com/containerd/nydus-snapshotter v0.14.0/go.mod h1:TT4jv2SnIDxEBu4H2YOvWQHPOap031ydTaHTuvc5VQk=
|
||||||
github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A=
|
github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A=
|
||||||
github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw=
|
github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw=
|
||||||
github.com/containerd/stargz-snapshotter v0.15.1 h1:fpsP4kf/Z4n2EYnU0WT8ZCE3eiKDwikDhL6VwxIlgeA=
|
github.com/containerd/stargz-snapshotter/estargz v0.16.3 h1:7evrXtoh1mSbGj/pfRccTampEyKpjpOnS3CyiV1Ebr8=
|
||||||
github.com/containerd/stargz-snapshotter/estargz v0.15.1 h1:eXJjw9RbkLFgioVaTG+G/ZW/0kEe2oEKCdS/ZxIyoCU=
|
github.com/containerd/stargz-snapshotter/estargz v0.16.3/go.mod h1:uyr4BfYfOj3G9WBVE8cOlQmXAbPN9VEQpBBeJIuOipU=
|
||||||
github.com/containerd/stargz-snapshotter/estargz v0.15.1/go.mod h1:gr2RNwukQ/S9Nv33Lt6UC7xEx58C+LHRdoqbEKjz1Kk=
|
|
||||||
github.com/containerd/ttrpc v1.2.5 h1:IFckT1EFQoFBMG4c3sMdT8EP3/aKfumK1msY+Ze4oLU=
|
github.com/containerd/ttrpc v1.2.5 h1:IFckT1EFQoFBMG4c3sMdT8EP3/aKfumK1msY+Ze4oLU=
|
||||||
github.com/containerd/ttrpc v1.2.5/go.mod h1:YCXHsb32f+Sq5/72xHubdiJRQY9inL4a4ZQrAbN1q9o=
|
github.com/containerd/ttrpc v1.2.5/go.mod h1:YCXHsb32f+Sq5/72xHubdiJRQY9inL4a4ZQrAbN1q9o=
|
||||||
github.com/containerd/typeurl/v2 v2.2.0 h1:6NBDbQzr7I5LHgp34xAXYF5DOTQDn05X58lsPEmzLso=
|
github.com/containerd/typeurl/v2 v2.2.0 h1:6NBDbQzr7I5LHgp34xAXYF5DOTQDn05X58lsPEmzLso=
|
||||||
@@ -216,8 +229,9 @@ github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSV
|
|||||||
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
|
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc=
|
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||||
|
github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0=
|
||||||
|
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||||
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
|
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
|
||||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||||
github.com/creack/pty v1.1.21 h1:1/QdRyBaHHJP61QkWMXlOIBfsgdDeeKfK8SYVUWJKf0=
|
github.com/creack/pty v1.1.21 h1:1/QdRyBaHHJP61QkWMXlOIBfsgdDeeKfK8SYVUWJKf0=
|
||||||
@@ -229,6 +243,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
|
|||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c h1:pFUpOrbxDR6AkioZ1ySsx5yxlDQZ8stG2b88gTPxgJU=
|
github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c h1:pFUpOrbxDR6AkioZ1ySsx5yxlDQZ8stG2b88gTPxgJU=
|
||||||
github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c/go.mod h1:6UhI8N9EjYm1c2odKpFpAYeR8dsBeM7PtzQhRgxRr9U=
|
github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c/go.mod h1:6UhI8N9EjYm1c2odKpFpAYeR8dsBeM7PtzQhRgxRr9U=
|
||||||
|
github.com/deckarep/golang-set/v2 v2.8.0 h1:swm0rlPCmdWn9mESxKOjWk8hXSqoxOp+ZlfuyaAdFlQ=
|
||||||
|
github.com/deckarep/golang-set/v2 v2.8.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4=
|
||||||
github.com/decred/dcrd/crypto/blake256 v1.0.1 h1:7PltbUIQB7u/FfZ39+DGa/ShuMyJ5ilcvdfma9wOH6Y=
|
github.com/decred/dcrd/crypto/blake256 v1.0.1 h1:7PltbUIQB7u/FfZ39+DGa/ShuMyJ5ilcvdfma9wOH6Y=
|
||||||
github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo=
|
github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo=
|
||||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnNEcHYvcCuK6dPZSg=
|
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnNEcHYvcCuK6dPZSg=
|
||||||
@@ -249,10 +265,12 @@ github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WA
|
|||||||
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
|
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
|
||||||
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
||||||
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||||
|
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
|
||||||
|
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||||
github.com/docker/buildx v0.18.0 h1:rSauXHeJt90NvtXrLK5J992Eb0UPJZs2vV3u1zTf1nE=
|
github.com/docker/buildx v0.18.0 h1:rSauXHeJt90NvtXrLK5J992Eb0UPJZs2vV3u1zTf1nE=
|
||||||
github.com/docker/buildx v0.18.0/go.mod h1:JGNSshOhHs5FhG3u51jXUf4lLOeD2QBIlJ2vaRB67p4=
|
github.com/docker/buildx v0.18.0/go.mod h1:JGNSshOhHs5FhG3u51jXUf4lLOeD2QBIlJ2vaRB67p4=
|
||||||
github.com/docker/cli v27.4.0-rc.4+incompatible h1:nnrHCfd5eODviSGzCpIyYFcJovrq+/dVW4K4oqQu7WA=
|
github.com/docker/cli v27.5.0+incompatible h1:aMphQkcGtpHixwwhAXJT1rrK/detk2JIvDaFkLctbGM=
|
||||||
github.com/docker/cli v27.4.0-rc.4+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
github.com/docker/cli v27.5.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||||
github.com/docker/compose/v2 v2.31.0 h1:8Sm0c4MjIhksguxIA5koYMXoTJDAp/CaZ1cdZrMvMdw=
|
github.com/docker/compose/v2 v2.31.0 h1:8Sm0c4MjIhksguxIA5koYMXoTJDAp/CaZ1cdZrMvMdw=
|
||||||
github.com/docker/compose/v2 v2.31.0/go.mod h1:oQq3UDEdsnB3AUO72AxaoeLbkCgmUu1+8tLzvmphmXA=
|
github.com/docker/compose/v2 v2.31.0/go.mod h1:oQq3UDEdsnB3AUO72AxaoeLbkCgmUu1+8tLzvmphmXA=
|
||||||
github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
|
github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
|
||||||
@@ -328,8 +346,8 @@ github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ4
|
|||||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||||
github.com/go-sql-driver/mysql v1.3.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
github.com/go-sql-driver/mysql v1.3.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
||||||
github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI=
|
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
|
||||||
github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
||||||
github.com/go-stack/stack v1.6.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
github.com/go-stack/stack v1.6.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||||
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
|
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
|
||||||
@@ -337,6 +355,8 @@ github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZ
|
|||||||
github.com/go-viper/mapstructure/v2 v2.0.0 h1:dhn8MZ1gZ0mzeodTG3jt5Vj/o87xZKuNAprG2mQfMfc=
|
github.com/go-viper/mapstructure/v2 v2.0.0 h1:dhn8MZ1gZ0mzeodTG3jt5Vj/o87xZKuNAprG2mQfMfc=
|
||||||
github.com/go-viper/mapstructure/v2 v2.0.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
github.com/go-viper/mapstructure/v2 v2.0.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||||
github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0=
|
github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0=
|
||||||
|
github.com/goccy/go-yaml v1.17.1 h1:LI34wktB2xEE3ONG/2Ar54+/HJVBriAGJ55PHls4YuY=
|
||||||
|
github.com/goccy/go-yaml v1.17.1/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||||
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
|
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
|
||||||
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||||
github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E=
|
github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E=
|
||||||
@@ -391,8 +411,10 @@ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
|||||||
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
|
github.com/google/go-containerregistry v0.20.2 h1:B1wPJ1SN/S7pB+ZAimcciVD+r+yV/l/DSArMxlbwseo=
|
||||||
|
github.com/google/go-containerregistry v0.20.2/go.mod h1:z38EKdKh4h7IP2gSfUUqEvalZBqs6AoLeWfUy34nQC8=
|
||||||
github.com/google/go-tpm v0.9.0 h1:sQF6YqWMi+SCXpsmS3fd21oPy/vSddwZry4JnmltHVk=
|
github.com/google/go-tpm v0.9.0 h1:sQF6YqWMi+SCXpsmS3fd21oPy/vSddwZry4JnmltHVk=
|
||||||
github.com/google/go-tpm v0.9.0/go.mod h1:FkNVkc6C+IsvDI9Jw1OveJmxGZUUaKxtrpOS47QWKfU=
|
github.com/google/go-tpm v0.9.0/go.mod h1:FkNVkc6C+IsvDI9Jw1OveJmxGZUUaKxtrpOS47QWKfU=
|
||||||
github.com/google/go-tpm-tools v0.4.4 h1:oiQfAIkc6xTy9Fl5NKTeTJkBTlXdHsxAofmQyxBKY98=
|
github.com/google/go-tpm-tools v0.4.4 h1:oiQfAIkc6xTy9Fl5NKTeTJkBTlXdHsxAofmQyxBKY98=
|
||||||
@@ -402,8 +424,8 @@ github.com/google/go-tspi v0.3.0/go.mod h1:xfMGI3G0PhxCdNVcYr1C4C+EizojDg/TXuX5b
|
|||||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8=
|
github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8=
|
||||||
github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo=
|
github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo=
|
||||||
github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad h1:a6HEuzUHeKH6hwfN/ZoQgRgVIWFJljSWa/zetS2WTvg=
|
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
|
||||||
github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
|
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
||||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||||
github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o=
|
github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o=
|
||||||
github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw=
|
github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw=
|
||||||
@@ -425,8 +447,8 @@ github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
|||||||
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
||||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0=
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0 h1:TmHmbvxPmaegwhDubVz0lICL0J5Ka2vwTzhoePEXsGE=
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k=
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0/go.mod h1:qztMSjm835F2bXf+5HKAPIS5qsmQDqZna/PgVt4rWtI=
|
||||||
github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed h1:5upAirOpQc1Q53c0bnx2ufif5kANL7bfZWcc6VJWJd8=
|
github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed h1:5upAirOpQc1Q53c0bnx2ufif5kANL7bfZWcc6VJWJd8=
|
||||||
github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4=
|
github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4=
|
||||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||||
@@ -473,6 +495,8 @@ github.com/hashicorp/memberlist v0.5.1 h1:mk5dRuzeDNis2bi6LLoQIXfMH7JQvAzt3mQD0v
|
|||||||
github.com/hashicorp/memberlist v0.5.1/go.mod h1:zGDXV6AqbDTKTM6yxW0I4+JtFzZAJVoIPvss4hV8F24=
|
github.com/hashicorp/memberlist v0.5.1/go.mod h1:zGDXV6AqbDTKTM6yxW0I4+JtFzZAJVoIPvss4hV8F24=
|
||||||
github.com/hashicorp/serf v0.10.1 h1:Z1H2J60yRKvfDYAOZLd2MU0ND4AH/WDz7xYHDWQsIPY=
|
github.com/hashicorp/serf v0.10.1 h1:Z1H2J60yRKvfDYAOZLd2MU0ND4AH/WDz7xYHDWQsIPY=
|
||||||
github.com/hashicorp/serf v0.10.1/go.mod h1:yL2t6BqATOLGc5HF7qbFkTfXoPIY0WZdWHfEvMqbG+4=
|
github.com/hashicorp/serf v0.10.1/go.mod h1:yL2t6BqATOLGc5HF7qbFkTfXoPIY0WZdWHfEvMqbG+4=
|
||||||
|
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
|
||||||
|
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
|
||||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||||
github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
||||||
github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
||||||
@@ -591,6 +615,8 @@ github.com/jinzhu/gorm v0.0.0-20170222002820-5409931a1bb8/go.mod h1:Vla75njaFJ8c
|
|||||||
github.com/jinzhu/inflection v0.0.0-20170102125226-1c35d901db3d h1:jRQLvyVGL+iVtDElaEIDdKwpPqUIZJfzkNLV34htpEc=
|
github.com/jinzhu/inflection v0.0.0-20170102125226-1c35d901db3d h1:jRQLvyVGL+iVtDElaEIDdKwpPqUIZJfzkNLV34htpEc=
|
||||||
github.com/jinzhu/inflection v0.0.0-20170102125226-1c35d901db3d/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
github.com/jinzhu/inflection v0.0.0-20170102125226-1c35d901db3d/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||||
github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||||
|
github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
|
||||||
|
github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
|
||||||
github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=
|
github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=
|
||||||
github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
|
github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
|
||||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||||
@@ -631,8 +657,9 @@ github.com/lib/pq v0.0.0-20150723085316-0dad96c0b94f/go.mod h1:5WUZQaWbwv1U+lTRe
|
|||||||
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||||
github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||||
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||||
github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8=
|
|
||||||
github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||||
|
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||||
|
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||||
github.com/libdns/libdns v0.2.2 h1:O6ws7bAfRPaBsgAYt8MDe2HcNBGC29hkZ9MX2eUSX3s=
|
github.com/libdns/libdns v0.2.2 h1:O6ws7bAfRPaBsgAYt8MDe2HcNBGC29hkZ9MX2eUSX3s=
|
||||||
github.com/libdns/libdns v0.2.2/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ=
|
github.com/libdns/libdns v0.2.2/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ=
|
||||||
github.com/libp2p/go-buffer-pool v0.1.0 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6cdF0Y8=
|
github.com/libp2p/go-buffer-pool v0.1.0 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6cdF0Y8=
|
||||||
@@ -693,6 +720,8 @@ github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh
|
|||||||
github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk=
|
github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk=
|
||||||
github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
|
github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
|
||||||
github.com/mattn/go-sqlite3 v1.6.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
github.com/mattn/go-sqlite3 v1.6.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||||
github.com/mdlayher/genetlink v1.3.2 h1:KdrNKe+CTu+IbZnm/GVUMXSqBBLqcGpRDa0xkQy56gw=
|
github.com/mdlayher/genetlink v1.3.2 h1:KdrNKe+CTu+IbZnm/GVUMXSqBBLqcGpRDa0xkQy56gw=
|
||||||
github.com/mdlayher/genetlink v1.3.2/go.mod h1:tcC3pkCrPUGIKKsCsp0B3AdaaKuHtaxoJRz3cc+528o=
|
github.com/mdlayher/genetlink v1.3.2/go.mod h1:tcC3pkCrPUGIKKsCsp0B3AdaaKuHtaxoJRz3cc+528o=
|
||||||
@@ -706,8 +735,8 @@ github.com/mholt/acmez/v2 v2.0.3 h1:CgDBlEwg3QBp6s45tPQmFIBrkRIkBT4rW4orMM6p4sw=
|
|||||||
github.com/mholt/acmez/v2 v2.0.3/go.mod h1:pQ1ysaDeGrIMvJ9dfJMk5kJNkn7L2sb3UhyrX6Q91cw=
|
github.com/mholt/acmez/v2 v2.0.3/go.mod h1:pQ1ysaDeGrIMvJ9dfJMk5kJNkn7L2sb3UhyrX6Q91cw=
|
||||||
github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
|
github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
|
||||||
github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI=
|
github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI=
|
||||||
github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ=
|
github.com/miekg/dns v1.1.65 h1:0+tIPHzUW0GCge7IiK3guGP57VAw7hoPDfApjkMD1Fc=
|
||||||
github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ=
|
github.com/miekg/dns v1.1.65/go.mod h1:Dzw9769uoKVaLuODMDZz9M6ynFU6Em65csPuoi8G0ck=
|
||||||
github.com/miekg/pkcs11 v1.0.2/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs=
|
github.com/miekg/pkcs11 v1.0.2/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs=
|
||||||
github.com/miekg/pkcs11 v1.1.1 h1:Ugu9pdy6vAYku5DEpVWVFPYnzV+bxB+iRdbuFSu7TvU=
|
github.com/miekg/pkcs11 v1.1.1 h1:Ugu9pdy6vAYku5DEpVWVFPYnzV+bxB+iRdbuFSu7TvU=
|
||||||
github.com/miekg/pkcs11 v1.1.1/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs=
|
github.com/miekg/pkcs11 v1.1.1/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs=
|
||||||
@@ -727,6 +756,7 @@ github.com/mitchellh/cli v1.1.5/go.mod h1:v8+iFts2sPIKUV1ltktPXMCC8fumSKFItNcD2c
|
|||||||
github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
|
github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
|
||||||
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
|
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
|
||||||
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
|
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
|
||||||
|
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||||
github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc=
|
github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc=
|
||||||
github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg=
|
github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg=
|
||||||
@@ -800,6 +830,8 @@ github.com/multiformats/go-varint v0.0.7/go.mod h1:r8PUYw/fD/SjBCiKOoDlGF6QawOEL
|
|||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||||
|
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
|
||||||
|
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
github.com/onsi/ginkgo v1.12.0/go.mod h1:oUhWkIvk5aDxtKvDDuw8gItl8pKl42LzjC9KZE0HfGg=
|
github.com/onsi/ginkgo v1.12.0/go.mod h1:oUhWkIvk5aDxtKvDDuw8gItl8pKl42LzjC9KZE0HfGg=
|
||||||
@@ -916,12 +948,14 @@ github.com/quic-go/webtransport-go v0.8.0 h1:HxSrwun11U+LlmwpgM1kEqIqH90IT4N8auv
|
|||||||
github.com/quic-go/webtransport-go v0.8.0/go.mod h1:N99tjprW432Ut5ONql/aUhSLT0YVSlwHohQsuac9WaM=
|
github.com/quic-go/webtransport-go v0.8.0/go.mod h1:N99tjprW432Ut5ONql/aUhSLT0YVSlwHohQsuac9WaM=
|
||||||
github.com/raulk/go-watchdog v1.3.0 h1:oUmdlHxdkXRJlwfG0O9omj8ukerm8MEQavSiDTEtBsk=
|
github.com/raulk/go-watchdog v1.3.0 h1:oUmdlHxdkXRJlwfG0O9omj8ukerm8MEQavSiDTEtBsk=
|
||||||
github.com/raulk/go-watchdog v1.3.0/go.mod h1:fIvOnLbF0b0ZwkB9YU4mOW9Did//4vPZtDqv66NfsMU=
|
github.com/raulk/go-watchdog v1.3.0/go.mod h1:fIvOnLbF0b0ZwkB9YU4mOW9Did//4vPZtDqv66NfsMU=
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||||
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
|
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||||
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
|
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
||||||
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
|
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
|
||||||
github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc=
|
github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc=
|
||||||
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||||
@@ -1015,6 +1049,7 @@ github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
|
|||||||
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
||||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
|
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
@@ -1048,8 +1083,8 @@ github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljT
|
|||||||
github.com/urfave/cli v1.22.10/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
|
github.com/urfave/cli v1.22.10/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
|
||||||
github.com/urfave/cli v1.22.16 h1:MH0k6uJxdwdeWQTwhSO42Pwr4YLrNLwBtg1MRgTqPdQ=
|
github.com/urfave/cli v1.22.16 h1:MH0k6uJxdwdeWQTwhSO42Pwr4YLrNLwBtg1MRgTqPdQ=
|
||||||
github.com/urfave/cli v1.22.16/go.mod h1:EeJR6BKodywf4zciqrdw6hpCPk68JO9z5LazXZMn5Po=
|
github.com/urfave/cli v1.22.16/go.mod h1:EeJR6BKodywf4zciqrdw6hpCPk68JO9z5LazXZMn5Po=
|
||||||
github.com/vbatts/tar-split v0.11.5 h1:3bHCTIheBm1qFTcgh9oPu+nNBtX+XJIupG/vacinCts=
|
github.com/vbatts/tar-split v0.11.6 h1:4SjTW5+PU11n6fZenf2IPoV8/tz3AaYHMWjf23envGs=
|
||||||
github.com/vbatts/tar-split v0.11.5/go.mod h1:yZbwRsSeGjusneWgA781EKej9HF8vme8okylkAeNKLk=
|
github.com/vbatts/tar-split v0.11.6/go.mod h1:dqKNtesIOr2j2Qv3W/cHjnvk9I8+G7oAkFDFN6TCBEI=
|
||||||
github.com/vishvananda/netlink v1.3.0 h1:X7l42GfcV4S6E4vHTsw48qbrV+9PVojNfIhZcwQdrZk=
|
github.com/vishvananda/netlink v1.3.0 h1:X7l42GfcV4S6E4vHTsw48qbrV+9PVojNfIhZcwQdrZk=
|
||||||
github.com/vishvananda/netlink v1.3.0/go.mod h1:i6NetklAujEcC6fK0JPjT8qSwWyO0HLn4UKG+hGqeJs=
|
github.com/vishvananda/netlink v1.3.0/go.mod h1:i6NetklAujEcC6fK0JPjT8qSwWyO0HLn4UKG+hGqeJs=
|
||||||
github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8=
|
github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8=
|
||||||
@@ -1058,6 +1093,15 @@ github.com/warpfork/go-testmark v0.12.1 h1:rMgCpJfwy1sJ50x0M0NgyphxYYPMOODIJHhsX
|
|||||||
github.com/warpfork/go-testmark v0.12.1/go.mod h1:kHwy7wfvGSPh1rQJYKayD4AbtNaeyZdcGi9tNJTaa5Y=
|
github.com/warpfork/go-testmark v0.12.1/go.mod h1:kHwy7wfvGSPh1rQJYKayD4AbtNaeyZdcGi9tNJTaa5Y=
|
||||||
github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0 h1:GDDkbFiaK8jsSDJfjId/PEGEShv6ugrt4kYsC5UIDaQ=
|
github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0 h1:GDDkbFiaK8jsSDJfjId/PEGEShv6ugrt4kYsC5UIDaQ=
|
||||||
github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0/go.mod h1:x6AKhvSSexNrVSrViXSHUEbICjmGXhtgABaHIySUSGw=
|
github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0/go.mod h1:x6AKhvSSexNrVSrViXSHUEbICjmGXhtgABaHIySUSGw=
|
||||||
|
github.com/weppos/publicsuffix-go v0.15.1-0.20210511084619-b1f36a2d6c0b h1:FsyNrX12e5BkplJq7wKOLk0+C6LZ+KGXvuEcKUYm5ss=
|
||||||
|
github.com/weppos/publicsuffix-go v0.15.1-0.20210511084619-b1f36a2d6c0b/go.mod h1:HYux0V0Zi04bHNwOHy4cXJVz/TQjYonnF6aoYhj+3QE=
|
||||||
|
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
|
||||||
|
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo=
|
||||||
|
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
|
||||||
|
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
|
||||||
|
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
|
||||||
|
github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
|
||||||
|
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
|
||||||
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
||||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
@@ -1070,37 +1114,43 @@ github.com/zeebo/blake3 v0.2.4/go.mod h1:7eeQ6d2iXWRGF6npfaxl2CU+xy2Fjo2gxeyZGCR
|
|||||||
github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo=
|
github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo=
|
||||||
github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4=
|
github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4=
|
||||||
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
|
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
|
||||||
|
github.com/zmap/zcrypto v0.0.0-20210511125630-18f1e0152cfc h1:zkGwegkOW709y0oiAraH/3D8njopUR/pARHv4tZZ6pw=
|
||||||
|
github.com/zmap/zcrypto v0.0.0-20210511125630-18f1e0152cfc/go.mod h1:FM4U1E3NzlNMRnSUTU3P1UdukWhYGifqEsjk9fn7BCk=
|
||||||
|
github.com/zmap/zlint/v3 v3.1.0 h1:WjVytZo79m/L1+/Mlphl09WBob6YTGljN5IGWZFpAv0=
|
||||||
|
github.com/zmap/zlint/v3 v3.1.0/go.mod h1:L7t8s3sEKkb0A2BxGy1IWrxt1ZATa1R4QfJZaQOD3zU=
|
||||||
go.etcd.io/bbolt v1.3.11 h1:yGEzV1wPz2yVCLsD8ZAiGHhHVlczyC9d1rP43/VCRJ0=
|
go.etcd.io/bbolt v1.3.11 h1:yGEzV1wPz2yVCLsD8ZAiGHhHVlczyC9d1rP43/VCRJ0=
|
||||||
go.etcd.io/bbolt v1.3.11/go.mod h1:dksAq7YMXoljX0xu6VF5DMZGbhYYoLUalEiSySYAS4I=
|
go.etcd.io/bbolt v1.3.11/go.mod h1:dksAq7YMXoljX0xu6VF5DMZGbhYYoLUalEiSySYAS4I=
|
||||||
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
|
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
|
||||||
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
|
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
|
||||||
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
|
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
|
||||||
|
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||||
|
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 h1:4Pp6oUg3+e/6M4C0A/3kJ2VYa++dsWVTtGgLVj5xtHg=
|
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 h1:4Pp6oUg3+e/6M4C0A/3kJ2VYa++dsWVTtGgLVj5xtHg=
|
||||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0/go.mod h1:Mjt1i1INqiaoZOMGR1RIUJN+i3ChKoFRqzrRQhlkbs0=
|
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0/go.mod h1:Mjt1i1INqiaoZOMGR1RIUJN+i3ChKoFRqzrRQhlkbs0=
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.46.1 h1:gbhw/u49SS3gkPWiYweQNJGm/uJN5GkI/FrosxSHT7A=
|
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.46.1 h1:gbhw/u49SS3gkPWiYweQNJGm/uJN5GkI/FrosxSHT7A=
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.46.1/go.mod h1:GnOaBaFQ2we3b9AGWJpsBa7v1S5RlQzlC3O7dRMxZhM=
|
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.46.1/go.mod h1:GnOaBaFQ2we3b9AGWJpsBa7v1S5RlQzlC3O7dRMxZhM=
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.52.0 h1:9l89oX4ba9kHbBol3Xin3leYJ+252h0zszDtBwyKe2A=
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 h1:yd02MEjBdJkG3uabWP9apV+OuWRIXGDuJEUJbOHmCFU=
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.52.0/go.mod h1:XLZfZboOJWHNKUv7eH0inh0E9VV6eWDFB/9yJyTLPp0=
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0/go.mod h1:umTcuxiv1n/s/S6/c2AT/g2CQ7u5C59sHDNmfSwgz7Q=
|
||||||
go.opentelemetry.io/otel v1.32.0 h1:WnBN+Xjcteh0zdk01SVqV55d/m62NJLJdIyb4y/WO5U=
|
go.opentelemetry.io/otel v1.33.0 h1:/FerN9bax5LoK51X/sI0SVYrjSE0/yUL7DpxW4K3FWw=
|
||||||
go.opentelemetry.io/otel v1.32.0/go.mod h1:00DCVSB0RQcnzlwyTfqtxSm+DRr9hpYrHjNGiBHVQIg=
|
go.opentelemetry.io/otel v1.33.0/go.mod h1:SUUkR6csvUQl+yjReHu5uM3EtVV7MBm5FHKRlNx4I8I=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.44.0 h1:jd0+5t/YynESZqsSyPz+7PAFdEop0dlN0+PkyHYo8oI=
|
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.44.0 h1:jd0+5t/YynESZqsSyPz+7PAFdEop0dlN0+PkyHYo8oI=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.44.0/go.mod h1:U707O40ee1FpQGyhvqnzmCJm1Wh6OX6GGBVn0E6Uyyk=
|
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.44.0/go.mod h1:U707O40ee1FpQGyhvqnzmCJm1Wh6OX6GGBVn0E6Uyyk=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.27.0 h1:R9DE4kQ4k+YtfLI2ULwX82VtNQ2J8yZmA7ZIF/D+7Mc=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0 h1:Vh5HayB/0HHfOQA7Ctx69E/Y/DcQSMPpKANYVMQ7fBA=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.27.0/go.mod h1:OQFyQVrDlbe+R7xrEyDr/2Wr67Ol0hRUgsfA+V5A95s=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0/go.mod h1:cpgtDBaqD/6ok/UG0jT15/uKjAY8mRA53diogHBg3UI=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0 h1:qFffATk0X+HD+f1Z8lswGiOQYKHRlzfmdJm0wEaVrFA=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0 h1:qFffATk0X+HD+f1Z8lswGiOQYKHRlzfmdJm0wEaVrFA=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0/go.mod h1:MOiCmryaYtc+V0Ei+Tx9o5S1ZjA7kzLucuVuyzBZloQ=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0/go.mod h1:MOiCmryaYtc+V0Ei+Tx9o5S1ZjA7kzLucuVuyzBZloQ=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.27.0 h1:QY7/0NeRPKlzusf40ZE4t1VlMKbqSNT7cJRYzWuja0s=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.27.0 h1:QY7/0NeRPKlzusf40ZE4t1VlMKbqSNT7cJRYzWuja0s=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.27.0/go.mod h1:HVkSiDhTM9BoUJU8qE6j2eSWLLXvi1USXjyd2BXT8PY=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.27.0/go.mod h1:HVkSiDhTM9BoUJU8qE6j2eSWLLXvi1USXjyd2BXT8PY=
|
||||||
go.opentelemetry.io/otel/metric v1.32.0 h1:xV2umtmNcThh2/a/aCP+h64Xx5wsj8qqnkYZktzNa0M=
|
go.opentelemetry.io/otel/metric v1.33.0 h1:r+JOocAyeRVXD8lZpjdQjzMadVZp2M4WmQ+5WtEnklQ=
|
||||||
go.opentelemetry.io/otel/metric v1.32.0/go.mod h1:jH7CIbbK6SH2V2wE16W05BHCtIDzauciCRLoc/SyMv8=
|
go.opentelemetry.io/otel/metric v1.33.0/go.mod h1:L9+Fyctbp6HFTddIxClbQkjtubW6O9QS3Ann/M82u6M=
|
||||||
go.opentelemetry.io/otel/sdk v1.32.0 h1:RNxepc9vK59A8XsgZQouW8ue8Gkb4jpWtJm9ge5lEG4=
|
go.opentelemetry.io/otel/sdk v1.33.0 h1:iax7M131HuAm9QkZotNHEfstof92xM+N8sr3uHXc2IM=
|
||||||
go.opentelemetry.io/otel/sdk v1.32.0/go.mod h1:LqgegDBjKMmb2GC6/PrTnteJG39I8/vJCAP9LlJXEjU=
|
go.opentelemetry.io/otel/sdk v1.33.0/go.mod h1:A1Q5oi7/9XaMlIWzPSxLRWOI8nG3FnzHJNbiENQuihM=
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.32.0 h1:rZvFnvmvawYb0alrYkjraqJq0Z4ZUJAiyYCU9snn1CU=
|
go.opentelemetry.io/otel/sdk/metric v1.32.0 h1:rZvFnvmvawYb0alrYkjraqJq0Z4ZUJAiyYCU9snn1CU=
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.32.0/go.mod h1:PWeZlq0zt9YkYAp3gjKZ0eicRYvOh1Gd+X99x6GHpCQ=
|
go.opentelemetry.io/otel/sdk/metric v1.32.0/go.mod h1:PWeZlq0zt9YkYAp3gjKZ0eicRYvOh1Gd+X99x6GHpCQ=
|
||||||
go.opentelemetry.io/otel/trace v1.32.0 h1:WIC9mYrXf8TmY/EXuULKc8hR17vE+Hjv2cssQDe03fM=
|
go.opentelemetry.io/otel/trace v1.33.0 h1:cCJuF7LRjUFso9LPnEAHJDB2pqzp+hbO8eu1qqW2d/s=
|
||||||
go.opentelemetry.io/otel/trace v1.32.0/go.mod h1:+i4rkvCraA+tG6AzwloGaCtkx53Fa+L+V8e9a7YvhT8=
|
go.opentelemetry.io/otel/trace v1.33.0/go.mod h1:uIcdVUZMpTAmz0tI1z04GoVSezK37CbGV4fr1f2nBck=
|
||||||
go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0=
|
go.opentelemetry.io/proto/otlp v1.4.0 h1:TA9WRvW6zMwP+Ssb6fLoUIuirti1gGbP28GcKG1jgeg=
|
||||||
go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8=
|
go.opentelemetry.io/proto/otlp v1.4.0/go.mod h1:PPBWZIP98o2ElSqI35IHfu7hIhSwvc5N38Jw8pXuGFY=
|
||||||
go.step.sm/cli-utils v0.9.0 h1:55jYcsQbnArNqepZyAwcato6Zy2MoZDRkWW+jF+aPfQ=
|
go.step.sm/cli-utils v0.9.0 h1:55jYcsQbnArNqepZyAwcato6Zy2MoZDRkWW+jF+aPfQ=
|
||||||
go.step.sm/cli-utils v0.9.0/go.mod h1:Y/CRoWl1FVR9j+7PnAewufAwKmBOTzR6l9+7EYGAnp8=
|
go.step.sm/cli-utils v0.9.0/go.mod h1:Y/CRoWl1FVR9j+7PnAewufAwKmBOTzR6l9+7EYGAnp8=
|
||||||
go.step.sm/crypto v0.45.0 h1:Z0WYAaaOYrJmKP9sJkPW+6wy3pgN3Ija8ek/D4serjc=
|
go.step.sm/crypto v0.45.0 h1:Z0WYAaaOYrJmKP9sJkPW+6wy3pgN3Ija8ek/D4serjc=
|
||||||
@@ -1161,8 +1211,8 @@ golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5y
|
|||||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
|
golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
|
||||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||||
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
|
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
|
||||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
|
||||||
golang.org/x/crypto/x509roots/fallback v0.0.0-20240507223354-67b13616a595 h1:TgSqweA595vD0Zt86JzLv3Pb/syKg8gd5KMGGbJPYFw=
|
golang.org/x/crypto/x509roots/fallback v0.0.0-20240507223354-67b13616a595 h1:TgSqweA595vD0Zt86JzLv3Pb/syKg8gd5KMGGbJPYFw=
|
||||||
golang.org/x/crypto/x509roots/fallback v0.0.0-20240507223354-67b13616a595/go.mod h1:kNa9WdvYnzFwC79zRpLRMJbdEFlhyM5RPFBBZp/wWH8=
|
golang.org/x/crypto/x509roots/fallback v0.0.0-20240507223354-67b13616a595/go.mod h1:kNa9WdvYnzFwC79zRpLRMJbdEFlhyM5RPFBBZp/wWH8=
|
||||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
@@ -1179,8 +1229,8 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
|||||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4=
|
golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM=
|
||||||
golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
|
golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
|
||||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
@@ -1202,8 +1252,8 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug
|
|||||||
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
|
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
|
||||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||||
golang.org/x/net v0.32.0 h1:ZqPmj8Kzc+Y6e0+skZsuACbx+wzMgo5MQsJh9Qd6aYI=
|
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
|
||||||
golang.org/x/net v0.32.0/go.mod h1:CwU0IoeOlnQQWJ6ioyFrfRuomB8GKF6KbYXZVyeXNfs=
|
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
|
||||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||||
golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE=
|
golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE=
|
||||||
golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||||
@@ -1217,8 +1267,8 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ
|
|||||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
|
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
|
||||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
@@ -1266,8 +1316,8 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|||||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
|
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
|
||||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
@@ -1275,8 +1325,8 @@ golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
|
|||||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||||
golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
|
golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
|
||||||
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
|
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
@@ -1287,8 +1337,8 @@ golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
|||||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||||
golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg=
|
golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg=
|
||||||
golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
@@ -1310,8 +1360,8 @@ golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4f
|
|||||||
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||||
golang.org/x/tools v0.28.0 h1:WuB6qZ4RPCQo5aP3WdKZS7i595EdWqWR8vqJTlwTVK8=
|
golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY=
|
||||||
golang.org/x/tools v0.28.0/go.mod h1:dcIOrVd3mfQKTgrDVQHqCPMWy6lnhfhtX3hLXYVLfRw=
|
golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY=
|
||||||
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
@@ -1334,10 +1384,10 @@ google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98
|
|||||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||||
google.golang.org/genproto v0.0.0-20240401170217-c3f982113cda h1:wu/KJm9KJwpfHWhkkZGohVC6KRrc1oJNr4jwtQMOQXw=
|
google.golang.org/genproto v0.0.0-20240401170217-c3f982113cda h1:wu/KJm9KJwpfHWhkkZGohVC6KRrc1oJNr4jwtQMOQXw=
|
||||||
google.golang.org/genproto v0.0.0-20240401170217-c3f982113cda/go.mod h1:g2LLCvCeCSir/JJSWosk19BR4NVxGqHUC6rxIRsd7Aw=
|
google.golang.org/genproto v0.0.0-20240401170217-c3f982113cda/go.mod h1:g2LLCvCeCSir/JJSWosk19BR4NVxGqHUC6rxIRsd7Aw=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 h1:hjSy6tcFQZ171igDaN5QHOw2n6vx40juYbC/x67CEhc=
|
google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 h1:CkkIfIt50+lT6NHAVoRYEyAvQGFM7xEwXUUywFvEb3Q=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:qpvKtACPCQhAdu3PyQgV4l3LMXZEtft7y8QcarRsp9I=
|
google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576/go.mod h1:1R3kvZ1dtP3+4p4d3G8uJ8rFk/fWlScl38vanWACI08=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20241007155032-5fefd90f89a9 h1:QCqS/PdaHTSWGvupk2F/ehwHtGc0/GYkT+3GAcR1CCc=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576 h1:8ZmaLZE4XWrtU3MyClkYqqtl6Oegr3235h7jxsDyqCY=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20241007155032-5fefd90f89a9/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU=
|
||||||
google.golang.org/grpc v1.0.5/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
|
google.golang.org/grpc v1.0.5/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
|
||||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||||
@@ -1345,8 +1395,8 @@ google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyac
|
|||||||
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||||
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
|
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
|
||||||
google.golang.org/grpc v1.68.0 h1:aHQeeJbo8zAkAa3pRzrVjZlbz6uSfeOXlJNQM0RAbz0=
|
google.golang.org/grpc v1.68.1 h1:oI5oTa11+ng8r8XMMN7jAOmWfPZWbYpCFaMUTACxkM0=
|
||||||
google.golang.org/grpc v1.68.0/go.mod h1:fmSPC5AsjSBCK54MyHRx48kpOti1/jRfOlwEWywNjWA=
|
google.golang.org/grpc v1.68.1/go.mod h1:+q1XYFJjShcqn0QZHvCyeR4CXPA+llXIeUIfIe00waw=
|
||||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||||
@@ -1356,8 +1406,8 @@ google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2
|
|||||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||||
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||||
google.golang.org/protobuf v1.36.0 h1:mjIs9gYtt56AzC4ZaffQuh88TZurBGhIJMBZGSxNerQ=
|
google.golang.org/protobuf v1.36.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU=
|
||||||
google.golang.org/protobuf v1.36.0/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||||
gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U=
|
gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U=
|
||||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||||
gopkg.in/cenkalti/backoff.v2 v2.2.1 h1:eJ9UAg01/HIHG987TwxvnzK2MgxXq97YY6rYDpY9aII=
|
gopkg.in/cenkalti/backoff.v2 v2.2.1 h1:eJ9UAg01/HIHG987TwxvnzK2MgxXq97YY6rYDpY9aII=
|
||||||
@@ -1403,3 +1453,27 @@ k8s.io/klog/v2 v2.110.1 h1:U/Af64HJf7FcwMcXyKm2RPM22WZzyR7OSpYj5tg3cL0=
|
|||||||
k8s.io/klog/v2 v2.110.1/go.mod h1:YGtd1984u+GgbuZ7e08/yBuAfKLSO0+uR1Fhi6ExXjo=
|
k8s.io/klog/v2 v2.110.1/go.mod h1:YGtd1984u+GgbuZ7e08/yBuAfKLSO0+uR1Fhi6ExXjo=
|
||||||
lukechampine.com/blake3 v1.3.0 h1:sJ3XhFINmHSrYCgl958hscfIa3bw8x4DqMP3u1YvoYE=
|
lukechampine.com/blake3 v1.3.0 h1:sJ3XhFINmHSrYCgl958hscfIa3bw8x4DqMP3u1YvoYE=
|
||||||
lukechampine.com/blake3 v1.3.0/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k=
|
lukechampine.com/blake3 v1.3.0/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k=
|
||||||
|
modernc.org/cc/v4 v4.24.4 h1:TFkx1s6dCkQpd6dKurBNmpo+G8Zl4Sq/ztJ+2+DEsh0=
|
||||||
|
modernc.org/cc/v4 v4.24.4/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
||||||
|
modernc.org/ccgo/v4 v4.23.16 h1:Z2N+kk38b7SfySC1ZkpGLN2vthNJP1+ZzGZIlH7uBxo=
|
||||||
|
modernc.org/ccgo/v4 v4.23.16/go.mod h1:nNma8goMTY7aQZQNTyN9AIoJfxav4nvTnvKThAeMDdo=
|
||||||
|
modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
|
||||||
|
modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=
|
||||||
|
modernc.org/gc/v2 v2.6.3 h1:aJVhcqAte49LF+mGveZ5KPlsp4tdGdAOT4sipJXADjw=
|
||||||
|
modernc.org/gc/v2 v2.6.3/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
||||||
|
modernc.org/libc v1.61.13 h1:3LRd6ZO1ezsFiX1y+bHd1ipyEHIJKvuprv0sLTBwLW8=
|
||||||
|
modernc.org/libc v1.61.13/go.mod h1:8F/uJWL/3nNil0Lgt1Dpz+GgkApWh04N3el3hxJcA6E=
|
||||||
|
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||||
|
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||||
|
modernc.org/memory v1.8.2 h1:cL9L4bcoAObu4NkxOlKWBWtNHIsnnACGF/TbqQ6sbcI=
|
||||||
|
modernc.org/memory v1.8.2/go.mod h1:ZbjSvMO5NQ1A2i3bWeDiVMxIorXwdClKE/0SZ+BMotU=
|
||||||
|
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
|
||||||
|
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
||||||
|
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
||||||
|
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
||||||
|
modernc.org/sqlite v1.36.3 h1:qYMYlFR+rtLDUzuXoST1SDIdEPbX8xzuhdF90WsX1ss=
|
||||||
|
modernc.org/sqlite v1.36.3/go.mod h1:ADySlx7K4FdY5MaJcEv86hTJ0PjedAloTUuif0YS3ws=
|
||||||
|
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||||
|
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||||
|
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||||
|
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||||
|
|||||||
@@ -1,86 +0,0 @@
|
|||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/docker/docker/api/types"
|
|
||||||
"regexp"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
LabelManaged = "uncloud.managed"
|
|
||||||
LabelServiceID = "uncloud.service.id"
|
|
||||||
LabelServiceName = "uncloud.service.name"
|
|
||||||
LabelServiceMode = "uncloud.service.mode"
|
|
||||||
LabelServicePorts = "uncloud.service.ports"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Container struct {
|
|
||||||
types.Container
|
|
||||||
}
|
|
||||||
|
|
||||||
// ServiceID returns the ID of the service this container belongs to.
|
|
||||||
func (c *Container) ServiceID() string {
|
|
||||||
return c.Labels[LabelServiceID]
|
|
||||||
}
|
|
||||||
|
|
||||||
// ServiceName returns the name of the service this container belongs to.
|
|
||||||
func (c *Container) ServiceName() string {
|
|
||||||
return c.Labels[LabelServiceName]
|
|
||||||
}
|
|
||||||
|
|
||||||
// ServiceMode returns the replication mode of the service this container belongs to.
|
|
||||||
func (c *Container) ServiceMode() string {
|
|
||||||
return c.Labels[LabelServiceMode]
|
|
||||||
}
|
|
||||||
|
|
||||||
// ServicePorts returns the ports this container publishes as part of its service.
|
|
||||||
func (c *Container) ServicePorts() ([]PortSpec, error) {
|
|
||||||
encoded, ok := c.Labels[LabelServicePorts]
|
|
||||||
if !ok {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
publishPorts := strings.Split(encoded, ",")
|
|
||||||
ports := make([]PortSpec, len(publishPorts))
|
|
||||||
for i, p := range publishPorts {
|
|
||||||
port, err := ParsePortSpec(strings.TrimSpace(p))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
ports[i] = port
|
|
||||||
}
|
|
||||||
|
|
||||||
return ports, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// runningStatusRegex matches the status string of a running container.
|
|
||||||
// - "Up 3 minutes (healthy)" -> groups: ["Up 3 minutes (healthy)", "healthy"]
|
|
||||||
// - "Up 5 seconds" -> groups: ["Up 5 seconds", ""]
|
|
||||||
// - "Up 2 hours (unhealthy)" -> groups: ["Up 2 hours (unhealthy)", "unhealthy"]
|
|
||||||
// - "Up 1 minute (health: starting)" -> groups: ["Up 1 minute (health: starting)", "health: starting"]
|
|
||||||
// - "Restarting (0) 5 seconds ago" -> no match
|
|
||||||
// See https://github.com/moby/moby/blob/c130ce1f5d1e38b98a97044a39557de43bc0d58f/container/state.go#L77-L90
|
|
||||||
// for more details on how the status string for a running container is formatted.
|
|
||||||
var runningStatusRegex = regexp.MustCompile(`^Up [^(]+(?:\(([^)]+)\))?$`)
|
|
||||||
|
|
||||||
// Healthy determines if the container is running and healthy based on its status string.
|
|
||||||
// A running container with no health check configured is considered healthy.
|
|
||||||
func (c *Container) Healthy() bool {
|
|
||||||
if c.State != "running" {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
matches := runningStatusRegex.FindStringSubmatch(c.Status)
|
|
||||||
// Not "Up" or invalid format.
|
|
||||||
if matches == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// If there's no health status (no health check configured so no parentheses), container is considered healthy.
|
|
||||||
if matches[1] == "" {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the health status in parentheses is "healthy", the container is considered healthy.
|
|
||||||
return matches[1] == types.Healthy
|
|
||||||
}
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/docker/docker/api/types"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestContainer_Healthy(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
t.Run("exited", func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
c := &Container{Container: types.Container{
|
|
||||||
State: "exited",
|
|
||||||
Status: "Exited (0) 2 minutes ago",
|
|
||||||
}}
|
|
||||||
assert.False(t, c.Healthy())
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("running with no health check", func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
c := &Container{Container: types.Container{
|
|
||||||
State: "running",
|
|
||||||
Status: "Up 5 minutes",
|
|
||||||
}}
|
|
||||||
assert.True(t, c.Healthy())
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("running and healthy", func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
c := &Container{Container: types.Container{
|
|
||||||
State: "running",
|
|
||||||
Status: "Up 3 minutes (healthy)",
|
|
||||||
}}
|
|
||||||
assert.True(t, c.Healthy())
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("running but unhealthy", func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
c := &Container{Container: types.Container{
|
|
||||||
State: "running",
|
|
||||||
Status: "Up 2 hours (unhealthy)",
|
|
||||||
}}
|
|
||||||
assert.False(t, c.Healthy())
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("running with health starting", func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
c := &Container{Container: types.Container{
|
|
||||||
State: "running",
|
|
||||||
Status: "Up 1 minute (health: starting)",
|
|
||||||
}}
|
|
||||||
assert.False(t, c.Healthy())
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("invalid up format no time", func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
c := &Container{Container: types.Container{
|
|
||||||
State: "running",
|
|
||||||
Status: "Up",
|
|
||||||
}}
|
|
||||||
assert.False(t, c.Healthy())
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("invalid up format empty parentheses", func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
c := &Container{Container: types.Container{
|
|
||||||
State: "running",
|
|
||||||
Status: "Up 5 minutes ()",
|
|
||||||
}}
|
|
||||||
assert.False(t, c.Healthy())
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("malformed status", func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
c := &Container{Container: types.Container{
|
|
||||||
State: "running",
|
|
||||||
Status: "Invalid status",
|
|
||||||
}}
|
|
||||||
assert.False(t, c.Healthy())
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("restarting", func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
c := &Container{Container: types.Container{
|
|
||||||
State: "running",
|
|
||||||
Status: "Restarting (0) 5 seconds ago",
|
|
||||||
}}
|
|
||||||
assert.False(t, c.Healthy())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,98 +0,0 @@
|
|||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"github.com/distribution/reference"
|
|
||||||
"uncloud/internal/machine/api/pb"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
ServiceModeReplicated = "replicated"
|
|
||||||
ServiceModeGlobal = "global"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ServiceSpec struct {
|
|
||||||
Container ContainerSpec
|
|
||||||
// Mode is the replication mode of the service. Default is ServiceModeReplicated if empty.
|
|
||||||
Mode string
|
|
||||||
Name string
|
|
||||||
// Ports defines what service ports to publish to make the service accessible outside the cluster.
|
|
||||||
Ports []PortSpec
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ServiceSpec) Validate() error {
|
|
||||||
if err := s.Container.Validate(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
switch s.Mode {
|
|
||||||
case "", ServiceModeGlobal, ServiceModeReplicated:
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("invalid mode: %q", s.Mode)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: validate there is no conflict between ports.
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type ContainerSpec struct {
|
|
||||||
Command []string
|
|
||||||
Image string
|
|
||||||
// Run a custom init inside the container. If nil, use the daemon's configured settings.
|
|
||||||
Init *bool
|
|
||||||
// List of volumes to bind mount into the container.
|
|
||||||
Volumes []string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ContainerSpec) Validate() error {
|
|
||||||
_, err := reference.ParseDockerRef(s.Image)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("invalid image: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type Service struct {
|
|
||||||
ID string
|
|
||||||
Name string
|
|
||||||
Mode string
|
|
||||||
Containers []MachineContainer
|
|
||||||
}
|
|
||||||
|
|
||||||
type MachineContainer struct {
|
|
||||||
MachineID string
|
|
||||||
Container Container
|
|
||||||
}
|
|
||||||
|
|
||||||
func ServiceFromProto(s *pb.Service) (Service, error) {
|
|
||||||
var err error
|
|
||||||
containers := make([]MachineContainer, len(s.Containers))
|
|
||||||
for i, sc := range s.Containers {
|
|
||||||
containers[i], err = machineContainerFromProto(sc)
|
|
||||||
if err != nil {
|
|
||||||
return Service{}, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Service{
|
|
||||||
ID: s.Id,
|
|
||||||
Name: s.Name,
|
|
||||||
Mode: s.Mode,
|
|
||||||
Containers: containers,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func machineContainerFromProto(sc *pb.Service_Container) (MachineContainer, error) {
|
|
||||||
var c Container
|
|
||||||
if err := json.Unmarshal(sc.Container, &c); err != nil {
|
|
||||||
return MachineContainer{}, fmt.Errorf("unmarshal container: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return MachineContainer{
|
|
||||||
MachineID: sc.MachineId,
|
|
||||||
Container: c,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,176 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
composetypes "github.com/compose-spec/compose-go/v2/types"
|
||||||
|
"github.com/distribution/reference"
|
||||||
|
"github.com/docker/cli/cli/config"
|
||||||
|
"github.com/docker/docker/api/types"
|
||||||
|
"github.com/docker/docker/api/types/image"
|
||||||
|
dockerclient "github.com/docker/docker/client"
|
||||||
|
"github.com/docker/docker/pkg/archive"
|
||||||
|
"github.com/docker/docker/pkg/jsonmessage"
|
||||||
|
"github.com/docker/docker/registry"
|
||||||
|
"github.com/moby/term"
|
||||||
|
)
|
||||||
|
|
||||||
|
type BuildOptions struct {
|
||||||
|
Files []string
|
||||||
|
Profiles []string
|
||||||
|
Services []string
|
||||||
|
Push bool
|
||||||
|
NoCache bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetServicesThatNeedBuild returns a map of services that require building
|
||||||
|
func GetServicesThatNeedBuild(project *composetypes.Project) map[string]composetypes.ServiceConfig {
|
||||||
|
servicesToBuild := make(map[string]composetypes.ServiceConfig, len(project.Services))
|
||||||
|
for serviceName, service := range project.Services {
|
||||||
|
if service.Build == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
servicesToBuild[serviceName] = service
|
||||||
|
}
|
||||||
|
return servicesToBuild
|
||||||
|
}
|
||||||
|
|
||||||
|
// BuildServices builds the services defined in the provided map.
|
||||||
|
func BuildServices(ctx context.Context, servicesToBuild map[string]composetypes.ServiceConfig, opts BuildOptions) error {
|
||||||
|
fmt.Println("Building services...")
|
||||||
|
|
||||||
|
// Init docker client (can be local or remote, depending on DOCKER_HOST environment variable)
|
||||||
|
dockerCli, err := dockerclient.NewClientWithOpts(dockerclient.FromEnv, dockerclient.WithAPIVersionNegotiation())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer dockerCli.Close()
|
||||||
|
|
||||||
|
serviceImages := make(map[string]string, len(servicesToBuild))
|
||||||
|
|
||||||
|
// Build the services using the local docker client and compose libraries
|
||||||
|
for _, service := range servicesToBuild {
|
||||||
|
fmt.Printf("Building service: %s\n", service.Name)
|
||||||
|
imageName, err := buildSingleService(ctx, dockerCli, service, opts)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("build service %s: %w", service.Name, err)
|
||||||
|
}
|
||||||
|
serviceImages[service.Name] = imageName
|
||||||
|
}
|
||||||
|
fmt.Printf("Service images are built.\n")
|
||||||
|
|
||||||
|
if opts.Push {
|
||||||
|
err = pushServiceImages(ctx, dockerCli, serviceImages)
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildSingleService builds a single service using the Docker client and Compose libraries.
|
||||||
|
func buildSingleService(ctx context.Context, dockerCli *dockerclient.Client, service composetypes.ServiceConfig, opts BuildOptions) (string, error) {
|
||||||
|
if service.Build == nil {
|
||||||
|
return "", fmt.Errorf("service %s has no build configuration", service.Name)
|
||||||
|
}
|
||||||
|
if service.Image == "" {
|
||||||
|
return "", fmt.Errorf("service %s has no image specified; building services without image is not supported yet", service.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
buildContextPath := service.Build.Context
|
||||||
|
imageName := service.Image
|
||||||
|
|
||||||
|
// Create a tar archive of the build context
|
||||||
|
buildContext, err := archive.TarWithOptions(buildContextPath, &archive.TarOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to create build context for service %s: %w", service.Name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
buildOptions := types.ImageBuildOptions{
|
||||||
|
// TODO: Support Dockerfiles outside the build context
|
||||||
|
// See https://github.com/docker/compose/blob/cf89fd1aa1328d5af77658ccc5a1e1b29981ae80/pkg/compose/build_classic.go#L92
|
||||||
|
Dockerfile: service.Build.Dockerfile,
|
||||||
|
Tags: []string{imageName},
|
||||||
|
Remove: true, // Remove intermediate containers
|
||||||
|
NoCache: opts.NoCache,
|
||||||
|
}
|
||||||
|
|
||||||
|
buildResponse, err := dockerCli.ImageBuild(ctx, buildContext, buildOptions)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to build image for service %s: %w", service.Name, err)
|
||||||
|
}
|
||||||
|
defer buildResponse.Body.Close()
|
||||||
|
|
||||||
|
// Display the build response
|
||||||
|
fd, isTerminal := term.GetFdInfo(os.Stdout)
|
||||||
|
if err := jsonmessage.DisplayJSONMessagesStream(buildResponse.Body, os.Stdout, fd, isTerminal, nil); err != nil {
|
||||||
|
return "", fmt.Errorf("failed to display build response for service %s: %w", service.Name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return imageName, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// pushSingleServiceImage pushes a single service image.
|
||||||
|
func pushSingleServiceImage(ctx context.Context, dockerCli *dockerclient.Client, serviceName string, imageName string) error {
|
||||||
|
ref, err := reference.ParseNormalizedNamed(imageName)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
repoInfo, err := registry.ParseRepositoryInfo(ref)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
registryKey := repoInfo.Index.Name
|
||||||
|
if repoInfo.Index.Official {
|
||||||
|
registryKey = registry.IndexServer
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load the Docker config file with auth details, if available
|
||||||
|
configFile := config.LoadDefaultConfigFile(os.Stderr)
|
||||||
|
|
||||||
|
authConfig, err := configFile.GetAuthConfig(registryKey)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
authJSON, err := json.Marshal(authConfig)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to marshal auth config for registry %s: %w", registryKey, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
authStr := base64.URLEncoding.EncodeToString(authJSON)
|
||||||
|
|
||||||
|
pushOptions := image.PushOptions{
|
||||||
|
RegistryAuth: authStr,
|
||||||
|
}
|
||||||
|
pushResponse, err := dockerCli.ImagePush(ctx, imageName, pushOptions)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to push image %s: %w", imageName, err)
|
||||||
|
}
|
||||||
|
defer pushResponse.Close()
|
||||||
|
|
||||||
|
fmt.Printf("Pushing image %s for service %s...\n", imageName, serviceName)
|
||||||
|
|
||||||
|
fd, isTerminal := term.GetFdInfo(os.Stdout)
|
||||||
|
if err := jsonmessage.DisplayJSONMessagesStream(pushResponse, os.Stdout, fd, isTerminal, nil); err != nil {
|
||||||
|
return fmt.Errorf("failed to display push response for image %s: %w", imageName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Image %s pushed successfully.\n", imageName)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// pushServiceImages pushes all built service images to the registry.
|
||||||
|
func pushServiceImages(ctx context.Context, dockerCli *dockerclient.Client, serviceImages map[string]string) error {
|
||||||
|
fmt.Printf("Pushing images...\n")
|
||||||
|
for serviceName, imageName := range serviceImages {
|
||||||
|
if err := pushSingleServiceImage(ctx, dockerCli, serviceName, imageName); err != nil {
|
||||||
|
return fmt.Errorf("push image for service %s: %w", serviceName, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
+295
-142
@@ -4,203 +4,352 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/charmbracelet/huh"
|
|
||||||
"google.golang.org/protobuf/types/known/emptypb"
|
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"uncloud/internal/cli/client"
|
"os"
|
||||||
"uncloud/internal/cli/client/connector"
|
"slices"
|
||||||
"uncloud/internal/cli/config"
|
|
||||||
"uncloud/internal/machine"
|
"github.com/docker/cli/cli/streams"
|
||||||
"uncloud/internal/machine/api/pb"
|
"github.com/psviderski/uncloud/internal/cli/config"
|
||||||
"uncloud/internal/sshexec"
|
"github.com/psviderski/uncloud/internal/fs"
|
||||||
|
"github.com/psviderski/uncloud/internal/machine"
|
||||||
|
"github.com/psviderski/uncloud/internal/machine/api/pb"
|
||||||
|
"github.com/psviderski/uncloud/internal/sshexec"
|
||||||
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
|
"github.com/psviderski/uncloud/pkg/client"
|
||||||
|
"github.com/psviderski/uncloud/pkg/client/connector"
|
||||||
|
"google.golang.org/grpc/codes"
|
||||||
|
"google.golang.org/grpc/status"
|
||||||
|
"google.golang.org/protobuf/types/known/emptypb"
|
||||||
)
|
)
|
||||||
|
|
||||||
const defaultClusterName = "default"
|
const (
|
||||||
|
// DefaultSSHKeyPath is the fallback location for the SSH private key when provisioning remote machines.
|
||||||
|
// Used when no key is explicitly provided and SSH agent authentication fails.
|
||||||
|
DefaultSSHKeyPath = "~/.ssh/id_ed25519"
|
||||||
|
DefaultContextName = "default"
|
||||||
|
)
|
||||||
|
|
||||||
type CLI struct {
|
type CLI struct {
|
||||||
config *config.Config
|
Config *config.Config
|
||||||
|
conn *config.MachineConnection
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(configPath string) (*CLI, error) {
|
|
||||||
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) error {
|
func (cli *CLI) CreateContext(name string) error {
|
||||||
if _, ok := cli.config.Clusters[name]; ok {
|
if _, ok := cli.Config.Contexts[name]; ok {
|
||||||
return fmt.Errorf("cluster %q already exists", name)
|
return fmt.Errorf("context '%s' already exists", name)
|
||||||
}
|
}
|
||||||
cli.config.Clusters[name] = &config.Cluster{
|
cli.Config.Contexts[name] = &config.Context{
|
||||||
Name: name,
|
Name: name,
|
||||||
}
|
}
|
||||||
return cli.config.Save()
|
return cli.Config.Save()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cli *CLI) SetCurrentCluster(name string) error {
|
func (cli *CLI) SetCurrentContext(name string) error {
|
||||||
if _, ok := cli.config.Clusters[name]; !ok {
|
if _, ok := cli.Config.Contexts[name]; !ok {
|
||||||
return client.ErrNotFound
|
return api.ErrNotFound
|
||||||
}
|
}
|
||||||
cli.config.CurrentCluster = name
|
cli.Config.CurrentContext = name
|
||||||
return cli.config.Save()
|
return cli.Config.Save()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cli *CLI) ConnectCluster(ctx context.Context, clusterName string) (*client.Client, error) {
|
// ConnectCluster connects to a cluster using the given context name or the current context if not specified.
|
||||||
if len(cli.config.Clusters) == 0 {
|
// If the CLI was initialised with a machine connection, the config is ignored and the connection is used instead.
|
||||||
return nil, errors.New(
|
func (cli *CLI) ConnectCluster(ctx context.Context, contextName string) (*client.Client, error) {
|
||||||
"no clusters found in the Uncloud config. " +
|
if cli.conn != nil {
|
||||||
"Please initialise a cluster with `uncloud machine init` first",
|
return connectCluster(ctx, *cli.conn)
|
||||||
)
|
|
||||||
}
|
}
|
||||||
if clusterName == "" {
|
|
||||||
// If the cluster is not specified, use the current cluster if set.
|
if len(cli.Config.Contexts) == 0 {
|
||||||
if cli.config.CurrentCluster == "" {
|
|
||||||
return nil, errors.New(
|
|
||||||
"the current cluster is not set in the Uncloud config. " +
|
|
||||||
"Please specify a cluster with the --cluster flag or set current_cluster in the config",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if _, ok := cli.config.Clusters[cli.config.CurrentCluster]; !ok {
|
|
||||||
return nil, fmt.Errorf(
|
return nil, fmt.Errorf(
|
||||||
"current cluster %q not found in the config. "+
|
"no cluster contexts found in the Uncloud config (%s). "+
|
||||||
"Please specify a cluster with the --cluster flag or update current_cluster in the config",
|
"Please initialise a cluster with 'uncloud machine init' first",
|
||||||
cli.config.CurrentCluster,
|
cli.Config.Path(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
clusterName = cli.config.CurrentCluster
|
if contextName == "" {
|
||||||
|
// If the cluster is not specified, use the current cluster if set.
|
||||||
|
if cli.Config.CurrentContext == "" {
|
||||||
|
return nil, fmt.Errorf(
|
||||||
|
"the current cluster context is not set in the Uncloud config (%s). "+
|
||||||
|
"Please specify the context with the '--context' flag or set 'current_context' in the config",
|
||||||
|
cli.Config.Path(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
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]
|
||||||
|
|
||||||
|
c, err := connectCluster(ctx, conn)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("connect to cluster (context '%s'): %w", contextName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func connectCluster(ctx context.Context, conn config.MachineConnection) (*client.Client, error) {
|
||||||
if conn.SSH != "" {
|
if conn.SSH != "" {
|
||||||
user, host, port, err := conn.SSH.Parse()
|
user, host, port, err := conn.SSH.Parse()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("parse SSH connection %q: %w", conn.SSH, err)
|
return nil, fmt.Errorf("parse SSH connection %q: %w", conn.SSH, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
keyPath := fs.ExpandHomeDir(conn.SSHKeyFile)
|
||||||
|
|
||||||
sshConfig := &connector.SSHConnectorConfig{
|
sshConfig := &connector.SSHConnectorConfig{
|
||||||
User: user,
|
User: user,
|
||||||
Host: host,
|
Host: host,
|
||||||
Port: port,
|
Port: port,
|
||||||
|
KeyPath: keyPath,
|
||||||
}
|
}
|
||||||
return client.New(ctx, connector.NewSSHConnector(sshConfig))
|
return client.New(ctx, connector.NewSSHConnector(sshConfig))
|
||||||
} else if conn.TCP.IsValid() {
|
} else if conn.TCP != nil && conn.TCP.IsValid() {
|
||||||
return client.New(ctx, connector.NewTCPConnector(conn.TCP))
|
return client.New(ctx, connector.NewTCPConnector(*conn.TCP))
|
||||||
}
|
|
||||||
return nil, errors.New("no valid connection configuration found for the cluster")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cli *CLI) InitCluster(
|
return nil, errors.New("connection configuration is invalid")
|
||||||
ctx context.Context, remoteMachine *RemoteMachine, clusterName, machineName string, netPrefix netip.Prefix,
|
}
|
||||||
) error {
|
|
||||||
if remoteMachine != nil {
|
type InitClusterOptions struct {
|
||||||
return cli.initRemoteMachine(ctx, *remoteMachine, clusterName, machineName, netPrefix)
|
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, err := cli.newContextName(opts.Context)
|
||||||
) error {
|
|
||||||
if clusterName == "" {
|
|
||||||
clusterName = defaultClusterName
|
|
||||||
}
|
|
||||||
if _, ok := cli.config.Clusters[clusterName]; ok {
|
|
||||||
return fmt.Errorf("cluster %q already exists", clusterName)
|
|
||||||
}
|
|
||||||
|
|
||||||
machineClient, err := cli.provisionRemoteMachine(ctx, remoteMachine)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer machineClient.Close()
|
|
||||||
|
machineClient, err := provisionRemoteMachine(ctx, opts.RemoteMachine, opts.Version)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
// Ensure machineClient is closed on error.
|
||||||
|
defer func() {
|
||||||
|
if err != nil {
|
||||||
|
machineClient.Close()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
// Check if the machine is already initialised as a cluster member and prompt the user to reset it first.
|
// Check if the machine is already initialised as a cluster member and prompt the user to reset it first.
|
||||||
minfo, err := machineClient.Inspect(ctx, &emptypb.Empty{})
|
minfo, err := machineClient.Inspect(ctx, &emptypb.Empty{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("inspect machine: %w", err)
|
return nil, fmt.Errorf("inspect machine: %w", err)
|
||||||
}
|
}
|
||||||
if minfo.Id != "" {
|
if minfo.Id != "" {
|
||||||
if err = cli.promptResetMachine(); err != nil {
|
if err = promptResetMachine(ctx, machineClient.MachineClient); 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),
|
||||||
}
|
}
|
||||||
|
if opts.PublicIP != nil {
|
||||||
|
if opts.PublicIP.IsValid() {
|
||||||
|
req.PublicIpConfig = &pb.InitClusterRequest_PublicIp{PublicIp: pb.NewIP(*opts.PublicIP)}
|
||||||
|
} else {
|
||||||
|
// Invalid or in other words zero IP means to automatically detect the public IP.
|
||||||
|
req.PublicIpConfig = &pb.InitClusterRequest_PublicIpAuto{PublicIpAuto: true}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
resp, err := machineClient.InitCluster(ctx, req)
|
resp, err := machineClient.InitCluster(ctx, req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("init cluster: %w", err)
|
return nil, fmt.Errorf("init cluster: %w", err)
|
||||||
}
|
}
|
||||||
fmt.Printf("Cluster %q initialised with machine %q\n", clusterName, resp.Machine.Name)
|
fmt.Printf("Cluster 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)
|
||||||
|
|
||||||
if err = cli.CreateCluster(clusterName); err != nil {
|
// Save the machine's SSH connection details in the context config.
|
||||||
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.
|
|
||||||
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 {
|
// newContextName returns a unique name for a new cluster context. If the provided name is not DefaultContextName,
|
||||||
c, err := cli.ConnectCluster(ctx, clusterName)
|
// and it's already taken, an error is returned. If the name is not provided or is DefaultContextName, the first
|
||||||
|
// available name "default[-N]" is returned.
|
||||||
|
func (cli *CLI) newContextName(name string) (string, error) {
|
||||||
|
if name == "" {
|
||||||
|
name = DefaultContextName
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, exists := cli.Config.Contexts[name]; !exists {
|
||||||
|
return name, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// If non-default context already exists, error out.
|
||||||
|
if name != DefaultContextName {
|
||||||
|
return "", fmt.Errorf("cluster context '%s' already exists", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The default context already exists, generate a numbered suffix to make it unique.
|
||||||
|
for i := 1; ; i++ {
|
||||||
|
name = fmt.Sprintf("%s-%d", DefaultContextName, i)
|
||||||
|
if _, exists := cli.Config.Contexts[name]; !exists {
|
||||||
|
return name, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type AddMachineOptions struct {
|
||||||
|
Context string
|
||||||
|
MachineName string
|
||||||
|
PublicIP *netip.Addr
|
||||||
|
RemoteMachine *RemoteMachine
|
||||||
|
Version string
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddMachine provisions a remote machine and adds it to the cluster. It returns a cluster client and a machine client.
|
||||||
|
// The cluster client is connected to the existing machine in the cluster. It was used to add the new machine to the
|
||||||
|
// cluster. The machine client is connected to the new machine and can be used to interact with it.
|
||||||
|
// Both client should be closed after use by the caller.
|
||||||
|
func (cli *CLI) AddMachine(ctx context.Context, opts AddMachineOptions) (*client.Client, *client.Client, error) {
|
||||||
|
contextName := opts.Context
|
||||||
|
if contextName == "" {
|
||||||
|
contextName = cli.Config.CurrentContext
|
||||||
|
}
|
||||||
|
c, err := cli.ConnectCluster(ctx, contextName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("connect to cluster: %w", err)
|
return nil, nil, fmt.Errorf("connect to cluster (context '%s'): %w", contextName, err)
|
||||||
}
|
}
|
||||||
defer func() {
|
defer func() {
|
||||||
_ = c.Close()
|
if err != nil {
|
||||||
|
c.Close()
|
||||||
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
machineClient, err := cli.provisionRemoteMachine(ctx, remoteMachine)
|
machineClient, err := provisionRemoteMachine(ctx, opts.RemoteMachine, opts.Version)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
defer machineClient.Close()
|
defer func() {
|
||||||
|
if err != nil {
|
||||||
|
machineClient.Close()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
// Check if the machine is already initialised as a cluster member and prompt the user to reset it first.
|
// 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, nil, fmt.Errorf("inspect machine: %w", err)
|
||||||
}
|
}
|
||||||
if minfo.Id != "" {
|
if minfo.Id != "" {
|
||||||
if err = cli.promptResetMachine(); err != nil {
|
// Check if the machine is already a member of this cluster.
|
||||||
return err
|
machines, err := c.ListMachines(ctx, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("list cluster machines: %w", err)
|
||||||
}
|
}
|
||||||
|
if slices.ContainsFunc(machines, func(m *pb.MachineMember) bool {
|
||||||
|
return m.Machine.Id == minfo.Id
|
||||||
|
}) {
|
||||||
|
return nil, nil, fmt.Errorf("machine is already a member of this cluster (%s)", minfo.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = promptResetMachine(ctx, machineClient.MachineClient); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check machine meets all necessary system requirements before proceeding.
|
||||||
|
checkResp, err := machineClient.CheckPrerequisites(ctx, &emptypb.Empty{})
|
||||||
|
// TODO(lhf): remove Unimplemented check when v0.9.0 is released.
|
||||||
|
if err != nil {
|
||||||
|
if status.Convert(err).Code() != codes.Unimplemented {
|
||||||
|
return nil, nil, fmt.Errorf("check machine prerequisites: %w", err)
|
||||||
|
}
|
||||||
|
} else if !checkResp.Satisfied {
|
||||||
|
return nil, nil, fmt.Errorf("machine prerequisites not satisfied: %s", checkResp.Error)
|
||||||
}
|
}
|
||||||
|
|
||||||
tokenResp, err := machineClient.Token(ctx, &emptypb.Empty{})
|
tokenResp, err := machineClient.Token(ctx, &emptypb.Empty{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("get remote machine token: %w", err)
|
return nil, nil, fmt.Errorf("get remote machine token: %w", err)
|
||||||
}
|
}
|
||||||
token, err := machine.ParseToken(tokenResp.Token)
|
token, err := machine.ParseToken(tokenResp.Token)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("parse remote machine token: %w", err)
|
return nil, nil, fmt.Errorf("parse remote machine token: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register the machine in the cluster using its public key and endpoints from the token.
|
// Register the machine in the cluster using its public key and endpoints from the token.
|
||||||
@@ -209,21 +358,30 @@ 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,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
addResp, err := c.AddMachine(ctx, addReq)
|
if opts.PublicIP != nil {
|
||||||
if err != nil {
|
if opts.PublicIP.IsValid() {
|
||||||
return fmt.Errorf("add machine to cluster: %w", err)
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// List other machines in the cluster to include them in the join request.
|
addResp, err := c.AddMachine(ctx, addReq)
|
||||||
machines, err := c.ListMachines(ctx)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("list cluster machines: %w", err)
|
return nil, nil, fmt.Errorf("add machine to cluster (context '%s'): %w", contextName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the most up-to-date list of other machines in the cluster to include them in the join request.
|
||||||
|
machines, err := c.ListMachines(ctx, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("list cluster machines: %w", err)
|
||||||
}
|
}
|
||||||
otherMachines := make([]*pb.MachineInfo, 0, len(machines)-1)
|
otherMachines := make([]*pb.MachineInfo, 0, len(machines)-1)
|
||||||
for _, m := range machines {
|
for _, m := range machines {
|
||||||
@@ -238,31 +396,45 @@ 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, 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, nil, fmt.Errorf("save config: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return c, machineClient, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// provisionRemoteMachine installs the Uncloud daemon and dependencies on the remote machine over SSH and returns
|
// provisionRemoteMachine installs the Uncloud daemon and dependencies on the remote machine over SSH and returns
|
||||||
// a machine API client to interact with the machine. The client should be closed after use by the caller.
|
// a machine API client to interact with the machine. The client should be closed after use by the caller.
|
||||||
func (cli *CLI) provisionRemoteMachine(ctx context.Context, remoteMachine RemoteMachine) (*client.Client, error) {
|
// The version parameter specifies the version of the Uncloud daemon to install. If empty, the latest version is used.
|
||||||
|
// The remoteMachine.SSHKeyPath could be updated to the default SSH key path if it is not set and the SSH agent
|
||||||
|
// authentication fails.
|
||||||
|
func provisionRemoteMachine(
|
||||||
|
ctx context.Context, remoteMachine *RemoteMachine, version string,
|
||||||
|
) (*client.Client, error) {
|
||||||
// Provision the remote machine by installing the Uncloud daemon and dependencies over SSH.
|
// Provision the remote machine by installing the Uncloud daemon and dependencies over SSH.
|
||||||
sshClient, err := sshexec.Connect(remoteMachine.User, remoteMachine.Host, remoteMachine.Port, remoteMachine.KeyPath)
|
sshClient, err := sshexec.Connect(remoteMachine.User, remoteMachine.Host, remoteMachine.Port, remoteMachine.KeyPath)
|
||||||
|
// If the SSH connection using SSH agent fails and no key path is provided, try to use the default SSH key.
|
||||||
|
if err != nil && remoteMachine.KeyPath == "" {
|
||||||
|
remoteMachine.KeyPath = DefaultSSHKeyPath
|
||||||
|
sshClient, err = sshexec.Connect(
|
||||||
|
remoteMachine.User, remoteMachine.Host, remoteMachine.Port, remoteMachine.KeyPath,
|
||||||
|
)
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf(
|
return nil, fmt.Errorf(
|
||||||
"SSH login to remote machine %s: %w",
|
"SSH login to remote machine %s: %w",
|
||||||
@@ -271,7 +443,7 @@ func (cli *CLI) provisionRemoteMachine(ctx context.Context, remoteMachine Remote
|
|||||||
}
|
}
|
||||||
exec := sshexec.NewRemote(sshClient)
|
exec := sshexec.NewRemote(sshClient)
|
||||||
// Install and run the Uncloud daemon and dependencies on the remote machine.
|
// Install and run the Uncloud daemon and dependencies on the remote machine.
|
||||||
if err = provisionMachine(ctx, exec); err != nil {
|
if err = provisionMachine(ctx, exec, version); err != nil {
|
||||||
return nil, fmt.Errorf("provision machine: %w", err)
|
return nil, fmt.Errorf("provision machine: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -296,26 +468,7 @@ func (cli *CLI) provisionRemoteMachine(ctx context.Context, remoteMachine Remote
|
|||||||
return machineClient, nil
|
return machineClient, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cli *CLI) promptResetMachine() error {
|
// ProgressOut returns an output stream for progress writer.
|
||||||
var confirm bool
|
func (cli *CLI) ProgressOut() *streams.Out {
|
||||||
form := huh.NewForm(
|
return streams.NewOut(os.Stdout)
|
||||||
huh.NewGroup(
|
|
||||||
huh.NewConfirm().
|
|
||||||
Title(
|
|
||||||
"The remote machine is already initialised as a cluster member. Do you want to reset it first?",
|
|
||||||
).
|
|
||||||
Affirmative("Yes!").
|
|
||||||
Negative("No").
|
|
||||||
Value(&confirm),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
if err := form.Run(); err != nil {
|
|
||||||
return fmt.Errorf("prompt user to confirm: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !confirm {
|
|
||||||
return fmt.Errorf("remote machine is already initialised as a cluster member")
|
|
||||||
}
|
|
||||||
// TODO: implement resetting the remote machine.
|
|
||||||
return fmt.Errorf("resetting the remote machine is not implemented yet")
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
package client
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"google.golang.org/protobuf/types/known/emptypb"
|
|
||||||
"uncloud/internal/machine/api/pb"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (cli *Client) ListMachines(ctx context.Context) ([]*pb.MachineMember, error) {
|
|
||||||
resp, err := cli.ClusterClient.ListMachines(ctx, &emptypb.Empty{})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return resp.Machines, nil
|
|
||||||
}
|
|
||||||
@@ -1,678 +0,0 @@
|
|||||||
package client
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"github.com/distribution/reference"
|
|
||||||
"github.com/docker/compose/v2/pkg/progress"
|
|
||||||
"github.com/docker/docker/api/types/container"
|
|
||||||
"github.com/docker/docker/api/types/filters"
|
|
||||||
"github.com/docker/docker/api/types/network"
|
|
||||||
dockerclient "github.com/docker/docker/client"
|
|
||||||
"github.com/docker/docker/pkg/jsonmessage"
|
|
||||||
"github.com/docker/go-connections/nat"
|
|
||||||
"google.golang.org/grpc/codes"
|
|
||||||
"google.golang.org/grpc/metadata"
|
|
||||||
"google.golang.org/grpc/status"
|
|
||||||
"slices"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"uncloud/internal/api"
|
|
||||||
"uncloud/internal/machine/api/pb"
|
|
||||||
machinedocker "uncloud/internal/machine/docker"
|
|
||||||
"uncloud/internal/secret"
|
|
||||||
)
|
|
||||||
|
|
||||||
type RunServiceResponse struct {
|
|
||||||
ID string
|
|
||||||
Name string
|
|
||||||
Containers []MachineContainerID
|
|
||||||
}
|
|
||||||
|
|
||||||
type MachineContainerID struct {
|
|
||||||
MachineID string
|
|
||||||
ContainerID string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cli *Client) RunService(ctx context.Context, spec api.ServiceSpec) (RunServiceResponse, error) {
|
|
||||||
var resp RunServiceResponse
|
|
||||||
|
|
||||||
if err := spec.Validate(); err != nil {
|
|
||||||
return resp, fmt.Errorf("invalid service spec: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
img, err := reference.ParseDockerRef(spec.Container.Image)
|
|
||||||
if err != nil {
|
|
||||||
return resp, fmt.Errorf("invalid image: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if spec.Name == "" {
|
|
||||||
// Generate a random service name from the image if not specified.
|
|
||||||
// Get the image name without the repository and tag/digest parts.
|
|
||||||
imageName := reference.FamiliarName(img)
|
|
||||||
// Get the last part of the image name (path), e.g. "nginx" from "bitnami/nginx".
|
|
||||||
if i := strings.LastIndex(imageName, "/"); i != -1 {
|
|
||||||
imageName = imageName[i+1:]
|
|
||||||
}
|
|
||||||
// Append a random suffix to the image name to generate an optimistically unique service name.
|
|
||||||
suffix, err := secret.RandomAlphaNumeric(4)
|
|
||||||
if err != nil {
|
|
||||||
return resp, fmt.Errorf("generate random suffix: %w", err)
|
|
||||||
}
|
|
||||||
spec.Name = fmt.Sprintf("%s-%s", imageName, suffix)
|
|
||||||
} else {
|
|
||||||
// Optimistically check if a service with the specified name already exists.
|
|
||||||
_, err := cli.InspectService(ctx, spec.Name)
|
|
||||||
if err == nil {
|
|
||||||
return resp, fmt.Errorf("service with name '%s' already exists", spec.Name)
|
|
||||||
}
|
|
||||||
if !errors.Is(err, ErrNotFound) {
|
|
||||||
return resp, fmt.Errorf("inspect service: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
serviceID, err := secret.NewID()
|
|
||||||
if err != nil {
|
|
||||||
return resp, fmt.Errorf("generate service ID: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = progress.RunWithTitle(ctx, func(ctx context.Context) error {
|
|
||||||
switch spec.Mode {
|
|
||||||
case "", api.ServiceModeReplicated:
|
|
||||||
resp, err = cli.runReplicatedService(ctx, serviceID, spec)
|
|
||||||
case api.ServiceModeGlobal:
|
|
||||||
resp, err = cli.runGlobalService(ctx, serviceID, spec)
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("invalid mode: %q", spec.Mode)
|
|
||||||
}
|
|
||||||
|
|
||||||
return err
|
|
||||||
}, cli.progressOut(), "Running service "+spec.Name)
|
|
||||||
|
|
||||||
return resp, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cli *Client) runReplicatedService(ctx context.Context, id string, spec api.ServiceSpec) (RunServiceResponse, error) {
|
|
||||||
resp := RunServiceResponse{
|
|
||||||
ID: id,
|
|
||||||
Name: spec.Name,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find a machine to run a service replica on.
|
|
||||||
machines, err := cli.ListMachines(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return resp, fmt.Errorf("list machines: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: support selecting a particular machine by ID or name through the user options.
|
|
||||||
//var machine *pb.MachineMember
|
|
||||||
//if opts.Machine != "" {
|
|
||||||
// // Check if the machine ID or name exists if it's explicitly specified.
|
|
||||||
// for _, m := range machines {
|
|
||||||
// if m.Machine.Name == opts.Machine || m.Machine.Id == opts.Machine {
|
|
||||||
// machine = m
|
|
||||||
// break
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// if machine == nil {
|
|
||||||
// return resp, fmt.Errorf("machine %q not found", opts.Machine)
|
|
||||||
// }
|
|
||||||
//}
|
|
||||||
|
|
||||||
m := firstAvailableMachine(machines)
|
|
||||||
if m == nil {
|
|
||||||
return resp, errors.New("no available machine to run the service")
|
|
||||||
}
|
|
||||||
|
|
||||||
runResp, err := cli.runContainer(ctx, id, spec, m.Machine)
|
|
||||||
if err != nil {
|
|
||||||
return resp, fmt.Errorf("run container: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
resp.Containers = append(resp.Containers, MachineContainerID{
|
|
||||||
MachineID: m.Machine.Id,
|
|
||||||
ContainerID: runResp.ID,
|
|
||||||
})
|
|
||||||
|
|
||||||
return resp, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func firstAvailableMachine(machines []*pb.MachineMember) *pb.MachineMember {
|
|
||||||
// Find the first UP machine.
|
|
||||||
for _, m := range machines {
|
|
||||||
if m.State == pb.MachineMember_UP {
|
|
||||||
return m
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// There is no UP machine, try to find the first SUSPECT machine.
|
|
||||||
for _, m := range machines {
|
|
||||||
if m.State == pb.MachineMember_SUSPECT {
|
|
||||||
return m
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cli *Client) runGlobalService(ctx context.Context, id string, spec api.ServiceSpec) (RunServiceResponse, error) {
|
|
||||||
resp := RunServiceResponse{
|
|
||||||
ID: id,
|
|
||||||
Name: spec.Name,
|
|
||||||
}
|
|
||||||
|
|
||||||
machines, err := cli.ListMachines(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return resp, fmt.Errorf("list machines: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
wg := sync.WaitGroup{}
|
|
||||||
errCh := make(chan error)
|
|
||||||
mu := sync.Mutex{}
|
|
||||||
|
|
||||||
// Run a service container on each available machine.
|
|
||||||
for _, m := range machines {
|
|
||||||
if m.State != pb.MachineMember_UP && m.State != pb.MachineMember_SUSPECT {
|
|
||||||
// TODO: return failed machines in the response.
|
|
||||||
fmt.Printf("WARNING: failed to run a service container on machine '%s' which is Down.\n", m.Machine.Name)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
wg.Add(1)
|
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
|
|
||||||
runResp, err := cli.runContainer(ctx, id, spec, m.Machine)
|
|
||||||
if err != nil {
|
|
||||||
errCh <- fmt.Errorf("run container on machine '%s': %w", m.Machine.Name, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
mu.Lock()
|
|
||||||
resp.Containers = append(resp.Containers, MachineContainerID{
|
|
||||||
MachineID: m.Machine.Id,
|
|
||||||
ContainerID: runResp.ID,
|
|
||||||
})
|
|
||||||
mu.Unlock()
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
wg.Wait()
|
|
||||||
close(errCh)
|
|
||||||
}()
|
|
||||||
|
|
||||||
err = nil
|
|
||||||
for e := range errCh {
|
|
||||||
err = errors.Join(err, e)
|
|
||||||
}
|
|
||||||
|
|
||||||
return resp, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cli *Client) runContainer(
|
|
||||||
ctx context.Context, serviceID string, spec api.ServiceSpec, machine *pb.MachineInfo,
|
|
||||||
) (container.CreateResponse, error) {
|
|
||||||
var resp container.CreateResponse
|
|
||||||
|
|
||||||
// Proxy Docker gRPC requests to the selected machine.
|
|
||||||
machineIP, _ := machine.Network.ManagementIp.ToAddr()
|
|
||||||
md := metadata.Pairs("machines", machineIP.String())
|
|
||||||
ctx = metadata.NewOutgoingContext(ctx, md)
|
|
||||||
|
|
||||||
suffix, err := secret.RandomAlphaNumeric(4)
|
|
||||||
if err != nil {
|
|
||||||
return resp, fmt.Errorf("generate random suffix: %w", err)
|
|
||||||
}
|
|
||||||
containerName := fmt.Sprintf("%s-%s", spec.Name, suffix)
|
|
||||||
|
|
||||||
config := &container.Config{
|
|
||||||
Cmd: spec.Container.Command,
|
|
||||||
Image: spec.Container.Image,
|
|
||||||
Labels: map[string]string{
|
|
||||||
api.LabelServiceID: serviceID,
|
|
||||||
api.LabelServiceName: spec.Name,
|
|
||||||
api.LabelManaged: "",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
if spec.Mode == api.ServiceModeGlobal {
|
|
||||||
config.Labels[api.LabelServiceMode] = api.ServiceModeGlobal
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(spec.Ports) > 0 {
|
|
||||||
encodedPorts := make([]string, len(spec.Ports))
|
|
||||||
for i, p := range spec.Ports {
|
|
||||||
encodedPorts[i], err = p.String()
|
|
||||||
if err != nil {
|
|
||||||
return resp, fmt.Errorf("encode service port spec: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
config.Labels[api.LabelServicePorts] = strings.Join(encodedPorts, ",")
|
|
||||||
}
|
|
||||||
|
|
||||||
portBindings := make(nat.PortMap)
|
|
||||||
for _, p := range spec.Ports {
|
|
||||||
if p.Mode != api.PortModeHost {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
port := nat.Port(fmt.Sprintf("%d/%s", p.ContainerPort, p.Protocol))
|
|
||||||
portBindings[port] = []nat.PortBinding{
|
|
||||||
{
|
|
||||||
HostPort: strconv.Itoa(int(p.PublishedPort)),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
if p.HostIP.IsValid() {
|
|
||||||
portBindings[port][0].HostIP = p.HostIP.String()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
hostConfig := &container.HostConfig{
|
|
||||||
Binds: spec.Container.Volumes,
|
|
||||||
Init: spec.Container.Init,
|
|
||||||
PortBindings: portBindings,
|
|
||||||
}
|
|
||||||
netConfig := &network.NetworkingConfig{
|
|
||||||
EndpointsConfig: map[string]*network.EndpointSettings{
|
|
||||||
machinedocker.NetworkName: {},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
pw := progress.ContextWriter(ctx)
|
|
||||||
eventID := fmt.Sprintf("Container %s on %s", containerName, machine.Name)
|
|
||||||
|
|
||||||
pw.Event(progress.CreatingEvent(eventID))
|
|
||||||
resp, err = cli.CreateContainer(ctx, config, hostConfig, netConfig, nil, containerName)
|
|
||||||
if err != nil {
|
|
||||||
if !dockerclient.IsErrNotFound(err) {
|
|
||||||
return resp, fmt.Errorf("create container: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pull the missing image and create the container again.
|
|
||||||
if err = cli.pullImageWithProgress(ctx, config.Image, machine.Name, eventID); err != nil {
|
|
||||||
return resp, err
|
|
||||||
}
|
|
||||||
if resp, err = cli.CreateContainer(ctx, config, hostConfig, netConfig, nil, containerName); err != nil {
|
|
||||||
return resp, fmt.Errorf("create container: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pw.Event(progress.CreatedEvent(eventID))
|
|
||||||
|
|
||||||
pw.Event(progress.StartingEvent(eventID))
|
|
||||||
if err = cli.StartContainer(ctx, resp.ID, container.StartOptions{}); err != nil {
|
|
||||||
return resp, fmt.Errorf("start container: %w", err)
|
|
||||||
}
|
|
||||||
pw.Event(progress.StartedEvent(eventID))
|
|
||||||
|
|
||||||
return resp, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cli *Client) pullImageWithProgress(ctx context.Context, image, machineName, parentEventID string) error {
|
|
||||||
pw := progress.ContextWriter(ctx)
|
|
||||||
eventID := fmt.Sprintf("Image %s on %s", image, machineName)
|
|
||||||
pw.Event(progress.Event{
|
|
||||||
ID: eventID,
|
|
||||||
ParentID: parentEventID,
|
|
||||||
Status: progress.Working,
|
|
||||||
StatusText: "Pulling",
|
|
||||||
})
|
|
||||||
|
|
||||||
pullCh, err := cli.PullImage(ctx, image)
|
|
||||||
if err != nil {
|
|
||||||
pw.Event(progress.Event{
|
|
||||||
ID: eventID,
|
|
||||||
ParentID: parentEventID,
|
|
||||||
Text: "Error",
|
|
||||||
Status: progress.Error,
|
|
||||||
StatusText: errors.Unwrap(err).Error(),
|
|
||||||
})
|
|
||||||
return fmt.Errorf("pull image: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait for pull to complete by reading all progress messages and converting them to events.
|
|
||||||
for msg := range pullCh {
|
|
||||||
if msg.Err != nil {
|
|
||||||
err = msg.Err
|
|
||||||
} else {
|
|
||||||
if msg.Message.Error != nil {
|
|
||||||
err = errors.New(msg.Message.Error.Message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
pw.Event(progress.Event{
|
|
||||||
ID: eventID,
|
|
||||||
ParentID: parentEventID,
|
|
||||||
Text: "Error",
|
|
||||||
Status: progress.Error,
|
|
||||||
StatusText: errors.Unwrap(err).Error(),
|
|
||||||
})
|
|
||||||
return fmt.Errorf("pull image: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: add like in compose: --quiet-pull Pull without printing progress information
|
|
||||||
e := toPullProgressEvent(msg.Message)
|
|
||||||
if e != nil {
|
|
||||||
e.ID = fmt.Sprintf("%s on %s", e.ID, machineName)
|
|
||||||
e.ParentID = eventID
|
|
||||||
// Grand children events are not printed by the tty progress writer but they are still required
|
|
||||||
// to calculate the progress line of their parent.
|
|
||||||
pw.Event(*e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pw.Event(progress.Event{
|
|
||||||
ID: eventID,
|
|
||||||
ParentID: parentEventID,
|
|
||||||
Status: progress.Done,
|
|
||||||
StatusText: "Pulled",
|
|
||||||
})
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// toPullProgressEvent converts a JSON progress message from the Docker API to a progress event.
|
|
||||||
// It's based on toPullProgressEvent from Docker Compose.
|
|
||||||
func toPullProgressEvent(jm jsonmessage.JSONMessage) *progress.Event {
|
|
||||||
if jm.ID == "" || jm.Progress == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
total int64
|
|
||||||
percent int
|
|
||||||
current int64
|
|
||||||
)
|
|
||||||
text := jm.Progress.String()
|
|
||||||
stat := progress.Working
|
|
||||||
|
|
||||||
switch jm.Status {
|
|
||||||
case "Preparing", "Waiting", "Pulling fs layer":
|
|
||||||
percent = 0
|
|
||||||
case "Downloading", "Extracting", "Verifying Checksum":
|
|
||||||
current = jm.Progress.Current
|
|
||||||
total = jm.Progress.Total
|
|
||||||
if jm.Progress.Total > 0 {
|
|
||||||
percent = int(jm.Progress.Current * 100 / jm.Progress.Total)
|
|
||||||
}
|
|
||||||
case "Download complete", "Already exists", "Pull complete":
|
|
||||||
stat = progress.Done
|
|
||||||
percent = 100
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.Contains(jm.Status, "Image is up to date") ||
|
|
||||||
strings.Contains(jm.Status, "Downloaded newer image") {
|
|
||||||
stat = progress.Done
|
|
||||||
percent = 100
|
|
||||||
}
|
|
||||||
|
|
||||||
return &progress.Event{
|
|
||||||
ID: jm.ID,
|
|
||||||
Current: current,
|
|
||||||
Total: total,
|
|
||||||
Percent: percent,
|
|
||||||
Text: jm.Status,
|
|
||||||
Status: stat,
|
|
||||||
StatusText: text,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// InspectService returns detailed information about a service and its containers.
|
|
||||||
// The id parameter can be either a service ID or name.
|
|
||||||
func (cli *Client) InspectService(ctx context.Context, id string) (api.Service, error) {
|
|
||||||
var svc api.Service
|
|
||||||
|
|
||||||
machines, err := cli.ListMachines(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return svc, fmt.Errorf("list machines: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Broadcast the container list request to all available machines.
|
|
||||||
machineIDByManagementIP := make(map[string]string)
|
|
||||||
md := metadata.New(nil)
|
|
||||||
for _, m := range machines {
|
|
||||||
if m.State == pb.MachineMember_UP || m.State == pb.MachineMember_SUSPECT {
|
|
||||||
machineIP, _ := m.Machine.Network.ManagementIp.ToAddr()
|
|
||||||
md.Append("machines", machineIP.String())
|
|
||||||
|
|
||||||
machineIDByManagementIP[machineIP.String()] = m.Machine.Id
|
|
||||||
}
|
|
||||||
// TODO: warning about machines that are DOWN.
|
|
||||||
}
|
|
||||||
listCtx := metadata.NewOutgoingContext(ctx, md)
|
|
||||||
|
|
||||||
// List only uncloud-managed containers that belong to some service.
|
|
||||||
opts := container.ListOptions{
|
|
||||||
All: true,
|
|
||||||
Filters: filters.NewArgs(
|
|
||||||
filters.Arg("label", api.LabelServiceID),
|
|
||||||
filters.Arg("label", api.LabelManaged),
|
|
||||||
),
|
|
||||||
}
|
|
||||||
machineContainers, err := cli.ListContainers(listCtx, opts)
|
|
||||||
if err != nil {
|
|
||||||
return svc, fmt.Errorf("list containers: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Collect all containers on all machines that belong to the specified service.
|
|
||||||
foundByID := false
|
|
||||||
var containers []api.MachineContainer
|
|
||||||
for _, mc := range machineContainers {
|
|
||||||
// Metadata can be nil if the request was broadcasted to only one machine.
|
|
||||||
if mc.Metadata == nil && len(machineContainers) > 1 {
|
|
||||||
return svc, errors.New("something went wrong with gRPC proxy: metadata is missing for a machine response")
|
|
||||||
}
|
|
||||||
if mc.Metadata != nil && mc.Metadata.Error != "" {
|
|
||||||
// TODO: return failed machines in the response.
|
|
||||||
fmt.Printf("WARNING: failed to list containers on machine '%s': %s\n",
|
|
||||||
mc.Metadata.Machine, mc.Metadata.Error)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
machineID := ""
|
|
||||||
if mc.Metadata == nil {
|
|
||||||
// ListContainers was proxied to only one machine.
|
|
||||||
for _, v := range machineIDByManagementIP {
|
|
||||||
machineID = v
|
|
||||||
break
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
var ok bool
|
|
||||||
machineID, ok = machineIDByManagementIP[mc.Metadata.Machine]
|
|
||||||
if !ok {
|
|
||||||
return svc, fmt.Errorf("machine name not found for management IP: %s", mc.Metadata.Machine)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, c := range mc.Containers {
|
|
||||||
ctr := api.Container{Container: c}
|
|
||||||
if ctr.ServiceID() == id || ctr.ServiceName() == id {
|
|
||||||
containers = append(containers, api.MachineContainer{
|
|
||||||
MachineID: machineID,
|
|
||||||
Container: ctr,
|
|
||||||
})
|
|
||||||
|
|
||||||
if ctr.ServiceID() == id {
|
|
||||||
foundByID = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(containers) == 0 {
|
|
||||||
return svc, ErrNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
// Containers from different services may share the same service name (distributed and eventually consistent store
|
|
||||||
// may not prevent this), or a service name might match another service's ID. In these cases, matching by ID takes
|
|
||||||
// priority over matching by name.
|
|
||||||
if foundByID {
|
|
||||||
containers = slices.DeleteFunc(containers, func(mc api.MachineContainer) bool {
|
|
||||||
return mc.Container.ServiceID() != id
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
// Matched only by name but there could be multiple services with the same name.
|
|
||||||
serviceID := containers[0].Container.ServiceID()
|
|
||||||
for _, mc := range containers[1:] {
|
|
||||||
if mc.Container.ServiceID() != serviceID {
|
|
||||||
return svc, fmt.Errorf("multiple services found with name: %s", id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
svc = api.Service{
|
|
||||||
ID: containers[0].Container.ServiceID(),
|
|
||||||
Name: containers[0].Container.ServiceName(),
|
|
||||||
Mode: containers[0].Container.ServiceMode(),
|
|
||||||
Containers: containers,
|
|
||||||
}
|
|
||||||
if svc.Mode == "" {
|
|
||||||
svc.Mode = api.ServiceModeReplicated
|
|
||||||
}
|
|
||||||
|
|
||||||
return svc, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// InspectServiceFromStore returns detailed information about a service and its containers from the distributed store.
|
|
||||||
// Due to eventual consistency of the store, the returned information may not reflect the most recent changes.
|
|
||||||
// The id parameter can be either a service ID or name.
|
|
||||||
func (cli *Client) InspectServiceFromStore(ctx context.Context, id string) (api.Service, error) {
|
|
||||||
var svc api.Service
|
|
||||||
|
|
||||||
resp, err := cli.MachineClient.InspectService(ctx, &pb.InspectServiceRequest{Id: id})
|
|
||||||
if err != nil {
|
|
||||||
if s, ok := status.FromError(err); ok {
|
|
||||||
if s.Code() == codes.NotFound {
|
|
||||||
return svc, ErrNotFound
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return svc, err
|
|
||||||
}
|
|
||||||
|
|
||||||
svc, err = api.ServiceFromProto(resp.Service)
|
|
||||||
if err != nil {
|
|
||||||
return svc, fmt.Errorf("from proto: %w", err)
|
|
||||||
}
|
|
||||||
return svc, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// RemoveService removes all containers on all machines that belong to the specified service.
|
|
||||||
// The id parameter can be either a service ID or name.
|
|
||||||
func (cli *Client) RemoveService(ctx context.Context, id string) error {
|
|
||||||
svc, err := cli.InspectService(ctx, id)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
machines, err := cli.ListMachines(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("list machines: %w", err)
|
|
||||||
}
|
|
||||||
machineManagementIPByID := make(map[string]string)
|
|
||||||
for _, m := range machines {
|
|
||||||
machineIP, _ := m.Machine.Network.ManagementIp.ToAddr()
|
|
||||||
machineManagementIPByID[m.Machine.Id] = machineIP.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
wg := sync.WaitGroup{}
|
|
||||||
errCh := make(chan error)
|
|
||||||
|
|
||||||
// Remove all containers on all machines that belong to the service.
|
|
||||||
for _, mc := range svc.Containers {
|
|
||||||
wg.Add(1)
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
|
|
||||||
machineIP, ok := machineManagementIPByID[mc.MachineID]
|
|
||||||
if !ok {
|
|
||||||
errCh <- fmt.Errorf("machine not found by ID: %s", mc.MachineID)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
removeCtx := metadata.NewOutgoingContext(ctx, metadata.Pairs("machines", machineIP))
|
|
||||||
// TODO: gracefully stop the container before removing it without force.
|
|
||||||
err := cli.RemoveContainer(removeCtx, mc.Container.ID, container.RemoveOptions{Force: true})
|
|
||||||
if err != nil {
|
|
||||||
if !dockerclient.IsErrNotFound(err) {
|
|
||||||
errCh <- fmt.Errorf("remove container '%s': %w", mc.Container.ID, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
wg.Wait()
|
|
||||||
close(errCh)
|
|
||||||
}()
|
|
||||||
|
|
||||||
err = nil
|
|
||||||
for e := range errCh {
|
|
||||||
err = errors.Join(err, e)
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListServices returns a list of all services and their containers.
|
|
||||||
func (cli *Client) ListServices(ctx context.Context) ([]api.Service, error) {
|
|
||||||
machines, err := cli.ListMachines(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("list machines: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Broadcast the container list request to all available machines.
|
|
||||||
md := metadata.New(nil)
|
|
||||||
for _, m := range machines {
|
|
||||||
if m.State == pb.MachineMember_UP || m.State == pb.MachineMember_SUSPECT {
|
|
||||||
machineIP, _ := m.Machine.Network.ManagementIp.ToAddr()
|
|
||||||
md.Append("machines", machineIP.String())
|
|
||||||
}
|
|
||||||
// TODO: warning about machines that are DOWN.
|
|
||||||
}
|
|
||||||
listCtx := metadata.NewOutgoingContext(ctx, md)
|
|
||||||
|
|
||||||
// List only uncloud-managed containers that belong to some service.
|
|
||||||
opts := container.ListOptions{
|
|
||||||
All: true,
|
|
||||||
Filters: filters.NewArgs(
|
|
||||||
filters.Arg("label", api.LabelServiceID),
|
|
||||||
filters.Arg("label", api.LabelManaged),
|
|
||||||
),
|
|
||||||
}
|
|
||||||
machineContainers, err := cli.ListContainers(listCtx, opts)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("list containers: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: optimise by extracting services from the list of all containers instead of inspecting each service.
|
|
||||||
// Most of the code can be reused in both InspectService and ListServices.
|
|
||||||
servicesByID := make(map[string]api.Service)
|
|
||||||
for _, mc := range machineContainers {
|
|
||||||
if mc.Metadata != nil && mc.Metadata.Error != "" {
|
|
||||||
// TODO: return failed machines in the response.
|
|
||||||
fmt.Printf("WARNING: failed to list containers on machine '%s': %s\n",
|
|
||||||
mc.Metadata.Machine, mc.Metadata.Error)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, c := range mc.Containers {
|
|
||||||
ctr := api.Container{Container: c}
|
|
||||||
if _, ok := servicesByID[ctr.ServiceID()]; ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
svc, err := cli.InspectService(ctx, ctr.ServiceID())
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, ErrNotFound) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
return nil, fmt.Errorf("inspect service: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
servicesByID[ctr.ServiceID()] = svc
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
services := make([]api.Service, 0, len(servicesByID))
|
|
||||||
for _, svc := range servicesByID {
|
|
||||||
services = append(services, svc)
|
|
||||||
}
|
|
||||||
return services, nil
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
package config
|
|
||||||
|
|
||||||
type Cluster struct {
|
|
||||||
Name string `toml:"-"`
|
|
||||||
Connections []MachineConnection `toml:"connections"`
|
|
||||||
}
|
|
||||||
@@ -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,41 @@ func NewFromFile(path string) (*Config, error) {
|
|||||||
return c, nil
|
return c, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Config) Read() error {
|
func (c *Config) Path() string {
|
||||||
_, err := toml.DecodeFile(c.path, c)
|
return c.path
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("read config file %q: %w", c.path, err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Config) Read() error {
|
||||||
|
data, err := os.ReadFile(c.path)
|
||||||
|
if err != nil {
|
||||||
|
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 dir is empty (e.g., when path is just a filename), use current directory
|
||||||
return fmt.Errorf("create config directory %q: %w", dir, err)
|
if dir == "" {
|
||||||
|
dir = "."
|
||||||
|
}
|
||||||
|
if err := os.MkdirAll(dir, 0o700); err != nil {
|
||||||
|
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, 0o600)
|
||||||
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()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestConfig_Save(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
// Create a temporary directory for the test
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
|
||||||
|
// Change to temp directory so relative paths resolve correctly
|
||||||
|
originalDir, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to get current directory: %v", err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if err := os.Chdir(originalDir); err != nil {
|
||||||
|
t.Logf("Failed to restore original directory: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
configPath string
|
||||||
|
contextName string
|
||||||
|
expectFileAt string // Expected file location for verification
|
||||||
|
useAbsolutePath bool // Whether to use absolute path for expectFileAt
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "relative path without prefix",
|
||||||
|
configPath: "test-config.yaml",
|
||||||
|
contextName: "test",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "relative path with prefix",
|
||||||
|
configPath: "./test-config-2.yaml",
|
||||||
|
contextName: "test2",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "absolute path",
|
||||||
|
configPath: filepath.Join(tmpDir, "absolute-config.yaml"),
|
||||||
|
contextName: "test3",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
if err := os.Chdir(tmpDir); err != nil {
|
||||||
|
t.Fatalf("Failed to change to temp directory: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := &Config{
|
||||||
|
CurrentContext: tt.contextName,
|
||||||
|
Contexts: map[string]*Context{
|
||||||
|
tt.contextName: {
|
||||||
|
Name: tt.contextName,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
path: tt.configPath,
|
||||||
|
}
|
||||||
|
|
||||||
|
// This should not fail when saving the config
|
||||||
|
err := cfg.Save()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Expected no error when saving config, got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify the file was created
|
||||||
|
if _, err := os.Stat(tt.configPath); os.IsNotExist(err) {
|
||||||
|
t.Errorf("Config file was not created at expected path: %s", tt.configPath)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,7 +5,8 @@ import (
|
|||||||
"net/netip"
|
"net/netip"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"uncloud/internal/secret"
|
|
||||||
|
"github.com/psviderski/uncloud/internal/secret"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -14,10 +15,13 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type MachineConnection struct {
|
type MachineConnection struct {
|
||||||
SSH SSHDestination `toml:"ssh,omitempty"`
|
SSH SSHDestination `yaml:"ssh,omitempty"`
|
||||||
TCP netip.AddrPort `toml:"tcp,omitempty"`
|
SSHKeyFile string `yaml:"ssh_key_file,omitempty"`
|
||||||
Host string `toml:"host,omitempty"`
|
// TCP is the address and port of the machine's API server.
|
||||||
PublicKey secret.Secret `toml:"public_key,omitempty"`
|
// 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
|
||||||
|
}
|
||||||
+109
-12
@@ -4,11 +4,21 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"uncloud/internal/sshexec"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/cenkalti/backoff/v4"
|
||||||
|
"github.com/charmbracelet/huh"
|
||||||
|
"github.com/psviderski/uncloud/internal/machine/api/pb"
|
||||||
|
"github.com/psviderski/uncloud/internal/sshexec"
|
||||||
|
"google.golang.org/protobuf/types/known/emptypb"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
// TODO: support pinning the script version to the CLI version.
|
// TODO: support pinning the script version to the CLI version.
|
||||||
const installScriptURL = "https://raw.githubusercontent.com/psviderski/uncloud/refs/heads/main/scripts/install.sh"
|
installScriptURL = "https://raw.githubusercontent.com/psviderski/uncloud/refs/heads/main/scripts/install.sh"
|
||||||
|
rootUser = "root"
|
||||||
|
)
|
||||||
|
|
||||||
type RemoteMachine struct {
|
type RemoteMachine struct {
|
||||||
User string
|
User string
|
||||||
@@ -17,26 +27,113 @@ 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 != rootUser {
|
||||||
|
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" {
|
if user != rootUser {
|
||||||
sudoPrefix = "sudo"
|
// 'sudo -n' is not used because it fails with 'sudo: a password is required' when the user has no password
|
||||||
// Add the SSH user (non-root) to the uncloud group to allow access to the Uncloud daemon unix socket.
|
// in /etc/shadow even though it may have valid sudo access.
|
||||||
env = "UNCLOUD_GROUP_ADD_USER=" + user
|
out, err := exec.Run(ctx, "sudo true")
|
||||||
|
if err != nil {
|
||||||
|
if strings.Contains(out, "password is required") {
|
||||||
|
return fmt.Errorf(
|
||||||
|
"user '%[1]s' requires a password for sudo, but Uncloud needs passwordless sudo or root access "+
|
||||||
|
"to install and configure the uncloudd daemon on the remote machine.\n\n"+
|
||||||
|
"Possible solutions:\n"+
|
||||||
|
"1. Use root user or a user with passwordless sudo instead.\n"+
|
||||||
|
"2. Configure passwordless sudo for the user '%[1]s' by running on the remote machine:\n"+
|
||||||
|
" echo '%[1]s ALL=(ALL) NOPASSWD:ALL' | sudo tee /etc/sudoers.d/%[1]s",
|
||||||
|
user)
|
||||||
|
}
|
||||||
|
return fmt.Errorf("sudo command failed for user '%s': %w. "+
|
||||||
|
"Please ensure the user has sudo privileges or use root user instead", user, err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cmd := installCmd(user, version)
|
||||||
|
|
||||||
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)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func promptResetMachine(ctx context.Context, machineClient pb.MachineClient) error {
|
||||||
|
var confirm bool
|
||||||
|
form := huh.NewForm(
|
||||||
|
huh.NewGroup(
|
||||||
|
huh.NewConfirm().
|
||||||
|
Title(
|
||||||
|
"The remote machine is already initialised as a cluster member. Do you want to reset it first?\n" +
|
||||||
|
"This will:\n" +
|
||||||
|
"- Remove all service containers from the machine\n" +
|
||||||
|
"- Reset the machine to the uninitialised state",
|
||||||
|
).
|
||||||
|
Affirmative("Yes!").
|
||||||
|
Negative("No").
|
||||||
|
Value(&confirm),
|
||||||
|
),
|
||||||
|
).WithAccessible(true)
|
||||||
|
if err := form.Run(); err != nil {
|
||||||
|
return fmt.Errorf("prompt user to confirm: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !confirm {
|
||||||
|
return fmt.Errorf("remote machine is already initialised as a cluster member")
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := machineClient.Reset(ctx, &pb.ResetRequest{}); err != nil {
|
||||||
|
return fmt.Errorf("reset remote machine: %w. You can also manually run 'uncloud-uninstall' "+
|
||||||
|
"on the remote machine to fully uninstall Uncloud from it", err)
|
||||||
|
}
|
||||||
|
fmt.Println("Resetting the remote machine...")
|
||||||
|
if err := waitMachineReady(ctx, machineClient, 1*time.Minute); err != nil {
|
||||||
|
return fmt.Errorf("wait for machine to be ready after reset: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// waitMachineReady waits for the machine to be ready to serve requests.
|
||||||
|
func waitMachineReady(ctx context.Context, machineClient pb.MachineClient, timeout time.Duration) error {
|
||||||
|
boff := backoff.WithContext(backoff.NewExponentialBackOff(
|
||||||
|
backoff.WithMaxInterval(1*time.Second),
|
||||||
|
backoff.WithMaxElapsedTime(timeout),
|
||||||
|
), ctx)
|
||||||
|
|
||||||
|
inspect := func() error {
|
||||||
|
_, err := machineClient.Inspect(ctx, &emptypb.Empty{})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("inspect machine: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return backoff.Retry(inspect, boff)
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -5,14 +5,15 @@ import (
|
|||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/cenkalti/backoff/v4"
|
|
||||||
"golang.org/x/net/http2"
|
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"net/url"
|
"net/url"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/cenkalti/backoff/v4"
|
||||||
|
"golang.org/x/net/http2"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|||||||
@@ -6,11 +6,12 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/cenkalti/backoff/v4"
|
|
||||||
"io"
|
"io"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/cenkalti/backoff/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ChangeType string
|
type ChangeType string
|
||||||
|
|||||||
@@ -3,9 +3,10 @@ package daemon
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
systemd "github.com/coreos/go-systemd/daemon"
|
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"uncloud/internal/machine"
|
|
||||||
|
systemd "github.com/coreos/go-systemd/daemon"
|
||||||
|
"github.com/psviderski/uncloud/internal/machine"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Daemon struct {
|
type Daemon struct {
|
||||||
|
|||||||
@@ -5,8 +5,9 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"os"
|
"os"
|
||||||
"uncloud/internal/machine"
|
|
||||||
"uncloud/internal/machine/network"
|
"github.com/psviderski/uncloud/internal/machine"
|
||||||
|
"github.com/psviderski/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 +38,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
|
||||||
|
}
|
||||||
@@ -4,10 +4,11 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/cenkalti/backoff/v4"
|
|
||||||
"github.com/docker/docker/client"
|
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/cenkalti/backoff/v4"
|
||||||
|
"github.com/docker/docker/client"
|
||||||
)
|
)
|
||||||
|
|
||||||
// WaitDaemonReady waits for the Docker daemon to start and be ready to serve requests.
|
// WaitDaemonReady waits for the Docker daemon to start and be ready to serve requests.
|
||||||
|
|||||||
@@ -5,8 +5,23 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"os/user"
|
"os/user"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func ExpandHomeDir(path string) string {
|
||||||
|
if len(path) == 0 {
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
if path[0] == '~' {
|
||||||
|
home, err := os.UserHomeDir()
|
||||||
|
if err != nil {
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
return strings.Replace(path, "~", home, 1)
|
||||||
|
}
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
|
||||||
// LookupUIDGID returns the user and group IDs for the given username.
|
// LookupUIDGID returns the user and group IDs for the given username.
|
||||||
func LookupUIDGID(username string) (uid, gid int, err error) {
|
func LookupUIDGID(username string) (uid, gid int, err error) {
|
||||||
usr, err := user.Lookup(username)
|
usr, err := user.Lookup(username)
|
||||||
|
|||||||
@@ -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"))
|
||||||
|
})
|
||||||
|
}
|
||||||
+10
-2
@@ -45,11 +45,19 @@ func (h *SlogTextHandler) Enabled(ctx context.Context, level slog.Level) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *SlogTextHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
|
func (h *SlogTextHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
|
||||||
return h.TextHandler.WithAttrs(attrs)
|
return &SlogTextHandler{
|
||||||
|
TextHandler: h.TextHandler.WithAttrs(attrs).(*slog.TextHandler),
|
||||||
|
opts: h.opts,
|
||||||
|
w: h.w,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *SlogTextHandler) WithGroup(name string) slog.Handler {
|
func (h *SlogTextHandler) WithGroup(name string) slog.Handler {
|
||||||
return h.TextHandler.WithGroup(name)
|
return &SlogTextHandler{
|
||||||
|
TextHandler: h.TextHandler.WithGroup(name).(*slog.TextHandler),
|
||||||
|
opts: h.opts,
|
||||||
|
w: h.w,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *SlogTextHandler) Handle(ctx context.Context, r slog.Record) error {
|
func (h *SlogTextHandler) Handle(ctx context.Context, r slog.Record) error {
|
||||||
|
|||||||
@@ -0,0 +1,176 @@
|
|||||||
|
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// protoc-gen-go v1.34.2
|
||||||
|
// protoc v5.27.3
|
||||||
|
// source: internal/machine/api/pb/caddy.proto
|
||||||
|
|
||||||
|
package pb
|
||||||
|
|
||||||
|
import (
|
||||||
|
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||||
|
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||||
|
emptypb "google.golang.org/protobuf/types/known/emptypb"
|
||||||
|
timestamppb "google.golang.org/protobuf/types/known/timestamppb"
|
||||||
|
reflect "reflect"
|
||||||
|
sync "sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Verify that this generated code is sufficiently up-to-date.
|
||||||
|
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
|
||||||
|
// Verify that runtime/protoimpl is sufficiently up-to-date.
|
||||||
|
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
||||||
|
)
|
||||||
|
|
||||||
|
type GetCaddyConfigResponse struct {
|
||||||
|
state protoimpl.MessageState
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
|
||||||
|
// The generated Caddyfile content.
|
||||||
|
Caddyfile string `protobuf:"bytes,1,opt,name=caddyfile,proto3" json:"caddyfile,omitempty"`
|
||||||
|
// Timestamp when the config was last modified.
|
||||||
|
ModifiedAt *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=modified_at,json=modifiedAt,proto3" json:"modified_at,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *GetCaddyConfigResponse) Reset() {
|
||||||
|
*x = GetCaddyConfigResponse{}
|
||||||
|
if protoimpl.UnsafeEnabled {
|
||||||
|
mi := &file_internal_machine_api_pb_caddy_proto_msgTypes[0]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *GetCaddyConfigResponse) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*GetCaddyConfigResponse) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *GetCaddyConfigResponse) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_internal_machine_api_pb_caddy_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 GetCaddyConfigResponse.ProtoReflect.Descriptor instead.
|
||||||
|
func (*GetCaddyConfigResponse) Descriptor() ([]byte, []int) {
|
||||||
|
return file_internal_machine_api_pb_caddy_proto_rawDescGZIP(), []int{0}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *GetCaddyConfigResponse) GetCaddyfile() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Caddyfile
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *GetCaddyConfigResponse) GetModifiedAt() *timestamppb.Timestamp {
|
||||||
|
if x != nil {
|
||||||
|
return x.ModifiedAt
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var File_internal_machine_api_pb_caddy_proto protoreflect.FileDescriptor
|
||||||
|
|
||||||
|
var file_internal_machine_api_pb_caddy_proto_rawDesc = []byte{
|
||||||
|
0x0a, 0x23, 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, 0x61, 0x64, 0x64, 0x79, 0x2e,
|
||||||
|
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x03, 0x61, 0x70, 0x69, 0x1a, 0x1b, 0x67, 0x6f, 0x6f, 0x67,
|
||||||
|
0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x65, 0x6d, 0x70, 0x74,
|
||||||
|
0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f,
|
||||||
|
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61,
|
||||||
|
0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x73, 0x0a, 0x16, 0x47, 0x65, 0x74, 0x43,
|
||||||
|
0x61, 0x64, 0x64, 0x79, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
|
||||||
|
0x73, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x63, 0x61, 0x64, 0x64, 0x79, 0x66, 0x69, 0x6c, 0x65, 0x18,
|
||||||
|
0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x63, 0x61, 0x64, 0x64, 0x79, 0x66, 0x69, 0x6c, 0x65,
|
||||||
|
0x12, 0x3b, 0x0a, 0x0b, 0x6d, 0x6f, 0x64, 0x69, 0x66, 0x69, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18,
|
||||||
|
0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70,
|
||||||
|
0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d,
|
||||||
|
0x70, 0x52, 0x0a, 0x6d, 0x6f, 0x64, 0x69, 0x66, 0x69, 0x65, 0x64, 0x41, 0x74, 0x32, 0x49, 0x0a,
|
||||||
|
0x05, 0x43, 0x61, 0x64, 0x64, 0x79, 0x12, 0x40, 0x0a, 0x09, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e,
|
||||||
|
0x66, 0x69, 0x67, 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, 0x1b, 0x2e, 0x61, 0x70,
|
||||||
|
0x69, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x61, 0x64, 0x64, 0x79, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67,
|
||||||
|
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 (
|
||||||
|
file_internal_machine_api_pb_caddy_proto_rawDescOnce sync.Once
|
||||||
|
file_internal_machine_api_pb_caddy_proto_rawDescData = file_internal_machine_api_pb_caddy_proto_rawDesc
|
||||||
|
)
|
||||||
|
|
||||||
|
func file_internal_machine_api_pb_caddy_proto_rawDescGZIP() []byte {
|
||||||
|
file_internal_machine_api_pb_caddy_proto_rawDescOnce.Do(func() {
|
||||||
|
file_internal_machine_api_pb_caddy_proto_rawDescData = protoimpl.X.CompressGZIP(file_internal_machine_api_pb_caddy_proto_rawDescData)
|
||||||
|
})
|
||||||
|
return file_internal_machine_api_pb_caddy_proto_rawDescData
|
||||||
|
}
|
||||||
|
|
||||||
|
var file_internal_machine_api_pb_caddy_proto_msgTypes = make([]protoimpl.MessageInfo, 1)
|
||||||
|
var file_internal_machine_api_pb_caddy_proto_goTypes = []any{
|
||||||
|
(*GetCaddyConfigResponse)(nil), // 0: api.GetCaddyConfigResponse
|
||||||
|
(*timestamppb.Timestamp)(nil), // 1: google.protobuf.Timestamp
|
||||||
|
(*emptypb.Empty)(nil), // 2: google.protobuf.Empty
|
||||||
|
}
|
||||||
|
var file_internal_machine_api_pb_caddy_proto_depIdxs = []int32{
|
||||||
|
1, // 0: api.GetCaddyConfigResponse.modified_at:type_name -> google.protobuf.Timestamp
|
||||||
|
2, // 1: api.Caddy.GetConfig:input_type -> google.protobuf.Empty
|
||||||
|
0, // 2: api.Caddy.GetConfig:output_type -> api.GetCaddyConfigResponse
|
||||||
|
2, // [2:3] is the sub-list for method output_type
|
||||||
|
1, // [1:2] is the sub-list for method input_type
|
||||||
|
1, // [1:1] is the sub-list for extension type_name
|
||||||
|
1, // [1:1] is the sub-list for extension extendee
|
||||||
|
0, // [0:1] is the sub-list for field type_name
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() { file_internal_machine_api_pb_caddy_proto_init() }
|
||||||
|
func file_internal_machine_api_pb_caddy_proto_init() {
|
||||||
|
if File_internal_machine_api_pb_caddy_proto != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !protoimpl.UnsafeEnabled {
|
||||||
|
file_internal_machine_api_pb_caddy_proto_msgTypes[0].Exporter = func(v any, i int) any {
|
||||||
|
switch v := v.(*GetCaddyConfigResponse); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
type x struct{}
|
||||||
|
out := protoimpl.TypeBuilder{
|
||||||
|
File: protoimpl.DescBuilder{
|
||||||
|
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||||
|
RawDescriptor: file_internal_machine_api_pb_caddy_proto_rawDesc,
|
||||||
|
NumEnums: 0,
|
||||||
|
NumMessages: 1,
|
||||||
|
NumExtensions: 0,
|
||||||
|
NumServices: 1,
|
||||||
|
},
|
||||||
|
GoTypes: file_internal_machine_api_pb_caddy_proto_goTypes,
|
||||||
|
DependencyIndexes: file_internal_machine_api_pb_caddy_proto_depIdxs,
|
||||||
|
MessageInfos: file_internal_machine_api_pb_caddy_proto_msgTypes,
|
||||||
|
}.Build()
|
||||||
|
File_internal_machine_api_pb_caddy_proto = out.File
|
||||||
|
file_internal_machine_api_pb_caddy_proto_rawDesc = nil
|
||||||
|
file_internal_machine_api_pb_caddy_proto_goTypes = nil
|
||||||
|
file_internal_machine_api_pb_caddy_proto_depIdxs = nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
package api;
|
||||||
|
|
||||||
|
option go_package = "github.com/psviderski/uncloud/internal/machine/api/pb";
|
||||||
|
|
||||||
|
import "google/protobuf/empty.proto";
|
||||||
|
import "google/protobuf/timestamp.proto";
|
||||||
|
|
||||||
|
service Caddy {
|
||||||
|
// GetConfig retrieves the current Caddy configuration from the machine.
|
||||||
|
rpc GetConfig(google.protobuf.Empty) returns (GetCaddyConfigResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetCaddyConfigResponse {
|
||||||
|
// The generated Caddyfile content.
|
||||||
|
string caddyfile = 1;
|
||||||
|
// Timestamp when the config was last modified.
|
||||||
|
google.protobuf.Timestamp modified_at = 2;
|
||||||
|
}
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
// 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/caddy.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 (
|
||||||
|
Caddy_GetConfig_FullMethodName = "/api.Caddy/GetConfig"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CaddyClient is the client API for Caddy 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 CaddyClient interface {
|
||||||
|
// GetConfig retrieves the current Caddy configuration from the machine.
|
||||||
|
GetConfig(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*GetCaddyConfigResponse, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type caddyClient struct {
|
||||||
|
cc grpc.ClientConnInterface
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCaddyClient(cc grpc.ClientConnInterface) CaddyClient {
|
||||||
|
return &caddyClient{cc}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *caddyClient) GetConfig(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*GetCaddyConfigResponse, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(GetCaddyConfigResponse)
|
||||||
|
err := c.cc.Invoke(ctx, Caddy_GetConfig_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CaddyServer is the server API for Caddy service.
|
||||||
|
// All implementations must embed UnimplementedCaddyServer
|
||||||
|
// for forward compatibility.
|
||||||
|
type CaddyServer interface {
|
||||||
|
// GetConfig retrieves the current Caddy configuration from the machine.
|
||||||
|
GetConfig(context.Context, *emptypb.Empty) (*GetCaddyConfigResponse, error)
|
||||||
|
mustEmbedUnimplementedCaddyServer()
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnimplementedCaddyServer 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 UnimplementedCaddyServer struct{}
|
||||||
|
|
||||||
|
func (UnimplementedCaddyServer) GetConfig(context.Context, *emptypb.Empty) (*GetCaddyConfigResponse, error) {
|
||||||
|
return nil, status.Errorf(codes.Unimplemented, "method GetConfig not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedCaddyServer) mustEmbedUnimplementedCaddyServer() {}
|
||||||
|
func (UnimplementedCaddyServer) testEmbeddedByValue() {}
|
||||||
|
|
||||||
|
// UnsafeCaddyServer may be embedded to opt out of forward compatibility for this service.
|
||||||
|
// Use of this interface is not recommended, as added methods to CaddyServer will
|
||||||
|
// result in compilation errors.
|
||||||
|
type UnsafeCaddyServer interface {
|
||||||
|
mustEmbedUnimplementedCaddyServer()
|
||||||
|
}
|
||||||
|
|
||||||
|
func RegisterCaddyServer(s grpc.ServiceRegistrar, srv CaddyServer) {
|
||||||
|
// If the following call pancis, it indicates UnimplementedCaddyServer 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(&Caddy_ServiceDesc, srv)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _Caddy_GetConfig_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.(CaddyServer).GetConfig(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: Caddy_GetConfig_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(CaddyServer).GetConfig(ctx, req.(*emptypb.Empty))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Caddy_ServiceDesc is the grpc.ServiceDesc for Caddy service.
|
||||||
|
// It's only intended for direct use with grpc.RegisterService,
|
||||||
|
// and not to be introspected or modified (even as a copy)
|
||||||
|
var Caddy_ServiceDesc = grpc.ServiceDesc{
|
||||||
|
ServiceName: "api.Caddy",
|
||||||
|
HandlerType: (*CaddyServer)(nil),
|
||||||
|
Methods: []grpc.MethodDesc{
|
||||||
|
{
|
||||||
|
MethodName: "GetConfig",
|
||||||
|
Handler: _Caddy_GetConfig_Handler,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Streams: []grpc.StreamDesc{},
|
||||||
|
Metadata: "internal/machine/api/pb/caddy.proto",
|
||||||
|
}
|
||||||
@@ -78,6 +78,55 @@ func (MachineMember_MembershipState) EnumDescriptor() ([]byte, []int) {
|
|||||||
return file_internal_machine_api_pb_cluster_proto_rawDescGZIP(), []int{2, 0}
|
return file_internal_machine_api_pb_cluster_proto_rawDescGZIP(), []int{2, 0}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type DNSRecord_RecordType int32
|
||||||
|
|
||||||
|
const (
|
||||||
|
DNSRecord_UNSPECIFIED DNSRecord_RecordType = 0
|
||||||
|
DNSRecord_A DNSRecord_RecordType = 1
|
||||||
|
DNSRecord_AAAA DNSRecord_RecordType = 2
|
||||||
|
)
|
||||||
|
|
||||||
|
// Enum value maps for DNSRecord_RecordType.
|
||||||
|
var (
|
||||||
|
DNSRecord_RecordType_name = map[int32]string{
|
||||||
|
0: "UNSPECIFIED",
|
||||||
|
1: "A",
|
||||||
|
2: "AAAA",
|
||||||
|
}
|
||||||
|
DNSRecord_RecordType_value = map[string]int32{
|
||||||
|
"UNSPECIFIED": 0,
|
||||||
|
"A": 1,
|
||||||
|
"AAAA": 2,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func (x DNSRecord_RecordType) Enum() *DNSRecord_RecordType {
|
||||||
|
p := new(DNSRecord_RecordType)
|
||||||
|
*p = x
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x DNSRecord_RecordType) String() string {
|
||||||
|
return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (DNSRecord_RecordType) Descriptor() protoreflect.EnumDescriptor {
|
||||||
|
return file_internal_machine_api_pb_cluster_proto_enumTypes[1].Descriptor()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (DNSRecord_RecordType) Type() protoreflect.EnumType {
|
||||||
|
return &file_internal_machine_api_pb_cluster_proto_enumTypes[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x DNSRecord_RecordType) Number() protoreflect.EnumNumber {
|
||||||
|
return protoreflect.EnumNumber(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use DNSRecord_RecordType.Descriptor instead.
|
||||||
|
func (DNSRecord_RecordType) EnumDescriptor() ([]byte, []int) {
|
||||||
|
return file_internal_machine_api_pb_cluster_proto_rawDescGZIP(), []int{11, 0}
|
||||||
|
}
|
||||||
|
|
||||||
type AddMachineRequest struct {
|
type AddMachineRequest struct {
|
||||||
state protoimpl.MessageState
|
state protoimpl.MessageState
|
||||||
sizeCache protoimpl.SizeCache
|
sizeCache protoimpl.SizeCache
|
||||||
@@ -85,6 +134,7 @@ type AddMachineRequest struct {
|
|||||||
|
|
||||||
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
|
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
|
||||||
Network *NetworkConfig `protobuf:"bytes,2,opt,name=network,proto3" json:"network,omitempty"`
|
Network *NetworkConfig `protobuf:"bytes,2,opt,name=network,proto3" json:"network,omitempty"`
|
||||||
|
PublicIp *IP `protobuf:"bytes,3,opt,name=public_ip,json=publicIp,proto3" json:"public_ip,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *AddMachineRequest) Reset() {
|
func (x *AddMachineRequest) Reset() {
|
||||||
@@ -133,6 +183,13 @@ func (x *AddMachineRequest) GetNetwork() *NetworkConfig {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (x *AddMachineRequest) GetPublicIp() *IP {
|
||||||
|
if x != nil {
|
||||||
|
return x.PublicIp
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
type AddMachineResponse struct {
|
type AddMachineResponse struct {
|
||||||
state protoimpl.MessageState
|
state protoimpl.MessageState
|
||||||
sizeCache protoimpl.SizeCache
|
sizeCache protoimpl.SizeCache
|
||||||
@@ -282,6 +339,424 @@ func (x *ListMachinesResponse) GetMachines() []*MachineMember {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type UpdateMachineRequest struct {
|
||||||
|
state protoimpl.MessageState
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
|
||||||
|
// Machine to update
|
||||||
|
MachineId string `protobuf:"bytes,1,opt,name=machine_id,json=machineId,proto3" json:"machine_id,omitempty"`
|
||||||
|
// Updated machine information
|
||||||
|
Name *string `protobuf:"bytes,2,opt,name=name,proto3,oneof" json:"name,omitempty"`
|
||||||
|
PublicIp *IP `protobuf:"bytes,3,opt,name=public_ip,json=publicIp,proto3,oneof" json:"public_ip,omitempty"`
|
||||||
|
Endpoints []*IPPort `protobuf:"bytes,4,rep,name=endpoints,proto3" json:"endpoints,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *UpdateMachineRequest) Reset() {
|
||||||
|
*x = UpdateMachineRequest{}
|
||||||
|
if protoimpl.UnsafeEnabled {
|
||||||
|
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[4]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *UpdateMachineRequest) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*UpdateMachineRequest) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *UpdateMachineRequest) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[4]
|
||||||
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use UpdateMachineRequest.ProtoReflect.Descriptor instead.
|
||||||
|
func (*UpdateMachineRequest) Descriptor() ([]byte, []int) {
|
||||||
|
return file_internal_machine_api_pb_cluster_proto_rawDescGZIP(), []int{4}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *UpdateMachineRequest) GetMachineId() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.MachineId
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *UpdateMachineRequest) GetName() string {
|
||||||
|
if x != nil && x.Name != nil {
|
||||||
|
return *x.Name
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *UpdateMachineRequest) GetPublicIp() *IP {
|
||||||
|
if x != nil {
|
||||||
|
return x.PublicIp
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *UpdateMachineRequest) GetEndpoints() []*IPPort {
|
||||||
|
if x != nil {
|
||||||
|
return x.Endpoints
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdateMachineResponse struct {
|
||||||
|
state protoimpl.MessageState
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
|
||||||
|
Machine *MachineInfo `protobuf:"bytes,1,opt,name=machine,proto3" json:"machine,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *UpdateMachineResponse) Reset() {
|
||||||
|
*x = UpdateMachineResponse{}
|
||||||
|
if protoimpl.UnsafeEnabled {
|
||||||
|
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[5]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *UpdateMachineResponse) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*UpdateMachineResponse) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *UpdateMachineResponse) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[5]
|
||||||
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use UpdateMachineResponse.ProtoReflect.Descriptor instead.
|
||||||
|
func (*UpdateMachineResponse) Descriptor() ([]byte, []int) {
|
||||||
|
return file_internal_machine_api_pb_cluster_proto_rawDescGZIP(), []int{5}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *UpdateMachineResponse) GetMachine() *MachineInfo {
|
||||||
|
if x != nil {
|
||||||
|
return x.Machine
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type RemoveMachineRequest struct {
|
||||||
|
state protoimpl.MessageState
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
|
||||||
|
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *RemoveMachineRequest) Reset() {
|
||||||
|
*x = RemoveMachineRequest{}
|
||||||
|
if protoimpl.UnsafeEnabled {
|
||||||
|
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[6]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *RemoveMachineRequest) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*RemoveMachineRequest) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *RemoveMachineRequest) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[6]
|
||||||
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use RemoveMachineRequest.ProtoReflect.Descriptor instead.
|
||||||
|
func (*RemoveMachineRequest) Descriptor() ([]byte, []int) {
|
||||||
|
return file_internal_machine_api_pb_cluster_proto_rawDescGZIP(), []int{6}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *RemoveMachineRequest) GetId() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Id
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
type Domain struct {
|
||||||
|
state protoimpl.MessageState
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
|
||||||
|
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Domain) Reset() {
|
||||||
|
*x = Domain{}
|
||||||
|
if protoimpl.UnsafeEnabled {
|
||||||
|
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[7]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Domain) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*Domain) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *Domain) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[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 Domain.ProtoReflect.Descriptor instead.
|
||||||
|
func (*Domain) Descriptor() ([]byte, []int) {
|
||||||
|
return file_internal_machine_api_pb_cluster_proto_rawDescGZIP(), []int{7}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Domain) GetName() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Name
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
type ReserveDomainRequest struct {
|
||||||
|
state protoimpl.MessageState
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
|
||||||
|
Endpoint string `protobuf:"bytes,1,opt,name=endpoint,proto3" json:"endpoint,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ReserveDomainRequest) Reset() {
|
||||||
|
*x = ReserveDomainRequest{}
|
||||||
|
if protoimpl.UnsafeEnabled {
|
||||||
|
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[8]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ReserveDomainRequest) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*ReserveDomainRequest) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *ReserveDomainRequest) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[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 ReserveDomainRequest.ProtoReflect.Descriptor instead.
|
||||||
|
func (*ReserveDomainRequest) Descriptor() ([]byte, []int) {
|
||||||
|
return file_internal_machine_api_pb_cluster_proto_rawDescGZIP(), []int{8}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ReserveDomainRequest) GetEndpoint() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Endpoint
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateDomainRecordsRequest struct {
|
||||||
|
state protoimpl.MessageState
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
|
||||||
|
Records []*DNSRecord `protobuf:"bytes,1,rep,name=records,proto3" json:"records,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *CreateDomainRecordsRequest) Reset() {
|
||||||
|
*x = CreateDomainRecordsRequest{}
|
||||||
|
if protoimpl.UnsafeEnabled {
|
||||||
|
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[9]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *CreateDomainRecordsRequest) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*CreateDomainRecordsRequest) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *CreateDomainRecordsRequest) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[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 CreateDomainRecordsRequest.ProtoReflect.Descriptor instead.
|
||||||
|
func (*CreateDomainRecordsRequest) Descriptor() ([]byte, []int) {
|
||||||
|
return file_internal_machine_api_pb_cluster_proto_rawDescGZIP(), []int{9}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *CreateDomainRecordsRequest) GetRecords() []*DNSRecord {
|
||||||
|
if x != nil {
|
||||||
|
return x.Records
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateDomainRecordsResponse struct {
|
||||||
|
state protoimpl.MessageState
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
|
||||||
|
Records []*DNSRecord `protobuf:"bytes,1,rep,name=records,proto3" json:"records,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *CreateDomainRecordsResponse) Reset() {
|
||||||
|
*x = CreateDomainRecordsResponse{}
|
||||||
|
if protoimpl.UnsafeEnabled {
|
||||||
|
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[10]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *CreateDomainRecordsResponse) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*CreateDomainRecordsResponse) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *CreateDomainRecordsResponse) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[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 CreateDomainRecordsResponse.ProtoReflect.Descriptor instead.
|
||||||
|
func (*CreateDomainRecordsResponse) Descriptor() ([]byte, []int) {
|
||||||
|
return file_internal_machine_api_pb_cluster_proto_rawDescGZIP(), []int{10}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *CreateDomainRecordsResponse) GetRecords() []*DNSRecord {
|
||||||
|
if x != nil {
|
||||||
|
return x.Records
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type DNSRecord struct {
|
||||||
|
state protoimpl.MessageState
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
|
||||||
|
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
|
||||||
|
Type DNSRecord_RecordType `protobuf:"varint,2,opt,name=type,proto3,enum=api.DNSRecord_RecordType" json:"type,omitempty"`
|
||||||
|
Values []string `protobuf:"bytes,3,rep,name=values,proto3" json:"values,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *DNSRecord) Reset() {
|
||||||
|
*x = DNSRecord{}
|
||||||
|
if protoimpl.UnsafeEnabled {
|
||||||
|
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[11]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *DNSRecord) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*DNSRecord) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *DNSRecord) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[11]
|
||||||
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use DNSRecord.ProtoReflect.Descriptor instead.
|
||||||
|
func (*DNSRecord) Descriptor() ([]byte, []int) {
|
||||||
|
return file_internal_machine_api_pb_cluster_proto_rawDescGZIP(), []int{11}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *DNSRecord) GetName() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Name
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *DNSRecord) GetType() DNSRecord_RecordType {
|
||||||
|
if x != nil {
|
||||||
|
return x.Type
|
||||||
|
}
|
||||||
|
return DNSRecord_UNSPECIFIED
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *DNSRecord) GetValues() []string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Values
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
var File_internal_machine_api_pb_cluster_proto protoreflect.FileDescriptor
|
var File_internal_machine_api_pb_cluster_proto protoreflect.FileDescriptor
|
||||||
|
|
||||||
var file_internal_machine_api_pb_cluster_proto_rawDesc = []byte{
|
var file_internal_machine_api_pb_cluster_proto_rawDesc = []byte{
|
||||||
@@ -289,48 +764,120 @@ var file_internal_machine_api_pb_cluster_proto_rawDesc = []byte{
|
|||||||
0x6e, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x62, 0x2f, 0x63, 0x6c, 0x75, 0x73, 0x74, 0x65,
|
0x6e, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x62, 0x2f, 0x63, 0x6c, 0x75, 0x73, 0x74, 0x65,
|
||||||
0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x03, 0x61, 0x70, 0x69, 0x1a, 0x1b, 0x67, 0x6f,
|
0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x03, 0x61, 0x70, 0x69, 0x1a, 0x1b, 0x67, 0x6f,
|
||||||
0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x65, 0x6d,
|
0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x65, 0x6d,
|
||||||
0x70, 0x74, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x25, 0x69, 0x6e, 0x74, 0x65, 0x72,
|
0x70, 0x74, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x24, 0x69, 0x6e, 0x74, 0x65, 0x72,
|
||||||
0x6e, 0x61, 0x6c, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f,
|
0x6e, 0x61, 0x6c, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f,
|
||||||
0x70, 0x62, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f,
|
0x70, 0x62, 0x2f, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a,
|
||||||
0x22, 0x55, 0x0a, 0x11, 0x41, 0x64, 0x64, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65,
|
0x25, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e,
|
||||||
0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20,
|
0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x62, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65,
|
||||||
0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x2c, 0x0a, 0x07, 0x6e, 0x65, 0x74,
|
0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x7b, 0x0a, 0x11, 0x41, 0x64, 0x64, 0x4d, 0x61, 0x63,
|
||||||
0x77, 0x6f, 0x72, 0x6b, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x61, 0x70, 0x69,
|
0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x6e,
|
||||||
0x2e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x07,
|
0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12,
|
||||||
0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x22, 0x40, 0x0a, 0x12, 0x41, 0x64, 0x64, 0x4d, 0x61,
|
0x2c, 0x0a, 0x07, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b,
|
||||||
0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2a, 0x0a,
|
0x32, 0x12, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x43, 0x6f,
|
||||||
0x07, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10,
|
0x6e, 0x66, 0x69, 0x67, 0x52, 0x07, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x12, 0x24, 0x0a,
|
||||||
0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f,
|
0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x69, 0x70, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b,
|
||||||
0x52, 0x07, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x22, 0xb4, 0x01, 0x0a, 0x0d, 0x4d, 0x61,
|
0x32, 0x07, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x50, 0x52, 0x08, 0x70, 0x75, 0x62, 0x6c, 0x69,
|
||||||
0x63, 0x68, 0x69, 0x6e, 0x65, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x12, 0x2a, 0x0a, 0x07, 0x6d,
|
0x63, 0x49, 0x70, 0x22, 0x40, 0x0a, 0x12, 0x41, 0x64, 0x64, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e,
|
||||||
|
0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2a, 0x0a, 0x07, 0x6d, 0x61, 0x63,
|
||||||
|
0x68, 0x69, 0x6e, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x61, 0x70, 0x69,
|
||||||
|
0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x07, 0x6d, 0x61,
|
||||||
|
0x63, 0x68, 0x69, 0x6e, 0x65, 0x22, 0xb4, 0x01, 0x0a, 0x0d, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e,
|
||||||
|
0x65, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x12, 0x2a, 0x0a, 0x07, 0x6d, 0x61, 0x63, 0x68, 0x69,
|
||||||
|
0x6e, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d,
|
||||||
|
0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x07, 0x6d, 0x61, 0x63, 0x68,
|
||||||
|
0x69, 0x6e, 0x65, 0x12, 0x38, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01,
|
||||||
|
0x28, 0x0e, 0x32, 0x22, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65,
|
||||||
|
0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x2e, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, 0x68, 0x69,
|
||||||
|
0x70, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x22, 0x3d, 0x0a,
|
||||||
|
0x0f, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, 0x68, 0x69, 0x70, 0x53, 0x74, 0x61, 0x74, 0x65,
|
||||||
|
0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x06, 0x0a,
|
||||||
|
0x02, 0x55, 0x50, 0x10, 0x01, 0x12, 0x0b, 0x0a, 0x07, 0x53, 0x55, 0x53, 0x50, 0x45, 0x43, 0x54,
|
||||||
|
0x10, 0x02, 0x12, 0x08, 0x0a, 0x04, 0x44, 0x4f, 0x57, 0x4e, 0x10, 0x03, 0x22, 0x46, 0x0a, 0x14,
|
||||||
|
0x4c, 0x69, 0x73, 0x74, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70,
|
||||||
|
0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2e, 0x0a, 0x08, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73,
|
||||||
|
0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d, 0x61, 0x63,
|
||||||
|
0x68, 0x69, 0x6e, 0x65, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x52, 0x08, 0x6d, 0x61, 0x63, 0x68,
|
||||||
|
0x69, 0x6e, 0x65, 0x73, 0x22, 0xbb, 0x01, 0x0a, 0x14, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d,
|
||||||
|
0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 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, 0x17, 0x0a, 0x04,
|
||||||
|
0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x04, 0x6e, 0x61,
|
||||||
|
0x6d, 0x65, 0x88, 0x01, 0x01, 0x12, 0x29, 0x0a, 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f,
|
||||||
|
0x69, 0x70, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x07, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49,
|
||||||
|
0x50, 0x48, 0x01, 0x52, 0x08, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x49, 0x70, 0x88, 0x01, 0x01,
|
||||||
|
0x12, 0x29, 0x0a, 0x09, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x18, 0x04, 0x20,
|
||||||
|
0x03, 0x28, 0x0b, 0x32, 0x0b, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x50, 0x50, 0x6f, 0x72, 0x74,
|
||||||
|
0x52, 0x09, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x42, 0x07, 0x0a, 0x05, 0x5f,
|
||||||
|
0x6e, 0x61, 0x6d, 0x65, 0x42, 0x0c, 0x0a, 0x0a, 0x5f, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f,
|
||||||
|
0x69, 0x70, 0x22, 0x43, 0x0a, 0x15, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x61, 0x63, 0x68,
|
||||||
|
0x69, 0x6e, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2a, 0x0a, 0x07, 0x6d,
|
||||||
0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x61,
|
0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x61,
|
||||||
0x70, 0x69, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x07,
|
0x70, 0x69, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x07,
|
||||||
0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x12, 0x38, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65,
|
0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x22, 0x26, 0x0a, 0x14, 0x52, 0x65, 0x6d, 0x6f, 0x76,
|
||||||
0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x22, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d, 0x61, 0x63,
|
0x65, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12,
|
||||||
0x68, 0x69, 0x6e, 0x65, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x2e, 0x4d, 0x65, 0x6d, 0x62, 0x65,
|
0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x22,
|
||||||
0x72, 0x73, 0x68, 0x69, 0x70, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74,
|
0x1c, 0x0a, 0x06, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d,
|
||||||
0x65, 0x22, 0x3d, 0x0a, 0x0f, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, 0x68, 0x69, 0x70, 0x53,
|
0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x32, 0x0a,
|
||||||
0x74, 0x61, 0x74, 0x65, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10,
|
0x14, 0x52, 0x65, 0x73, 0x65, 0x72, 0x76, 0x65, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x52, 0x65,
|
||||||
0x00, 0x12, 0x06, 0x0a, 0x02, 0x55, 0x50, 0x10, 0x01, 0x12, 0x0b, 0x0a, 0x07, 0x53, 0x55, 0x53,
|
0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e,
|
||||||
0x50, 0x45, 0x43, 0x54, 0x10, 0x02, 0x12, 0x08, 0x0a, 0x04, 0x44, 0x4f, 0x57, 0x4e, 0x10, 0x03,
|
0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e,
|
||||||
0x22, 0x46, 0x0a, 0x14, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73,
|
0x74, 0x22, 0x46, 0x0a, 0x1a, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x44, 0x6f, 0x6d, 0x61, 0x69,
|
||||||
0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2e, 0x0a, 0x08, 0x6d, 0x61, 0x63, 0x68,
|
0x6e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12,
|
||||||
0x69, 0x6e, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x61, 0x70, 0x69,
|
0x28, 0x0a, 0x07, 0x72, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b,
|
||||||
0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x52, 0x08,
|
0x32, 0x0e, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x44, 0x4e, 0x53, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64,
|
||||||
0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x32, 0x8b, 0x01, 0x0a, 0x07, 0x43, 0x6c, 0x75,
|
0x52, 0x07, 0x72, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x22, 0x47, 0x0a, 0x1b, 0x43, 0x72, 0x65,
|
||||||
0x73, 0x74, 0x65, 0x72, 0x12, 0x3d, 0x0a, 0x0a, 0x41, 0x64, 0x64, 0x4d, 0x61, 0x63, 0x68, 0x69,
|
0x61, 0x74, 0x65, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73,
|
||||||
0x6e, 0x65, 0x12, 0x16, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x41, 0x64, 0x64, 0x4d, 0x61, 0x63, 0x68,
|
0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x28, 0x0a, 0x07, 0x72, 0x65, 0x63, 0x6f,
|
||||||
0x69, 0x6e, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x17, 0x2e, 0x61, 0x70, 0x69,
|
0x72, 0x64, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x61, 0x70, 0x69, 0x2e,
|
||||||
0x2e, 0x41, 0x64, 0x64, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f,
|
0x44, 0x4e, 0x53, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x52, 0x07, 0x72, 0x65, 0x63, 0x6f, 0x72,
|
||||||
0x6e, 0x73, 0x65, 0x12, 0x41, 0x0a, 0x0c, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x61, 0x63, 0x68, 0x69,
|
0x64, 0x73, 0x22, 0x96, 0x01, 0x0a, 0x09, 0x44, 0x4e, 0x53, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64,
|
||||||
0x6e, 0x65, 0x73, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f,
|
0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04,
|
||||||
0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x19, 0x2e, 0x61, 0x70,
|
0x6e, 0x61, 0x6d, 0x65, 0x12, 0x2d, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01,
|
||||||
0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x52, 0x65,
|
0x28, 0x0e, 0x32, 0x19, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x44, 0x4e, 0x53, 0x52, 0x65, 0x63, 0x6f,
|
||||||
0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x37, 0x5a, 0x35, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62,
|
0x72, 0x64, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74,
|
||||||
0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x70, 0x73, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x6b, 0x69, 0x2f,
|
0x79, 0x70, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x03, 0x20,
|
||||||
0x75, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c,
|
0x03, 0x28, 0x09, 0x52, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x22, 0x2e, 0x0a, 0x0a, 0x52,
|
||||||
0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x62, 0x62,
|
0x65, 0x63, 0x6f, 0x72, 0x64, 0x54, 0x79, 0x70, 0x65, 0x12, 0x0f, 0x0a, 0x0b, 0x55, 0x4e, 0x53,
|
||||||
0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x05, 0x0a, 0x01, 0x41, 0x10,
|
||||||
|
0x01, 0x12, 0x08, 0x0a, 0x04, 0x41, 0x41, 0x41, 0x41, 0x10, 0x02, 0x32, 0x92, 0x04, 0x0a, 0x07,
|
||||||
|
0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x12, 0x3d, 0x0a, 0x0a, 0x41, 0x64, 0x64, 0x4d, 0x61,
|
||||||
|
0x63, 0x68, 0x69, 0x6e, 0x65, 0x12, 0x16, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x41, 0x64, 0x64, 0x4d,
|
||||||
|
0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x17, 0x2e,
|
||||||
|
0x61, 0x70, 0x69, 0x2e, 0x41, 0x64, 0x64, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65,
|
||||||
|
0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x41, 0x0a, 0x0c, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x61,
|
||||||
|
0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e,
|
||||||
|
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x19,
|
||||||
|
0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65,
|
||||||
|
0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x46, 0x0a, 0x0d, 0x55, 0x70, 0x64,
|
||||||
|
0x61, 0x74, 0x65, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x12, 0x19, 0x2e, 0x61, 0x70, 0x69,
|
||||||
|
0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65,
|
||||||
|
0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x55, 0x70, 0x64, 0x61,
|
||||||
|
0x74, 0x65, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
|
||||||
|
0x65, 0x12, 0x42, 0x0a, 0x0d, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x4d, 0x61, 0x63, 0x68, 0x69,
|
||||||
|
0x6e, 0x65, 0x12, 0x19, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x4d,
|
||||||
|
0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e,
|
||||||
|
0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e,
|
||||||
|
0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x37, 0x0a, 0x0d, 0x52, 0x65, 0x73, 0x65, 0x72, 0x76, 0x65,
|
||||||
|
0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x19, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x52, 0x65, 0x73,
|
||||||
|
0x65, 0x72, 0x76, 0x65, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
|
||||||
|
0x74, 0x1a, 0x0b, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x30,
|
||||||
|
0x0a, 0x09, 0x47, 0x65, 0x74, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x16, 0x2e, 0x67, 0x6f,
|
||||||
|
0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d,
|
||||||
|
0x70, 0x74, 0x79, 0x1a, 0x0b, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e,
|
||||||
|
0x12, 0x34, 0x0a, 0x0d, 0x52, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x44, 0x6f, 0x6d, 0x61, 0x69,
|
||||||
|
0x6e, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f,
|
||||||
|
0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x0b, 0x2e, 0x61, 0x70, 0x69, 0x2e,
|
||||||
|
0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x58, 0x0a, 0x13, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65,
|
||||||
|
0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x12, 0x1f, 0x2e,
|
||||||
|
0x61, 0x70, 0x69, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e,
|
||||||
|
0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20,
|
||||||
|
0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x44, 0x6f, 0x6d, 0x61, 0x69,
|
||||||
|
0x6e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
|
||||||
|
0x42, 0x37, 0x5a, 0x35, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x70,
|
||||||
|
0x73, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x6b, 0x69, 0x2f, 0x75, 0x6e, 0x63, 0x6c, 0x6f, 0x75,
|
||||||
|
0x64, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69,
|
||||||
|
0x6e, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f,
|
||||||
|
0x33,
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -345,33 +892,63 @@ func file_internal_machine_api_pb_cluster_proto_rawDescGZIP() []byte {
|
|||||||
return file_internal_machine_api_pb_cluster_proto_rawDescData
|
return file_internal_machine_api_pb_cluster_proto_rawDescData
|
||||||
}
|
}
|
||||||
|
|
||||||
var file_internal_machine_api_pb_cluster_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
|
var file_internal_machine_api_pb_cluster_proto_enumTypes = make([]protoimpl.EnumInfo, 2)
|
||||||
var file_internal_machine_api_pb_cluster_proto_msgTypes = make([]protoimpl.MessageInfo, 4)
|
var file_internal_machine_api_pb_cluster_proto_msgTypes = make([]protoimpl.MessageInfo, 12)
|
||||||
var file_internal_machine_api_pb_cluster_proto_goTypes = []any{
|
var file_internal_machine_api_pb_cluster_proto_goTypes = []any{
|
||||||
(MachineMember_MembershipState)(0), // 0: api.MachineMember.MembershipState
|
(MachineMember_MembershipState)(0), // 0: api.MachineMember.MembershipState
|
||||||
(*AddMachineRequest)(nil), // 1: api.AddMachineRequest
|
(DNSRecord_RecordType)(0), // 1: api.DNSRecord.RecordType
|
||||||
(*AddMachineResponse)(nil), // 2: api.AddMachineResponse
|
(*AddMachineRequest)(nil), // 2: api.AddMachineRequest
|
||||||
(*MachineMember)(nil), // 3: api.MachineMember
|
(*AddMachineResponse)(nil), // 3: api.AddMachineResponse
|
||||||
(*ListMachinesResponse)(nil), // 4: api.ListMachinesResponse
|
(*MachineMember)(nil), // 4: api.MachineMember
|
||||||
(*NetworkConfig)(nil), // 5: api.NetworkConfig
|
(*ListMachinesResponse)(nil), // 5: api.ListMachinesResponse
|
||||||
(*MachineInfo)(nil), // 6: api.MachineInfo
|
(*UpdateMachineRequest)(nil), // 6: api.UpdateMachineRequest
|
||||||
(*emptypb.Empty)(nil), // 7: google.protobuf.Empty
|
(*UpdateMachineResponse)(nil), // 7: api.UpdateMachineResponse
|
||||||
|
(*RemoveMachineRequest)(nil), // 8: api.RemoveMachineRequest
|
||||||
|
(*Domain)(nil), // 9: api.Domain
|
||||||
|
(*ReserveDomainRequest)(nil), // 10: api.ReserveDomainRequest
|
||||||
|
(*CreateDomainRecordsRequest)(nil), // 11: api.CreateDomainRecordsRequest
|
||||||
|
(*CreateDomainRecordsResponse)(nil), // 12: api.CreateDomainRecordsResponse
|
||||||
|
(*DNSRecord)(nil), // 13: api.DNSRecord
|
||||||
|
(*NetworkConfig)(nil), // 14: api.NetworkConfig
|
||||||
|
(*IP)(nil), // 15: api.IP
|
||||||
|
(*MachineInfo)(nil), // 16: api.MachineInfo
|
||||||
|
(*IPPort)(nil), // 17: api.IPPort
|
||||||
|
(*emptypb.Empty)(nil), // 18: google.protobuf.Empty
|
||||||
}
|
}
|
||||||
var file_internal_machine_api_pb_cluster_proto_depIdxs = []int32{
|
var file_internal_machine_api_pb_cluster_proto_depIdxs = []int32{
|
||||||
5, // 0: api.AddMachineRequest.network:type_name -> api.NetworkConfig
|
14, // 0: api.AddMachineRequest.network:type_name -> api.NetworkConfig
|
||||||
6, // 1: api.AddMachineResponse.machine:type_name -> api.MachineInfo
|
15, // 1: api.AddMachineRequest.public_ip:type_name -> api.IP
|
||||||
6, // 2: api.MachineMember.machine:type_name -> api.MachineInfo
|
16, // 2: api.AddMachineResponse.machine:type_name -> api.MachineInfo
|
||||||
0, // 3: api.MachineMember.state:type_name -> api.MachineMember.MembershipState
|
16, // 3: api.MachineMember.machine:type_name -> api.MachineInfo
|
||||||
3, // 4: api.ListMachinesResponse.machines:type_name -> api.MachineMember
|
0, // 4: api.MachineMember.state:type_name -> api.MachineMember.MembershipState
|
||||||
1, // 5: api.Cluster.AddMachine:input_type -> api.AddMachineRequest
|
4, // 5: api.ListMachinesResponse.machines:type_name -> api.MachineMember
|
||||||
7, // 6: api.Cluster.ListMachines:input_type -> google.protobuf.Empty
|
15, // 6: api.UpdateMachineRequest.public_ip:type_name -> api.IP
|
||||||
2, // 7: api.Cluster.AddMachine:output_type -> api.AddMachineResponse
|
17, // 7: api.UpdateMachineRequest.endpoints:type_name -> api.IPPort
|
||||||
4, // 8: api.Cluster.ListMachines:output_type -> api.ListMachinesResponse
|
16, // 8: api.UpdateMachineResponse.machine:type_name -> api.MachineInfo
|
||||||
7, // [7:9] is the sub-list for method output_type
|
13, // 9: api.CreateDomainRecordsRequest.records:type_name -> api.DNSRecord
|
||||||
5, // [5:7] is the sub-list for method input_type
|
13, // 10: api.CreateDomainRecordsResponse.records:type_name -> api.DNSRecord
|
||||||
5, // [5:5] is the sub-list for extension type_name
|
1, // 11: api.DNSRecord.type:type_name -> api.DNSRecord.RecordType
|
||||||
5, // [5:5] is the sub-list for extension extendee
|
2, // 12: api.Cluster.AddMachine:input_type -> api.AddMachineRequest
|
||||||
0, // [0:5] is the sub-list for field type_name
|
18, // 13: api.Cluster.ListMachines:input_type -> google.protobuf.Empty
|
||||||
|
6, // 14: api.Cluster.UpdateMachine:input_type -> api.UpdateMachineRequest
|
||||||
|
8, // 15: api.Cluster.RemoveMachine:input_type -> api.RemoveMachineRequest
|
||||||
|
10, // 16: api.Cluster.ReserveDomain:input_type -> api.ReserveDomainRequest
|
||||||
|
18, // 17: api.Cluster.GetDomain:input_type -> google.protobuf.Empty
|
||||||
|
18, // 18: api.Cluster.ReleaseDomain:input_type -> google.protobuf.Empty
|
||||||
|
11, // 19: api.Cluster.CreateDomainRecords:input_type -> api.CreateDomainRecordsRequest
|
||||||
|
3, // 20: api.Cluster.AddMachine:output_type -> api.AddMachineResponse
|
||||||
|
5, // 21: api.Cluster.ListMachines:output_type -> api.ListMachinesResponse
|
||||||
|
7, // 22: api.Cluster.UpdateMachine:output_type -> api.UpdateMachineResponse
|
||||||
|
18, // 23: api.Cluster.RemoveMachine:output_type -> google.protobuf.Empty
|
||||||
|
9, // 24: api.Cluster.ReserveDomain:output_type -> api.Domain
|
||||||
|
9, // 25: api.Cluster.GetDomain:output_type -> api.Domain
|
||||||
|
9, // 26: api.Cluster.ReleaseDomain:output_type -> api.Domain
|
||||||
|
12, // 27: api.Cluster.CreateDomainRecords:output_type -> api.CreateDomainRecordsResponse
|
||||||
|
20, // [20:28] is the sub-list for method output_type
|
||||||
|
12, // [12:20] 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_cluster_proto_init() }
|
func init() { file_internal_machine_api_pb_cluster_proto_init() }
|
||||||
@@ -379,6 +956,7 @@ func file_internal_machine_api_pb_cluster_proto_init() {
|
|||||||
if File_internal_machine_api_pb_cluster_proto != nil {
|
if File_internal_machine_api_pb_cluster_proto != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
file_internal_machine_api_pb_common_proto_init()
|
||||||
file_internal_machine_api_pb_machine_proto_init()
|
file_internal_machine_api_pb_machine_proto_init()
|
||||||
if !protoimpl.UnsafeEnabled {
|
if !protoimpl.UnsafeEnabled {
|
||||||
file_internal_machine_api_pb_cluster_proto_msgTypes[0].Exporter = func(v any, i int) any {
|
file_internal_machine_api_pb_cluster_proto_msgTypes[0].Exporter = func(v any, i int) any {
|
||||||
@@ -429,14 +1007,111 @@ func file_internal_machine_api_pb_cluster_proto_init() {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
file_internal_machine_api_pb_cluster_proto_msgTypes[4].Exporter = func(v any, i int) any {
|
||||||
|
switch v := v.(*UpdateMachineRequest); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
file_internal_machine_api_pb_cluster_proto_msgTypes[5].Exporter = func(v any, i int) any {
|
||||||
|
switch v := v.(*UpdateMachineResponse); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file_internal_machine_api_pb_cluster_proto_msgTypes[6].Exporter = func(v any, i int) any {
|
||||||
|
switch v := v.(*RemoveMachineRequest); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file_internal_machine_api_pb_cluster_proto_msgTypes[7].Exporter = func(v any, i int) any {
|
||||||
|
switch v := v.(*Domain); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file_internal_machine_api_pb_cluster_proto_msgTypes[8].Exporter = func(v any, i int) any {
|
||||||
|
switch v := v.(*ReserveDomainRequest); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file_internal_machine_api_pb_cluster_proto_msgTypes[9].Exporter = func(v any, i int) any {
|
||||||
|
switch v := v.(*CreateDomainRecordsRequest); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file_internal_machine_api_pb_cluster_proto_msgTypes[10].Exporter = func(v any, i int) any {
|
||||||
|
switch v := v.(*CreateDomainRecordsResponse); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file_internal_machine_api_pb_cluster_proto_msgTypes[11].Exporter = func(v any, i int) any {
|
||||||
|
switch v := v.(*DNSRecord); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file_internal_machine_api_pb_cluster_proto_msgTypes[4].OneofWrappers = []any{}
|
||||||
type x struct{}
|
type x struct{}
|
||||||
out := protoimpl.TypeBuilder{
|
out := protoimpl.TypeBuilder{
|
||||||
File: protoimpl.DescBuilder{
|
File: protoimpl.DescBuilder{
|
||||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||||
RawDescriptor: file_internal_machine_api_pb_cluster_proto_rawDesc,
|
RawDescriptor: file_internal_machine_api_pb_cluster_proto_rawDesc,
|
||||||
NumEnums: 1,
|
NumEnums: 2,
|
||||||
NumMessages: 4,
|
NumMessages: 12,
|
||||||
NumExtensions: 0,
|
NumExtensions: 0,
|
||||||
NumServices: 1,
|
NumServices: 1,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -5,16 +5,25 @@ package api;
|
|||||||
option go_package = "github.com/psviderski/uncloud/internal/machine/api/pb";
|
option go_package = "github.com/psviderski/uncloud/internal/machine/api/pb";
|
||||||
|
|
||||||
import "google/protobuf/empty.proto";
|
import "google/protobuf/empty.proto";
|
||||||
|
import "internal/machine/api/pb/common.proto";
|
||||||
import "internal/machine/api/pb/machine.proto";
|
import "internal/machine/api/pb/machine.proto";
|
||||||
|
|
||||||
service Cluster {
|
service Cluster {
|
||||||
rpc AddMachine(AddMachineRequest) returns (AddMachineResponse);
|
rpc AddMachine(AddMachineRequest) returns (AddMachineResponse);
|
||||||
rpc ListMachines(google.protobuf.Empty) returns (ListMachinesResponse);
|
rpc ListMachines(google.protobuf.Empty) returns (ListMachinesResponse);
|
||||||
|
rpc UpdateMachine(UpdateMachineRequest) returns (UpdateMachineResponse);
|
||||||
|
rpc RemoveMachine(RemoveMachineRequest) returns (google.protobuf.Empty);
|
||||||
|
|
||||||
|
rpc ReserveDomain(ReserveDomainRequest) returns (Domain);
|
||||||
|
rpc GetDomain(google.protobuf.Empty) returns (Domain);
|
||||||
|
rpc ReleaseDomain(google.protobuf.Empty) returns (Domain);
|
||||||
|
rpc CreateDomainRecords(CreateDomainRecordsRequest) returns (CreateDomainRecordsResponse);
|
||||||
}
|
}
|
||||||
|
|
||||||
message AddMachineRequest {
|
message AddMachineRequest {
|
||||||
string name = 1;
|
string name = 1;
|
||||||
NetworkConfig network = 2;
|
NetworkConfig network = 2;
|
||||||
|
IP public_ip = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
message AddMachineResponse {
|
message AddMachineResponse {
|
||||||
@@ -41,3 +50,49 @@ message MachineMember {
|
|||||||
message ListMachinesResponse {
|
message ListMachinesResponse {
|
||||||
repeated MachineMember machines = 1;
|
repeated MachineMember machines = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message UpdateMachineRequest {
|
||||||
|
// Machine to update
|
||||||
|
string machine_id = 1;
|
||||||
|
|
||||||
|
// Updated machine information
|
||||||
|
optional string name = 2;
|
||||||
|
optional IP public_ip = 3;
|
||||||
|
repeated IPPort endpoints = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message UpdateMachineResponse {
|
||||||
|
MachineInfo machine = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message RemoveMachineRequest {
|
||||||
|
string id = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message Domain {
|
||||||
|
string name = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ReserveDomainRequest {
|
||||||
|
string endpoint = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message CreateDomainRecordsRequest {
|
||||||
|
repeated DNSRecord records = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message CreateDomainRecordsResponse {
|
||||||
|
repeated DNSRecord records = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message DNSRecord {
|
||||||
|
string name = 1;
|
||||||
|
|
||||||
|
enum RecordType {
|
||||||
|
UNSPECIFIED = 0;
|
||||||
|
A = 1;
|
||||||
|
AAAA = 2;
|
||||||
|
}
|
||||||
|
RecordType type = 2;
|
||||||
|
repeated string values = 3;
|
||||||
|
}
|
||||||
|
|||||||
@@ -22,6 +22,12 @@ 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_UpdateMachine_FullMethodName = "/api.Cluster/UpdateMachine"
|
||||||
|
Cluster_RemoveMachine_FullMethodName = "/api.Cluster/RemoveMachine"
|
||||||
|
Cluster_ReserveDomain_FullMethodName = "/api.Cluster/ReserveDomain"
|
||||||
|
Cluster_GetDomain_FullMethodName = "/api.Cluster/GetDomain"
|
||||||
|
Cluster_ReleaseDomain_FullMethodName = "/api.Cluster/ReleaseDomain"
|
||||||
|
Cluster_CreateDomainRecords_FullMethodName = "/api.Cluster/CreateDomainRecords"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ClusterClient is the client API for Cluster service.
|
// ClusterClient is the client API for Cluster service.
|
||||||
@@ -30,6 +36,12 @@ 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)
|
||||||
|
UpdateMachine(ctx context.Context, in *UpdateMachineRequest, opts ...grpc.CallOption) (*UpdateMachineResponse, error)
|
||||||
|
RemoveMachine(ctx context.Context, in *RemoveMachineRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
|
||||||
|
ReserveDomain(ctx context.Context, in *ReserveDomainRequest, opts ...grpc.CallOption) (*Domain, error)
|
||||||
|
GetDomain(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*Domain, error)
|
||||||
|
ReleaseDomain(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*Domain, error)
|
||||||
|
CreateDomainRecords(ctx context.Context, in *CreateDomainRecordsRequest, opts ...grpc.CallOption) (*CreateDomainRecordsResponse, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type clusterClient struct {
|
type clusterClient struct {
|
||||||
@@ -60,12 +72,78 @@ func (c *clusterClient) ListMachines(ctx context.Context, in *emptypb.Empty, opt
|
|||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *clusterClient) UpdateMachine(ctx context.Context, in *UpdateMachineRequest, opts ...grpc.CallOption) (*UpdateMachineResponse, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(UpdateMachineResponse)
|
||||||
|
err := c.cc.Invoke(ctx, Cluster_UpdateMachine_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *clusterClient) RemoveMachine(ctx context.Context, in *RemoveMachineRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(emptypb.Empty)
|
||||||
|
err := c.cc.Invoke(ctx, Cluster_RemoveMachine_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *clusterClient) ReserveDomain(ctx context.Context, in *ReserveDomainRequest, opts ...grpc.CallOption) (*Domain, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(Domain)
|
||||||
|
err := c.cc.Invoke(ctx, Cluster_ReserveDomain_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *clusterClient) GetDomain(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*Domain, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(Domain)
|
||||||
|
err := c.cc.Invoke(ctx, Cluster_GetDomain_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *clusterClient) ReleaseDomain(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*Domain, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(Domain)
|
||||||
|
err := c.cc.Invoke(ctx, Cluster_ReleaseDomain_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *clusterClient) CreateDomainRecords(ctx context.Context, in *CreateDomainRecordsRequest, opts ...grpc.CallOption) (*CreateDomainRecordsResponse, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(CreateDomainRecordsResponse)
|
||||||
|
err := c.cc.Invoke(ctx, Cluster_CreateDomainRecords_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
// ClusterServer is the server API for Cluster service.
|
// ClusterServer is the server API for Cluster service.
|
||||||
// All implementations must embed UnimplementedClusterServer
|
// All implementations must embed UnimplementedClusterServer
|
||||||
// for forward compatibility.
|
// for forward compatibility.
|
||||||
type ClusterServer interface {
|
type ClusterServer interface {
|
||||||
AddMachine(context.Context, *AddMachineRequest) (*AddMachineResponse, error)
|
AddMachine(context.Context, *AddMachineRequest) (*AddMachineResponse, error)
|
||||||
ListMachines(context.Context, *emptypb.Empty) (*ListMachinesResponse, error)
|
ListMachines(context.Context, *emptypb.Empty) (*ListMachinesResponse, error)
|
||||||
|
UpdateMachine(context.Context, *UpdateMachineRequest) (*UpdateMachineResponse, error)
|
||||||
|
RemoveMachine(context.Context, *RemoveMachineRequest) (*emptypb.Empty, error)
|
||||||
|
ReserveDomain(context.Context, *ReserveDomainRequest) (*Domain, error)
|
||||||
|
GetDomain(context.Context, *emptypb.Empty) (*Domain, error)
|
||||||
|
ReleaseDomain(context.Context, *emptypb.Empty) (*Domain, error)
|
||||||
|
CreateDomainRecords(context.Context, *CreateDomainRecordsRequest) (*CreateDomainRecordsResponse, error)
|
||||||
mustEmbedUnimplementedClusterServer()
|
mustEmbedUnimplementedClusterServer()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,6 +160,24 @@ 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) UpdateMachine(context.Context, *UpdateMachineRequest) (*UpdateMachineResponse, error) {
|
||||||
|
return nil, status.Errorf(codes.Unimplemented, "method UpdateMachine not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedClusterServer) RemoveMachine(context.Context, *RemoveMachineRequest) (*emptypb.Empty, error) {
|
||||||
|
return nil, status.Errorf(codes.Unimplemented, "method RemoveMachine not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedClusterServer) ReserveDomain(context.Context, *ReserveDomainRequest) (*Domain, error) {
|
||||||
|
return nil, status.Errorf(codes.Unimplemented, "method ReserveDomain not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedClusterServer) GetDomain(context.Context, *emptypb.Empty) (*Domain, error) {
|
||||||
|
return nil, status.Errorf(codes.Unimplemented, "method GetDomain not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedClusterServer) ReleaseDomain(context.Context, *emptypb.Empty) (*Domain, error) {
|
||||||
|
return nil, status.Errorf(codes.Unimplemented, "method ReleaseDomain not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedClusterServer) CreateDomainRecords(context.Context, *CreateDomainRecordsRequest) (*CreateDomainRecordsResponse, error) {
|
||||||
|
return nil, status.Errorf(codes.Unimplemented, "method CreateDomainRecords not implemented")
|
||||||
|
}
|
||||||
func (UnimplementedClusterServer) mustEmbedUnimplementedClusterServer() {}
|
func (UnimplementedClusterServer) mustEmbedUnimplementedClusterServer() {}
|
||||||
func (UnimplementedClusterServer) testEmbeddedByValue() {}
|
func (UnimplementedClusterServer) testEmbeddedByValue() {}
|
||||||
|
|
||||||
@@ -139,6 +235,114 @@ 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_UpdateMachine_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(UpdateMachineRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(ClusterServer).UpdateMachine(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: Cluster_UpdateMachine_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(ClusterServer).UpdateMachine(ctx, req.(*UpdateMachineRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _Cluster_RemoveMachine_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(RemoveMachineRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(ClusterServer).RemoveMachine(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: Cluster_RemoveMachine_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(ClusterServer).RemoveMachine(ctx, req.(*RemoveMachineRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _Cluster_ReserveDomain_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(ReserveDomainRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(ClusterServer).ReserveDomain(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: Cluster_ReserveDomain_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(ClusterServer).ReserveDomain(ctx, req.(*ReserveDomainRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _Cluster_GetDomain_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(emptypb.Empty)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(ClusterServer).GetDomain(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: Cluster_GetDomain_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(ClusterServer).GetDomain(ctx, req.(*emptypb.Empty))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _Cluster_ReleaseDomain_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(emptypb.Empty)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(ClusterServer).ReleaseDomain(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: Cluster_ReleaseDomain_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(ClusterServer).ReleaseDomain(ctx, req.(*emptypb.Empty))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _Cluster_CreateDomainRecords_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(CreateDomainRecordsRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(ClusterServer).CreateDomainRecords(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: Cluster_CreateDomainRecords_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(ClusterServer).CreateDomainRecords(ctx, req.(*CreateDomainRecordsRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
// Cluster_ServiceDesc is the grpc.ServiceDesc for Cluster service.
|
// Cluster_ServiceDesc is the grpc.ServiceDesc for Cluster service.
|
||||||
// It's only intended for direct use with grpc.RegisterService,
|
// It's only intended for direct use with grpc.RegisterService,
|
||||||
// and not to be introspected or modified (even as a copy)
|
// and not to be introspected or modified (even as a copy)
|
||||||
@@ -154,6 +358,30 @@ var Cluster_ServiceDesc = grpc.ServiceDesc{
|
|||||||
MethodName: "ListMachines",
|
MethodName: "ListMachines",
|
||||||
Handler: _Cluster_ListMachines_Handler,
|
Handler: _Cluster_ListMachines_Handler,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
MethodName: "UpdateMachine",
|
||||||
|
Handler: _Cluster_UpdateMachine_Handler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MethodName: "RemoveMachine",
|
||||||
|
Handler: _Cluster_RemoveMachine_Handler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MethodName: "ReserveDomain",
|
||||||
|
Handler: _Cluster_ReserveDomain_Handler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MethodName: "GetDomain",
|
||||||
|
Handler: _Cluster_GetDomain_Handler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MethodName: "ReleaseDomain",
|
||||||
|
Handler: _Cluster_ReleaseDomain_Handler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MethodName: "CreateDomainRecords",
|
||||||
|
Handler: _Cluster_CreateDomainRecords_Handler,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
Streams: []grpc.StreamDesc{},
|
Streams: []grpc.StreamDesc{},
|
||||||
Metadata: "internal/machine/api/pb/cluster.proto",
|
Metadata: "internal/machine/api/pb/cluster.proto",
|
||||||
|
|||||||
+1603
-120
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user