mirror of
https://github.com/psviderski/uncloud.git
synced 2026-08-26 11:03:34 +00:00
Compare commits
141
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f280003d87 | ||
|
|
531cebfff0 | ||
|
|
15037f382a | ||
|
|
c3e666056f | ||
|
|
12c82bdb05 | ||
|
|
7eaf42444d | ||
|
|
117c2abd23 | ||
|
|
9fb20c30b3 | ||
|
|
ba73259887 | ||
|
|
a0dbe4a983 | ||
|
|
21c65b9dbe | ||
|
|
86251a9354 | ||
|
|
30bcb021d2 | ||
|
|
ada9bec2d4 | ||
|
|
18ea1bc4a6 | ||
|
|
be0f833e21 | ||
|
|
98439b7743 | ||
|
|
2ab58d24d4 | ||
|
|
a5baf6e5c3 | ||
|
|
6c75277e59 | ||
|
|
76880d901a | ||
|
|
3f95583dd4 | ||
|
|
f5a77b82b2 | ||
|
|
b4b1232375 | ||
|
|
e4cb71b878 | ||
|
|
95f80a7fda | ||
|
|
442f1d97a1 | ||
|
|
d2703cbc6b | ||
|
|
0f4c211002 | ||
|
|
68661e57c3 | ||
|
|
1735ef9521 | ||
|
|
838839a7e5 | ||
|
|
940732e91c | ||
|
|
9e4c759ffc | ||
|
|
1525d182b5 | ||
|
|
8ae38cfd57 | ||
|
|
6cc3a05ed2 | ||
|
|
29a682f1ce | ||
|
|
b2fce73254 | ||
|
|
7f92a5db27 | ||
|
|
a5c9085105 | ||
|
|
d344392c1b | ||
|
|
2992bad05d | ||
|
|
255448a59c | ||
|
|
cf3234aef9 | ||
|
|
9e39f03f75 | ||
|
|
33fcb2fe74 | ||
|
|
d5687cd713 | ||
|
|
a67f936cc1 | ||
|
|
51b8c29d63 | ||
|
|
e6f013dbc8 | ||
|
|
897f03aafd | ||
|
|
7b306f25ce | ||
|
|
78a3551c77 | ||
|
|
38c7dda6a0 | ||
|
|
51c7b5ba8f | ||
|
|
a507b202ec | ||
|
|
d9934b9dba | ||
|
|
1f328e99d5 | ||
|
|
3c435434a8 | ||
|
|
e54871758b | ||
|
|
c039390ade | ||
|
|
bad0f97b66 | ||
|
|
2405b156e4 | ||
|
|
4de17cb63c | ||
|
|
2222434934 | ||
|
|
3f1a772288 | ||
|
|
7b9bed0780 | ||
|
|
aef4b2213f | ||
|
|
44013da332 | ||
|
|
b48ebe2acc | ||
|
|
eaa4455320 | ||
|
|
ce3cb8a34f | ||
|
|
e6705599b3 | ||
|
|
7b88040c73 | ||
|
|
7a9eac593f | ||
|
|
17c1bb5c21 | ||
|
|
a5a9ef5444 | ||
|
|
2940965e31 | ||
|
|
185cf19a0c | ||
|
|
4443fa9e0e | ||
|
|
3056e2af11 | ||
|
|
7bfaf31138 | ||
|
|
f8d1945544 | ||
|
|
93d7f29d31 | ||
|
|
18ebd29032 | ||
|
|
fe56d6d708 | ||
|
|
14056cc584 | ||
|
|
d3ee7399eb | ||
|
|
5c54e9d059 | ||
|
|
ab6f856987 | ||
|
|
6e59657fe2 | ||
|
|
215f21439d | ||
|
|
53bf446502 | ||
|
|
2588279e52 | ||
|
|
f48dc250a9 | ||
|
|
15d9ceb4d1 | ||
|
|
a4a8e70f9c | ||
|
|
1d4dbd96ce | ||
|
|
476a2d57ca | ||
|
|
ce46a67d6d | ||
|
|
87e49dfb62 | ||
|
|
808bda67d4 | ||
|
|
c91a964513 | ||
|
|
e0a63a3f49 | ||
|
|
0b2e1bb7a2 | ||
|
|
7c4f739d07 | ||
|
|
3bd9400051 | ||
|
|
1caa5ccf0e | ||
|
|
76affeb6b2 | ||
|
|
a945291371 | ||
|
|
06c3fba96e | ||
|
|
7e739dd2f0 | ||
|
|
6c2f85d38a | ||
|
|
1ea1d15845 | ||
|
|
5e0d4d0f05 | ||
|
|
9a1d2e0d46 | ||
|
|
878f96a4b7 | ||
|
|
9efaab2beb | ||
|
|
2c34ba7eff | ||
|
|
57e2e22266 | ||
|
|
4ec5c15254 | ||
|
|
33a3683196 | ||
|
|
4be1ea3506 | ||
|
|
f86e63c851 | ||
|
|
f6cf73190f | ||
|
|
5308c87651 | ||
|
|
9bd5ffcd2d | ||
|
|
8d8acd5410 | ||
|
|
4d97c30a9c | ||
|
|
aa71ab0220 | ||
|
|
2345ee9856 | ||
|
|
65e9cdd444 | ||
|
|
85a2615db1 | ||
|
|
777615bc56 | ||
|
|
bac34ebd0c | ||
|
|
b2322e1c49 | ||
|
|
436671457d | ||
|
|
fc847093d2 | ||
|
|
d2a7af744d | ||
|
|
5acf557675 |
@@ -18,6 +18,8 @@ on:
|
|||||||
- "test/**"
|
- "test/**"
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
env:
|
||||||
|
MISE_VERSION: "2026.3.7"
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -25,10 +27,12 @@ jobs:
|
|||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
|
|
||||||
- name: Set up Go
|
- name: Install Mise
|
||||||
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
|
uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1
|
||||||
with:
|
with:
|
||||||
go-version: "1.25.1"
|
version: ${{ env.MISE_VERSION }}
|
||||||
|
- name: Reset changes in mise.lock after fresh mise install
|
||||||
|
run: git checkout mise.lock
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
@@ -36,9 +40,15 @@ jobs:
|
|||||||
git diff --exit-code ||
|
git diff --exit-code ||
|
||||||
(echo "go.mod or go.sum has changed. Please run 'go mod tidy' and commit the changes." && exit 1)
|
(echo "go.mod or go.sum has changed. Please run 'go mod tidy' and commit the changes." && exit 1)
|
||||||
|
|
||||||
|
- name: Generate docs
|
||||||
|
run: |
|
||||||
|
make cli-docs
|
||||||
|
git diff --exit-code ||
|
||||||
|
(echo "Documentation is out of date. Please run 'make cli-docs' and commit the changes." && exit 1)
|
||||||
|
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: |
|
run: |
|
||||||
make ucind-image
|
mise ucind:image
|
||||||
make test
|
make test
|
||||||
timeout-minutes: 15
|
timeout-minutes: 15
|
||||||
|
|
||||||
@@ -55,15 +65,18 @@ jobs:
|
|||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
|
|
||||||
- name: Install Mise
|
- name: Install Mise
|
||||||
uses: jdx/mise-action@13abe502c30c1559a5c37dff303831bab82c9402 # v2.2.3
|
uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1
|
||||||
with:
|
with:
|
||||||
version: "2025.9.6"
|
version: ${{ env.MISE_VERSION }}
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ github.token }}
|
# TODO: figure out how to properly lock all platforms in the lockfile and remove this hack.
|
||||||
|
# `mise lock` (mise run mise:lock) doesn't generate checksums for some of the tools, e.g. protoc*
|
||||||
|
- name: Reset changes in mise.lock after fresh mise install
|
||||||
|
run: git checkout mise.lock
|
||||||
|
|
||||||
- name: Generate Protobuf files
|
- name: Generate Protobuf files
|
||||||
run: |
|
run: |
|
||||||
make proto
|
mise run proto
|
||||||
# check if the generated files are up to date
|
# Check if the generated files are up to date.
|
||||||
git diff --exit-code ||
|
git diff --exit-code ||
|
||||||
(echo "Generated protobuf files are not up to date. Please run 'make proto' and commit the changes." && exit 1)
|
(echo "Generated protobuf files are not up to date. Please run 'mise run proto' and commit the changes." && exit 1)
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ on:
|
|||||||
- "go.*"
|
- "go.*"
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
env:
|
||||||
|
MISE_VERSION: "2026.3.7"
|
||||||
jobs:
|
jobs:
|
||||||
lint:
|
lint:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -20,20 +22,14 @@ jobs:
|
|||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
|
|
||||||
- name: Set up Go
|
- name: Install Mise
|
||||||
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
|
uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1
|
||||||
with:
|
with:
|
||||||
go-version: "1.25.1"
|
version: ${{ env.MISE_VERSION }}
|
||||||
|
- name: Reset changes in mise.lock after fresh mise install
|
||||||
|
run: git checkout mise.lock
|
||||||
|
|
||||||
- name: golangci-lint
|
- name: Lint
|
||||||
uses: golangci/golangci-lint-action@4afd733a84b1f43292c63897423277bb7f4313a9 # v8.0.0
|
run: make lint
|
||||||
with:
|
|
||||||
version: v2.4.0
|
|
||||||
|
|
||||||
- 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
|
timeout-minutes: 10
|
||||||
|
|||||||
@@ -7,6 +7,10 @@
|
|||||||
*.dll
|
*.dll
|
||||||
*.so
|
*.so
|
||||||
*.dylib
|
*.dylib
|
||||||
|
/uncloud
|
||||||
|
/uncloud-*
|
||||||
|
/uncloudd
|
||||||
|
/uncloudd-*
|
||||||
|
|
||||||
# OS X
|
# OS X
|
||||||
.DS_Store
|
.DS_Store
|
||||||
@@ -37,3 +41,6 @@ node_modules/
|
|||||||
|
|
||||||
# VS Code
|
# VS Code
|
||||||
.vscode
|
.vscode
|
||||||
|
.devcontainer/
|
||||||
|
|
||||||
|
.zed/
|
||||||
|
|||||||
-97
@@ -1,97 +0,0 @@
|
|||||||
[[tools."aqua:vektra/mockery"]]
|
|
||||||
version = "3.5.3"
|
|
||||||
backend = "aqua:vektra/mockery"
|
|
||||||
|
|
||||||
[tools."aqua:vektra/mockery".platforms.linux-x64]
|
|
||||||
checksum = "sha256:ebce416b0175338525246c376885a1579ca6cd4d4015140ba0c70e6b5339a39c"
|
|
||||||
size = 5498859
|
|
||||||
url = "https://github.com/vektra/mockery/releases/download/v3.5.3/mockery_3.5.3_Linux_x86_64.tar.gz"
|
|
||||||
|
|
||||||
[tools."aqua:vektra/mockery".platforms.macos-arm64]
|
|
||||||
checksum = "sha256:a3a94b14c7414e148f2252199ffc4a0108d311358f3d336cbe05bb73cb203704"
|
|
||||||
size = 5238877
|
|
||||||
url = "https://github.com/vektra/mockery/releases/download/v3.5.3/mockery_3.5.3_Darwin_arm64.tar.gz"
|
|
||||||
|
|
||||||
[[tools.go]]
|
|
||||||
version = "1.25.1"
|
|
||||||
backend = "core:go"
|
|
||||||
|
|
||||||
[tools.go.platforms.linux-x64]
|
|
||||||
checksum = "sha256:7716a0d940a0f6ae8e1f3b3f4f36299dc53e31b16840dbd171254312c41ca12e"
|
|
||||||
size = 59660846
|
|
||||||
url = "https://dl.google.com/go/go1.25.1.linux-amd64.tar.gz"
|
|
||||||
|
|
||||||
[tools.go.platforms.macos-arm64]
|
|
||||||
checksum = "sha256:68deebb214f39d542e518ebb0598a406ab1b5a22bba8ec9ade9f55fb4dd94a6c"
|
|
||||||
size = 57906702
|
|
||||||
url = "https://dl.google.com/go/go1.25.1.darwin-arm64.tar.gz"
|
|
||||||
|
|
||||||
[[tools.golangci-lint]]
|
|
||||||
version = "2.4.0"
|
|
||||||
backend = "aqua:golangci/golangci-lint"
|
|
||||||
|
|
||||||
[tools.golangci-lint.platforms.linux-x64]
|
|
||||||
checksum = "sha256:fae792524c04424c0ac369f5b8076f04b45cf29fc945a370e55d369a8dc11840"
|
|
||||||
size = 14126779
|
|
||||||
url = "https://github.com/golangci/golangci-lint/releases/download/v2.4.0/golangci-lint-2.4.0-linux-amd64.tar.gz"
|
|
||||||
|
|
||||||
[tools.golangci-lint.platforms.macos-arm64]
|
|
||||||
checksum = "sha256:cd4dd53fa09b6646baff5fd22b8c64d91db02c21c7496df27992d75d34feec59"
|
|
||||||
size = 13542526
|
|
||||||
url = "https://github.com/golangci/golangci-lint/releases/download/v2.4.0/golangci-lint-2.4.0-darwin-arm64.tar.gz"
|
|
||||||
|
|
||||||
[[tools.node]]
|
|
||||||
version = "22.20.0"
|
|
||||||
backend = "core:node"
|
|
||||||
|
|
||||||
[tools.node.platforms.linux-x64]
|
|
||||||
checksum = "sha256:eeaccb0378b79406f2208e8b37a62479c70595e20be6b659125eb77dd1ab2a29"
|
|
||||||
size = 56645685
|
|
||||||
url = "https://nodejs.org/dist/v22.20.0/node-v22.20.0-linux-x64.tar.gz"
|
|
||||||
|
|
||||||
[tools.node.platforms.macos-arm64]
|
|
||||||
checksum = "sha256:cc04a76a09f79290194c0646f48fec40354d88969bec467789a5d55dd097f949"
|
|
||||||
size = 49838299
|
|
||||||
url = "https://nodejs.org/dist/v22.20.0/node-v22.20.0-darwin-arm64.tar.gz"
|
|
||||||
|
|
||||||
[[tools.protoc]]
|
|
||||||
version = "27.3"
|
|
||||||
backend = "aqua:protocolbuffers/protobuf/protoc"
|
|
||||||
|
|
||||||
[tools.protoc.platforms.linux-x64]
|
|
||||||
checksum = "blake3:8c2938accf7061164ea3968529a47ced1db4824ca553857f6903c68ae4347739"
|
|
||||||
size = 3235873
|
|
||||||
url = "https://github.com/protocolbuffers/protobuf/releases/download/v27.3/protoc-27.3-linux-x86_64.zip"
|
|
||||||
|
|
||||||
[tools.protoc.platforms.macos-arm64]
|
|
||||||
checksum = "blake3:650c2494c003e9fddd6f4164a0a36e31da51ae7e1e5208da8cb560a4fe886fcd"
|
|
||||||
size = 2372763
|
|
||||||
url = "https://github.com/protocolbuffers/protobuf/releases/download/v27.3/protoc-27.3-osx-aarch_64.zip"
|
|
||||||
|
|
||||||
[[tools.protoc-gen-go]]
|
|
||||||
version = "1.34.2"
|
|
||||||
backend = "aqua:protocolbuffers/protobuf-go/protoc-gen-go"
|
|
||||||
|
|
||||||
[tools.protoc-gen-go.platforms.linux-x64]
|
|
||||||
checksum = "blake3:f765e393e0f0b25070c13848d88f5e6303e66090132afa1b753e981507a4174e"
|
|
||||||
size = 1848331
|
|
||||||
url = "https://github.com/protocolbuffers/protobuf-go/releases/download/v1.34.2/protoc-gen-go.v1.34.2.linux.amd64.tar.gz"
|
|
||||||
|
|
||||||
[tools.protoc-gen-go.platforms.macos-arm64]
|
|
||||||
checksum = "blake3:c237256a7f718ac4ccdc037ee30025a0b7c3f79300d7831ec59d76d782443fd5"
|
|
||||||
size = 1784028
|
|
||||||
url = "https://github.com/protocolbuffers/protobuf-go/releases/download/v1.34.2/protoc-gen-go.v1.34.2.darwin.arm64.tar.gz"
|
|
||||||
|
|
||||||
[[tools.protoc-gen-go-grpc]]
|
|
||||||
version = "1.5.1"
|
|
||||||
backend = "aqua:grpc/grpc-go/protoc-gen-go-grpc"
|
|
||||||
|
|
||||||
[tools.protoc-gen-go-grpc.platforms.linux-x64]
|
|
||||||
checksum = "blake3:197739132f0408f017d0c83773ac1ace85c0572473fdd658ec6e4ccbf20e0552"
|
|
||||||
size = 4859466
|
|
||||||
url = "https://github.com/grpc/grpc-go/releases/download/cmd/protoc-gen-go-grpc/v1.5.1/protoc-gen-go-grpc.v1.5.1.linux.amd64.tar.gz"
|
|
||||||
|
|
||||||
[tools.protoc-gen-go-grpc.platforms.macos-arm64]
|
|
||||||
checksum = "blake3:3c668207565d23ea2cf006d699207e3a804ebb929a534bd38a3b4ff4e941e00c"
|
|
||||||
size = 4565944
|
|
||||||
url = "https://github.com/grpc/grpc-go/releases/download/cmd/protoc-gen-go-grpc/v1.5.1/protoc-gen-go-grpc.v1.5.1.darwin.arm64.tar.gz"
|
|
||||||
-14
@@ -1,14 +0,0 @@
|
|||||||
[settings]
|
|
||||||
experimental = true
|
|
||||||
|
|
||||||
[tools]
|
|
||||||
"aqua:vektra/mockery" = "3.5.3"
|
|
||||||
go = "1.25.1"
|
|
||||||
golangci-lint = "2.4.0"
|
|
||||||
node = "22.20.0"
|
|
||||||
protoc = "27.3"
|
|
||||||
protoc-gen-go = "1.34.2"
|
|
||||||
protoc-gen-go-grpc = "1.5.1"
|
|
||||||
|
|
||||||
[env]
|
|
||||||
_.file = ".env"
|
|
||||||
Executable
+24
@@ -0,0 +1,24 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
#MISE description="Build and install uncloud binaries on dev machines, restart daemon if running"
|
||||||
|
#MISE depends=["build:uncloudd-amd64", "build:uncloudd-arm64"]
|
||||||
|
#MISE raw=true
|
||||||
|
#USAGE arg "<hosts>" var=#true var_min=1 help="Hosts in [user@]host[/arch] format, arch defaults to amd64"
|
||||||
|
|
||||||
|
for arg in ${usage_hosts}; do
|
||||||
|
if [[ "$arg" == */* ]]; then
|
||||||
|
host="${arg%/*}"
|
||||||
|
arch="${arg##*/}"
|
||||||
|
else
|
||||||
|
host="$arg"
|
||||||
|
arch="amd64"
|
||||||
|
fi
|
||||||
|
|
||||||
|
binary="uncloudd-linux-${arch}"
|
||||||
|
echo "==> Installing uncloudd on ${host} (${arch})..."
|
||||||
|
scp "$binary" "${host}:/tmp/"
|
||||||
|
ssh "$host" "sudo install /tmp/${binary} /usr/local/bin/uncloudd && rm /tmp/${binary} && sudo systemctl try-restart uncloud"
|
||||||
|
done
|
||||||
|
|
||||||
|
rm -f uncloudd-linux-amd64 uncloudd-linux-arm64
|
||||||
Executable
+14
@@ -0,0 +1,14 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
#MISE description="Stop uncloud daemon and wipe state on dev machines"
|
||||||
|
#USAGE arg "<hosts>" var=#true var_min=1 help="Hosts in [user@]host format"
|
||||||
|
|
||||||
|
for host in ${usage_hosts}; do
|
||||||
|
# Strip optional /arch suffix since reset doesn't need it.
|
||||||
|
host="${host%/*}"
|
||||||
|
echo "==> Resetting ${host}..."
|
||||||
|
ssh "$host" "sudo systemctl stop uncloud && sudo rm -rf /var/lib/uncloud"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "==> Done"
|
||||||
+38
-31
@@ -1,10 +1,14 @@
|
|||||||
# AI.md - Uncloud Project Guide
|
# AGENTS.md - Uncloud Project Guide
|
||||||
|
|
||||||
This document provides comprehensive information about the Uncloud project for AI assistants to understand the codebase, architecture, and development practices.
|
This document provides comprehensive information about the Uncloud project for AI assistants to understand the codebase,
|
||||||
|
architecture, and development practices.
|
||||||
|
|
||||||
## Project Overview
|
## 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.
|
**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
|
### Key Characteristics
|
||||||
|
|
||||||
@@ -68,29 +72,29 @@ This document provides comprehensive information about the Uncloud project for A
|
|||||||
|
|
||||||
- **`cmd/`**: Contains main applications
|
- **`cmd/`**: Contains main applications
|
||||||
|
|
||||||
- `uncloud/`: CLI tool with subcommands for machine, service, volume management
|
- `uncloud/`: CLI tool with subcommands for machine, service, volume management
|
||||||
- `uncloudd/`: Daemon that runs on each machine
|
- `uncloudd/`: Daemon that runs on each machine
|
||||||
- `ucind/`: Development cluster management for testing
|
- `ucind/`: Development cluster management for testing
|
||||||
|
|
||||||
- **`internal/`**: Internal implementation packages
|
- **`internal/`**: Internal implementation packages
|
||||||
|
|
||||||
- `cli/`: Command-line interface logic
|
- `cli/`: Command-line interface logic
|
||||||
- `machine/`: Machine lifecycle and state management
|
- `machine/`: Machine lifecycle and state management
|
||||||
- `daemon/`: Daemon implementation and gRPC services
|
- `daemon/`: Daemon implementation and gRPC services
|
||||||
- `dns/`: Internal DNS server for service discovery
|
- `dns/`: Internal DNS server for service discovery
|
||||||
|
|
||||||
- **`pkg/`**: Public API packages for external use
|
- **`pkg/`**: Public API packages for external use
|
||||||
|
|
||||||
- `api/`: Core API types and definitions
|
- `api/`: Core API types and definitions
|
||||||
- `client/`: Client libraries for interacting with Uncloud
|
- `client/`: Client libraries for interacting with Uncloud
|
||||||
|
|
||||||
- **`experiment/`**: Experimental features and prototypes
|
- **`experiment/`**: Experimental features and prototypes
|
||||||
- **`scripts/`**: Installation and utility scripts
|
- **`scripts/`**: Installation and utility scripts
|
||||||
- **`test/`**: Test suites and test infrastructure
|
- **`test/`**: Test suites and test infrastructure
|
||||||
- **`website/`**: Documentation website (Docusaurus)
|
- **`website/`**: Documentation website (Docusaurus)
|
||||||
|
|
||||||
- `landing/`: Landing page
|
- `landing/`: Landing page
|
||||||
- `docs/`: User documentation
|
- `docs/`: User documentation
|
||||||
|
|
||||||
- **`misc/`**: Design documents and guides
|
- **`misc/`**: Design documents and guides
|
||||||
|
|
||||||
@@ -100,25 +104,25 @@ This document provides comprehensive information about the Uncloud project for A
|
|||||||
|
|
||||||
```go
|
```go
|
||||||
// Networking and orchestration
|
// Networking and orchestration
|
||||||
github.com/docker/docker // Docker API client
|
github.com/docker/docker // Docker API client
|
||||||
github.com/docker/compose/v2 // Docker Compose integration
|
github.com/docker/compose/v2 // Docker Compose integration
|
||||||
golang.zx2c4.com/wireguard // WireGuard implementation
|
golang.zx2c4.com/wireguard // WireGuard implementation
|
||||||
github.com/hashicorp/serf // Gossip protocol
|
github.com/hashicorp/serf // Gossip protocol
|
||||||
|
|
||||||
// State management
|
// State management
|
||||||
github.com/ipfs/go-ds-crdt // CRDT distributed storage
|
github.com/ipfs/go-ds-crdt // CRDT distributed storage
|
||||||
github.com/dgraph-io/badger/v3 // Embedded database
|
github.com/dgraph-io/badger/v3 // Embedded database
|
||||||
|
|
||||||
// Web proxy
|
// Web proxy
|
||||||
github.com/caddyserver/caddy/v2 // HTTP server and reverse proxy
|
github.com/caddyserver/caddy/v2 // HTTP server and reverse proxy
|
||||||
|
|
||||||
// CLI and UX
|
// CLI and UX
|
||||||
github.com/spf13/cobra // CLI framework
|
github.com/spf13/cobra // CLI framework
|
||||||
github.com/charmbracelet/huh // Interactive forms
|
github.com/charmbracelet/huh // Interactive forms
|
||||||
|
|
||||||
// gRPC and networking
|
// gRPC and networking
|
||||||
google.golang.org/grpc // gRPC framework
|
google.golang.org/grpc // gRPC framework
|
||||||
github.com/siderolabs/grpc-proxy // gRPC proxy for forwarding
|
github.com/siderolabs/grpc-proxy // gRPC proxy for forwarding
|
||||||
```
|
```
|
||||||
|
|
||||||
## Development Workflow
|
## Development Workflow
|
||||||
@@ -189,9 +193,9 @@ uc context use <name> # Switch context
|
|||||||
### Testing
|
### Testing
|
||||||
|
|
||||||
- Test files and locations
|
- Test files and locations
|
||||||
- Unit tests alongside source files (`*_test.go`)
|
- Unit tests alongside source files (`*_test.go`)
|
||||||
- Integration tests in `test/e2e/`
|
- Integration tests in `test/e2e/`
|
||||||
- Test fixtures in `test/fixtures/`
|
- Test fixtures in `test/fixtures/`
|
||||||
- Use table driven tests whenever possible
|
- Use table driven tests whenever possible
|
||||||
|
|
||||||
### Dependencies
|
### Dependencies
|
||||||
@@ -225,8 +229,8 @@ uc context use <name> # Switch context
|
|||||||
### Logs and Monitoring
|
### Logs and Monitoring
|
||||||
|
|
||||||
- Systemd services (getting logs via `journalctl -u SERVICE_NAME`)
|
- Systemd services (getting logs via `journalctl -u SERVICE_NAME`)
|
||||||
- `uncloud` -- Uncloud daemon
|
- `uncloud` -- Uncloud daemon
|
||||||
- `uncloud-corrosion` -- Corrosion process
|
- `uncloud-corrosion` -- Corrosion process
|
||||||
- Machine daemon logs
|
- Machine daemon logs
|
||||||
- Container logs via Docker
|
- Container logs via Docker
|
||||||
|
|
||||||
@@ -247,7 +251,8 @@ uc context use <name> # Switch context
|
|||||||
- `Makefile`: Build and development tasks
|
- `Makefile`: Build and development tasks
|
||||||
- `Dockerfile`: Container build instructions forUncloud-in-Docker (used for testing)
|
- `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.
|
This document should help AI assistants understand the project structure, make informed suggestions, and contribute
|
||||||
|
effectively to the Uncloud codebase.
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
@@ -255,4 +260,6 @@ Instructions when generating documentation:
|
|||||||
|
|
||||||
- Use conversational language – write as if you were speaking to a friend.
|
- Use conversational language – write as if you were speaking to a friend.
|
||||||
- Keep sentences simple, optimize for clarity and understanding.
|
- Keep sentences simple, optimize for clarity and understanding.
|
||||||
- Place the subject before the action whenever possible. Example: prefer “The function loads data” over “Data is loaded by the function.”
|
- Do not use em dashes (—) or semicolons (;) in sentences. Instead, break complex sentences into simpler ones.
|
||||||
|
- Place the subject before the action whenever possible. Example: prefer "The function loads data" over "Data is loaded
|
||||||
|
by the function."
|
||||||
+5
-4
@@ -1,6 +1,6 @@
|
|||||||
ARG ALPINE_VERSION=3.20.3
|
ARG ALPINE_VERSION=3.23.3
|
||||||
|
|
||||||
FROM golang:1.25.1-alpine AS uncloudd
|
FROM golang:1.26.1-alpine AS uncloudd
|
||||||
|
|
||||||
ARG TARGETOS
|
ARG TARGETOS
|
||||||
ARG TARGETARCH
|
ARG TARGETARCH
|
||||||
@@ -39,10 +39,11 @@ CMD ["corrosion", "agent"]
|
|||||||
|
|
||||||
FROM alpine:${ALPINE_VERSION} AS corrosion-image-tarball
|
FROM alpine:${ALPINE_VERSION} AS corrosion-image-tarball
|
||||||
ARG CORROSION_IMAGE="ghcr.io/psviderski/corrosion:latest"
|
ARG CORROSION_IMAGE="ghcr.io/psviderski/corrosion:latest"
|
||||||
|
ARG TARGETOS
|
||||||
|
ARG TARGETARCH
|
||||||
|
|
||||||
RUN apk --no-cache add crane
|
RUN apk --no-cache add crane
|
||||||
RUN crane pull "${CORROSION_IMAGE}" /corrosion.tar
|
RUN crane pull --platform ${TARGETOS}/${TARGETARCH} "${CORROSION_IMAGE}" /corrosion.tar
|
||||||
|
|
||||||
|
|
||||||
# Uncloud-in-Docker (ucind) image for running Uncloud test clusters using Docker.
|
# Uncloud-in-Docker (ucind) image for running Uncloud test clusters using Docker.
|
||||||
FROM docker:27.3.1-dind AS ucind
|
FROM docker:27.3.1-dind AS ucind
|
||||||
|
|||||||
+154
@@ -0,0 +1,154 @@
|
|||||||
|
# Developing Uncloud
|
||||||
|
|
||||||
|
A short guide on setting up your local development environment.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- [mise](https://mise.jdx.dev/) (development environment setup tool)
|
||||||
|
- [Docker](https://docs.docker.com/get-docker/) (for running end-to-end tests)
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
Uncloud uses `mise` to install and manage development tools and dependencies. It makes it easy to create reproducible
|
||||||
|
development environments on developer machines and CI.
|
||||||
|
|
||||||
|
Follow the [Installing Mise](https://mise.jdx.dev/installing-mise.html) guide to get it set up on your machine.
|
||||||
|
|
||||||
|
Mark the project as trusted and install all required tools (Go, `protoc`, `golangci-lint`, etc.) by running from the
|
||||||
|
project root:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
mise trust
|
||||||
|
mise install
|
||||||
|
```
|
||||||
|
|
||||||
|
This reads [`mise.toml`](mise.toml) and installs the exact versions of tools specified there. They will be available in
|
||||||
|
your `PATH` when you are in the project directory, so you can run `go` or `golangci-lint` directly. The tools won't be
|
||||||
|
available outside the project, so they don't interfere with system packages.
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
Build the CLI:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
go build -o uc ./cmd/uncloud
|
||||||
|
```
|
||||||
|
|
||||||
|
Or build and run the CLI with a single command:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
go run ./cmd/uncloud --help
|
||||||
|
```
|
||||||
|
|
||||||
|
The Uncloud daemon (`uncloudd`) only supports Linux, so you need to cross-compile it if you're developing on macOS or
|
||||||
|
Windows:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
GOOS=linux GOARCH=amd64 go build -o uncloudd ./cmd/uncloudd
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
Run all tests (unit and e2e):
|
||||||
|
|
||||||
|
```shell
|
||||||
|
make test
|
||||||
|
```
|
||||||
|
|
||||||
|
### End-to-end tests
|
||||||
|
|
||||||
|
E2e tests run inside Docker using the [Uncloud-in-Docker](#uncloud-in-docker-ucind) image. Build it first:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
mise ucind:image
|
||||||
|
```
|
||||||
|
|
||||||
|
⚠️ NOTE: You need to rebuild the `ucind` image every time you make changes to the daemon code if you want to test them
|
||||||
|
in e2e tests.
|
||||||
|
|
||||||
|
Then run the tests:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
make test-e2e
|
||||||
|
```
|
||||||
|
|
||||||
|
If tests leave leftover containers, clean them up with:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
make test-clean
|
||||||
|
```
|
||||||
|
|
||||||
|
### Manual testing on dev machines
|
||||||
|
|
||||||
|
After making changes to the daemon code, you may want to test them in your dev cluster. Build and install `uncloudd` on
|
||||||
|
one or multiple dev machines over SSH with:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
mise run dev:install user@host1 user@host2/arm64
|
||||||
|
```
|
||||||
|
|
||||||
|
It will also restart the `uncloud.service` daemon on those machines to pick up the new version. The host format is
|
||||||
|
`[user@]host[/arch]`. The architecture of a machine defaults to `amd64` if not specified.
|
||||||
|
|
||||||
|
To stop the daemon and wipe all Uncloud data on the dev machines, run:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
mise run dev:reset user@host1 user@host2
|
||||||
|
```
|
||||||
|
|
||||||
|
## Linting and formatting
|
||||||
|
|
||||||
|
Lint and format the code:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
make lint-and-fix
|
||||||
|
```
|
||||||
|
|
||||||
|
## Code generation
|
||||||
|
|
||||||
|
Update the generated Go code for the machine gRPC API after modifying `.proto` files:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
mise run proto
|
||||||
|
```
|
||||||
|
|
||||||
|
Regenerate mocks:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
make mocks
|
||||||
|
```
|
||||||
|
|
||||||
|
## Uncloud in Docker (UCinD)
|
||||||
|
|
||||||
|
The [`ucind`](./cmd/ucind) CLI lets you run Uncloud clusters locally using Docker containers instead of real machines.
|
||||||
|
Each cluster machine runs as a Docker container connected to a shared Docker network.
|
||||||
|
|
||||||
|
Cluster machines use the `ghcr.io/psviderski/ucind:latest` Docker image by default. It may not always have the
|
||||||
|
latest changes to the `uncloudd` daemon code as it's not automatically rebuilt on CI yet.
|
||||||
|
|
||||||
|
You can rebuild it locally to pick up the latest changes, including your work in progress, and test them in a UCinD
|
||||||
|
cluster:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
mise ucind:image
|
||||||
|
```
|
||||||
|
|
||||||
|
Create a cluster with 3 machines:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
mise ucind cluster create -m 3
|
||||||
|
```
|
||||||
|
|
||||||
|
This also sets the new cluster as the active context, so you can interact with it using the `uc` CLI right away.
|
||||||
|
|
||||||
|
Remove the cluster when you're done:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
mise ucind cluster rm
|
||||||
|
```
|
||||||
|
|
||||||
|
Sometimes crashed tests or interrupted runs can leave orphaned containers and networks behind. Clean them all up with:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
mise ucind:cleanup
|
||||||
|
```
|
||||||
@@ -1,55 +1,12 @@
|
|||||||
|
# TODO: Makefile is deprecated, add new targets as mise tasks in mise.toml instead.
|
||||||
|
|
||||||
CORROSION_IMAGE ?= ghcr.io/psviderski/corrosion:latest
|
CORROSION_IMAGE ?= ghcr.io/psviderski/corrosion:latest
|
||||||
UCIND_IMAGE ?= ghcr.io/psviderski/ucind:latest
|
UCIND_IMAGE ?= ghcr.io/psviderski/ucind:latest
|
||||||
|
|
||||||
update-dev:
|
|
||||||
GOOS=linux GOARCH=amd64 go build -o uncloudd-linux-amd64 ./cmd/uncloudd && \
|
|
||||||
scp uncloudd-linux-amd64 spy@192.168.40.243:~/ && \
|
|
||||||
ssh spy@192.168.40.243 sudo install ./uncloudd-linux-amd64 /usr/local/bin/uncloudd
|
|
||||||
scp uncloudd-linux-amd64 spy@192.168.40.176:~/ && \
|
|
||||||
ssh spy@192.168.40.176 sudo install ./uncloudd-linux-amd64 /usr/local/bin/uncloudd && \
|
|
||||||
rm uncloudd-linux-amd64
|
|
||||||
# GOOS=linux GOARCH=arm64 go build -o uncloudd-linux-arm64 ./cmd/uncloudd && \
|
|
||||||
# scp uncloudd-linux-arm64 ubuntu@152.67.101.197:~/ && \
|
|
||||||
# ssh ubuntu@152.67.101.197 sudo install ./uncloudd-linux-arm64 /usr/local/bin/uncloudd && \
|
|
||||||
# rm uncloudd-linux-arm64
|
|
||||||
|
|
||||||
update-restart-dev:
|
|
||||||
GOOS=linux GOARCH=amd64 go build -o uncloudd-linux-amd64 ./cmd/uncloudd && \
|
|
||||||
scp uncloudd-linux-amd64 spy@192.168.40.243:~/ && \
|
|
||||||
ssh spy@192.168.40.243 "sudo install ./uncloudd-linux-amd64 /usr/local/bin/uncloudd && sudo systemctl restart uncloud" && \
|
|
||||||
scp uncloudd-linux-amd64 spy@192.168.40.176:~/ && \
|
|
||||||
ssh spy@192.168.40.176 "sudo install ./uncloudd-linux-amd64 /usr/local/bin/uncloudd && sudo systemctl restart uncloud" && \
|
|
||||||
rm uncloudd-linux-amd64
|
|
||||||
# GOOS=linux GOARCH=arm64 go build -o uncloudd-linux-arm64 ./cmd/uncloudd && \
|
|
||||||
# scp uncloudd-linux-arm64 ubuntu@152.67.101.197:~/ && \
|
|
||||||
# ssh ubuntu@152.67.101.197 "sudo install ./uncloudd-linux-arm64 /usr/local/bin/uncloudd && sudo systemctl restart uncloud" && \
|
|
||||||
# rm uncloudd-linux-arm64
|
|
||||||
|
|
||||||
reset-dev:
|
|
||||||
ssh spy@192.168.40.243 "sudo systemctl stop uncloud && sudo rm -rf /var/lib/uncloud"
|
|
||||||
ssh spy@192.168.40.176 "sudo systemctl stop uncloud && sudo rm -rf /var/lib/uncloud"
|
|
||||||
ssh ubuntu@152.67.101.197 "sudo systemctl stop uncloud && sudo rm -rf /var/lib/uncloud"
|
|
||||||
|
|
||||||
demo-reset:
|
|
||||||
rm -fv ~/.config/uncloud/config.yaml
|
|
||||||
ssh ubuntu@152.67.101.197 "AUTO_CONFIRM=true sudo -E uncloud-uninstall && docker rmi caddy:2.9.1"
|
|
||||||
ssh root@5.223.45.199 "AUTO_CONFIRM=true sudo -E uncloud-uninstall"
|
|
||||||
ssh spy@192.168.40.243 "AUTO_CONFIRM=true sudo -E uncloud-uninstall"
|
|
||||||
|
|
||||||
.PHONY: ucind-cluster
|
.PHONY: ucind-cluster
|
||||||
ucind-cluster:
|
ucind-cluster:
|
||||||
go run ./cmd/ucind cluster rm && go run ./cmd/ucind cluster create -m $(if $(MACHINES_COUNT),$(MACHINES_COUNT),3)
|
go run ./cmd/ucind cluster rm && go run ./cmd/ucind cluster create -m $(if $(MACHINES_COUNT),$(MACHINES_COUNT),3)
|
||||||
|
|
||||||
.PHONY: proto
|
|
||||||
proto:
|
|
||||||
protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative \
|
|
||||||
--proto_path=. --proto_path=internal/machine/api/vendor internal/machine/api/pb/*.proto
|
|
||||||
|
|
||||||
.PHONY: 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 .
|
||||||
@@ -58,10 +15,6 @@ corrosion-image:
|
|||||||
corrosion-multiarch-image-push:
|
corrosion-multiarch-image-push:
|
||||||
docker buildx build --push --platform linux/amd64,linux/arm64 -t "$(CORROSION_IMAGE)" --target corrosion .
|
docker buildx build --push --platform linux/amd64,linux/arm64 -t "$(CORROSION_IMAGE)" --target corrosion .
|
||||||
|
|
||||||
.PHONY: ucind-image
|
|
||||||
ucind-image:
|
|
||||||
docker build -t "$(UCIND_IMAGE)" --target ucind .
|
|
||||||
|
|
||||||
.PHONY: ucind-multiarch-image-push
|
.PHONY: ucind-multiarch-image-push
|
||||||
ucind-multiarch-image-push:
|
ucind-multiarch-image-push:
|
||||||
docker buildx build --push --platform linux/amd64,linux/arm64 -t "$(UCIND_IMAGE)" --target ucind .
|
docker buildx build --push --platform linux/amd64,linux/arm64 -t "$(UCIND_IMAGE)" --target ucind .
|
||||||
@@ -73,23 +26,23 @@ mocks:
|
|||||||
.PHONY: test
|
.PHONY: test
|
||||||
test:
|
test:
|
||||||
ifeq ($(TEST_NAME),)
|
ifeq ($(TEST_NAME),)
|
||||||
go test -count=1 -v ./...
|
go test -shuffle=on -count=1 -v ./...
|
||||||
else
|
else
|
||||||
go test -race -count=1 -v -run ^$(TEST_NAME)$$ ./...
|
go test -shuffle=on -race -count=1 -v -run ^$(TEST_NAME)$$ ./...
|
||||||
endif
|
endif
|
||||||
|
|
||||||
.PHONY: test-e2e
|
.PHONY: test-e2e
|
||||||
test-e2e:
|
test-e2e:
|
||||||
go test -race -count=1 -v ./test/e2e
|
go test -shuffle=on -race -count=1 -v ./test/e2e
|
||||||
|
|
||||||
.PHONY: test-clean
|
.PHONY: test-clean
|
||||||
test-clean:
|
test-clean:
|
||||||
@CONTAINERS=$$(docker ps --filter "name=ucind-test" -q); \
|
@CONTAINERS=$$(docker ps --filter "label=ucind.managed" -q); \
|
||||||
if [ -n "$$CONTAINERS" ]; then \
|
if [ -n "$$CONTAINERS" ]; then \
|
||||||
echo "Killing containers..."; \
|
echo "Killing containers..."; \
|
||||||
docker kill $$CONTAINERS; \
|
docker kill $$CONTAINERS; \
|
||||||
fi; \
|
fi; \
|
||||||
CONTAINERS_STOPPED=$$(docker ps -a --filter "name=ucind-test" -q); \
|
CONTAINERS_STOPPED=$$(docker ps -a --filter "label=ucind.managed" -q); \
|
||||||
if [ -n "$$CONTAINERS_STOPPED" ]; then \
|
if [ -n "$$CONTAINERS_STOPPED" ]; then \
|
||||||
echo "Removing stopped containers..."; \
|
echo "Removing stopped containers..."; \
|
||||||
docker rm $$CONTAINERS_STOPPED; \
|
docker rm $$CONTAINERS_STOPPED; \
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<div align="center">
|
<div align="center">
|
||||||
<img src="./website/landing/images/logo-title.svg#gh-light-mode-only" alt="Uncloud logo"/>
|
<img src="./website/landing/images/logo-title.svg#gh-light-mode-only" alt="Uncloud logo"/>
|
||||||
<img src="./website/landing/images/logo-title-dark.svg#gh-dark-mode-only" alt="Uncloud logo"/>
|
<img src="./website/landing/images/logo-title-dark.svg#gh-dark-mode-only" alt="Uncloud logo"/>
|
||||||
<p><strong>▸ Docker simplicity. Multi-machine power ◂</strong></p>
|
<p><strong>▸ Deploy and scale containerised apps across servers without Swarm or Kubernetes overhead ◂</strong></p>
|
||||||
|
|
||||||
<p>
|
<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://uncloud.run/docs"><img src="https://img.shields.io/badge/Docs-blue.svg?style=for-the-badge&logo=gitbook&logoColor=white" alt="Documentation"></a>
|
||||||
@@ -61,7 +61,7 @@ Here is a more advanced use case. Deploy a highly available web app with automat
|
|||||||
on-premises in just a couple minutes.
|
on-premises in just a couple minutes.
|
||||||
|
|
||||||
<a href="https://uncloud.wistia.com/medias/k47uwt9uau?wvideo=k47uwt9uau">
|
<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" />
|
<img src="https://embed-ssl.wistia.com/deliveries/4fb71ea442905b81b1ad4e25fc02132e.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>
|
</a>
|
||||||
|
|
||||||
<br>
|
<br>
|
||||||
@@ -281,6 +281,16 @@ using the CLI or directly over SSH. They all have the complete cluster state and
|
|||||||
machine is a full backup of your control plane.
|
machine is a full backup of your control plane.
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
|
## 🧪 Interactive tutorials
|
||||||
|
|
||||||
|
To give you a chance to play with Uncloud without even leaving your browser, we're providing interactive tutorials and playgrounds on the [iximiuz Labs](https://labs.iximiuz.com/) platform.
|
||||||
|
|
||||||
|
Available tutorials:
|
||||||
|
|
||||||
|
1. [Setting up a new Uncloud cluster](https://labs.iximiuz.com/tutorials/uncloud-create-cluster-ebebf72b) - the tutorial walks you through creating a new cluster with two machines and then deploying a simple web service to it.
|
||||||
|
|
||||||
|
You can also launch the [Uncloud playground](https://labs.iximiuz.com/playgrounds/uncloud-cluster-64523f7c) where you can play with an already initialized Uncloud cluster.
|
||||||
|
|
||||||
## 🏗 Project status
|
## 🏗 Project status
|
||||||
|
|
||||||
Uncloud is currently in active development and is **not ready for production use**. Features may change significantly
|
Uncloud is currently in active development and is **not ready for production use**. Features may change significantly
|
||||||
|
|||||||
@@ -98,6 +98,8 @@ func runBuild(ctx context.Context, uncli *cli.CLI, opts buildOptions) error {
|
|||||||
return fmt.Errorf("load compose file(s): %w", err)
|
return fmt.Errorf("load compose file(s): %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
uncli.SetClusterContextIfUnset(compose.ClusterContext(project))
|
||||||
|
|
||||||
servicesToBuild, err := cli.ServicesThatNeedBuild(project, opts.Services, opts.Deps)
|
servicesToBuild, err := cli.ServicesThatNeedBuild(project, opts.Services, opts.Deps)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("determine services to build: %w", err)
|
return fmt.Errorf("determine services to build: %w", err)
|
||||||
|
|||||||
+47
-15
@@ -9,9 +9,11 @@ import (
|
|||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"charm.land/lipgloss/v2"
|
||||||
"github.com/docker/cli/cli/streams"
|
"github.com/docker/cli/cli/streams"
|
||||||
"github.com/docker/compose/v2/pkg/progress"
|
"github.com/docker/compose/v2/pkg/progress"
|
||||||
"github.com/psviderski/uncloud/internal/cli"
|
"github.com/psviderski/uncloud/internal/cli"
|
||||||
|
"github.com/psviderski/uncloud/internal/cli/tui"
|
||||||
"github.com/psviderski/uncloud/internal/machine/api/pb"
|
"github.com/psviderski/uncloud/internal/machine/api/pb"
|
||||||
"github.com/psviderski/uncloud/pkg/api"
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
"github.com/psviderski/uncloud/pkg/client"
|
"github.com/psviderski/uncloud/pkg/client"
|
||||||
@@ -70,9 +72,11 @@ func runDeploy(ctx context.Context, uncli *cli.CLI, opts deployOptions) error {
|
|||||||
if !errors.Is(err, api.ErrNotFound) {
|
if !errors.Is(err, api.ErrNotFound) {
|
||||||
return fmt.Errorf("inspect caddy service: %w", err)
|
return fmt.Errorf("inspect caddy service: %w", err)
|
||||||
}
|
}
|
||||||
fmt.Printf("Service: %s (not running)\n", client.CaddyServiceName)
|
fmt.Println(tui.Faint.Render("service: ") + tui.NameStyle.Render(client.CaddyServiceName) +
|
||||||
|
tui.Faint.Render(" (not running)"))
|
||||||
} else {
|
} else {
|
||||||
fmt.Printf("Service: %s (%s mode)\n", svc.Name, svc.Mode)
|
fmt.Println(tui.Faint.Render("service: ") + tui.NameStyle.Render(svc.Name) +
|
||||||
|
tui.Faint.Render(" ("+svc.Mode+" mode)"))
|
||||||
|
|
||||||
// Collect unique images of all containers in the running caddy service.
|
// Collect unique images of all containers in the running caddy service.
|
||||||
images := make(map[string]struct{}, len(svc.Containers))
|
images := make(map[string]struct{}, len(svc.Containers))
|
||||||
@@ -82,15 +86,19 @@ func runDeploy(ctx context.Context, uncli *cli.CLI, opts deployOptions) error {
|
|||||||
currentImages := slices.Collect(maps.Keys(images))
|
currentImages := slices.Collect(maps.Keys(images))
|
||||||
|
|
||||||
if len(currentImages) > 1 {
|
if len(currentImages) > 1 {
|
||||||
commaSeparatedImages := strings.Join(currentImages, ", ")
|
formattedImages := make([]string, len(currentImages))
|
||||||
fmt.Printf("Current images (multiple versions detected): %s\n", commaSeparatedImages)
|
for i, img := range currentImages {
|
||||||
|
formattedImages[i] = tui.FormatImage(img, lipgloss.NewStyle())
|
||||||
|
}
|
||||||
|
fmt.Println(tui.Faint.Render("current images (multiple versions detected): ") +
|
||||||
|
strings.Join(formattedImages, tui.Faint.Render(", ")))
|
||||||
} else {
|
} else {
|
||||||
fmt.Printf("Current image: %s\n", currentImages[0])
|
fmt.Println(tui.Faint.Render("current image: ") + tui.FormatImage(currentImages[0], lipgloss.NewStyle()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if opts.image != "" {
|
if opts.image != "" {
|
||||||
fmt.Printf("Target image: %s\n", opts.image)
|
fmt.Println(tui.Faint.Render("target image: ") + tui.FormatImage(opts.image, tui.Green))
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
@@ -105,7 +113,8 @@ func runDeploy(ctx context.Context, uncli *cli.CLI, opts deployOptions) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if opts.image == "" {
|
if opts.image == "" {
|
||||||
fmt.Printf("Target image: %s (latest stable)\n", d.Spec.Container.Image)
|
fmt.Println(tui.Faint.Render("target image: ") + tui.FormatImage(d.Spec.Container.Image,
|
||||||
|
tui.Green) + tui.Faint.Render(" (latest stable)"))
|
||||||
}
|
}
|
||||||
|
|
||||||
plan, err := d.Plan(ctx)
|
plan, err := d.Plan(ctx)
|
||||||
@@ -124,18 +133,41 @@ func runDeploy(ctx context.Context, uncli *cli.CLI, opts deployOptions) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialise a machine and container name resolver to properly format the plan output.
|
fmt.Println()
|
||||||
resolver, err := clusterClient.ServiceOperationNameResolver(ctx, svc)
|
fmt.Println(tui.Bold.Underline(true).Render("Deployment plan"))
|
||||||
if err != nil {
|
fmt.Println()
|
||||||
return fmt.Errorf("create machine and container name resolver for service operations: %w", err)
|
|
||||||
|
directConn := uncli.DirectConnection()
|
||||||
|
contextName := uncli.ContextOverrideOrCurrent()
|
||||||
|
deployTarget := ""
|
||||||
|
if directConn != "" {
|
||||||
|
deployTarget = directConn
|
||||||
|
fmt.Println(tui.Faint.Render("connection: ") + tui.NameStyle.Render(directConn))
|
||||||
|
fmt.Println()
|
||||||
|
} else if contextName != "" && len(uncli.Config.Contexts) > 1 {
|
||||||
|
// Only show context if there's more than one to avoid unnecessary clutter.
|
||||||
|
deployTarget = contextName
|
||||||
|
fmt.Println(tui.Faint.Render("context: ") + tui.NameStyle.Render(contextName))
|
||||||
|
fmt.Println()
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println()
|
fmt.Println(plan.Format())
|
||||||
fmt.Println("Deployment plan:")
|
|
||||||
fmt.Println(plan.Format(resolver))
|
summary := plan.FormatSummary()
|
||||||
|
fmt.Println(tui.Faint.Render(strings.Repeat("─", lipgloss.Width(summary))))
|
||||||
|
fmt.Println(summary)
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
|
|
||||||
confirmed, err := cli.Confirm()
|
title := "Proceed with deployment?"
|
||||||
|
// Include the direct connection or context name in the confirmation prompt to avoid accidentally
|
||||||
|
// deploying to the wrong cluster.
|
||||||
|
if deployTarget != "" {
|
||||||
|
isDark := lipgloss.HasDarkBackground(os.Stdin, os.Stdout)
|
||||||
|
confirmStyle := tui.ThemeConfirm().Theme(isDark).Focused.Title
|
||||||
|
title = "Proceed with deployment to " + tui.NameStyle.Render(deployTarget) + confirmStyle.Render("?")
|
||||||
|
}
|
||||||
|
|
||||||
|
confirmed, err := tui.Confirm(title)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("confirm deployment: %w", err)
|
return fmt.Errorf("confirm deployment: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package context
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/charmbracelet/huh"
|
"charm.land/huh/v2"
|
||||||
"github.com/psviderski/uncloud/internal/cli"
|
"github.com/psviderski/uncloud/internal/cli"
|
||||||
"github.com/psviderski/uncloud/internal/cli/config"
|
"github.com/psviderski/uncloud/internal/cli/config"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
@@ -11,8 +11,9 @@ import (
|
|||||||
|
|
||||||
func NewConnectionCommand() *cobra.Command {
|
func NewConnectionCommand() *cobra.Command {
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "connection",
|
Use: "connection",
|
||||||
Short: "Choose a new default connection for the current context.",
|
Aliases: []string{"conn"},
|
||||||
|
Short: "Choose a new default connection for the current context.",
|
||||||
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 selectConnection(uncli)
|
return selectConnection(uncli)
|
||||||
@@ -59,7 +60,7 @@ func selectConnection(uncli *cli.CLI) error {
|
|||||||
return fmt.Errorf("save config: %w", err)
|
return fmt.Errorf("save config: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("Default connection for context '%s' is now '%s'.\n", currentCtxName, selected)
|
fmt.Printf("Default connection for context '%s' is now '%s'.\n", currentCtxName, selected.String())
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
"maps"
|
"maps"
|
||||||
"slices"
|
"slices"
|
||||||
|
|
||||||
"github.com/charmbracelet/huh"
|
"charm.land/huh/v2"
|
||||||
"github.com/psviderski/uncloud/internal/cli"
|
"github.com/psviderski/uncloud/internal/cli"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|||||||
+53
-53
@@ -4,13 +4,13 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"os"
|
||||||
|
|
||||||
"github.com/charmbracelet/lipgloss"
|
"charm.land/lipgloss/v2"
|
||||||
composecli "github.com/compose-spec/compose-go/v2/cli"
|
composecli "github.com/compose-spec/compose-go/v2/cli"
|
||||||
"github.com/docker/compose/v2/pkg/progress"
|
"github.com/docker/compose/v2/pkg/progress"
|
||||||
"github.com/psviderski/uncloud/internal/cli"
|
"github.com/psviderski/uncloud/internal/cli"
|
||||||
"github.com/psviderski/uncloud/pkg/api"
|
"github.com/psviderski/uncloud/internal/cli/tui"
|
||||||
"github.com/psviderski/uncloud/pkg/client"
|
"github.com/psviderski/uncloud/pkg/client"
|
||||||
"github.com/psviderski/uncloud/pkg/client/compose"
|
"github.com/psviderski/uncloud/pkg/client/compose"
|
||||||
"github.com/psviderski/uncloud/pkg/client/deploy"
|
"github.com/psviderski/uncloud/pkg/client/deploy"
|
||||||
@@ -20,12 +20,13 @@ import (
|
|||||||
type deployOptions struct {
|
type deployOptions struct {
|
||||||
cli.BuildServicesOptions
|
cli.BuildServicesOptions
|
||||||
|
|
||||||
files []string
|
files []string
|
||||||
profiles []string
|
profiles []string
|
||||||
services []string
|
services []string
|
||||||
noBuild bool
|
noBuild bool
|
||||||
recreate bool
|
recreate bool
|
||||||
yes bool
|
skipHealth bool
|
||||||
|
yes bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewDeployCommand creates a new command to deploy services from a Compose file.
|
// NewDeployCommand creates a new command to deploy services from a Compose file.
|
||||||
@@ -60,6 +61,10 @@ func NewDeployCommand() *cobra.Command {
|
|||||||
"One or more Compose profiles to enable.")
|
"One or more Compose profiles to enable.")
|
||||||
cmd.Flags().BoolVar(&opts.recreate, "recreate", false,
|
cmd.Flags().BoolVar(&opts.recreate, "recreate", false,
|
||||||
"Recreate containers even if their configuration and image haven't changed.")
|
"Recreate containers even if their configuration and image haven't changed.")
|
||||||
|
cmd.Flags().BoolVar(&opts.skipHealth, "skip-health", false,
|
||||||
|
"Skip the monitoring period and health checks after starting new containers. Useful for faster emergency "+
|
||||||
|
"deployments.\n"+
|
||||||
|
"Warning: This may cause downtime if new containers fail to start properly.")
|
||||||
cmd.Flags().BoolVarP(&opts.yes, "yes", "y", false,
|
cmd.Flags().BoolVarP(&opts.yes, "yes", "y", false,
|
||||||
"Auto-confirm deployment plan. Should be explicitly set when running non-interactively,\n"+
|
"Auto-confirm deployment plan. Should be explicitly set when running non-interactively,\n"+
|
||||||
"e.g., in CI/CD pipelines. [$UNCLOUD_AUTO_CONFIRM]")
|
"e.g., in CI/CD pipelines. [$UNCLOUD_AUTO_CONFIRM]")
|
||||||
@@ -77,6 +82,8 @@ func runDeploy(ctx context.Context, uncli *cli.CLI, opts deployOptions) error {
|
|||||||
return fmt.Errorf("load compose file(s): %w", err)
|
return fmt.Errorf("load compose file(s): %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
uncli.SetClusterContextIfUnset(compose.ClusterContext(project))
|
||||||
|
|
||||||
if len(opts.services) > 0 {
|
if len(opts.services) > 0 {
|
||||||
// Includes service dependencies by default. This is the default docker compose behavior.
|
// Includes service dependencies by default. This is the default docker compose behavior.
|
||||||
project, err = project.WithSelectedServices(opts.services)
|
project, err = project.WithSelectedServices(opts.services)
|
||||||
@@ -148,9 +155,9 @@ func runDeploy(ctx context.Context, uncli *cli.CLI, opts deployOptions) error {
|
|||||||
fmt.Println()
|
fmt.Println()
|
||||||
}
|
}
|
||||||
|
|
||||||
var strategy deploy.Strategy
|
strategy := &deploy.RollingStrategy{
|
||||||
if opts.recreate {
|
ForceRecreate: opts.recreate,
|
||||||
strategy = &deploy.RollingStrategy{ForceRecreate: true}
|
SkipHealthMonitor: opts.skipHealth,
|
||||||
}
|
}
|
||||||
composeDeploy, err := compose.NewDeploymentWithStrategy(ctx, clusterClient, project, strategy)
|
composeDeploy, err := compose.NewDeploymentWithStrategy(ctx, clusterClient, project, strategy)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -162,25 +169,47 @@ func runDeploy(ctx context.Context, uncli *cli.CLI, opts deployOptions) error {
|
|||||||
return fmt.Errorf("plan deployment: %w", err)
|
return fmt.Errorf("plan deployment: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(plan.Operations) == 0 {
|
if plan.IsEmpty() {
|
||||||
fmt.Println("Services are up to date.")
|
fmt.Println("Services are up to date.")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println(lipgloss.NewStyle().Bold(true).Render("Deployment plan"))
|
fmt.Println(tui.Bold.Underline(true).Render("Deployment plan"))
|
||||||
if err = printPlan(ctx, clusterClient, plan); err != nil {
|
|
||||||
return fmt.Errorf("print deployment plan: %w", err)
|
|
||||||
}
|
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
|
|
||||||
|
directConn := uncli.DirectConnection()
|
||||||
|
contextName := uncli.ContextOverrideOrCurrent()
|
||||||
|
deployTarget := ""
|
||||||
|
if directConn != "" {
|
||||||
|
deployTarget = directConn
|
||||||
|
fmt.Println(tui.Faint.Render("connection: ") + tui.NameStyle.Render(directConn))
|
||||||
|
fmt.Println()
|
||||||
|
} else if contextName != "" && len(uncli.Config.Contexts) > 1 {
|
||||||
|
// Only show context if there's more than one to avoid unnecessary clutter.
|
||||||
|
deployTarget = contextName
|
||||||
|
fmt.Println(tui.Faint.Render("context: ") + tui.NameStyle.Render(contextName))
|
||||||
|
fmt.Println()
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println(plan.Format())
|
||||||
|
|
||||||
// Ask for plan confirmation before proceeding with the deployment unless auto-confirmed with --yes.
|
// Ask for plan confirmation before proceeding with the deployment unless auto-confirmed with --yes.
|
||||||
if !opts.yes {
|
if !opts.yes {
|
||||||
if !cli.IsStdinTerminal() {
|
if !tui.IsStdinTerminal() {
|
||||||
return errors.New("cannot ask to confirm deployment plan in non-interactive mode, " +
|
return errors.New("cannot ask to confirm deployment plan in non-interactive mode, " +
|
||||||
"use --yes flag or set UNCLOUD_AUTO_CONFIRM=true to auto-confirm")
|
"use --yes flag or set UNCLOUD_AUTO_CONFIRM=true to auto-confirm")
|
||||||
}
|
}
|
||||||
|
|
||||||
confirmed, err := cli.Confirm()
|
title := "Proceed with deployment?"
|
||||||
|
// Include the direct connection or context name in the confirmation prompt to avoid accidentally
|
||||||
|
// deploying to the wrong cluster.
|
||||||
|
if deployTarget != "" {
|
||||||
|
isDark := lipgloss.HasDarkBackground(os.Stdin, os.Stdout)
|
||||||
|
confirmStyle := tui.ThemeConfirm().Theme(isDark).Focused.Title
|
||||||
|
title = "Proceed with deployment to " + tui.NameStyle.Render(deployTarget) + confirmStyle.Render("?")
|
||||||
|
}
|
||||||
|
|
||||||
|
confirmed, err := tui.Confirm(title)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("confirm deployment: %w", err)
|
return fmt.Errorf("confirm deployment: %w", err)
|
||||||
}
|
}
|
||||||
@@ -190,43 +219,14 @@ func runDeploy(ctx context.Context, uncli *cli.CLI, opts deployOptions) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
title := "Deploying"
|
||||||
|
if deployTarget != "" {
|
||||||
|
title += " to " + tui.NameStyle.Render(deployTarget)
|
||||||
|
}
|
||||||
return progress.RunWithTitle(ctx, func(ctx context.Context) error {
|
return progress.RunWithTitle(ctx, func(ctx context.Context) error {
|
||||||
if err := plan.Execute(ctx, clusterClient); err != nil {
|
if err := plan.Execute(ctx, clusterClient); err != nil {
|
||||||
return fmt.Errorf("deploy services: %w", err)
|
return fmt.Errorf("deploy services: %w", err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}, uncli.ProgressOut(), "Deploying services")
|
}, uncli.ProgressOut(), title)
|
||||||
}
|
|
||||||
|
|
||||||
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")
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,17 +3,19 @@ package image
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
"slices"
|
"slices"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/charmbracelet/lipgloss"
|
"charm.land/lipgloss/v2"
|
||||||
"github.com/charmbracelet/lipgloss/table"
|
"charm.land/lipgloss/v2/table"
|
||||||
|
"github.com/charmbracelet/colorprofile"
|
||||||
"github.com/containerd/platforms"
|
"github.com/containerd/platforms"
|
||||||
"github.com/docker/docker/api/types/image"
|
"github.com/docker/docker/api/types/image"
|
||||||
"github.com/docker/go-units"
|
"github.com/docker/go-units"
|
||||||
"github.com/muesli/termenv"
|
|
||||||
"github.com/psviderski/uncloud/internal/cli"
|
"github.com/psviderski/uncloud/internal/cli"
|
||||||
"github.com/psviderski/uncloud/pkg/api"
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
@@ -222,7 +224,7 @@ func formatPlatforms(platforms []string) string {
|
|||||||
Foreground(lipgloss.Color("0")).
|
Foreground(lipgloss.Color("0")).
|
||||||
Background(lipgloss.Color("152"))
|
Background(lipgloss.Color("152"))
|
||||||
// Use fancy pill borders only if the output is a terminal with color support.
|
// Use fancy pill borders only if the output is a terminal with color support.
|
||||||
if lipgloss.ColorProfile() != termenv.Ascii {
|
if colorprofile.Detect(os.Stdout, os.Environ()) > colorprofile.ASCII {
|
||||||
platformStyle = platformStyle.Border(lipgloss.Border{Left: "", Right: ""}, false, true, false, true)
|
platformStyle = platformStyle.Border(lipgloss.Border{Left: "", Right: ""}, false, true, false, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+112
-63
@@ -8,25 +8,28 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/cenkalti/backoff/v4"
|
"charm.land/huh/v2/spinner"
|
||||||
|
"charm.land/lipgloss/v2"
|
||||||
"github.com/docker/compose/v2/pkg/progress"
|
"github.com/docker/compose/v2/pkg/progress"
|
||||||
"github.com/psviderski/uncloud/cmd/uncloud/caddy"
|
"github.com/psviderski/uncloud/cmd/uncloud/caddy"
|
||||||
"github.com/psviderski/uncloud/internal/cli"
|
"github.com/psviderski/uncloud/internal/cli"
|
||||||
"github.com/psviderski/uncloud/internal/cli/config"
|
"github.com/psviderski/uncloud/internal/cli/config"
|
||||||
|
"github.com/psviderski/uncloud/internal/cli/tui"
|
||||||
|
"github.com/psviderski/uncloud/internal/machine/network"
|
||||||
"github.com/psviderski/uncloud/pkg/api"
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
"github.com/psviderski/uncloud/pkg/client"
|
"github.com/psviderski/uncloud/pkg/client"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"google.golang.org/grpc/codes"
|
|
||||||
"google.golang.org/grpc/status"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type addOptions struct {
|
type addOptions struct {
|
||||||
name string
|
name string
|
||||||
noCaddy bool
|
noCaddy bool
|
||||||
noInstall bool
|
noInstall bool
|
||||||
publicIP string
|
publicIP string
|
||||||
sshKey string
|
sshKey string
|
||||||
version string
|
version string
|
||||||
|
wgEndpoints []string
|
||||||
|
yes bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAddCommand() *cobra.Command {
|
func NewAddCommand() *cobra.Command {
|
||||||
@@ -41,6 +44,8 @@ Connection methods:
|
|||||||
ssh+cli://user@host - Use system SSH command (supports ProxyJump, SSH config)`,
|
ssh+cli://user@host - Use system SSH command (supports ProxyJump, SSH config)`,
|
||||||
Args: cobra.ExactArgs(1),
|
Args: cobra.ExactArgs(1),
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
cli.BindEnvToFlag(cmd, "yes", "UNCLOUD_AUTO_CONFIRM")
|
||||||
|
|
||||||
uncli := cmd.Context().Value("cli").(*cli.CLI)
|
uncli := cmd.Context().Value("cli").(*cli.CLI)
|
||||||
|
|
||||||
// Determine if SSH CLI needs to be used and strip scheme
|
// Determine if SSH CLI needs to be used and strip scheme
|
||||||
@@ -88,6 +93,17 @@ Connection methods:
|
|||||||
&opts.version, "version", "latest",
|
&opts.version, "version", "latest",
|
||||||
"Version of the Uncloud daemon to install on the machine.",
|
"Version of the Uncloud daemon to install on the machine.",
|
||||||
)
|
)
|
||||||
|
cmd.Flags().StringSliceVar(
|
||||||
|
&opts.wgEndpoints, "wg-endpoint", nil,
|
||||||
|
fmt.Sprintf("WireGuard endpoint address in format: IP, IP:PORT, IPv6, or [IPv6]:PORT. "+
|
||||||
|
"Default port %d is used if omitted.\n", network.WireGuardPort)+
|
||||||
|
"Other machines in the cluster will use this endpoint to establish a WireGuard connection to this machine.\n"+
|
||||||
|
"Multiple endpoints can be specified by repeating the flag or using a comma-separated list.\n"+
|
||||||
|
"If not specified, the machine's routable IPs and public IP are auto-detected and used as endpoints.",
|
||||||
|
)
|
||||||
|
cmd.Flags().BoolVarP(&opts.yes, "yes", "y", false,
|
||||||
|
"Auto-confirm prompts (e.g., resetting an already initialised machine).\n"+
|
||||||
|
"Should be explicitly set when running non-interactively, e.g., in CI/CD pipelines. [$UNCLOUD_AUTO_CONFIRM]")
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
@@ -107,13 +123,24 @@ func add(ctx context.Context, uncli *cli.CLI, remoteMachine *cli.RemoteMachine,
|
|||||||
publicIP = &ip
|
publicIP = &ip
|
||||||
}
|
}
|
||||||
|
|
||||||
clusterClient, machineClient, err := uncli.AddMachine(ctx, cli.AddMachineOptions{
|
addOpts := cli.AddMachineOptions{
|
||||||
MachineName: opts.name,
|
MachineName: opts.name,
|
||||||
PublicIP: publicIP,
|
PublicIP: publicIP,
|
||||||
RemoteMachine: remoteMachine,
|
RemoteMachine: remoteMachine,
|
||||||
SkipInstall: opts.noInstall,
|
SkipInstall: opts.noInstall,
|
||||||
Version: opts.version,
|
Version: opts.version,
|
||||||
})
|
AutoConfirm: opts.yes,
|
||||||
|
}
|
||||||
|
if len(opts.wgEndpoints) > 0 {
|
||||||
|
expanded := cli.ExpandCommaSeparatedValues(opts.wgEndpoints)
|
||||||
|
endpoints, err := cli.ParseWireGuardEndpoints(expanded)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("parse WireGuard endpoint (--wg-endpoint): %w", err)
|
||||||
|
}
|
||||||
|
addOpts.WireguardEndpoints = endpoints
|
||||||
|
}
|
||||||
|
|
||||||
|
clusterClient, machineClient, err := uncli.AddMachine(ctx, addOpts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -124,79 +151,101 @@ func add(ctx context.Context, uncli *cli.CLI, remoteMachine *cli.RemoteMachine,
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait for the cluster to be initialised to be able to deploy the Caddy service.
|
// Wait for the cluster to be initialised on the machine to be able to deploy the Caddy service.
|
||||||
fmt.Println("Waiting for the machine to be ready...")
|
err = spinner.New().
|
||||||
fmt.Println()
|
Title(" Waiting for the machine to join the cluster...").
|
||||||
if err = waitClusterInitialised(ctx, machineClient); err != nil {
|
Type(spinner.MiniDot).
|
||||||
return fmt.Errorf("wait for cluster to be initialised on machine: %w", err)
|
WithTheme(spinner.ThemeFunc(func(isDark bool) *spinner.Styles {
|
||||||
|
return &spinner.Styles{
|
||||||
|
Spinner: lipgloss.NewStyle().Foreground(lipgloss.Yellow),
|
||||||
|
Title: lipgloss.NewStyle(),
|
||||||
|
}
|
||||||
|
})).
|
||||||
|
ActionWithErr(func(ctx context.Context) error {
|
||||||
|
return machineClient.WaitClusterReady(ctx, 5*time.Minute)
|
||||||
|
}).
|
||||||
|
Run()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("wait for machine to join the cluster: %w", err)
|
||||||
}
|
}
|
||||||
|
fmt.Println("Machine joined the cluster.")
|
||||||
|
|
||||||
|
// TODO: scale the existing Caddy service to the new machine instead of running a new deployment
|
||||||
|
// that may cause a small downtime.
|
||||||
// Deploy a Caddy service container to the added machine. If caddy service is already deployed on other machines,
|
// 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.
|
// use the deployed image version.
|
||||||
// NOTE: We use the cluster client to inspect and scale the Caddy service because the newly added machine may have
|
// 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.
|
// issues accessing the Machine API of existing machines in the cluster.
|
||||||
// See the issue for more details: https://github.com/psviderski/uncloud/issues/65.
|
// See the issue for more details: https://github.com/psviderski/uncloud/issues/65.
|
||||||
caddyImage := ""
|
caddyImage := ""
|
||||||
caddySvc, err := clusterClient.InspectService(ctx, client.CaddyServiceName)
|
caddySvc, err := clusterClient.InspectService(ctx, client.CaddyServiceName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !errors.Is(err, api.ErrNotFound) {
|
if errors.Is(err, api.ErrNotFound) {
|
||||||
return fmt.Errorf("inspect caddy service: %w", err)
|
// Caddy service is not deployed.
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
} else {
|
return fmt.Errorf("inspect caddy service: %w", err)
|
||||||
caddyImage = caddySvc.Containers[0].Container.Config.Image
|
}
|
||||||
// Find the latest created container and use its image.
|
caddyImage = caddySvc.Containers[0].Container.Config.Image
|
||||||
var latestCreated time.Time
|
// Find the latest created container and use its image.
|
||||||
for _, c := range caddySvc.Containers[1:] {
|
var latestCreated time.Time
|
||||||
created, err := time.Parse(time.RFC3339Nano, c.Container.Created)
|
for _, c := range caddySvc.Containers[1:] {
|
||||||
if err != nil {
|
created, err := time.Parse(time.RFC3339Nano, c.Container.Created)
|
||||||
continue
|
if err != nil {
|
||||||
}
|
continue
|
||||||
if created.After(latestCreated) {
|
}
|
||||||
latestCreated = created
|
if created.After(latestCreated) {
|
||||||
caddyImage = c.Container.Config.Image
|
latestCreated = created
|
||||||
}
|
caddyImage = c.Container.Config.Image
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: scale the existing Caddy service to the new machine instead of running a new deployment
|
fmt.Println()
|
||||||
// that may cause a small downtime.
|
fmt.Println("Preparing Caddy deployment...")
|
||||||
d, err := clusterClient.NewCaddyDeployment(caddyImage, "", api.Placement{})
|
d, err := clusterClient.NewCaddyDeployment(caddyImage, "", api.Placement{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("create caddy deployment: %w", err)
|
return fmt.Errorf("create caddy deployment: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = progress.RunWithTitle(ctx, func(ctx context.Context) error {
|
plan, err := d.Plan(ctx)
|
||||||
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 {
|
if err != nil {
|
||||||
return err
|
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 {
|
||||||
|
fmt.Println(tui.Bold.Underline(true).Render("Deployment plan"))
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Print(plan.Format())
|
||||||
|
|
||||||
|
summary := plan.FormatSummary()
|
||||||
|
fmt.Println(tui.Faint.Render(strings.Repeat("─", lipgloss.Width(summary))))
|
||||||
|
fmt.Println(summary)
|
||||||
|
fmt.Println()
|
||||||
|
|
||||||
|
if !opts.yes {
|
||||||
|
confirmed, err := tui.Confirm("Proceed with deployment?")
|
||||||
|
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()
|
fmt.Println()
|
||||||
return caddy.UpdateDomainRecords(ctx, machineClient, uncli.ProgressOut())
|
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)
|
|
||||||
}
|
|
||||||
|
|||||||
+62
-11
@@ -5,7 +5,10 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"charm.land/huh/v2/spinner"
|
||||||
|
"charm.land/lipgloss/v2"
|
||||||
"github.com/docker/compose/v2/pkg/progress"
|
"github.com/docker/compose/v2/pkg/progress"
|
||||||
"github.com/psviderski/uncloud/cmd/uncloud/caddy"
|
"github.com/psviderski/uncloud/cmd/uncloud/caddy"
|
||||||
"github.com/psviderski/uncloud/cmd/uncloud/dns"
|
"github.com/psviderski/uncloud/cmd/uncloud/dns"
|
||||||
@@ -13,11 +16,13 @@ import (
|
|||||||
"github.com/psviderski/uncloud/internal/cli/config"
|
"github.com/psviderski/uncloud/internal/cli/config"
|
||||||
"github.com/psviderski/uncloud/internal/machine/api/pb"
|
"github.com/psviderski/uncloud/internal/machine/api/pb"
|
||||||
"github.com/psviderski/uncloud/internal/machine/cluster"
|
"github.com/psviderski/uncloud/internal/machine/cluster"
|
||||||
|
"github.com/psviderski/uncloud/internal/machine/network"
|
||||||
"github.com/psviderski/uncloud/pkg/api"
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
type initOptions struct {
|
type initOptions struct {
|
||||||
|
context string
|
||||||
dnsEndpoint string
|
dnsEndpoint string
|
||||||
name string
|
name string
|
||||||
network string
|
network string
|
||||||
@@ -27,7 +32,8 @@ type initOptions struct {
|
|||||||
publicIP string
|
publicIP string
|
||||||
sshKey string
|
sshKey string
|
||||||
version string
|
version string
|
||||||
context string
|
wgEndpoints []string
|
||||||
|
yes bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewInitCommand() *cobra.Command {
|
func NewInitCommand() *cobra.Command {
|
||||||
@@ -56,6 +62,8 @@ Connection methods:
|
|||||||
// TODO: support initialising a cluster on the local 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 {
|
||||||
|
cli.BindEnvToFlag(cmd, "yes", "UNCLOUD_AUTO_CONFIRM")
|
||||||
|
|
||||||
uncli := cmd.Context().Value("cli").(*cli.CLI)
|
uncli := cmd.Context().Value("cli").(*cli.CLI)
|
||||||
|
|
||||||
var remoteMachine *cli.RemoteMachine
|
var remoteMachine *cli.RemoteMachine
|
||||||
@@ -82,6 +90,11 @@ Connection methods:
|
|||||||
return initCluster(cmd.Context(), uncli, remoteMachine, opts)
|
return initCluster(cmd.Context(), uncli, remoteMachine, opts)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cmd.Flags().StringVarP(
|
||||||
|
&opts.context, "context", "c", cli.DefaultContextName,
|
||||||
|
"Name of the new context to be created in the Uncloud config to manage the cluster.",
|
||||||
|
)
|
||||||
cmd.Flags().StringVar(&opts.dnsEndpoint, "dns-endpoint", dns.DefaultUncloudDNSAPIEndpoint,
|
cmd.Flags().StringVar(&opts.dnsEndpoint, "dns-endpoint", dns.DefaultUncloudDNSAPIEndpoint,
|
||||||
"API endpoint for the Uncloud DNS service.")
|
"API endpoint for the Uncloud DNS service.")
|
||||||
cmd.Flags().StringVarP(
|
cmd.Flags().StringVarP(
|
||||||
@@ -119,10 +132,17 @@ Connection methods:
|
|||||||
&opts.version, "version", "latest",
|
&opts.version, "version", "latest",
|
||||||
"Version of the Uncloud daemon to install on the machine.",
|
"Version of the Uncloud daemon to install on the machine.",
|
||||||
)
|
)
|
||||||
cmd.Flags().StringVarP(
|
cmd.Flags().StringSliceVar(
|
||||||
&opts.context, "context", "c", cli.DefaultContextName,
|
&opts.wgEndpoints, "wg-endpoint", nil,
|
||||||
"Name of the new context to be created in the Uncloud config to manage the cluster.",
|
fmt.Sprintf("WireGuard endpoint address in format: IP, IP:PORT, IPv6, or [IPv6]:PORT. "+
|
||||||
|
"Default port %d is used if omitted.\n", network.WireGuardPort)+
|
||||||
|
"Other machines in the cluster will use this endpoint to establish a WireGuard connection to this machine.\n"+
|
||||||
|
"Multiple endpoints can be specified by repeating the flag or using a comma-separated list.\n"+
|
||||||
|
"If not specified, the machine's routable IPs and public IP are auto-detected and used as endpoints.",
|
||||||
)
|
)
|
||||||
|
cmd.Flags().BoolVarP(&opts.yes, "yes", "y", false,
|
||||||
|
"Auto-confirm prompts (e.g., resetting an already initialised machine).\n"+
|
||||||
|
"Should be explicitly set when running non-interactively, e.g., in CI/CD pipelines. [$UNCLOUD_AUTO_CONFIRM]")
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
@@ -130,7 +150,10 @@ Connection methods:
|
|||||||
func initCluster(ctx context.Context, uncli *cli.CLI, remoteMachine *cli.RemoteMachine, opts initOptions) error {
|
func initCluster(ctx context.Context, uncli *cli.CLI, remoteMachine *cli.RemoteMachine, opts initOptions) error {
|
||||||
if uncli.Config == nil {
|
if uncli.Config == nil {
|
||||||
// Config is nil when connecting directly to a remote machine (--connect) without using Uncloud config.
|
// Config is nil when connecting directly to a remote machine (--connect) without using Uncloud config.
|
||||||
return fmt.Errorf("do not specify --connect when initialising a new cluster")
|
return fmt.Errorf(
|
||||||
|
"do not use --connect when initialising a new cluster: --connect is for overriding the connection " +
|
||||||
|
"to an existing cluster, but 'machine init' creates a new one and writes the new cluster context " +
|
||||||
|
"to the Uncloud config file (--uncloud-config)")
|
||||||
}
|
}
|
||||||
|
|
||||||
netPrefix, err := netip.ParsePrefix(opts.network)
|
netPrefix, err := netip.ParsePrefix(opts.network)
|
||||||
@@ -151,7 +174,7 @@ func initCluster(ctx context.Context, uncli *cli.CLI, remoteMachine *cli.RemoteM
|
|||||||
}
|
}
|
||||||
publicIP = &ip
|
publicIP = &ip
|
||||||
}
|
}
|
||||||
client, err := uncli.InitCluster(ctx, cli.InitClusterOptions{
|
initOpts := cli.InitClusterOptions{
|
||||||
Context: opts.context,
|
Context: opts.context,
|
||||||
MachineName: opts.name,
|
MachineName: opts.name,
|
||||||
Network: netPrefix,
|
Network: netPrefix,
|
||||||
@@ -159,20 +182,48 @@ func initCluster(ctx context.Context, uncli *cli.CLI, remoteMachine *cli.RemoteM
|
|||||||
RemoteMachine: remoteMachine,
|
RemoteMachine: remoteMachine,
|
||||||
SkipInstall: opts.noInstall,
|
SkipInstall: opts.noInstall,
|
||||||
Version: opts.version,
|
Version: opts.version,
|
||||||
})
|
AutoConfirm: opts.yes,
|
||||||
|
}
|
||||||
|
if len(opts.wgEndpoints) > 0 {
|
||||||
|
expanded := cli.ExpandCommaSeparatedValues(opts.wgEndpoints)
|
||||||
|
endpoints, err := cli.ParseWireGuardEndpoints(expanded)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("parse WireGuard endpoint (--wg-endpoint): %w", err)
|
||||||
|
}
|
||||||
|
initOpts.WireguardEndpoints = endpoints
|
||||||
|
}
|
||||||
|
|
||||||
|
client, err := uncli.InitCluster(ctx, initOpts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer client.Close()
|
defer client.Close()
|
||||||
|
|
||||||
|
// Since the cluster API needs a few moments to become ready after cluster initialisation,
|
||||||
|
// we keep the user informed during this wait. We wait here even if no Caddy or DNS is requested
|
||||||
|
// as the cluster needs to be ready so that commands such as 'uc machine ls' work immediately after init.
|
||||||
|
err = spinner.New().
|
||||||
|
Title(" Waiting for the cluster to be ready...").
|
||||||
|
Type(spinner.MiniDot).
|
||||||
|
WithTheme(spinner.ThemeFunc(func(isDark bool) *spinner.Styles {
|
||||||
|
return &spinner.Styles{
|
||||||
|
Spinner: lipgloss.NewStyle().Foreground(lipgloss.Yellow),
|
||||||
|
Title: lipgloss.NewStyle(),
|
||||||
|
}
|
||||||
|
})).
|
||||||
|
ActionWithErr(func(ctx context.Context) error {
|
||||||
|
return client.WaitClusterReady(ctx, 1*time.Minute)
|
||||||
|
}).
|
||||||
|
Run()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("wait for cluster to be ready: %w", err)
|
||||||
|
}
|
||||||
|
fmt.Println("Cluster is ready.")
|
||||||
|
|
||||||
if opts.noCaddy && opts.noDNS {
|
if opts.noCaddy && opts.noDNS {
|
||||||
return nil
|
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()
|
fmt.Println()
|
||||||
|
|
||||||
if !opts.noDNS {
|
if !opts.noDNS {
|
||||||
|
|||||||
@@ -9,11 +9,12 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/charmbracelet/lipgloss"
|
"charm.land/lipgloss/v2"
|
||||||
"github.com/charmbracelet/lipgloss/tree"
|
"charm.land/lipgloss/v2/tree"
|
||||||
"github.com/docker/compose/v2/pkg/progress"
|
"github.com/docker/compose/v2/pkg/progress"
|
||||||
"github.com/docker/docker/api/types/container"
|
"github.com/docker/docker/api/types/container"
|
||||||
"github.com/psviderski/uncloud/internal/cli"
|
"github.com/psviderski/uncloud/internal/cli"
|
||||||
|
"github.com/psviderski/uncloud/internal/cli/tui"
|
||||||
"github.com/psviderski/uncloud/internal/machine/api/pb"
|
"github.com/psviderski/uncloud/internal/machine/api/pb"
|
||||||
"github.com/psviderski/uncloud/pkg/api"
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
@@ -118,7 +119,7 @@ func remove(ctx context.Context, uncli *cli.CLI, nameOrID string, opts removeOpt
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !opts.yes {
|
if !opts.yes {
|
||||||
confirmed, err := cli.Confirm()
|
confirmed, err := tui.Confirm("")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("confirm removal: %w", err)
|
return fmt.Errorf("confirm removal: %w", err)
|
||||||
}
|
}
|
||||||
@@ -154,7 +155,7 @@ func remove(ctx context.Context, uncli *cli.CLI, nameOrID string, opts removeOpt
|
|||||||
|
|
||||||
// Remove the connection to the machine from the uncloud config if it exists.
|
// Remove the connection to the machine from the uncloud config if it exists.
|
||||||
if uncli.Config != nil {
|
if uncli.Config != nil {
|
||||||
contextName := uncli.GetContextOverrideOrCurrent()
|
contextName := uncli.ContextOverrideOrCurrent()
|
||||||
if context, ok := uncli.Config.Contexts[contextName]; ok {
|
if context, ok := uncli.Config.Contexts[contextName]; ok {
|
||||||
for i, c := range context.Connections {
|
for i, c := range context.Connections {
|
||||||
if c.MachineID == m.Id {
|
if c.MachineID == m.Id {
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ func NewRootCommand() *cobra.Command {
|
|||||||
NewRenameCommand(),
|
NewRenameCommand(),
|
||||||
NewRmCommand(),
|
NewRmCommand(),
|
||||||
NewUpdateCommand(),
|
NewUpdateCommand(),
|
||||||
NewTokenCommand(),
|
|
||||||
)
|
)
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,39 +0,0 @@
|
|||||||
package machine
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/psviderski/uncloud/internal/daemon"
|
|
||||||
"github.com/psviderski/uncloud/internal/machine"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
)
|
|
||||||
|
|
||||||
type tokenOptions struct {
|
|
||||||
dataDir string
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewTokenCommand() *cobra.Command {
|
|
||||||
opts := tokenOptions{}
|
|
||||||
cmd := &cobra.Command{
|
|
||||||
Use: "token",
|
|
||||||
Short: "Print the local machine's token for adding it to a cluster.",
|
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
|
||||||
token, err := daemon.MachineToken(opts.dataDir)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("get machine token: %w", err)
|
|
||||||
}
|
|
||||||
tokenStr, err := token.String()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("encode machine token: %w", err)
|
|
||||||
}
|
|
||||||
fmt.Println(tokenStr)
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd.Flags().StringVarP(&opts.dataDir, "data-dir", "d", machine.DefaultDataDir,
|
|
||||||
"Directory for storing persistent machine state.")
|
|
||||||
_ = cmd.MarkFlagDirname("data-dir")
|
|
||||||
|
|
||||||
return cmd
|
|
||||||
}
|
|
||||||
@@ -4,29 +4,43 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/psviderski/uncloud/internal/cli"
|
"github.com/psviderski/uncloud/internal/cli"
|
||||||
"github.com/psviderski/uncloud/internal/machine/api/pb"
|
"github.com/psviderski/uncloud/internal/machine/api/pb"
|
||||||
|
"github.com/psviderski/uncloud/internal/machine/network"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
type updateOptions struct {
|
type updateOptions struct {
|
||||||
name string
|
name string
|
||||||
publicIP string
|
publicIP string
|
||||||
|
wgEndpoints []string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewUpdateCommand() *cobra.Command {
|
func NewUpdateCommand() *cobra.Command {
|
||||||
opts := updateOptions{}
|
opts := updateOptions{}
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "update",
|
Use: "update MACHINE [flags]",
|
||||||
Short: "Update machine configuration in the cluster.",
|
Short: "Update machine configuration in the cluster.",
|
||||||
Long: `Update machine configuration in the cluster.
|
Long: `Update machine configuration in the cluster.
|
||||||
|
|
||||||
This command allows setting various machine properties including:
|
Change the name, public IP address, or WireGuard endpoints of an existing machine.
|
||||||
- Machine name (--name)
|
At least one flag must be specified to perform an update.`,
|
||||||
- Public IP address (--public-ip)
|
Example: ` # Rename a machine.
|
||||||
|
uc machine update machine1 --name web-server
|
||||||
|
|
||||||
At least one flag must be specified to perform an update operation.`,
|
# Set the public IP address of a machine.
|
||||||
|
uc machine update machine1 --public-ip 203.0.113.10
|
||||||
|
|
||||||
|
# Remove the public IP address from a machine.
|
||||||
|
uc machine update machine1 --public-ip none
|
||||||
|
|
||||||
|
# Update WireGuard endpoints for a machine.
|
||||||
|
uc machine update machine1 --wg-endpoint 203.0.113.10 --wg-endpoint 192.168.1.5
|
||||||
|
|
||||||
|
# Update multiple properties at once.
|
||||||
|
uc machine update machine1 --name web-server --public-ip 203.0.113.10`,
|
||||||
Args: cobra.ExactArgs(1),
|
Args: cobra.ExactArgs(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)
|
||||||
@@ -40,16 +54,24 @@ At least one flag must be specified to perform an update operation.`,
|
|||||||
)
|
)
|
||||||
cmd.Flags().StringVar(
|
cmd.Flags().StringVar(
|
||||||
&opts.publicIP, "public-ip", "",
|
&opts.publicIP, "public-ip", "",
|
||||||
fmt.Sprintf("Public IP address of the machine for ingress configuration. Use '%s' or '' to remove the public IP.", PublicIPNone),
|
fmt.Sprintf("Public IP address of the machine for ingress configuration. Use '%s' or '' to remove the public IP.",
|
||||||
|
PublicIPNone),
|
||||||
|
)
|
||||||
|
cmd.Flags().StringSliceVar(
|
||||||
|
&opts.wgEndpoints, "wg-endpoint", nil,
|
||||||
|
fmt.Sprintf("WireGuard endpoint address in format: IP, IP:PORT, IPv6, or [IPv6]:PORT. "+
|
||||||
|
"Default port %d is used if omitted.\n", network.WireGuardPort)+
|
||||||
|
"Other machines in the cluster will use this endpoint to establish a WireGuard connection to this machine.\n"+
|
||||||
|
"Multiple endpoints can be specified by repeating the flag or using a comma-separated list.",
|
||||||
)
|
)
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
func update(ctx context.Context, uncli *cli.CLI, cmd *cobra.Command, opts updateOptions, machineNameOrID string) error {
|
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
|
// Check if at least one flag was explicitly set.
|
||||||
if !cmd.Flags().Changed("name") && !cmd.Flags().Changed("public-ip") {
|
if !cmd.Flags().Changed("name") && !cmd.Flags().Changed("public-ip") && !cmd.Flags().Changed("wg-endpoint") {
|
||||||
return fmt.Errorf("at least one update flag must be specified (--name, --public-ip)")
|
return fmt.Errorf("at least one update flag must be specified (--name, --public-ip, --wg-endpoint)")
|
||||||
}
|
}
|
||||||
|
|
||||||
client, err := uncli.ConnectCluster(ctx)
|
client, err := uncli.ConnectCluster(ctx)
|
||||||
@@ -87,6 +109,19 @@ func update(ctx context.Context, uncli *cli.CLI, cmd *cobra.Command, opts update
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Parse and set endpoints if the flag was explicitly provided.
|
||||||
|
if cmd.Flags().Changed("wg-endpoint") {
|
||||||
|
expanded := cli.ExpandCommaSeparatedValues(opts.wgEndpoints)
|
||||||
|
endpoints, err := cli.ParseWireGuardEndpoints(expanded)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(endpoints) == 0 {
|
||||||
|
return fmt.Errorf("at least one endpoint must be specified if --wg-endpoint flag is used")
|
||||||
|
}
|
||||||
|
req.Endpoints = endpoints
|
||||||
|
}
|
||||||
|
|
||||||
// Perform the update operation
|
// Perform the update operation
|
||||||
updatedMachine, err := client.UpdateMachine(ctx, req)
|
updatedMachine, err := client.UpdateMachine(ctx, req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -113,8 +148,24 @@ func update(ctx context.Context, uncli *cli.CLI, cmd *cobra.Command, opts update
|
|||||||
}
|
}
|
||||||
changes = append(changes, fmt.Sprintf("public IP: %s -> %s", oldIP, newIP))
|
changes = append(changes, fmt.Sprintf("public IP: %s -> %s", oldIP, newIP))
|
||||||
}
|
}
|
||||||
|
if cmd.Flags().Changed("wg-endpoint") {
|
||||||
|
formatEndpoints := func(eps []*pb.IPPort) string {
|
||||||
|
if len(eps) == 0 {
|
||||||
|
return "none"
|
||||||
|
}
|
||||||
|
parts := make([]string, len(eps))
|
||||||
|
for i, ep := range eps {
|
||||||
|
ap, _ := ep.ToAddrPort()
|
||||||
|
parts[i] = ap.String()
|
||||||
|
}
|
||||||
|
return strings.Join(parts, ", ")
|
||||||
|
}
|
||||||
|
oldEndpoints := formatEndpoints(machine.Machine.Network.Endpoints)
|
||||||
|
newEndpoints := formatEndpoints(updatedMachine.Network.Endpoints)
|
||||||
|
changes = append(changes, fmt.Sprintf("endpoints: %s -> %s", oldEndpoints, newEndpoints))
|
||||||
|
}
|
||||||
|
|
||||||
fmt.Printf("Machine %q (ID: %s) configuration updated:\n", updatedMachine.Name, updatedMachine.Id)
|
fmt.Printf("Machine '%s' (ID: %s) configuration updated:\n", updatedMachine.Name, updatedMachine.Id)
|
||||||
for _, change := range changes {
|
for _, change := range changes {
|
||||||
fmt.Printf(" %s\n", change)
|
fmt.Printf(" %s\n", change)
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-1
@@ -6,7 +6,7 @@ import (
|
|||||||
"net/netip"
|
"net/netip"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/charmbracelet/lipgloss"
|
"charm.land/lipgloss/v2"
|
||||||
"github.com/psviderski/uncloud/cmd/uncloud/caddy"
|
"github.com/psviderski/uncloud/cmd/uncloud/caddy"
|
||||||
cmdcontext "github.com/psviderski/uncloud/cmd/uncloud/context"
|
cmdcontext "github.com/psviderski/uncloud/cmd/uncloud/context"
|
||||||
"github.com/psviderski/uncloud/cmd/uncloud/dns"
|
"github.com/psviderski/uncloud/cmd/uncloud/dns"
|
||||||
@@ -14,6 +14,7 @@ import (
|
|||||||
"github.com/psviderski/uncloud/cmd/uncloud/machine"
|
"github.com/psviderski/uncloud/cmd/uncloud/machine"
|
||||||
"github.com/psviderski/uncloud/cmd/uncloud/service"
|
"github.com/psviderski/uncloud/cmd/uncloud/service"
|
||||||
"github.com/psviderski/uncloud/cmd/uncloud/volume"
|
"github.com/psviderski/uncloud/cmd/uncloud/volume"
|
||||||
|
"github.com/psviderski/uncloud/cmd/uncloud/wg"
|
||||||
"github.com/psviderski/uncloud/internal/cli"
|
"github.com/psviderski/uncloud/internal/cli"
|
||||||
"github.com/psviderski/uncloud/internal/cli/config"
|
"github.com/psviderski/uncloud/internal/cli/config"
|
||||||
"github.com/psviderski/uncloud/internal/fs"
|
"github.com/psviderski/uncloud/internal/fs"
|
||||||
@@ -134,6 +135,7 @@ func main() {
|
|||||||
service.NewStartCommand("service"),
|
service.NewStartCommand("service"),
|
||||||
service.NewStopCommand("service"),
|
service.NewStopCommand("service"),
|
||||||
volume.NewRootCommand(),
|
volume.NewRootCommand(),
|
||||||
|
wg.NewRootCommand(),
|
||||||
)
|
)
|
||||||
cobra.CheckErr(cmd.Execute())
|
cobra.CheckErr(cmd.Execute())
|
||||||
}
|
}
|
||||||
|
|||||||
+17
-11
@@ -6,11 +6,12 @@ import (
|
|||||||
"sort"
|
"sort"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/charmbracelet/huh/spinner"
|
"charm.land/huh/v2/spinner"
|
||||||
"github.com/charmbracelet/lipgloss"
|
"charm.land/lipgloss/v2"
|
||||||
"github.com/charmbracelet/lipgloss/table"
|
"charm.land/lipgloss/v2/table"
|
||||||
"github.com/docker/docker/api/types/container"
|
"github.com/docker/docker/api/types/container"
|
||||||
"github.com/docker/go-units"
|
"github.com/docker/go-units"
|
||||||
|
"github.com/psviderski/uncloud/internal/cli/tui"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
"github.com/psviderski/uncloud/internal/cli"
|
"github.com/psviderski/uncloud/internal/cli"
|
||||||
@@ -85,7 +86,12 @@ func runPs(ctx context.Context, uncli *cli.CLI, opts psOptions) error {
|
|||||||
err = spinner.New().
|
err = spinner.New().
|
||||||
Title(" Collecting container info...").
|
Title(" Collecting container info...").
|
||||||
Type(spinner.MiniDot).
|
Type(spinner.MiniDot).
|
||||||
Style(lipgloss.NewStyle().Foreground(lipgloss.Color("3"))).
|
WithTheme(spinner.ThemeFunc(func(isDark bool) *spinner.Styles {
|
||||||
|
return &spinner.Styles{
|
||||||
|
Spinner: lipgloss.NewStyle().Foreground(lipgloss.Yellow),
|
||||||
|
Title: lipgloss.NewStyle(),
|
||||||
|
}
|
||||||
|
})).
|
||||||
ActionWithErr(func(ctx context.Context) error {
|
ActionWithErr(func(ctx context.Context) error {
|
||||||
containers, err = collectContainers(ctx, clusterClient)
|
containers, err = collectContainers(ctx, clusterClient)
|
||||||
return err
|
return err
|
||||||
@@ -205,18 +211,12 @@ func collectContainers(ctx context.Context, cli *client.Client) ([]containerInfo
|
|||||||
|
|
||||||
var containers []containerInfo
|
var containers []containerInfo
|
||||||
for _, msc := range machineContainers {
|
for _, msc := range machineContainers {
|
||||||
machineName := "unknown"
|
|
||||||
|
|
||||||
// Metadata can be nil if the request was broadcasted to only one machine.
|
// Metadata can be nil if the request was broadcasted to only one machine.
|
||||||
if msc.Metadata == nil && len(machineContainers) > 1 {
|
if msc.Metadata == nil && len(machineContainers) > 1 {
|
||||||
return nil, fmt.Errorf("something went wrong with gRPC proxy: metadata is missing for a machine response")
|
return nil, fmt.Errorf("something went wrong with gRPC proxy: metadata is missing for a machine response")
|
||||||
}
|
}
|
||||||
if msc.Metadata != nil && msc.Metadata.Error != "" {
|
|
||||||
client.PrintWarning(fmt.Sprintf("failed to list containers on machine %s: %s", machineName,
|
|
||||||
msc.Metadata.Error))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
|
machineName := "unknown"
|
||||||
if msc.Metadata != nil {
|
if msc.Metadata != nil {
|
||||||
var ok bool
|
var ok bool
|
||||||
machineName, ok = machinesNamesByIP[msc.Metadata.Machine]
|
machineName, ok = machinesNamesByIP[msc.Metadata.Machine]
|
||||||
@@ -231,6 +231,12 @@ func collectContainers(ctx context.Context, cli *client.Client) ([]containerInfo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if msc.Metadata != nil && msc.Metadata.Error != "" {
|
||||||
|
tui.PrintWarning(fmt.Sprintf("failed to list containers on machine %s: %s", machineName,
|
||||||
|
msc.Metadata.Error))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
for _, ctr := range msc.Containers {
|
for _, ctr := range msc.Containers {
|
||||||
if ctr.Container.State == nil || ctr.Container.Config == nil {
|
if ctr.Container.State == nil || ctr.Container.Config == nil {
|
||||||
continue
|
continue
|
||||||
|
|||||||
+36
-36
@@ -39,25 +39,25 @@ func (m *mockClusterClient) ListMachines(ctx context.Context, in *emptypb.Empty,
|
|||||||
|
|
||||||
func TestCollectContainers_NilMetadata(t *testing.T) {
|
func TestCollectContainers_NilMetadata(t *testing.T) {
|
||||||
// Setup container data
|
// Setup container data
|
||||||
containerData := map[string]interface{}{
|
containerData := map[string]any{
|
||||||
"Id": "container1",
|
"Id": "container1",
|
||||||
"Name": "test-container",
|
"Name": "test-container",
|
||||||
"Config": map[string]interface{}{
|
"Config": map[string]any{
|
||||||
"Image": "test-image",
|
"Image": "test-image",
|
||||||
},
|
},
|
||||||
"State": map[string]interface{}{
|
"State": map[string]any{
|
||||||
"Status": "running",
|
"Status": "running",
|
||||||
"StartedAt": "2023-01-01T12:00:00Z",
|
"StartedAt": "2023-01-01T12:00:00Z",
|
||||||
"FinishedAt": "0001-01-01T00:00:00Z",
|
"FinishedAt": "0001-01-01T00:00:00Z",
|
||||||
},
|
},
|
||||||
"NetworkSettings": map[string]interface{}{
|
"NetworkSettings": map[string]any{
|
||||||
"Networks": map[string]interface{}{},
|
"Networks": map[string]any{},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
containerJSON, err := json.Marshal(containerData)
|
containerJSON, err := json.Marshal(containerData)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
serviceSpecJSON, err := json.Marshal(map[string]interface{}{})
|
serviceSpecJSON, err := json.Marshal(map[string]any{})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Setup mocks
|
// Setup mocks
|
||||||
@@ -120,28 +120,28 @@ func TestCollectContainers_NilMetadata_MultipleMachines_Error(t *testing.T) {
|
|||||||
// If we have multiple machines but receive nil metadata, it should return an error as it is ambiguous
|
// If we have multiple machines but receive nil metadata, it should return an error as it is ambiguous
|
||||||
|
|
||||||
// Setup container data
|
// Setup container data
|
||||||
containerData1 := map[string]interface{}{
|
containerData1 := map[string]any{
|
||||||
"Id": "container1",
|
"Id": "container1",
|
||||||
}
|
}
|
||||||
containerJSON1, _ := json.Marshal(containerData1)
|
containerJSON1, _ := json.Marshal(containerData1)
|
||||||
|
|
||||||
containerData2 := map[string]interface{}{
|
containerData2 := map[string]any{
|
||||||
"Id": "container2",
|
"Id": "container2",
|
||||||
"Config": map[string]interface{}{
|
"Config": map[string]any{
|
||||||
"Image": "test-image",
|
"Image": "test-image",
|
||||||
},
|
},
|
||||||
"State": map[string]interface{}{
|
"State": map[string]any{
|
||||||
"Status": "running",
|
"Status": "running",
|
||||||
"StartedAt": "2023-01-01T12:00:00Z",
|
"StartedAt": "2023-01-01T12:00:00Z",
|
||||||
"FinishedAt": "0001-01-01T00:00:00Z",
|
"FinishedAt": "0001-01-01T00:00:00Z",
|
||||||
},
|
},
|
||||||
"NetworkSettings": map[string]interface{}{
|
"NetworkSettings": map[string]any{
|
||||||
"Networks": map[string]interface{}{},
|
"Networks": map[string]any{},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
containerJSON2, _ := json.Marshal(containerData2)
|
containerJSON2, _ := json.Marshal(containerData2)
|
||||||
|
|
||||||
serviceSpecJSON, _ := json.Marshal(map[string]interface{}{})
|
serviceSpecJSON, _ := json.Marshal(map[string]any{})
|
||||||
|
|
||||||
// Setup mocks
|
// Setup mocks
|
||||||
mockDocker := &mockDockerClient{
|
mockDocker := &mockDockerClient{
|
||||||
@@ -210,41 +210,41 @@ func TestCollectContainers_MetadataPresent_MultipleMachines(t *testing.T) {
|
|||||||
// Verify correct mapping of containers to machines when metadata is present
|
// Verify correct mapping of containers to machines when metadata is present
|
||||||
|
|
||||||
// Setup container data
|
// Setup container data
|
||||||
containerData1 := map[string]interface{}{
|
containerData1 := map[string]any{
|
||||||
"Id": "container1",
|
"Id": "container1",
|
||||||
"Name": "container-1",
|
"Name": "container-1",
|
||||||
"Config": map[string]interface{}{
|
"Config": map[string]any{
|
||||||
"Image": "image-1",
|
"Image": "image-1",
|
||||||
},
|
},
|
||||||
"State": map[string]interface{}{
|
"State": map[string]any{
|
||||||
"Status": "running",
|
"Status": "running",
|
||||||
"StartedAt": "2023-01-01T12:00:00Z",
|
"StartedAt": "2023-01-01T12:00:00Z",
|
||||||
"FinishedAt": "0001-01-01T00:00:00Z",
|
"FinishedAt": "0001-01-01T00:00:00Z",
|
||||||
},
|
},
|
||||||
"NetworkSettings": map[string]interface{}{
|
"NetworkSettings": map[string]any{
|
||||||
"Networks": map[string]interface{}{},
|
"Networks": map[string]any{},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
containerJSON1, _ := json.Marshal(containerData1)
|
containerJSON1, _ := json.Marshal(containerData1)
|
||||||
|
|
||||||
containerData2 := map[string]interface{}{
|
containerData2 := map[string]any{
|
||||||
"Id": "container2",
|
"Id": "container2",
|
||||||
"Name": "container-2",
|
"Name": "container-2",
|
||||||
"Config": map[string]interface{}{
|
"Config": map[string]any{
|
||||||
"Image": "image-2",
|
"Image": "image-2",
|
||||||
},
|
},
|
||||||
"State": map[string]interface{}{
|
"State": map[string]any{
|
||||||
"Status": "running",
|
"Status": "running",
|
||||||
"StartedAt": "2023-01-01T12:00:00Z",
|
"StartedAt": "2023-01-01T12:00:00Z",
|
||||||
"FinishedAt": "0001-01-01T00:00:00Z",
|
"FinishedAt": "0001-01-01T00:00:00Z",
|
||||||
},
|
},
|
||||||
"NetworkSettings": map[string]interface{}{
|
"NetworkSettings": map[string]any{
|
||||||
"Networks": map[string]interface{}{},
|
"Networks": map[string]any{},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
containerJSON2, _ := json.Marshal(containerData2)
|
containerJSON2, _ := json.Marshal(containerData2)
|
||||||
|
|
||||||
serviceSpecJSON, _ := json.Marshal(map[string]interface{}{})
|
serviceSpecJSON, _ := json.Marshal(map[string]any{})
|
||||||
|
|
||||||
// Setup mocks
|
// Setup mocks
|
||||||
mockDocker := &mockDockerClient{
|
mockDocker := &mockDockerClient{
|
||||||
@@ -321,22 +321,22 @@ func TestCollectContainers_MetadataPresent_MultipleMachines(t *testing.T) {
|
|||||||
func TestCollectContainers_NilMetadata_NoMachines(t *testing.T) {
|
func TestCollectContainers_NilMetadata_NoMachines(t *testing.T) {
|
||||||
// Case: 1 msc with nil metadata but no machines at all
|
// Case: 1 msc with nil metadata but no machines at all
|
||||||
|
|
||||||
containerData := map[string]interface{}{
|
containerData := map[string]any{
|
||||||
"Id": "container1",
|
"Id": "container1",
|
||||||
"Config": map[string]interface{}{
|
"Config": map[string]any{
|
||||||
"Image": "test-image",
|
"Image": "test-image",
|
||||||
},
|
},
|
||||||
"State": map[string]interface{}{
|
"State": map[string]any{
|
||||||
"Status": "running",
|
"Status": "running",
|
||||||
"StartedAt": "2023-01-01T12:00:00Z",
|
"StartedAt": "2023-01-01T12:00:00Z",
|
||||||
"FinishedAt": "0001-01-01T00:00:00Z",
|
"FinishedAt": "0001-01-01T00:00:00Z",
|
||||||
},
|
},
|
||||||
"NetworkSettings": map[string]interface{}{
|
"NetworkSettings": map[string]any{
|
||||||
"Networks": map[string]interface{}{},
|
"Networks": map[string]any{},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
containerJSON, _ := json.Marshal(containerData)
|
containerJSON, _ := json.Marshal(containerData)
|
||||||
serviceSpecJSON, _ := json.Marshal(map[string]interface{}{})
|
serviceSpecJSON, _ := json.Marshal(map[string]any{})
|
||||||
|
|
||||||
mockDocker := &mockDockerClient{
|
mockDocker := &mockDockerClient{
|
||||||
listResp: &pb.ListServiceContainersResponse{
|
listResp: &pb.ListServiceContainersResponse{
|
||||||
@@ -378,22 +378,22 @@ func TestCollectContainers_NilMetadata_NoMachines(t *testing.T) {
|
|||||||
func TestCollectContainers_MetadataPresent_NotInMapping(t *testing.T) {
|
func TestCollectContainers_MetadataPresent_NotInMapping(t *testing.T) {
|
||||||
// Case: msc with metadata that is not in the IP-to-name mapping
|
// Case: msc with metadata that is not in the IP-to-name mapping
|
||||||
|
|
||||||
containerData := map[string]interface{}{
|
containerData := map[string]any{
|
||||||
"Id": "container1",
|
"Id": "container1",
|
||||||
"Config": map[string]interface{}{
|
"Config": map[string]any{
|
||||||
"Image": "test-image",
|
"Image": "test-image",
|
||||||
},
|
},
|
||||||
"State": map[string]interface{}{
|
"State": map[string]any{
|
||||||
"Status": "running",
|
"Status": "running",
|
||||||
"StartedAt": "2023-01-01T12:00:00Z",
|
"StartedAt": "2023-01-01T12:00:00Z",
|
||||||
"FinishedAt": "0001-01-01T00:00:00Z",
|
"FinishedAt": "0001-01-01T00:00:00Z",
|
||||||
},
|
},
|
||||||
"NetworkSettings": map[string]interface{}{
|
"NetworkSettings": map[string]any{
|
||||||
"Networks": map[string]interface{}{},
|
"Networks": map[string]any{},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
containerJSON, _ := json.Marshal(containerData)
|
containerJSON, _ := json.Marshal(containerData)
|
||||||
serviceSpecJSON, _ := json.Marshal(map[string]interface{}{})
|
serviceSpecJSON, _ := json.Marshal(map[string]any{})
|
||||||
|
|
||||||
mockDocker := &mockDockerClient{
|
mockDocker := &mockDockerClient{
|
||||||
listResp: &pb.ListServiceContainersResponse{
|
listResp: &pb.ListServiceContainersResponse{
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
|
|
||||||
"github.com/docker/cli/cli/streams"
|
"github.com/docker/cli/cli/streams"
|
||||||
"github.com/psviderski/uncloud/internal/cli"
|
"github.com/psviderski/uncloud/internal/cli"
|
||||||
|
"github.com/psviderski/uncloud/internal/cli/tui"
|
||||||
"github.com/psviderski/uncloud/pkg/api"
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
@@ -59,7 +60,7 @@ If the service has multiple replicas and no container ID is specified, the comma
|
|||||||
|
|
||||||
execCmd.Flags().BoolVarP(&opts.detach, "detach", "d", false, "Detached mode: run command in the background")
|
execCmd.Flags().BoolVarP(&opts.detach, "detach", "d", false, "Detached mode: run command in the background")
|
||||||
|
|
||||||
execCmd.Flags().BoolVarP(&opts.noTty, "no-tty", "T", !cli.IsStdoutTerminal(),
|
execCmd.Flags().BoolVarP(&opts.noTty, "no-tty", "T", false,
|
||||||
"Disable pseudo-TTY allocation. By default 'uc exec' allocates a TTY when connected to a terminal.")
|
"Disable pseudo-TTY allocation. By default 'uc exec' allocates a TTY when connected to a terminal.")
|
||||||
|
|
||||||
// Keep "-i" and "-t" flags hidden for compatibility with docker exec
|
// Keep "-i" and "-t" flags hidden for compatibility with docker exec
|
||||||
@@ -82,6 +83,11 @@ If the service has multiple replicas and no container ID is specified, the comma
|
|||||||
}
|
}
|
||||||
|
|
||||||
func runExec(ctx context.Context, uncli *cli.CLI, serviceName string, command []string, opts execCliOptions) error {
|
func runExec(ctx context.Context, uncli *cli.CLI, serviceName string, command []string, opts execCliOptions) error {
|
||||||
|
// Disable TTY allocation if not connected to a terminal
|
||||||
|
if !tui.IsStdoutTerminal() {
|
||||||
|
opts.noTty = true
|
||||||
|
}
|
||||||
|
|
||||||
if !opts.detach {
|
if !opts.detach {
|
||||||
// Check if we're trying to attach to a TTY from a non-TTY client, e.g.
|
// Check if we're trying to attach to a TTY from a non-TTY client, e.g.
|
||||||
// when doing an 'cmd | uc exec ...'
|
// when doing an 'cmd | uc exec ...'
|
||||||
|
|||||||
+42
-15
@@ -4,16 +4,18 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"image/color"
|
||||||
"os"
|
"os"
|
||||||
"slices"
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/charmbracelet/lipgloss"
|
"charm.land/lipgloss/v2"
|
||||||
mapset "github.com/deckarep/golang-set/v2"
|
mapset "github.com/deckarep/golang-set/v2"
|
||||||
"github.com/docker/docker/pkg/stringid"
|
"github.com/docker/docker/pkg/stringid"
|
||||||
"github.com/psviderski/uncloud/internal/cli"
|
"github.com/psviderski/uncloud/internal/cli"
|
||||||
|
"github.com/psviderski/uncloud/internal/cli/tui"
|
||||||
"github.com/psviderski/uncloud/pkg/api"
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
"github.com/psviderski/uncloud/pkg/client"
|
"github.com/psviderski/uncloud/pkg/client"
|
||||||
"github.com/psviderski/uncloud/pkg/client/compose"
|
"github.com/psviderski/uncloud/pkg/client/compose"
|
||||||
@@ -99,15 +101,20 @@ If no services are specified, streams logs from all services defined in the Comp
|
|||||||
|
|
||||||
func runLogs(ctx context.Context, uncli *cli.CLI, serviceNames []string, opts logsOptions) error {
|
func runLogs(ctx context.Context, uncli *cli.CLI, serviceNames []string, opts logsOptions) error {
|
||||||
// If no services specified, try to load them from the Compose file(s).
|
// If no services specified, try to load them from the Compose file(s).
|
||||||
|
fromCompose := false
|
||||||
if len(serviceNames) == 0 {
|
if len(serviceNames) == 0 {
|
||||||
|
fromCompose = true
|
||||||
project, err := compose.LoadProject(ctx, opts.files)
|
project, err := compose.LoadProject(ctx, opts.files)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("load compose file(s): %w", err)
|
return fmt.Errorf("load Compose file(s): %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
uncli.SetClusterContextIfUnset(compose.ClusterContext(project))
|
||||||
|
|
||||||
// View logs for all services, including disabled by inactive profiles.
|
// View logs for all services, including disabled by inactive profiles.
|
||||||
serviceNames = append(project.ServiceNames(), project.DisabledServiceNames()...)
|
serviceNames = append(project.ServiceNames(), project.DisabledServiceNames()...)
|
||||||
if len(serviceNames) == 0 {
|
if len(serviceNames) == 0 {
|
||||||
return errors.New("no services found in compose file(s)")
|
return errors.New("no services found in Compose file(s)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,20 +142,40 @@ func runLogs(ctx context.Context, uncli *cli.CLI, serviceNames []string, opts lo
|
|||||||
Machines: cli.ExpandCommaSeparatedValues(opts.machines),
|
Machines: cli.ExpandCommaSeparatedValues(opts.machines),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Collect log streams from all services.
|
// Collect log streams from all services. When service names come from a Compose file,
|
||||||
|
// skip the ones that are not found in the cluster (they may have been removed or not deployed yet).
|
||||||
machineIDsSet := mapset.NewSet[string]()
|
machineIDsSet := mapset.NewSet[string]()
|
||||||
svcStreams := make([]<-chan api.ServiceLogEntry, 0, len(serviceNames))
|
svcStreams := make([]<-chan api.ServiceLogEntry, 0, len(serviceNames))
|
||||||
|
var foundServices, notFoundServices []string
|
||||||
|
|
||||||
for _, serviceName := range serviceNames {
|
for _, serviceName := range serviceNames {
|
||||||
svc, ch, err := c.ServiceLogs(ctx, serviceName, logsOpts)
|
svc, ch, err := c.ServiceLogs(ctx, serviceName, logsOpts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if errors.Is(err, api.ErrNotFound) && fromCompose {
|
||||||
|
notFoundServices = append(notFoundServices, serviceName)
|
||||||
|
continue
|
||||||
|
}
|
||||||
return fmt.Errorf("stream logs for service '%s': %w", serviceName, err)
|
return fmt.Errorf("stream logs for service '%s': %w", serviceName, err)
|
||||||
}
|
}
|
||||||
svcStreams = append(svcStreams, ch)
|
svcStreams = append(svcStreams, ch)
|
||||||
|
foundServices = append(foundServices, serviceName)
|
||||||
|
|
||||||
machineIDs := svc.MachineIDs()
|
machineIDs := svc.MachineIDs()
|
||||||
machineIDsSet.Append(machineIDs...)
|
machineIDsSet.Append(machineIDs...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if fromCompose {
|
||||||
|
if len(foundServices) == 0 {
|
||||||
|
return fmt.Errorf("stream logs for services defined in %s: no services found in the cluster",
|
||||||
|
strings.Join(opts.files, ", "))
|
||||||
|
}
|
||||||
|
serviceNames = foundServices
|
||||||
|
|
||||||
|
for _, name := range notFoundServices {
|
||||||
|
tui.PrintWarning(fmt.Sprintf("service '%s' not found in the cluster, skipping", name))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var stream <-chan api.ServiceLogEntry
|
var stream <-chan api.ServiceLogEntry
|
||||||
if len(serviceNames) == 1 {
|
if len(serviceNames) == 1 {
|
||||||
stream = svcStreams[0]
|
stream = svcStreams[0]
|
||||||
@@ -183,17 +210,17 @@ func runLogs(ctx context.Context, uncli *cli.CLI, serviceNames []string, opts lo
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Available colors for machine/service differentiation.
|
// Available colors for machine/service differentiation.
|
||||||
var colorPalette = []lipgloss.Color{
|
var colorPalette = []color.Color{
|
||||||
lipgloss.Color("10"), // Bright green
|
lipgloss.BrightGreen,
|
||||||
lipgloss.Color("11"), // Bright yellow
|
lipgloss.BrightYellow,
|
||||||
lipgloss.Color("12"), // Bright blue
|
lipgloss.BrightBlue,
|
||||||
lipgloss.Color("13"), // Bright magenta
|
lipgloss.BrightMagenta,
|
||||||
lipgloss.Color("14"), // Bright cyan
|
lipgloss.BrightCyan,
|
||||||
lipgloss.Color("2"), // Green
|
lipgloss.Green,
|
||||||
lipgloss.Color("3"), // Yellow
|
lipgloss.Yellow,
|
||||||
lipgloss.Color("4"), // Blue
|
lipgloss.Blue,
|
||||||
lipgloss.Color("5"), // Magenta
|
lipgloss.Magenta,
|
||||||
lipgloss.Color("6"), // Cyan
|
lipgloss.Cyan,
|
||||||
}
|
}
|
||||||
|
|
||||||
// logFormatter handles formatting and printing of log entries with dynamic column alignment.
|
// logFormatter handles formatting and printing of log entries with dynamic column alignment.
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
dockeropts "github.com/docker/cli/opts"
|
dockeropts "github.com/docker/cli/opts"
|
||||||
"github.com/docker/compose/v2/pkg/progress"
|
"github.com/docker/compose/v2/pkg/progress"
|
||||||
"github.com/docker/docker/daemon/names"
|
"github.com/docker/docker/daemon/names"
|
||||||
|
"github.com/docker/go-units"
|
||||||
"github.com/psviderski/uncloud/internal/cli"
|
"github.com/psviderski/uncloud/internal/cli"
|
||||||
"github.com/psviderski/uncloud/internal/secret"
|
"github.com/psviderski/uncloud/internal/secret"
|
||||||
"github.com/psviderski/uncloud/pkg/api"
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
@@ -27,11 +28,13 @@ type runOptions struct {
|
|||||||
machines []string
|
machines []string
|
||||||
memory dockeropts.MemBytes
|
memory dockeropts.MemBytes
|
||||||
mode string
|
mode string
|
||||||
|
shmSize dockeropts.MemBytes
|
||||||
name string
|
name string
|
||||||
privileged bool
|
privileged bool
|
||||||
publish []string
|
publish []string
|
||||||
pull string
|
pull string
|
||||||
replicas uint
|
replicas uint
|
||||||
|
ulimits []string
|
||||||
user string
|
user string
|
||||||
volumes []string
|
volumes []string
|
||||||
}
|
}
|
||||||
@@ -76,8 +79,12 @@ func NewRunCommand(groupID string) *cobra.Command {
|
|||||||
"Placement constraint by machine names, limiting which machines the service can run on. Can be specified "+
|
"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)")
|
"multiple times or as a comma-separated list of machine names. (default is any suitable machine)")
|
||||||
cmd.Flags().VarP(&opts.memory, "memory", "",
|
cmd.Flags().VarP(&opts.memory, "memory", "",
|
||||||
"Maximum amount of memory a service container can use. Value is a positive integer with optional unit suffix "+
|
"Maximum amount of memory a service container can use. Value is a positive integer with optional unit suffix (b, k, m, g).\n"+
|
||||||
"(b, k, m, g). Default unit is bytes if no suffix specified.\n"+
|
"Default unit is bytes if no suffix specified.\n"+
|
||||||
|
"Examples: 1073741824, 1024m, 1g (all equal 1 gibibyte)")
|
||||||
|
cmd.Flags().VarP(&opts.shmSize, "shm-size", "",
|
||||||
|
"Maximum amount of shared memory (mounted at /dev/shm) a service container can use. Value is a positive integer\n"+
|
||||||
|
"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)")
|
"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.")
|
||||||
@@ -102,6 +109,12 @@ func NewRunCommand(groupID string) *cobra.Command {
|
|||||||
cmd.Flags().StringVarP(&opts.user, "user", "u", "",
|
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"+
|
"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.")
|
"Format: USER[:GROUP] or UID[:GID]. If not specified, the user is set to the default user of the image.")
|
||||||
|
cmd.Flags().StringSliceVar(&opts.ulimits, "ulimit", nil,
|
||||||
|
"Set resource limits for service containers. Can be specified multiple times.\n"+
|
||||||
|
"Format: type=soft_limit[:hard_limit]. If hard limit is not specified, soft limit is used for both.\n"+
|
||||||
|
"Examples:\n"+
|
||||||
|
" --ulimit nofile=1024:2048 Set soft limit to 1024 and hard limit to 2048 for number of open files\n"+
|
||||||
|
" --ulimit nproc=65535 Set both soft and hard limits to 65535 for number of processes")
|
||||||
cmd.Flags().StringSliceVarP(&opts.volumes, "volume", "v", nil,
|
cmd.Flags().StringSliceVarP(&opts.volumes, "volume", "v", nil,
|
||||||
"Mount a data volume or host path into service containers. Service containers will be scheduled on the machine(s) where\n"+
|
"Mount a data volume or host path into service containers. Service containers will be scheduled on the machine(s) where\n"+
|
||||||
"the volume is located. Can be specified multiple times.\n"+
|
"the volume is located. Can be specified multiple times.\n"+
|
||||||
@@ -194,13 +207,18 @@ func prepareServiceSpec(opts runOptions) (api.ServiceSpec, error) {
|
|||||||
ports[i] = port
|
ports[i] = port
|
||||||
}
|
}
|
||||||
|
|
||||||
volumes, mounts, err := parseVolumeFlags(opts.volumes)
|
placement := api.Placement{
|
||||||
|
Machines: cli.ExpandCommaSeparatedValues(opts.machines),
|
||||||
|
}
|
||||||
|
|
||||||
|
ulimits, err := parseUlimits(opts.ulimits)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return spec, err
|
return spec, err
|
||||||
}
|
}
|
||||||
|
|
||||||
placement := api.Placement{
|
volumes, mounts, err := parseVolumeFlags(opts.volumes)
|
||||||
Machines: cli.ExpandCommaSeparatedValues(opts.machines),
|
if err != nil {
|
||||||
|
return spec, err
|
||||||
}
|
}
|
||||||
|
|
||||||
spec = api.ServiceSpec{
|
spec = api.ServiceSpec{
|
||||||
@@ -211,8 +229,10 @@ func prepareServiceSpec(opts runOptions) (api.ServiceSpec, error) {
|
|||||||
Privileged: opts.privileged,
|
Privileged: opts.privileged,
|
||||||
PullPolicy: opts.pull,
|
PullPolicy: opts.pull,
|
||||||
Resources: api.ContainerResources{
|
Resources: api.ContainerResources{
|
||||||
CPU: opts.cpu.Value(),
|
CPU: opts.cpu.Value(),
|
||||||
Memory: opts.memory.Value(),
|
Memory: opts.memory.Value(),
|
||||||
|
SharedMemory: opts.shmSize.Value(),
|
||||||
|
Ulimits: ulimits,
|
||||||
},
|
},
|
||||||
User: opts.user,
|
User: opts.user,
|
||||||
VolumeMounts: mounts,
|
VolumeMounts: mounts,
|
||||||
@@ -302,6 +322,24 @@ func parseVolumeFlags(volumes []string) ([]api.VolumeSpec, []api.VolumeMount, er
|
|||||||
return specs, mounts, nil
|
return specs, mounts, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// parseUlimits parses ulimit flag values in Docker CLI format (type=soft[:hard]).
|
||||||
|
func parseUlimits(ulimits []string) (map[string]api.Ulimit, error) {
|
||||||
|
if len(ulimits) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
result := make(map[string]api.Ulimit, len(ulimits))
|
||||||
|
for _, u := range ulimits {
|
||||||
|
parsed, err := units.ParseUlimit(u)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid ulimit '%s': %w", u, err)
|
||||||
|
}
|
||||||
|
result[parsed.Name] = api.Ulimit{Soft: parsed.Soft, Hard: parsed.Hard}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
func parseVolumeFlagValue(volume string) (api.VolumeSpec, api.VolumeMount, error) {
|
func parseVolumeFlagValue(volume string) (api.VolumeSpec, api.VolumeMount, error) {
|
||||||
var spec api.VolumeSpec
|
var spec api.VolumeSpec
|
||||||
var mount api.VolumeMount
|
var mount api.VolumeMount
|
||||||
@@ -320,8 +358,8 @@ func parseVolumeFlagValue(volume string) (api.VolumeSpec, api.VolumeMount, error
|
|||||||
volumeNoCopy := false
|
volumeNoCopy := false
|
||||||
|
|
||||||
if len(parts) == 3 {
|
if len(parts) == 3 {
|
||||||
opts := strings.Split(parts[2], ",")
|
opts := strings.SplitSeq(parts[2], ",")
|
||||||
for _, opt := range opts {
|
for opt := range opts {
|
||||||
switch opt {
|
switch opt {
|
||||||
case "ro", "readonly":
|
case "ro", "readonly":
|
||||||
mount.ReadOnly = true
|
mount.ReadOnly = true
|
||||||
|
|||||||
@@ -2,12 +2,16 @@ package service
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/charmbracelet/huh"
|
"charm.land/lipgloss/v2"
|
||||||
"github.com/docker/compose/v2/pkg/progress"
|
"github.com/docker/compose/v2/pkg/progress"
|
||||||
"github.com/psviderski/uncloud/internal/cli"
|
"github.com/psviderski/uncloud/internal/cli"
|
||||||
|
"github.com/psviderski/uncloud/internal/cli/tui"
|
||||||
"github.com/psviderski/uncloud/pkg/api"
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
@@ -15,6 +19,7 @@ import (
|
|||||||
type scaleOptions struct {
|
type scaleOptions struct {
|
||||||
service string
|
service string
|
||||||
replicas uint
|
replicas uint
|
||||||
|
yes bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewScaleCommand(groupID string) *cobra.Command {
|
func NewScaleCommand(groupID string) *cobra.Command {
|
||||||
@@ -22,9 +27,11 @@ func NewScaleCommand(groupID string) *cobra.Command {
|
|||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "scale SERVICE REPLICAS",
|
Use: "scale SERVICE REPLICAS",
|
||||||
Short: "Scale a replicated service by changing the number of 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.",
|
Long: "Scale a replicated service by changing the number of replicas.",
|
||||||
Args: cobra.ExactArgs(2),
|
Args: cobra.ExactArgs(2),
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
cli.BindEnvToFlag(cmd, "yes", "UNCLOUD_AUTO_CONFIRM")
|
||||||
|
|
||||||
uncli := cmd.Context().Value("cli").(*cli.CLI)
|
uncli := cmd.Context().Value("cli").(*cli.CLI)
|
||||||
|
|
||||||
opts.service = args[0]
|
opts.service = args[0]
|
||||||
@@ -39,6 +46,10 @@ func NewScaleCommand(groupID string) *cobra.Command {
|
|||||||
GroupID: groupID,
|
GroupID: groupID,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cmd.Flags().BoolVarP(&opts.yes, "yes", "y", false,
|
||||||
|
"Auto-confirm scaling plan. Should be explicitly set when running non-interactively,\n"+
|
||||||
|
"e.g., in CI/CD pipelines. [$UNCLOUD_AUTO_CONFIRM]")
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,7 +82,8 @@ func scale(ctx context.Context, uncli *cli.CLI, opts scaleOptions) error {
|
|||||||
currentReplicas := uint(len(svc.Containers))
|
currentReplicas := uint(len(svc.Containers))
|
||||||
|
|
||||||
if currentReplicas == opts.replicas {
|
if currentReplicas == opts.replicas {
|
||||||
fmt.Printf("Service '%s' already has %d replicas. No changes required.\n", svc.Name, currentReplicas)
|
fmt.Printf("Service %s already has %s replicas. No changes required.\n",
|
||||||
|
tui.NameStyle.Render(svc.Name), tui.Bold.Render(fmt.Sprintf("%d", currentReplicas)))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,23 +98,51 @@ func scale(ctx context.Context, uncli *cli.CLI, opts scaleOptions) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(plan.Operations) == 0 {
|
if len(plan.Operations) == 0 {
|
||||||
fmt.Printf("Service '%s' is already scaled to %d replicas.\n", svc.Name, opts.replicas)
|
fmt.Printf("Service %s is already scaled to %d replicas.\n", tui.NameStyle.Render(svc.Name), opts.replicas)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if opts.replicas < currentReplicas {
|
fmt.Println(tui.Bold.Underline(true).Render("Scaling plan"))
|
||||||
// Initialise a machine and container name resolver to properly format the plan output.
|
fmt.Println()
|
||||||
resolver, err := clusterClient.ServiceOperationNameResolver(ctx, svc)
|
|
||||||
if err != nil {
|
directConn := uncli.DirectConnection()
|
||||||
return fmt.Errorf("create machine and container name resolver for service operations: %w", err)
|
contextName := uncli.ContextOverrideOrCurrent()
|
||||||
|
deployTarget := ""
|
||||||
|
if directConn != "" {
|
||||||
|
deployTarget = directConn
|
||||||
|
fmt.Println(tui.Faint.Render("connection: ") + tui.NameStyle.Render(directConn))
|
||||||
|
fmt.Println()
|
||||||
|
} else if contextName != "" && len(uncli.Config.Contexts) > 1 {
|
||||||
|
// Only show context if there's more than one to avoid unnecessary clutter.
|
||||||
|
deployTarget = contextName
|
||||||
|
fmt.Println(tui.Faint.Render("context: ") + tui.NameStyle.Render(contextName))
|
||||||
|
fmt.Println()
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println(plan.Format())
|
||||||
|
|
||||||
|
summary := plan.FormatSummary()
|
||||||
|
fmt.Println(tui.Faint.Render(strings.Repeat("─", lipgloss.Width(summary))))
|
||||||
|
fmt.Println(summary)
|
||||||
|
fmt.Println()
|
||||||
|
|
||||||
|
// Ask for confirmation unless auto-confirmed with --yes.
|
||||||
|
if !opts.yes {
|
||||||
|
if !tui.IsStdinTerminal() {
|
||||||
|
return errors.New("cannot ask to confirm scaling plan in non-interactive mode, " +
|
||||||
|
"use --yes flag or set UNCLOUD_AUTO_CONFIRM=true to auto-confirm")
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("Scaling plan for service %s (%d → %d replicas):\n", svc.Name, currentReplicas, opts.replicas)
|
title := "Proceed with scaling?"
|
||||||
fmt.Println(plan.Format(resolver))
|
// Include the direct connection or context name in the confirmation prompt to avoid accidentally
|
||||||
fmt.Println()
|
// scaling on the wrong cluster.
|
||||||
|
if deployTarget != "" {
|
||||||
|
isDark := lipgloss.HasDarkBackground(os.Stdin, os.Stdout)
|
||||||
|
confirmStyle := tui.ThemeConfirm().Theme(isDark).Focused.Title
|
||||||
|
title = "Proceed with scaling on " + tui.NameStyle.Render(deployTarget) + confirmStyle.Render("?")
|
||||||
|
}
|
||||||
|
|
||||||
// Ask for confirmation before scaling down as it may cause data loss.
|
confirmed, err := tui.Confirm(title)
|
||||||
confirmed, err := confirm()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("confirm scaling: %w", err)
|
return fmt.Errorf("confirm scaling: %w", err)
|
||||||
}
|
}
|
||||||
@@ -112,7 +152,8 @@ func scale(ctx context.Context, uncli *cli.CLI, opts scaleOptions) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
title := fmt.Sprintf("Scaling service %s (%d → %d replicas)", svc.Name, currentReplicas, opts.replicas)
|
title := fmt.Sprintf("Scaling service %s (%d → %d replicas)",
|
||||||
|
tui.NameStyle.Render(svc.Name), currentReplicas, opts.replicas)
|
||||||
err = progress.RunWithTitle(ctx, func(ctx context.Context) error {
|
err = progress.RunWithTitle(ctx, func(ctx context.Context) error {
|
||||||
if _, err = deployment.Run(ctx); err != nil {
|
if _, err = deployment.Run(ctx); err != nil {
|
||||||
return fmt.Errorf("deploy service: %w", err)
|
return fmt.Errorf("deploy service: %w", err)
|
||||||
@@ -125,23 +166,3 @@ func scale(ctx context.Context, uncli *cli.CLI, opts scaleOptions) error {
|
|||||||
|
|
||||||
return nil
|
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
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/charmbracelet/huh"
|
"charm.land/huh/v2"
|
||||||
"github.com/docker/docker/api/types/volume"
|
"github.com/docker/docker/api/types/volume"
|
||||||
"github.com/psviderski/uncloud/internal/cli"
|
"github.com/psviderski/uncloud/internal/cli"
|
||||||
"github.com/psviderski/uncloud/internal/machine/api/pb"
|
"github.com/psviderski/uncloud/internal/machine/api/pb"
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/psviderski/uncloud/internal/cli"
|
"github.com/psviderski/uncloud/internal/cli"
|
||||||
|
"github.com/psviderski/uncloud/internal/cli/tui"
|
||||||
"github.com/psviderski/uncloud/pkg/api"
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
@@ -79,7 +80,7 @@ func remove(ctx context.Context, uncli *cli.CLI, names []string, opts removeOpti
|
|||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
confirmed, err := cli.Confirm()
|
confirmed, err := tui.Confirm("")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("confirm removal: %w", err)
|
return fmt.Errorf("confirm removal: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,132 @@
|
|||||||
|
package wg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"text/tabwriter"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/docker/go-units"
|
||||||
|
"github.com/psviderski/uncloud/internal/cli"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
||||||
|
"google.golang.org/grpc/codes"
|
||||||
|
"google.golang.org/grpc/status"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewRootCommand() *cobra.Command {
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "wg",
|
||||||
|
Short: "Inspect WireGuard network",
|
||||||
|
}
|
||||||
|
cmd.AddCommand(newShowCommand())
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
type showOptions struct {
|
||||||
|
machine string
|
||||||
|
}
|
||||||
|
|
||||||
|
func newShowCommand() *cobra.Command {
|
||||||
|
opts := showOptions{}
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "show",
|
||||||
|
Short: "Show WireGuard network configuration for a machine.",
|
||||||
|
Long: "Show the WireGuard network configuration for the machine currently connected to " +
|
||||||
|
"(or specified by the global --connect flag).",
|
||||||
|
Args: cobra.NoArgs,
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
uncli := cmd.Context().Value("cli").(*cli.CLI)
|
||||||
|
return runShow(cmd.Context(), uncli, opts)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
cmd.Flags().StringVarP(&opts.machine, "machine", "m", "",
|
||||||
|
"Name or ID of the machine to show the configuration for. (default is connected machine)")
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func runShow(ctx context.Context, uncli *cli.CLI, opts showOptions) error {
|
||||||
|
client, err := uncli.ConnectCluster(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("connection failed: %w", err)
|
||||||
|
}
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
if opts.machine != "" {
|
||||||
|
// Proxy requests to the specified machine.
|
||||||
|
ctx, _, err = client.ProxyMachinesContext(ctx, []string{opts.machine})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := client.MachineClient.InspectWireGuardNetwork(ctx, nil)
|
||||||
|
if err != nil {
|
||||||
|
if status.Code(err) == codes.Unimplemented {
|
||||||
|
return fmt.Errorf("inspect WireGuard network: "+
|
||||||
|
"make sure the target machine is running uncloudd daemon version >= 0.16.0: %w", err)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
machines, err := client.ListMachines(ctx, nil)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("list machines: %w", err)
|
||||||
|
}
|
||||||
|
machinesNamesByPublicKey := make(map[string]string)
|
||||||
|
for _, m := range machines {
|
||||||
|
publicKey := wgtypes.Key(m.Machine.Network.PublicKey).String()
|
||||||
|
machinesNamesByPublicKey[publicKey] = m.Machine.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch the machine's name for more descriptive output
|
||||||
|
inspectResp, err := client.MachineClient.InspectMachine(ctx, nil)
|
||||||
|
if err == nil {
|
||||||
|
fmt.Printf("Machine name: %s\n", inspectResp.Machines[0].Machine.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("WireGuard interface: %s\n", resp.InterfaceName)
|
||||||
|
fmt.Printf("WireGuard public key: %s\n", wgtypes.Key(resp.PublicKey).String())
|
||||||
|
fmt.Printf("WireGuard port: %d\n", resp.ListenPort)
|
||||||
|
fmt.Println()
|
||||||
|
|
||||||
|
if len(resp.Peers) == 0 {
|
||||||
|
fmt.Println("No WireGuard peers configured.")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
tw := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', 0)
|
||||||
|
if _, err = fmt.Fprintln(tw, "PEER\tPUBLIC KEY\tENDPOINT\tHANDSHAKE\tRECEIVED\tSENT\tALLOWED IPS"); err != nil {
|
||||||
|
return fmt.Errorf("write header: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, peer := range resp.Peers {
|
||||||
|
machineName, ok := machinesNamesByPublicKey[wgtypes.Key(peer.PublicKey).String()]
|
||||||
|
if !ok {
|
||||||
|
machineName = "(unknown)"
|
||||||
|
}
|
||||||
|
|
||||||
|
lastHandshake := ""
|
||||||
|
if peer.LastHandshakeTime != nil {
|
||||||
|
lastHandshake = time.Since(peer.LastHandshakeTime.AsTime()).Round(time.Second).String() + " ago"
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = fmt.Fprintf(
|
||||||
|
tw,
|
||||||
|
"%s\t%s\t%s\t%s\t%s\t%s\t%s\n",
|
||||||
|
machineName,
|
||||||
|
wgtypes.Key(peer.PublicKey).String(),
|
||||||
|
peer.Endpoint,
|
||||||
|
lastHandshake,
|
||||||
|
units.HumanSize(float64(peer.ReceiveBytes)),
|
||||||
|
units.HumanSize(float64(peer.TransmitBytes)),
|
||||||
|
strings.Join(peer.AllowedIps, ", "),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("write row: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return tw.Flush()
|
||||||
|
}
|
||||||
+12
-12
@@ -17,57 +17,57 @@ func newIPFSLogger(l *slog.Logger) *ipfsLogger {
|
|||||||
return &ipfsLogger{log: *l}
|
return &ipfsLogger{log: *l}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *ipfsLogger) Debug(args ...interface{}) {
|
func (l *ipfsLogger) Debug(args ...any) {
|
||||||
l.log.Debug(fmt.Sprint(args...))
|
l.log.Debug(fmt.Sprint(args...))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *ipfsLogger) Debugf(format string, args ...interface{}) {
|
func (l *ipfsLogger) Debugf(format string, args ...any) {
|
||||||
l.log.Debug(fmt.Sprintf(format, args...))
|
l.log.Debug(fmt.Sprintf(format, args...))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *ipfsLogger) Error(args ...interface{}) {
|
func (l *ipfsLogger) Error(args ...any) {
|
||||||
l.log.Error(fmt.Sprint(args...))
|
l.log.Error(fmt.Sprint(args...))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *ipfsLogger) Errorf(format string, args ...interface{}) {
|
func (l *ipfsLogger) Errorf(format string, args ...any) {
|
||||||
l.log.Error(fmt.Sprintf(format, args...))
|
l.log.Error(fmt.Sprintf(format, args...))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *ipfsLogger) Fatal(args ...interface{}) {
|
func (l *ipfsLogger) Fatal(args ...any) {
|
||||||
l.log.Error(fmt.Sprint(args...))
|
l.log.Error(fmt.Sprint(args...))
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *ipfsLogger) Fatalf(format string, args ...interface{}) {
|
func (l *ipfsLogger) Fatalf(format string, args ...any) {
|
||||||
l.log.Error(fmt.Sprintf(format, args...))
|
l.log.Error(fmt.Sprintf(format, args...))
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *ipfsLogger) Info(args ...interface{}) {
|
func (l *ipfsLogger) Info(args ...any) {
|
||||||
l.log.Info(fmt.Sprint(args...))
|
l.log.Info(fmt.Sprint(args...))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *ipfsLogger) Infof(format string, args ...interface{}) {
|
func (l *ipfsLogger) Infof(format string, args ...any) {
|
||||||
l.log.Info(fmt.Sprintf(format, args...))
|
l.log.Info(fmt.Sprintf(format, args...))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *ipfsLogger) Panic(args ...interface{}) {
|
func (l *ipfsLogger) Panic(args ...any) {
|
||||||
msg := fmt.Sprint(args...)
|
msg := fmt.Sprint(args...)
|
||||||
l.log.Error(msg)
|
l.log.Error(msg)
|
||||||
panic(msg)
|
panic(msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *ipfsLogger) Panicf(format string, args ...interface{}) {
|
func (l *ipfsLogger) Panicf(format string, args ...any) {
|
||||||
msg := fmt.Sprintf(format, args...)
|
msg := fmt.Sprintf(format, args...)
|
||||||
l.log.Error(msg)
|
l.log.Error(msg)
|
||||||
panic(msg)
|
panic(msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *ipfsLogger) Warn(args ...interface{}) {
|
func (l *ipfsLogger) Warn(args ...any) {
|
||||||
l.log.Warn(fmt.Sprint(args...))
|
l.log.Warn(fmt.Sprint(args...))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *ipfsLogger) Warnf(format string, args ...interface{}) {
|
func (l *ipfsLogger) Warnf(format string, args ...any) {
|
||||||
l.log.Warn(fmt.Sprintf(format, args...))
|
l.log.Warn(fmt.Sprintf(format, args...))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
module github.com/psviderski/uncloud
|
module github.com/psviderski/uncloud
|
||||||
|
|
||||||
go 1.25
|
go 1.26
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
charm.land/bubbles/v2 v2.0.0
|
||||||
|
charm.land/bubbletea/v2 v2.0.2
|
||||||
|
charm.land/huh/v2 v2.0.1
|
||||||
|
charm.land/lipgloss/v2 v2.0.1
|
||||||
github.com/BurntSushi/toml v1.5.0
|
github.com/BurntSushi/toml v1.5.0
|
||||||
github.com/Masterminds/semver v1.5.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/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/bubbles v0.21.0
|
github.com/charmbracelet/colorprofile v0.4.2
|
||||||
github.com/charmbracelet/bubbletea v1.3.10
|
|
||||||
github.com/charmbracelet/huh v0.6.0
|
|
||||||
github.com/charmbracelet/huh/spinner v0.0.0-20251110114415-25888d17260b
|
|
||||||
github.com/charmbracelet/lipgloss v1.1.0
|
|
||||||
github.com/compose-spec/compose-go/v2 v2.9.0
|
github.com/compose-spec/compose-go/v2 v2.9.0
|
||||||
github.com/containerd/errdefs v1.0.0
|
github.com/containerd/errdefs v1.0.0
|
||||||
github.com/containerd/platforms v1.0.0-rc.1
|
github.com/containerd/platforms v1.0.0-rc.1
|
||||||
@@ -43,7 +43,6 @@ require (
|
|||||||
github.com/miekg/dns v1.1.65
|
github.com/miekg/dns v1.1.65
|
||||||
github.com/mitchellh/mapstructure v1.5.0
|
github.com/mitchellh/mapstructure v1.5.0
|
||||||
github.com/moby/term v0.5.2
|
github.com/moby/term v0.5.2
|
||||||
github.com/muesli/termenv v0.16.0
|
|
||||||
github.com/opencontainers/go-digest v1.0.0
|
github.com/opencontainers/go-digest v1.0.0
|
||||||
github.com/opencontainers/image-spec v1.1.1
|
github.com/opencontainers/image-spec v1.1.1
|
||||||
github.com/psviderski/unregistry v0.4.1
|
github.com/psviderski/unregistry v0.4.1
|
||||||
@@ -57,8 +56,8 @@ require (
|
|||||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba
|
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba
|
||||||
golang.org/x/crypto v0.41.0
|
golang.org/x/crypto v0.41.0
|
||||||
golang.org/x/net v0.43.0
|
golang.org/x/net v0.43.0
|
||||||
golang.org/x/sync v0.17.0
|
golang.org/x/sync v0.19.0
|
||||||
golang.org/x/sys v0.36.0
|
golang.org/x/sys v0.42.0
|
||||||
golang.org/x/term v0.34.0
|
golang.org/x/term v0.34.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
|
||||||
@@ -66,6 +65,7 @@ require (
|
|||||||
google.golang.org/grpc v1.74.2
|
google.golang.org/grpc v1.74.2
|
||||||
google.golang.org/protobuf v1.36.9
|
google.golang.org/protobuf v1.36.9
|
||||||
modernc.org/sqlite v1.36.3
|
modernc.org/sqlite v1.36.3
|
||||||
|
tags.cncf.io/container-device-interface v1.0.1
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
@@ -102,21 +102,24 @@ require (
|
|||||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4 // indirect
|
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/sts v1.30.3 // indirect
|
github.com/aws/aws-sdk-go-v2/service/sts v1.30.3 // indirect
|
||||||
github.com/aws/smithy-go v1.20.3 // indirect
|
github.com/aws/smithy-go v1.20.3 // indirect
|
||||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
|
||||||
github.com/beorn7/perks v1.0.1 // indirect
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
github.com/bgentry/speakeasy v0.2.0 // indirect
|
github.com/bgentry/speakeasy v0.2.0 // indirect
|
||||||
github.com/buger/goterm v1.0.4 // indirect
|
github.com/buger/goterm v1.0.4 // indirect
|
||||||
github.com/caddyserver/certmagic v0.21.4 // indirect
|
github.com/caddyserver/certmagic v0.21.4 // indirect
|
||||||
github.com/caddyserver/zerossl v0.1.3 // indirect
|
github.com/caddyserver/zerossl v0.1.3 // indirect
|
||||||
github.com/catppuccin/go v0.2.0 // indirect
|
github.com/catppuccin/go v0.3.0 // indirect
|
||||||
github.com/cespare/xxhash v1.1.0 // indirect
|
github.com/cespare/xxhash v1.1.0 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
|
github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8 // indirect
|
||||||
github.com/charmbracelet/x/ansi v0.10.1 // indirect
|
github.com/charmbracelet/x/ansi v0.11.6 // indirect
|
||||||
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect
|
github.com/charmbracelet/x/exp/ordered v0.1.0 // 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.1 // indirect
|
github.com/charmbracelet/x/term v0.2.2 // indirect
|
||||||
|
github.com/charmbracelet/x/termios v0.1.1 // indirect
|
||||||
|
github.com/charmbracelet/x/windows v0.2.2 // indirect
|
||||||
github.com/chzyer/readline v1.5.1 // indirect
|
github.com/chzyer/readline v1.5.1 // indirect
|
||||||
|
github.com/clipperhouse/displaywidth v0.11.0 // indirect
|
||||||
|
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
|
||||||
github.com/containerd/cgroups/v3 v3.0.5 // indirect
|
github.com/containerd/cgroups/v3 v3.0.5 // indirect
|
||||||
github.com/containerd/console v1.0.5 // indirect
|
github.com/containerd/console v1.0.5 // indirect
|
||||||
github.com/containerd/containerd/api v1.9.0 // indirect
|
github.com/containerd/containerd/api v1.9.0 // indirect
|
||||||
@@ -149,7 +152,6 @@ require (
|
|||||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
github.com/eiannone/keyboard v0.0.0-20220611211555-0d226195f203 // indirect
|
github.com/eiannone/keyboard v0.0.0-20220611211555-0d226195f203 // indirect
|
||||||
github.com/emicklei/go-restful/v3 v3.11.0 // indirect
|
github.com/emicklei/go-restful/v3 v3.11.0 // indirect
|
||||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
|
||||||
github.com/fatih/color v1.18.0 // indirect
|
github.com/fatih/color v1.18.0 // indirect
|
||||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||||
github.com/fsnotify/fsevents v0.2.0 // indirect
|
github.com/fsnotify/fsevents v0.2.0 // indirect
|
||||||
@@ -238,13 +240,12 @@ require (
|
|||||||
github.com/libp2p/go-libp2p v0.35.4 // indirect
|
github.com/libp2p/go-libp2p v0.35.4 // indirect
|
||||||
github.com/libp2p/go-libp2p-pubsub v0.11.0 // indirect
|
github.com/libp2p/go-libp2p-pubsub v0.11.0 // indirect
|
||||||
github.com/libp2p/go-msgio v0.3.0 // indirect
|
github.com/libp2p/go-msgio v0.3.0 // indirect
|
||||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
|
||||||
github.com/mailru/easyjson v0.7.7 // indirect
|
github.com/mailru/easyjson v0.7.7 // indirect
|
||||||
github.com/manifoldco/promptui v0.9.0 // indirect
|
github.com/manifoldco/promptui v0.9.0 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/mattn/go-localereader v0.0.1 // indirect
|
github.com/mattn/go-runewidth v0.0.20 // indirect
|
||||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
|
||||||
github.com/mattn/go-shellwords v1.0.12 // indirect
|
github.com/mattn/go-shellwords v1.0.12 // indirect
|
||||||
github.com/mdlayher/genetlink v1.3.2 // indirect
|
github.com/mdlayher/genetlink v1.3.2 // indirect
|
||||||
github.com/mdlayher/netlink v1.7.2 // indirect
|
github.com/mdlayher/netlink v1.7.2 // indirect
|
||||||
@@ -277,7 +278,6 @@ require (
|
|||||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
github.com/modern-go/reflect2 v1.0.2 // 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/cancelreader v0.2.2 // indirect
|
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||||
github.com/multiformats/go-base32 v0.1.0 // indirect
|
github.com/multiformats/go-base32 v0.1.0 // indirect
|
||||||
github.com/multiformats/go-base36 v0.2.0 // indirect
|
github.com/multiformats/go-base36 v0.2.0 // indirect
|
||||||
@@ -415,5 +415,4 @@ require (
|
|||||||
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect
|
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect
|
||||||
sigs.k8s.io/structured-merge-diff/v4 v4.4.2 // indirect
|
sigs.k8s.io/structured-merge-diff/v4 v4.4.2 // indirect
|
||||||
sigs.k8s.io/yaml v1.4.0 // indirect
|
sigs.k8s.io/yaml v1.4.0 // indirect
|
||||||
tags.cncf.io/container-device-interface v1.0.1 // indirect
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,3 +1,11 @@
|
|||||||
|
charm.land/bubbles/v2 v2.0.0 h1:tE3eK/pHjmtrDiRdoC9uGNLgpopOd8fjhEe31B/ai5s=
|
||||||
|
charm.land/bubbles/v2 v2.0.0/go.mod h1:rCHoleP2XhU8um45NTuOWBPNVHxnkXKTiZqcclL/qOI=
|
||||||
|
charm.land/bubbletea/v2 v2.0.2 h1:4CRtRnuZOdFDTWSff9r8QFt/9+z6Emubz3aDMnf/dx0=
|
||||||
|
charm.land/bubbletea/v2 v2.0.2/go.mod h1:3LRff2U4WIYXy7MTxfbAQ+AdfM3D8Xuvz2wbsOD9OHQ=
|
||||||
|
charm.land/huh/v2 v2.0.1 h1:9vhBjlIDuikdPKH+qnoG++GERVxqY0Lkv14xW57lj98=
|
||||||
|
charm.land/huh/v2 v2.0.1/go.mod h1:93eEveeeqn47MwiC3tf+2atZ2l7Is88rAtmZNZ8x9Wc=
|
||||||
|
charm.land/lipgloss/v2 v2.0.1 h1:6Xzrn49+Py1Um5q/wZG1gWgER2+7dUyZ9XMEufqPSys=
|
||||||
|
charm.land/lipgloss/v2 v2.0.1/go.mod h1:KjPle2Qd3YmvP1KL5OMHiHysGcNwq6u83MUjYkFvEkM=
|
||||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||||
cloud.google.com/go v0.115.1 h1:Jo0SM9cQnSkYfp44+v+NQXHpcHqlnRJk2qxh6yvxxxQ=
|
cloud.google.com/go v0.115.1 h1:Jo0SM9cQnSkYfp44+v+NQXHpcHqlnRJk2qxh6yvxxxQ=
|
||||||
cloud.google.com/go v0.115.1/go.mod h1:DuujITeaufu3gL68/lOFIirVNJwQeyf5UXyi+Wbgknc=
|
cloud.google.com/go v0.115.1/go.mod h1:DuujITeaufu3gL68/lOFIirVNJwQeyf5UXyi+Wbgknc=
|
||||||
@@ -119,10 +127,8 @@ github.com/aws/aws-sdk-go-v2/service/sts v1.30.3 h1:ZsDKRLXGWHk8WdtyYMoGNO7bTudr
|
|||||||
github.com/aws/aws-sdk-go-v2/service/sts v1.30.3/go.mod h1:zwySh8fpFyXp9yOr/KVzxOl8SRqgf/IDw5aUt9UKFcQ=
|
github.com/aws/aws-sdk-go-v2/service/sts v1.30.3/go.mod h1:zwySh8fpFyXp9yOr/KVzxOl8SRqgf/IDw5aUt9UKFcQ=
|
||||||
github.com/aws/smithy-go v1.20.3 h1:ryHwveWzPV5BIof6fyDvor6V3iUL7nTfiTKXHiW05nE=
|
github.com/aws/smithy-go v1.20.3 h1:ryHwveWzPV5BIof6fyDvor6V3iUL7nTfiTKXHiW05nE=
|
||||||
github.com/aws/smithy-go v1.20.3/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E=
|
github.com/aws/smithy-go v1.20.3/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E=
|
||||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
github.com/aymanbagabas/go-udiff v0.4.1 h1:OEIrQ8maEeDBXQDoGCbbTTXYJMYRCRO1fnodZ12Gv5o=
|
||||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
github.com/aymanbagabas/go-udiff v0.4.1/go.mod h1:0L9PGwj20lrtmEMeyw4WKJ/TMyDtvAoK9bf2u/mNo3w=
|
||||||
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=
|
||||||
@@ -157,8 +163,8 @@ github.com/caddyserver/certmagic v0.21.4 h1:e7VobB8rffHv8ZZpSiZtEwnLDHUwLVYLWzWS
|
|||||||
github.com/caddyserver/certmagic v0.21.4/go.mod h1:swUXjQ1T9ZtMv95qj7/InJvWLXURU85r+CfG0T+ZbDE=
|
github.com/caddyserver/certmagic v0.21.4/go.mod h1:swUXjQ1T9ZtMv95qj7/InJvWLXURU85r+CfG0T+ZbDE=
|
||||||
github.com/caddyserver/zerossl v0.1.3 h1:onS+pxp3M8HnHpN5MMbOMyNjmTheJyWRaZYwn+YTAyA=
|
github.com/caddyserver/zerossl v0.1.3 h1:onS+pxp3M8HnHpN5MMbOMyNjmTheJyWRaZYwn+YTAyA=
|
||||||
github.com/caddyserver/zerossl v0.1.3/go.mod h1:CxA0acn7oEGO6//4rtrRjYgEoa4MFw/XofZnrYwGqG4=
|
github.com/caddyserver/zerossl v0.1.3/go.mod h1:CxA0acn7oEGO6//4rtrRjYgEoa4MFw/XofZnrYwGqG4=
|
||||||
github.com/catppuccin/go v0.2.0 h1:ktBeIrIP42b/8FGiScP9sgrWOss3lw0Z5SktRoithGA=
|
github.com/catppuccin/go v0.3.0 h1:d+0/YicIq+hSTo5oPuRi5kOpqkVA5tAsU6dNhvRu+aY=
|
||||||
github.com/catppuccin/go v0.2.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc=
|
github.com/catppuccin/go v0.3.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc=
|
||||||
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
||||||
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||||
@@ -168,28 +174,30 @@ github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL
|
|||||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs=
|
github.com/charmbracelet/colorprofile v0.4.2 h1:BdSNuMjRbotnxHSfxy+PCSa4xAmz7szw70ktAtWRYrY=
|
||||||
github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg=
|
github.com/charmbracelet/colorprofile v0.4.2/go.mod h1:0rTi81QpwDElInthtrQ6Ni7cG0sDtwAd4C4le060fT8=
|
||||||
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
|
github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8 h1:eyFRbAmexyt43hVfeyBofiGSEmJ7krjLOYt/9CF5NKA=
|
||||||
github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
|
github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8/go.mod h1:SQpCTRNBtzJkwku5ye4S3HEuthAlGy2n9VXZnWkEW98=
|
||||||
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
|
github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8=
|
||||||
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
|
github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ=
|
||||||
github.com/charmbracelet/huh v0.6.0 h1:mZM8VvZGuE0hoDXq6XLxRtgfWyTI3b2jZNKh0xWmax8=
|
github.com/charmbracelet/x/conpty v0.1.1 h1:s1bUxjoi7EpqiXysVtC+a8RrvPPNcNvAjfi4jxsAuEs=
|
||||||
github.com/charmbracelet/huh v0.6.0/go.mod h1:GGNKeWCeNzKpEOh/OJD8WBwTQjV3prFAtQPpLv+AVwU=
|
github.com/charmbracelet/x/conpty v0.1.1/go.mod h1:OmtR77VODEFbiTzGE9G1XiRJAga6011PIm4u5fTNZpk=
|
||||||
github.com/charmbracelet/huh/spinner v0.0.0-20251110114415-25888d17260b h1:oefmPctgff7OB4cDz3Ndp+ewwrh7i+fwth70qvZCieI=
|
github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86 h1:JSt3B+U9iqk37QUU2Rvb6DSBYRLtWqFqfxf8l5hOZUA=
|
||||||
github.com/charmbracelet/huh/spinner v0.0.0-20251110114415-25888d17260b/go.mod h1:OMqKat/mm9a/qOnpuNOPyYO9bPzRNnmzLnRZT5KYltg=
|
github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86/go.mod h1:2P0UgXMEa6TsToMSuFqKFQR+fZTO9CNGUNokkPatT/0=
|
||||||
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
|
github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f h1:pk6gmGpCE7F3FcjaOEKYriCvpmIN4+6OS/RD0vm4uIA=
|
||||||
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
|
github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f/go.mod h1:IfZAMTHB6XkZSeXUqriemErjAWCCzT0LwjKFYCZyw0I=
|
||||||
github.com/charmbracelet/x/ansi v0.10.1 h1:rL3Koar5XvX0pHGfovN03f5cxLbCF2YvLeyz7D2jVDQ=
|
github.com/charmbracelet/x/exp/ordered v0.1.0 h1:55/qLwjIh0gL0Vni+QAWk7T/qRVP6sBf+2agPBgnOFE=
|
||||||
github.com/charmbracelet/x/ansi v0.10.1/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE=
|
github.com/charmbracelet/x/exp/ordered v0.1.0/go.mod h1:5UHwmG+is5THxMyCJHNPCn2/ecI07aKNrW+LcResjJ8=
|
||||||
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8=
|
|
||||||
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
|
|
||||||
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ=
|
|
||||||
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/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.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
|
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
|
||||||
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
|
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
|
||||||
|
github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY=
|
||||||
|
github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo=
|
||||||
|
github.com/charmbracelet/x/windows v0.2.2 h1:IofanmuvaxnKHuV04sC0eBy/smG6kIKrWG2/jYn2GuM=
|
||||||
|
github.com/charmbracelet/x/windows v0.2.2/go.mod h1:/8XtdKZzedat74NQFn0NGlGL4soHB0YQZrETF96h75k=
|
||||||
|
github.com/charmbracelet/x/xpty v0.1.3 h1:eGSitii4suhzrISYH50ZfufV3v085BXQwIytcOdFSsw=
|
||||||
|
github.com/charmbracelet/x/xpty v0.1.3/go.mod h1:poPYpWuLDBFCKmKLDnhBp51ATa0ooD8FhypRwEFtH3Y=
|
||||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||||
github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM=
|
github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM=
|
||||||
github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ=
|
github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ=
|
||||||
@@ -202,6 +210,10 @@ 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/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8=
|
||||||
|
github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0=
|
||||||
|
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
|
||||||
|
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
|
||||||
github.com/cloudflare/cfssl v0.0.0-20180223231731-4e2dcbde5004 h1:lkAMpLVBDaj17e85keuznYcH5rqI438v41pKcBl4ZxQ=
|
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/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=
|
||||||
@@ -341,8 +353,6 @@ github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymF
|
|||||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
|
|
||||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
|
|
||||||
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0=
|
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0=
|
||||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||||
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
|
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
|
||||||
@@ -766,8 +776,8 @@ github.com/libp2p/go-yamux/v4 v4.0.1 h1:FfDR4S1wj6Bw2Pqbc8Uz7pCxeRBPbwsBbEdfwiCy
|
|||||||
github.com/libp2p/go-yamux/v4 v4.0.1/go.mod h1:NWjl8ZTLOGlozrXSOZ/HlfG++39iKNnM5wwmtQP1YB4=
|
github.com/libp2p/go-yamux/v4 v4.0.1/go.mod h1:NWjl8ZTLOGlozrXSOZ/HlfG++39iKNnM5wwmtQP1YB4=
|
||||||
github.com/lmittmann/tint v1.0.5 h1:NQclAutOfYsqs2F1Lenue6OoWCajs5wJcP3DfWVpePw=
|
github.com/lmittmann/tint v1.0.5 h1:NQclAutOfYsqs2F1Lenue6OoWCajs5wJcP3DfWVpePw=
|
||||||
github.com/lmittmann/tint v1.0.5/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE=
|
github.com/lmittmann/tint v1.0.5/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE=
|
||||||
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
|
||||||
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||||
github.com/magiconair/properties v1.5.3/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
github.com/magiconair/properties v1.5.3/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||||
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||||
github.com/magiconair/properties v1.8.9 h1:nWcCbLq1N2v/cpNsy5WvQ37Fb+YElfq20WJ/a8RkpQM=
|
github.com/magiconair/properties v1.8.9 h1:nWcCbLq1N2v/cpNsy5WvQ37Fb+YElfq20WJ/a8RkpQM=
|
||||||
@@ -795,10 +805,8 @@ github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27k
|
|||||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
|
github.com/mattn/go-runewidth v0.0.20 h1:WcT52H91ZUAwy8+HUkdM3THM6gXqXuLJi9O3rjcQQaQ=
|
||||||
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
|
github.com/mattn/go-runewidth v0.0.20/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
||||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
|
||||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
|
||||||
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=
|
||||||
@@ -895,12 +903,8 @@ github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7P
|
|||||||
github.com/mr-tron/base58 v1.1.2/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc=
|
github.com/mr-tron/base58 v1.1.2/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc=
|
||||||
github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o=
|
github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o=
|
||||||
github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc=
|
github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc=
|
||||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
|
|
||||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
|
|
||||||
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
|
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
|
||||||
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
||||||
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
|
|
||||||
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
|
|
||||||
github.com/multiformats/go-base32 v0.1.0 h1:pVx9xoSPqEIQG8o+UbAe7DNi51oej1NtK+aGkbLYxPE=
|
github.com/multiformats/go-base32 v0.1.0 h1:pVx9xoSPqEIQG8o+UbAe7DNi51oej1NtK+aGkbLYxPE=
|
||||||
github.com/multiformats/go-base32 v0.1.0/go.mod h1:Kj3tFY6zNr+ABYMqeUNeGvkIC/UYgtWibDcT0rExnbI=
|
github.com/multiformats/go-base32 v0.1.0/go.mod h1:Kj3tFY6zNr+ABYMqeUNeGvkIC/UYgtWibDcT0rExnbI=
|
||||||
github.com/multiformats/go-base36 v0.2.0 h1:lFsAbNOGeKtuKozrtBsAkSVhv1p9D0/qedU9rQyccr0=
|
github.com/multiformats/go-base36 v0.2.0 h1:lFsAbNOGeKtuKozrtBsAkSVhv1p9D0/qedU9rQyccr0=
|
||||||
@@ -1059,7 +1063,6 @@ github.com/redis/go-redis/v9 v9.7.3 h1:YpPyAayJV+XErNsatSElgRZZVCwXX9QzkKYNvO7x0
|
|||||||
github.com/redis/go-redis/v9 v9.7.3/go.mod h1:bGUrSggJ9X9GUmZpZNEOQKaANxSGgOEBRltRTZHSvrA=
|
github.com/redis/go-redis/v9 v9.7.3/go.mod h1:bGUrSggJ9X9GUmZpZNEOQKaANxSGgOEBRltRTZHSvrA=
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
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/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
|
||||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
github.com/rivo/uniseg v0.4.7 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=
|
||||||
@@ -1418,8 +1421,8 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
|||||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||||
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||||
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=
|
||||||
@@ -1452,7 +1455,6 @@ golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBc
|
|||||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
@@ -1471,8 +1473,8 @@ golang.org/x/sys v0.12.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.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
|
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
||||||
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||||
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=
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/charmbracelet/lipgloss"
|
"charm.land/lipgloss/v2"
|
||||||
composetypes "github.com/compose-spec/compose-go/v2/types"
|
composetypes "github.com/compose-spec/compose-go/v2/types"
|
||||||
mapset "github.com/deckarep/golang-set/v2"
|
mapset "github.com/deckarep/golang-set/v2"
|
||||||
"github.com/docker/cli/cli/command"
|
"github.com/docker/cli/cli/command"
|
||||||
|
|||||||
+82
-28
@@ -71,12 +71,22 @@ func (cli *CLI) SetCurrentContext(name string) error {
|
|||||||
return cli.Config.Save()
|
return cli.Config.Save()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cli *CLI) GetContextOverrideOrCurrent() string {
|
func (cli *CLI) ContextOverrideOrCurrent() string {
|
||||||
contextName := cli.contextOverride
|
if cli.contextOverride != "" {
|
||||||
if contextName == "" {
|
return cli.contextOverride
|
||||||
contextName = cli.Config.CurrentContext
|
|
||||||
}
|
}
|
||||||
return contextName
|
if cli.Config != nil {
|
||||||
|
return cli.Config.CurrentContext
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// DirectConnection returns the connection string if --connect or UNCLOUD_CONNECT was specified.
|
||||||
|
func (cli *CLI) DirectConnection() string {
|
||||||
|
if cli.conn != nil {
|
||||||
|
return cli.conn.String()
|
||||||
|
}
|
||||||
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// ConnectCluster connects to a cluster using the context override or the current context if not specified.
|
// ConnectCluster connects to a cluster using the context override or the current context if not specified.
|
||||||
@@ -155,13 +165,15 @@ func (cli *CLI) ConnectClusterWithOptions(ctx context.Context, opts ConnectOptio
|
|||||||
}
|
}
|
||||||
|
|
||||||
type InitClusterOptions struct {
|
type InitClusterOptions struct {
|
||||||
Context string
|
Context string
|
||||||
MachineName string
|
MachineName string
|
||||||
Network netip.Prefix
|
Network netip.Prefix
|
||||||
PublicIP *netip.Addr
|
PublicIP *netip.Addr
|
||||||
RemoteMachine *RemoteMachine
|
RemoteMachine *RemoteMachine
|
||||||
SkipInstall bool
|
SkipInstall bool
|
||||||
Version string
|
Version string
|
||||||
|
AutoConfirm bool
|
||||||
|
WireguardEndpoints []*pb.IPPort
|
||||||
}
|
}
|
||||||
|
|
||||||
// InitCluster initialises a new cluster on a remote machine and returns a client to interact with the cluster.
|
// InitCluster initialises a new cluster on a remote machine and returns a client to interact with the cluster.
|
||||||
@@ -197,7 +209,12 @@ func (cli *CLI) initRemoteMachine(ctx context.Context, opts InitClusterOptions)
|
|||||||
return nil, fmt.Errorf("inspect machine: %w", err)
|
return nil, fmt.Errorf("inspect machine: %w", err)
|
||||||
}
|
}
|
||||||
if minfo.Id != "" {
|
if minfo.Id != "" {
|
||||||
if err = promptResetMachine(ctx, machineClient.MachineClient); err != nil {
|
if !opts.AutoConfirm {
|
||||||
|
if err = promptResetMachine(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err = resetAndWaitMachine(ctx, machineClient.MachineClient); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -214,8 +231,9 @@ func (cli *CLI) initRemoteMachine(ctx context.Context, opts InitClusterOptions)
|
|||||||
}
|
}
|
||||||
|
|
||||||
req := &pb.InitClusterRequest{
|
req := &pb.InitClusterRequest{
|
||||||
MachineName: opts.MachineName,
|
MachineName: opts.MachineName,
|
||||||
Network: pb.NewIPPrefix(opts.Network),
|
Network: pb.NewIPPrefix(opts.Network),
|
||||||
|
WireguardEndpoints: opts.WireguardEndpoints,
|
||||||
}
|
}
|
||||||
if opts.PublicIP != nil {
|
if opts.PublicIP != nil {
|
||||||
if opts.PublicIP.IsValid() {
|
if opts.PublicIP.IsValid() {
|
||||||
@@ -292,11 +310,13 @@ func (cli *CLI) newContextName(name string) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type AddMachineOptions struct {
|
type AddMachineOptions struct {
|
||||||
MachineName string
|
MachineName string
|
||||||
PublicIP *netip.Addr
|
PublicIP *netip.Addr
|
||||||
RemoteMachine *RemoteMachine
|
RemoteMachine *RemoteMachine
|
||||||
SkipInstall bool
|
SkipInstall bool
|
||||||
Version string
|
Version string
|
||||||
|
AutoConfirm bool
|
||||||
|
WireguardEndpoints []*pb.IPPort
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddMachine provisions a remote machine and adds it to the cluster. It returns a cluster client and a machine client.
|
// AddMachine provisions a remote machine and adds it to the cluster. It returns a cluster client and a machine client.
|
||||||
@@ -304,7 +324,7 @@ type AddMachineOptions struct {
|
|||||||
// cluster. The machine client is connected to the new machine and can be used to interact with it.
|
// 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.
|
// Both client should be closed after use by the caller.
|
||||||
func (cli *CLI) AddMachine(ctx context.Context, opts AddMachineOptions) (*client.Client, *client.Client, error) {
|
func (cli *CLI) AddMachine(ctx context.Context, opts AddMachineOptions) (*client.Client, *client.Client, error) {
|
||||||
contextName := cli.GetContextOverrideOrCurrent()
|
contextName := cli.ContextOverrideOrCurrent()
|
||||||
c, err := cli.ConnectCluster(ctx)
|
c, err := cli.ConnectCluster(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, fmt.Errorf("connect to cluster (context '%s'): %w", contextName, err)
|
return nil, nil, fmt.Errorf("connect to cluster (context '%s'): %w", contextName, err)
|
||||||
@@ -326,6 +346,7 @@ func (cli *CLI) AddMachine(ctx context.Context, opts AddMachineOptions) (*client
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
// 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.
|
||||||
|
// TODO: refactor to use client.InspectMachine.
|
||||||
minfo, err := machineClient.Inspect(ctx, &emptypb.Empty{})
|
minfo, err := machineClient.Inspect(ctx, &emptypb.Empty{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, fmt.Errorf("inspect machine: %w", err)
|
return nil, nil, fmt.Errorf("inspect machine: %w", err)
|
||||||
@@ -342,7 +363,12 @@ func (cli *CLI) AddMachine(ctx context.Context, opts AddMachineOptions) (*client
|
|||||||
return nil, nil, fmt.Errorf("machine is already a member of this cluster (%s)", minfo.Name)
|
return nil, nil, fmt.Errorf("machine is already a member of this cluster (%s)", minfo.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = promptResetMachine(ctx, machineClient.MachineClient); err != nil {
|
if !opts.AutoConfirm {
|
||||||
|
if err = promptResetMachine(); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err = resetAndWaitMachine(ctx, machineClient.MachineClient); err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -367,10 +393,16 @@ func (cli *CLI) AddMachine(ctx context.Context, opts AddMachineOptions) (*client
|
|||||||
return nil, nil, 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 WireGuard endpoints from the token,
|
||||||
endpoints := make([]*pb.IPPort, len(token.Endpoints))
|
// or use the explicitly provided WireGuard endpoints if any.
|
||||||
for i, addrPort := range token.Endpoints {
|
var endpoints []*pb.IPPort
|
||||||
endpoints[i] = pb.NewIPPort(addrPort)
|
if len(opts.WireguardEndpoints) > 0 {
|
||||||
|
endpoints = opts.WireguardEndpoints
|
||||||
|
} else {
|
||||||
|
endpoints = make([]*pb.IPPort, len(token.Endpoints))
|
||||||
|
for i, addrPort := range token.Endpoints {
|
||||||
|
endpoints[i] = pb.NewIPPort(addrPort)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
addReq := &pb.AddMachineRequest{
|
addReq := &pb.AddMachineRequest{
|
||||||
Name: opts.MachineName,
|
Name: opts.MachineName,
|
||||||
@@ -393,6 +425,18 @@ func (cli *CLI) AddMachine(ctx context.Context, opts AddMachineOptions) (*client
|
|||||||
return nil, nil, fmt.Errorf("add machine to cluster (context '%s'): %w", contextName, err)
|
return nil, nil, fmt.Errorf("add machine to cluster (context '%s'): %w", contextName, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get the current store DB version from the cluster to pass to the join request.
|
||||||
|
var storeDBVersion int64
|
||||||
|
inspectResp, err := c.MachineClient.InspectMachine(ctx, &emptypb.Empty{})
|
||||||
|
if err != nil {
|
||||||
|
// TODO(lhf): remove Unimplemented check when v0.17.0 is released.
|
||||||
|
if status.Convert(err).Code() != codes.Unimplemented {
|
||||||
|
return nil, nil, fmt.Errorf("inspect current cluster machine: %w", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
storeDBVersion = inspectResp.Machines[0].StoreDbVersion
|
||||||
|
}
|
||||||
|
|
||||||
// Get the most up-to-date list of other machines in the cluster to include them in the join request.
|
// 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)
|
machines, err := c.ListMachines(ctx, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -407,8 +451,9 @@ func (cli *CLI) AddMachine(ctx context.Context, opts AddMachineOptions) (*client
|
|||||||
|
|
||||||
// Configure the remote machine to join the cluster.
|
// Configure the remote machine to join the cluster.
|
||||||
joinReq := &pb.JoinClusterRequest{
|
joinReq := &pb.JoinClusterRequest{
|
||||||
Machine: addResp.Machine,
|
Machine: addResp.Machine,
|
||||||
OtherMachines: otherMachines,
|
OtherMachines: otherMachines,
|
||||||
|
MinStoreDbVersion: storeDBVersion,
|
||||||
}
|
}
|
||||||
if _, err = machineClient.JoinCluster(ctx, joinReq); err != nil {
|
if _, err = machineClient.JoinCluster(ctx, joinReq); err != nil {
|
||||||
return nil, nil, fmt.Errorf("join cluster: %w", err)
|
return nil, nil, fmt.Errorf("join cluster: %w", err)
|
||||||
@@ -526,6 +571,15 @@ func provisionOrConnectRemoteMachine(
|
|||||||
return machineClient, nil
|
return machineClient, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetClusterContextIfUnset sets the cluster context override only if no --context flag was used
|
||||||
|
// and no --connect direct connection is active.
|
||||||
|
func (cli *CLI) SetClusterContextIfUnset(name string) {
|
||||||
|
if name == "" || cli.contextOverride != "" || cli.conn != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cli.contextOverride = name
|
||||||
|
}
|
||||||
|
|
||||||
// ProgressOut returns an output stream for progress writer.
|
// ProgressOut returns an output stream for progress writer.
|
||||||
func (cli *CLI) ProgressOut() *streams.Out {
|
func (cli *CLI) ProgressOut() *streams.Out {
|
||||||
return streams.NewOut(os.Stdout)
|
return streams.NewOut(os.Stdout)
|
||||||
|
|||||||
@@ -11,11 +11,6 @@ import (
|
|||||||
"github.com/psviderski/uncloud/internal/secret"
|
"github.com/psviderski/uncloud/internal/secret"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
DefaultSSHUser = "root"
|
|
||||||
DefaultSSHPort = 22
|
|
||||||
)
|
|
||||||
|
|
||||||
type MachineConnection struct {
|
type MachineConnection struct {
|
||||||
SSH SSHDestination `yaml:"ssh,omitempty"`
|
SSH SSHDestination `yaml:"ssh,omitempty"`
|
||||||
SSHCLI SSHDestination `yaml:"ssh_cli,omitempty"`
|
SSHCLI SSHDestination `yaml:"ssh_cli,omitempty"`
|
||||||
@@ -30,7 +25,7 @@ type MachineConnection struct {
|
|||||||
MachineID string `yaml:"machine_id,omitempty"`
|
MachineID string `yaml:"machine_id,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c MachineConnection) String() string {
|
func (c *MachineConnection) String() string {
|
||||||
if c.SSH != "" {
|
if c.SSH != "" {
|
||||||
return "ssh://" + string(c.SSH)
|
return "ssh://" + string(c.SSH)
|
||||||
} else if c.SSHCLI != "" {
|
} else if c.SSHCLI != "" {
|
||||||
@@ -69,36 +64,37 @@ func (c *MachineConnection) Validate() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 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".
|
||||||
// The default user "root" and port 22 can be omitted.
|
// Empty user or port components are omitted.
|
||||||
type SSHDestination string
|
type SSHDestination string
|
||||||
|
|
||||||
|
// NewSSHDestination constructs an SSHDestination from user, host, and port components.
|
||||||
|
// If user is empty, it is omitted.
|
||||||
|
// If port is 0, it is omitted.
|
||||||
func NewSSHDestination(user, host string, port int) SSHDestination {
|
func NewSSHDestination(user, host string, port int) SSHDestination {
|
||||||
dst := host
|
dst := host
|
||||||
if port != 0 && port != DefaultSSHPort {
|
if port != 0 {
|
||||||
dst = net.JoinHostPort(host, strconv.Itoa(port))
|
dst = net.JoinHostPort(host, strconv.Itoa(port))
|
||||||
}
|
}
|
||||||
if user == "" {
|
if user != "" {
|
||||||
user = DefaultSSHUser
|
dst = fmt.Sprintf("%s@%s", user, dst)
|
||||||
}
|
}
|
||||||
dst = user + "@" + dst
|
|
||||||
return SSHDestination(dst)
|
return SSHDestination(dst)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Parse parses the SSH destination string into user, host, and port components.
|
||||||
|
// If user is not specified, it returns an empty string.
|
||||||
|
// If port is not specified, it returns 0.
|
||||||
func (d SSHDestination) Parse() (user string, host string, port int, err error) {
|
func (d SSHDestination) Parse() (user string, host string, port int, err error) {
|
||||||
host = string(d)
|
host = string(d)
|
||||||
if strings.Contains(host, "@") {
|
if strings.Contains(host, "@") {
|
||||||
user, host, _ = strings.Cut(host, "@")
|
user, host, _ = strings.Cut(host, "@")
|
||||||
}
|
}
|
||||||
if user == "" {
|
|
||||||
user = DefaultSSHUser
|
|
||||||
}
|
|
||||||
h, p, sErr := net.SplitHostPort(host)
|
h, p, sErr := net.SplitHostPort(host)
|
||||||
if sErr == nil {
|
if sErr == nil {
|
||||||
host = h
|
host = h
|
||||||
port, err = strconv.Atoi(p)
|
port, err = strconv.Atoi(p)
|
||||||
}
|
}
|
||||||
if port == 0 {
|
|
||||||
port = DefaultSSHPort
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
+12
-11
@@ -7,10 +7,11 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/charmbracelet/bubbles/spinner"
|
"charm.land/bubbles/v2/spinner"
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "charm.land/bubbletea/v2"
|
||||||
"github.com/charmbracelet/lipgloss"
|
"charm.land/lipgloss/v2"
|
||||||
"github.com/psviderski/uncloud/internal/cli/config"
|
"github.com/psviderski/uncloud/internal/cli/config"
|
||||||
|
"github.com/psviderski/uncloud/internal/cli/tui"
|
||||||
"github.com/psviderski/uncloud/internal/fs"
|
"github.com/psviderski/uncloud/internal/fs"
|
||||||
"github.com/psviderski/uncloud/pkg/client"
|
"github.com/psviderski/uncloud/pkg/client"
|
||||||
"github.com/psviderski/uncloud/pkg/client/connector"
|
"github.com/psviderski/uncloud/pkg/client/connector"
|
||||||
@@ -33,7 +34,7 @@ func ConnectCluster(ctx context.Context, conn config.MachineConnection, opts Con
|
|||||||
// If the stdout is not a terminal, it falls back to simple progress logs to stderr.
|
// If the stdout is not a terminal, it falls back to simple progress logs to stderr.
|
||||||
func connectClusterWithProgress(ctx context.Context, conn config.MachineConnection) (*client.Client, error) {
|
func connectClusterWithProgress(ctx context.Context, conn config.MachineConnection) (*client.Client, error) {
|
||||||
// If stdout is not a terminal, fall back to simple progress logs.
|
// If stdout is not a terminal, fall back to simple progress logs.
|
||||||
if !IsStdoutTerminal() {
|
if !tui.IsStdoutTerminal() {
|
||||||
fmt.Fprintln(os.Stderr, "Connecting to", conn.String())
|
fmt.Fprintln(os.Stderr, "Connecting to", conn.String())
|
||||||
cli, err := connectCluster(ctx, conn)
|
cli, err := connectCluster(ctx, conn)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -125,7 +126,7 @@ type showSpinnerMsg struct{}
|
|||||||
func newConnectModel(ctx context.Context, conn config.MachineConnection) connectModel {
|
func newConnectModel(ctx context.Context, conn config.MachineConnection) connectModel {
|
||||||
s := spinner.New()
|
s := spinner.New()
|
||||||
s.Spinner = spinner.MiniDot
|
s.Spinner = spinner.MiniDot
|
||||||
s.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("3")) // the same yellow as in compose progress
|
s.Style = lipgloss.NewStyle().Foreground(lipgloss.Yellow) // the same yellow as in compose progress
|
||||||
|
|
||||||
return connectModel{
|
return connectModel{
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
@@ -187,8 +188,8 @@ func (m connectModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
m.spinner, cmd = m.spinner.Update(msg)
|
m.spinner, cmd = m.spinner.Update(msg)
|
||||||
return m, cmd
|
return m, cmd
|
||||||
|
|
||||||
case tea.KeyMsg:
|
case tea.KeyPressMsg:
|
||||||
if msg.Type == tea.KeyCtrlC {
|
if msg.String() == "ctrl+c" {
|
||||||
m.result.err = fmt.Errorf("connection cancelled")
|
m.result.err = fmt.Errorf("connection cancelled")
|
||||||
m.done = true
|
m.done = true
|
||||||
return m, tea.Quit
|
return m, tea.Quit
|
||||||
@@ -198,15 +199,15 @@ func (m connectModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m connectModel) View() string {
|
func (m connectModel) View() tea.View {
|
||||||
// Don't show anything if done or spinner not yet visible.
|
// Don't show anything if done or spinner not yet visible.
|
||||||
if m.done || !m.showSpinner {
|
if m.done || !m.showSpinner {
|
||||||
return ""
|
return tea.NewView("")
|
||||||
}
|
}
|
||||||
|
|
||||||
style := lipgloss.NewStyle().Foreground(lipgloss.Color("153"))
|
style := lipgloss.NewStyle().Foreground(lipgloss.Color("153"))
|
||||||
return fmt.Sprintf("%s %s\n",
|
return tea.NewView(fmt.Sprintf("%s %s\n",
|
||||||
m.spinner.View(),
|
m.spinner.View(),
|
||||||
fmt.Sprintf("Connecting to %s", style.Render(m.conn.String())),
|
fmt.Sprintf("Connecting to %s", style.Render(m.conn.String())),
|
||||||
)
|
))
|
||||||
}
|
}
|
||||||
|
|||||||
+24
-1
@@ -1,10 +1,14 @@
|
|||||||
package cli
|
package cli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
"net/netip"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/psviderski/uncloud/internal/machine/api/pb"
|
||||||
|
"github.com/psviderski/uncloud/internal/machine/network"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -16,7 +20,7 @@ func ExpandCommaSeparatedValues(values []string) []string {
|
|||||||
|
|
||||||
var expanded []string
|
var expanded []string
|
||||||
for _, value := range values {
|
for _, value := range values {
|
||||||
for _, v := range strings.Split(value, ",") {
|
for v := range strings.SplitSeq(value, ",") {
|
||||||
if v = strings.TrimSpace(v); v != "" {
|
if v = strings.TrimSpace(v); v != "" {
|
||||||
expanded = append(expanded, v)
|
expanded = append(expanded, v)
|
||||||
}
|
}
|
||||||
@@ -34,3 +38,22 @@ func BindEnvToFlag(cmd *cobra.Command, flagName, envVar string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ParseWireGuardEndpoints parses a list of endpoint strings into a list of IPPort protobuf messages. Each value can
|
||||||
|
// be an IP address, IP:PORT, IPv6, or [IPv6]:PORT. If the port is omitted, the default WireGuard port is used.
|
||||||
|
func ParseWireGuardEndpoints(values []string) ([]*pb.IPPort, error) {
|
||||||
|
endpoints := make([]*pb.IPPort, 0, len(values))
|
||||||
|
for _, v := range values {
|
||||||
|
ap, err := netip.ParseAddrPort(v)
|
||||||
|
if err != nil {
|
||||||
|
// Try parsing as a bare IP address and use the default WireGuard port.
|
||||||
|
addr, addrErr := netip.ParseAddr(v)
|
||||||
|
if addrErr != nil {
|
||||||
|
return nil, fmt.Errorf("invalid endpoint '%s': must be IP, IPv6, IP:PORT, or [IPv6]:PORT", v)
|
||||||
|
}
|
||||||
|
ap = netip.AddrPortFrom(addr, network.WireGuardPort)
|
||||||
|
}
|
||||||
|
endpoints = append(endpoints, pb.NewIPPort(ap))
|
||||||
|
}
|
||||||
|
return endpoints, nil
|
||||||
|
}
|
||||||
|
|||||||
+15
-2
@@ -2,13 +2,15 @@ package cli
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"charm.land/huh/v2"
|
||||||
"github.com/cenkalti/backoff/v4"
|
"github.com/cenkalti/backoff/v4"
|
||||||
"github.com/charmbracelet/huh"
|
"github.com/psviderski/uncloud/internal/cli/tui"
|
||||||
"github.com/psviderski/uncloud/internal/machine/api/pb"
|
"github.com/psviderski/uncloud/internal/machine/api/pb"
|
||||||
"github.com/psviderski/uncloud/internal/sshexec"
|
"github.com/psviderski/uncloud/internal/sshexec"
|
||||||
"google.golang.org/protobuf/types/known/emptypb"
|
"google.golang.org/protobuf/types/known/emptypb"
|
||||||
@@ -86,7 +88,13 @@ func provisionMachine(ctx context.Context, exec sshexec.Executor, version string
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func promptResetMachine(ctx context.Context, machineClient pb.MachineClient) error {
|
func promptResetMachine() error {
|
||||||
|
if !tui.IsStdinTerminal() {
|
||||||
|
return errors.New("the remote machine is already initialised as a cluster member; " +
|
||||||
|
"cannot ask to confirm reset in non-interactive mode, " +
|
||||||
|
"use --yes flag or set UNCLOUD_AUTO_CONFIRM=true to auto-confirm")
|
||||||
|
}
|
||||||
|
|
||||||
var confirm bool
|
var confirm bool
|
||||||
form := huh.NewForm(
|
form := huh.NewForm(
|
||||||
huh.NewGroup(
|
huh.NewGroup(
|
||||||
@@ -110,10 +118,15 @@ func promptResetMachine(ctx context.Context, machineClient pb.MachineClient) err
|
|||||||
return fmt.Errorf("remote machine is already initialised as a cluster member")
|
return fmt.Errorf("remote machine is already initialised as a cluster member")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func resetAndWaitMachine(ctx context.Context, machineClient pb.MachineClient) error {
|
||||||
if _, err := machineClient.Reset(ctx, &pb.ResetRequest{}); err != nil {
|
if _, err := machineClient.Reset(ctx, &pb.ResetRequest{}); err != nil {
|
||||||
return fmt.Errorf("reset remote machine: %w. You can also manually run 'uncloud-uninstall' "+
|
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)
|
"on the remote machine to fully uninstall Uncloud from it", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println("Resetting the remote machine...")
|
fmt.Println("Resetting the remote machine...")
|
||||||
if err := waitMachineReady(ctx, machineClient, 1*time.Minute); err != nil {
|
if err := waitMachineReady(ctx, machineClient, 1*time.Minute); err != nil {
|
||||||
return fmt.Errorf("wait for machine to be ready after reset: %w", err)
|
return fmt.Errorf("wait for machine to be ready after reset: %w", err)
|
||||||
|
|||||||
@@ -1,38 +0,0 @@
|
|||||||
package cli
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/charmbracelet/huh"
|
|
||||||
"golang.org/x/term"
|
|
||||||
)
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsStdinTerminal checks if the standard input is a terminal (TTY).
|
|
||||||
func IsStdinTerminal() bool {
|
|
||||||
return term.IsTerminal(int(os.Stdin.Fd()))
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsStdoutTerminal checks if the standard output is a terminal (TTY).
|
|
||||||
func IsStdoutTerminal() bool {
|
|
||||||
return term.IsTerminal(int(os.Stdout.Fd()))
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package tui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
func PrintWarning(msg string) {
|
||||||
|
styledMsg := BoldYellow.Render(fmt.Sprintf("WARNING: %s", msg))
|
||||||
|
fmt.Fprintln(os.Stderr, styledMsg)
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
package tui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"charm.land/huh/v2"
|
||||||
|
"charm.land/lipgloss/v2"
|
||||||
|
"golang.org/x/term"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Confirm shows a confirmation prompt with a yellow-styled title.
|
||||||
|
// If title is empty, it defaults to "Do you want to continue?".
|
||||||
|
func Confirm(title string) (bool, error) {
|
||||||
|
if title == "" {
|
||||||
|
title = "Do you want to continue?"
|
||||||
|
}
|
||||||
|
|
||||||
|
var confirmed bool
|
||||||
|
form := huh.NewForm(
|
||||||
|
huh.NewGroup(
|
||||||
|
huh.NewConfirm().
|
||||||
|
Title(title).
|
||||||
|
Affirmative("Yes!").
|
||||||
|
Negative("No").
|
||||||
|
Value(&confirmed),
|
||||||
|
),
|
||||||
|
).WithTheme(ThemeConfirm()).
|
||||||
|
WithAccessible(true)
|
||||||
|
if err := form.Run(); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return confirmed, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ThemeConfirm returns a huh theme with a bold yellow title style for the confirmation prompt.
|
||||||
|
func ThemeConfirm() huh.Theme {
|
||||||
|
return huh.ThemeFunc(func(isDark bool) *huh.Styles {
|
||||||
|
t := huh.ThemeBase(isDark)
|
||||||
|
t.Focused.Title = t.Focused.Title.Foreground(lipgloss.Yellow).Bold(true)
|
||||||
|
return t
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsStdinTerminal checks if the standard input is a terminal (TTY).
|
||||||
|
func IsStdinTerminal() bool {
|
||||||
|
return term.IsTerminal(int(os.Stdin.Fd()))
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsStdoutTerminal checks if the standard output is a terminal (TTY).
|
||||||
|
func IsStdoutTerminal() bool {
|
||||||
|
return term.IsTerminal(int(os.Stdout.Fd()))
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package tui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"charm.land/lipgloss/v2"
|
||||||
|
"github.com/distribution/reference"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
Faint = lipgloss.NewStyle().Faint(true)
|
||||||
|
Red = lipgloss.NewStyle().Foreground(lipgloss.Red)
|
||||||
|
Green = lipgloss.NewStyle().Foreground(lipgloss.Green)
|
||||||
|
Yellow = lipgloss.NewStyle().Foreground(lipgloss.Yellow)
|
||||||
|
|
||||||
|
Bold = lipgloss.NewStyle().Bold(true)
|
||||||
|
BoldRed = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Red)
|
||||||
|
BoldGreen = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Green)
|
||||||
|
BoldYellow = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Yellow)
|
||||||
|
|
||||||
|
NameStyle = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("152"))
|
||||||
|
)
|
||||||
|
|
||||||
|
// FormatImage renders an image reference with the given style, using a faint colon separator for tagged images.
|
||||||
|
// Returns the styled original string if parsing fails.
|
||||||
|
func FormatImage(image string, style lipgloss.Style) string {
|
||||||
|
ref, err := reference.ParseDockerRef(image)
|
||||||
|
if err != nil {
|
||||||
|
return style.Render(image)
|
||||||
|
}
|
||||||
|
|
||||||
|
if tagged, ok := ref.(reference.NamedTagged); ok {
|
||||||
|
return style.Render(reference.FamiliarName(ref)) +
|
||||||
|
Faint.Render(":") +
|
||||||
|
style.Render(tagged.Tag())
|
||||||
|
}
|
||||||
|
return style.Render(reference.FamiliarString(ref))
|
||||||
|
}
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
package daemon
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"net/netip"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"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.
|
|
||||||
// TODO: ideally, this should be an RPC call to the daemon API to ensure the config is created and up-to-date.
|
|
||||||
func MachineToken(dataDir string) (machine.Token, error) {
|
|
||||||
state, err := machine.ParseState(machine.StatePath(dataDir))
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, os.ErrNotExist) {
|
|
||||||
return machine.Token{}, fmt.Errorf("load machine config (is uncloudd daemon running?): %w", err)
|
|
||||||
}
|
|
||||||
return machine.Token{}, fmt.Errorf("load machine config: %w", err)
|
|
||||||
}
|
|
||||||
if len(state.Network.PublicKey) == 0 {
|
|
||||||
return machine.Token{}, errors.New("public key is not set in machine config")
|
|
||||||
}
|
|
||||||
|
|
||||||
ips, err := network.ListRoutableIPs()
|
|
||||||
if err != nil {
|
|
||||||
return machine.Token{}, fmt.Errorf("list routable addresses: %w", err)
|
|
||||||
}
|
|
||||||
publicIP, err := network.GetPublicIP()
|
|
||||||
// Ignore the error if failed to get the public IP using API services.
|
|
||||||
if err == nil {
|
|
||||||
ips = append(ips, publicIP)
|
|
||||||
}
|
|
||||||
|
|
||||||
endpoints := make([]netip.AddrPort, len(ips))
|
|
||||||
for i, ip := range ips {
|
|
||||||
endpoints[i] = netip.AddrPortFrom(ip, network.WireGuardPort)
|
|
||||||
}
|
|
||||||
return machine.NewToken(state.Network.PublicKey, publicIP, endpoints), nil
|
|
||||||
}
|
|
||||||
+1
-1
@@ -26,7 +26,7 @@ type RecordResponse struct {
|
|||||||
type AuthErrorResponse struct {
|
type AuthErrorResponse struct {
|
||||||
Status int `json:"status,omitempty"`
|
Status int `json:"status,omitempty"`
|
||||||
Message string `json:"msg,omitempty"`
|
Message string `json:"msg,omitempty"`
|
||||||
Data authErrorData `json:"data,omitempty"`
|
Data authErrorData `json:"data"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type authErrorData struct {
|
type authErrorData struct {
|
||||||
|
|||||||
@@ -36,10 +36,12 @@ func InspectGitState(dir string) (GitState, error) {
|
|||||||
return state, nil
|
return state, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the current commit SHA.
|
// Get the current commit SHA. An initialised but empty repo has no HEAD yet,
|
||||||
sha, err := gitCommand(dir, "rev-parse", "HEAD")
|
// so treat it as a non-repo so callers can fall back to non-git logic.
|
||||||
|
sha, err := gitCommand(dir, "rev-parse", "--verify", "HEAD")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return state, fmt.Errorf("get current commit SHA: %w", err)
|
state.IsRepo = false
|
||||||
|
return state, nil
|
||||||
}
|
}
|
||||||
state.SHA = strings.TrimSpace(sha)
|
state.SHA = strings.TrimSpace(sha)
|
||||||
|
|
||||||
|
|||||||
@@ -46,6 +46,22 @@ func TestInspectGitState_NotARepo(t *testing.T) {
|
|||||||
assert.False(t, state.IsDirty)
|
assert.False(t, state.IsDirty)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestInspectGitState_EmptyRepo(t *testing.T) {
|
||||||
|
// Create a temporary git repo with no commits.
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
initGitRepo(t, tmpDir)
|
||||||
|
|
||||||
|
state, err := InspectGitState(tmpDir)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// An empty repo should be treated as a non-repo so that callers fall back to non-git logic.
|
||||||
|
assert.False(t, state.IsRepo)
|
||||||
|
assert.Empty(t, state.SHA)
|
||||||
|
assert.Empty(t, state.ShortSHA(7))
|
||||||
|
assert.True(t, state.Date.IsZero())
|
||||||
|
assert.False(t, state.IsDirty)
|
||||||
|
}
|
||||||
|
|
||||||
func TestInspectGitState_CleanRepo(t *testing.T) {
|
func TestInspectGitState_CleanRepo(t *testing.T) {
|
||||||
// Create a temporary git repo.
|
// Create a temporary git repo.
|
||||||
tmpDir := t.TempDir()
|
tmpDir := t.TempDir()
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||||
emptypb "google.golang.org/protobuf/types/known/emptypb"
|
emptypb "google.golang.org/protobuf/types/known/emptypb"
|
||||||
|
timestamppb "google.golang.org/protobuf/types/known/timestamppb"
|
||||||
reflect "reflect"
|
reflect "reflect"
|
||||||
sync "sync"
|
sync "sync"
|
||||||
)
|
)
|
||||||
@@ -232,6 +233,8 @@ type InitClusterRequest struct {
|
|||||||
// *InitClusterRequest_PublicIp
|
// *InitClusterRequest_PublicIp
|
||||||
// *InitClusterRequest_PublicIpAuto
|
// *InitClusterRequest_PublicIpAuto
|
||||||
PublicIpConfig isInitClusterRequest_PublicIpConfig `protobuf_oneof:"public_ip_config"`
|
PublicIpConfig isInitClusterRequest_PublicIpConfig `protobuf_oneof:"public_ip_config"`
|
||||||
|
// Optional WireGuard endpoints other machines will use to connect to this machine instead of auto-discovered ones.
|
||||||
|
WireguardEndpoints []*IPPort `protobuf:"bytes,5,rep,name=wireguard_endpoints,json=wireguardEndpoints,proto3" json:"wireguard_endpoints,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *InitClusterRequest) Reset() {
|
func (x *InitClusterRequest) Reset() {
|
||||||
@@ -301,6 +304,13 @@ func (x *InitClusterRequest) GetPublicIpAuto() bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (x *InitClusterRequest) GetWireguardEndpoints() []*IPPort {
|
||||||
|
if x != nil {
|
||||||
|
return x.WireguardEndpoints
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
type isInitClusterRequest_PublicIpConfig interface {
|
type isInitClusterRequest_PublicIpConfig interface {
|
||||||
isInitClusterRequest_PublicIpConfig()
|
isInitClusterRequest_PublicIpConfig()
|
||||||
}
|
}
|
||||||
@@ -371,6 +381,8 @@ type JoinClusterRequest struct {
|
|||||||
|
|
||||||
Machine *MachineInfo `protobuf:"bytes,1,opt,name=machine,proto3" json:"machine,omitempty"`
|
Machine *MachineInfo `protobuf:"bytes,1,opt,name=machine,proto3" json:"machine,omitempty"`
|
||||||
OtherMachines []*MachineInfo `protobuf:"bytes,3,rep,name=other_machines,json=otherMachines,proto3" json:"other_machines,omitempty"`
|
OtherMachines []*MachineInfo `protobuf:"bytes,3,rep,name=other_machines,json=otherMachines,proto3" json:"other_machines,omitempty"`
|
||||||
|
// Minimum store database version the new machine should sync to before starting cluster operations.
|
||||||
|
MinStoreDbVersion int64 `protobuf:"varint,4,opt,name=min_store_db_version,json=minStoreDbVersion,proto3" json:"min_store_db_version,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *JoinClusterRequest) Reset() {
|
func (x *JoinClusterRequest) Reset() {
|
||||||
@@ -419,6 +431,125 @@ func (x *JoinClusterRequest) GetOtherMachines() []*MachineInfo {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (x *JoinClusterRequest) GetMinStoreDbVersion() int64 {
|
||||||
|
if x != nil {
|
||||||
|
return x.MinStoreDbVersion
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
type InspectMachineResponse struct {
|
||||||
|
state protoimpl.MessageState
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
|
||||||
|
// Must contain only one repeated messages field to allow broadcasting InspectMachine requests to multiple machines.
|
||||||
|
Machines []*MachineDetails `protobuf:"bytes,1,rep,name=machines,proto3" json:"machines,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *InspectMachineResponse) Reset() {
|
||||||
|
*x = InspectMachineResponse{}
|
||||||
|
if protoimpl.UnsafeEnabled {
|
||||||
|
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[6]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *InspectMachineResponse) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*InspectMachineResponse) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *InspectMachineResponse) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[6]
|
||||||
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use InspectMachineResponse.ProtoReflect.Descriptor instead.
|
||||||
|
func (*InspectMachineResponse) Descriptor() ([]byte, []int) {
|
||||||
|
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{6}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *InspectMachineResponse) GetMachines() []*MachineDetails {
|
||||||
|
if x != nil {
|
||||||
|
return x.Machines
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type MachineDetails struct {
|
||||||
|
state protoimpl.MessageState
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
|
||||||
|
Metadata *Metadata `protobuf:"bytes,1,opt,name=metadata,proto3" json:"metadata,omitempty"`
|
||||||
|
Machine *MachineInfo `protobuf:"bytes,2,opt,name=machine,proto3" json:"machine,omitempty"`
|
||||||
|
// Current Corrosion cr-sqlite database version (Lamport timestamp) of the cluster store.
|
||||||
|
StoreDbVersion int64 `protobuf:"varint,3,opt,name=store_db_version,json=storeDbVersion,proto3" json:"store_db_version,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *MachineDetails) Reset() {
|
||||||
|
*x = MachineDetails{}
|
||||||
|
if protoimpl.UnsafeEnabled {
|
||||||
|
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[7]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *MachineDetails) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*MachineDetails) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *MachineDetails) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[7]
|
||||||
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use MachineDetails.ProtoReflect.Descriptor instead.
|
||||||
|
func (*MachineDetails) Descriptor() ([]byte, []int) {
|
||||||
|
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{7}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *MachineDetails) GetMetadata() *Metadata {
|
||||||
|
if x != nil {
|
||||||
|
return x.Metadata
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *MachineDetails) GetMachine() *MachineInfo {
|
||||||
|
if x != nil {
|
||||||
|
return x.Machine
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *MachineDetails) GetStoreDbVersion() int64 {
|
||||||
|
if x != nil {
|
||||||
|
return x.StoreDbVersion
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
type TokenResponse struct {
|
type TokenResponse struct {
|
||||||
state protoimpl.MessageState
|
state protoimpl.MessageState
|
||||||
sizeCache protoimpl.SizeCache
|
sizeCache protoimpl.SizeCache
|
||||||
@@ -430,7 +561,7 @@ type TokenResponse struct {
|
|||||||
func (x *TokenResponse) Reset() {
|
func (x *TokenResponse) Reset() {
|
||||||
*x = TokenResponse{}
|
*x = TokenResponse{}
|
||||||
if protoimpl.UnsafeEnabled {
|
if protoimpl.UnsafeEnabled {
|
||||||
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[6]
|
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[8]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@@ -443,7 +574,7 @@ func (x *TokenResponse) String() string {
|
|||||||
func (*TokenResponse) ProtoMessage() {}
|
func (*TokenResponse) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *TokenResponse) ProtoReflect() protoreflect.Message {
|
func (x *TokenResponse) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[6]
|
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[8]
|
||||||
if protoimpl.UnsafeEnabled && x != nil {
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
@@ -456,7 +587,7 @@ func (x *TokenResponse) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use TokenResponse.ProtoReflect.Descriptor instead.
|
// Deprecated: Use TokenResponse.ProtoReflect.Descriptor instead.
|
||||||
func (*TokenResponse) Descriptor() ([]byte, []int) {
|
func (*TokenResponse) Descriptor() ([]byte, []int) {
|
||||||
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{6}
|
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{8}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *TokenResponse) GetToken() string {
|
func (x *TokenResponse) GetToken() string {
|
||||||
@@ -475,7 +606,7 @@ type ResetRequest struct {
|
|||||||
func (x *ResetRequest) Reset() {
|
func (x *ResetRequest) Reset() {
|
||||||
*x = ResetRequest{}
|
*x = ResetRequest{}
|
||||||
if protoimpl.UnsafeEnabled {
|
if protoimpl.UnsafeEnabled {
|
||||||
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[7]
|
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[9]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@@ -488,7 +619,7 @@ func (x *ResetRequest) String() string {
|
|||||||
func (*ResetRequest) ProtoMessage() {}
|
func (*ResetRequest) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *ResetRequest) ProtoReflect() protoreflect.Message {
|
func (x *ResetRequest) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[7]
|
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[9]
|
||||||
if protoimpl.UnsafeEnabled && x != nil {
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
@@ -501,7 +632,7 @@ func (x *ResetRequest) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use ResetRequest.ProtoReflect.Descriptor instead.
|
// Deprecated: Use ResetRequest.ProtoReflect.Descriptor instead.
|
||||||
func (*ResetRequest) Descriptor() ([]byte, []int) {
|
func (*ResetRequest) Descriptor() ([]byte, []int) {
|
||||||
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{7}
|
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{9}
|
||||||
}
|
}
|
||||||
|
|
||||||
type Service struct {
|
type Service struct {
|
||||||
@@ -518,7 +649,7 @@ type Service struct {
|
|||||||
func (x *Service) Reset() {
|
func (x *Service) Reset() {
|
||||||
*x = Service{}
|
*x = Service{}
|
||||||
if protoimpl.UnsafeEnabled {
|
if protoimpl.UnsafeEnabled {
|
||||||
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[8]
|
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[10]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@@ -531,7 +662,7 @@ func (x *Service) String() string {
|
|||||||
func (*Service) ProtoMessage() {}
|
func (*Service) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *Service) ProtoReflect() protoreflect.Message {
|
func (x *Service) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[8]
|
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[10]
|
||||||
if protoimpl.UnsafeEnabled && x != nil {
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
@@ -544,7 +675,7 @@ func (x *Service) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use Service.ProtoReflect.Descriptor instead.
|
// Deprecated: Use Service.ProtoReflect.Descriptor instead.
|
||||||
func (*Service) Descriptor() ([]byte, []int) {
|
func (*Service) Descriptor() ([]byte, []int) {
|
||||||
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{8}
|
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{10}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *Service) GetId() string {
|
func (x *Service) GetId() string {
|
||||||
@@ -586,7 +717,7 @@ type InspectServiceRequest struct {
|
|||||||
func (x *InspectServiceRequest) Reset() {
|
func (x *InspectServiceRequest) Reset() {
|
||||||
*x = InspectServiceRequest{}
|
*x = InspectServiceRequest{}
|
||||||
if protoimpl.UnsafeEnabled {
|
if protoimpl.UnsafeEnabled {
|
||||||
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[9]
|
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[11]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@@ -599,7 +730,7 @@ func (x *InspectServiceRequest) String() string {
|
|||||||
func (*InspectServiceRequest) ProtoMessage() {}
|
func (*InspectServiceRequest) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *InspectServiceRequest) ProtoReflect() protoreflect.Message {
|
func (x *InspectServiceRequest) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[9]
|
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[11]
|
||||||
if protoimpl.UnsafeEnabled && x != nil {
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
@@ -612,7 +743,7 @@ func (x *InspectServiceRequest) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use InspectServiceRequest.ProtoReflect.Descriptor instead.
|
// Deprecated: Use InspectServiceRequest.ProtoReflect.Descriptor instead.
|
||||||
func (*InspectServiceRequest) Descriptor() ([]byte, []int) {
|
func (*InspectServiceRequest) Descriptor() ([]byte, []int) {
|
||||||
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{9}
|
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{11}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *InspectServiceRequest) GetId() string {
|
func (x *InspectServiceRequest) GetId() string {
|
||||||
@@ -633,7 +764,7 @@ type InspectServiceResponse struct {
|
|||||||
func (x *InspectServiceResponse) Reset() {
|
func (x *InspectServiceResponse) Reset() {
|
||||||
*x = InspectServiceResponse{}
|
*x = InspectServiceResponse{}
|
||||||
if protoimpl.UnsafeEnabled {
|
if protoimpl.UnsafeEnabled {
|
||||||
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[10]
|
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[12]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@@ -646,7 +777,7 @@ func (x *InspectServiceResponse) String() string {
|
|||||||
func (*InspectServiceResponse) ProtoMessage() {}
|
func (*InspectServiceResponse) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *InspectServiceResponse) ProtoReflect() protoreflect.Message {
|
func (x *InspectServiceResponse) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[10]
|
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[12]
|
||||||
if protoimpl.UnsafeEnabled && x != nil {
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
@@ -659,7 +790,7 @@ func (x *InspectServiceResponse) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use InspectServiceResponse.ProtoReflect.Descriptor instead.
|
// Deprecated: Use InspectServiceResponse.ProtoReflect.Descriptor instead.
|
||||||
func (*InspectServiceResponse) Descriptor() ([]byte, []int) {
|
func (*InspectServiceResponse) Descriptor() ([]byte, []int) {
|
||||||
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{10}
|
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{12}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *InspectServiceResponse) GetService() *Service {
|
func (x *InspectServiceResponse) GetService() *Service {
|
||||||
@@ -669,6 +800,164 @@ func (x *InspectServiceResponse) GetService() *Service {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type InspectWireGuardNetworkResponse struct {
|
||||||
|
state protoimpl.MessageState
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
|
||||||
|
InterfaceName string `protobuf:"bytes,1,opt,name=interface_name,json=interfaceName,proto3" json:"interface_name,omitempty"`
|
||||||
|
PublicKey []byte `protobuf:"bytes,2,opt,name=public_key,json=publicKey,proto3" json:"public_key,omitempty"`
|
||||||
|
ListenPort int32 `protobuf:"varint,3,opt,name=listen_port,json=listenPort,proto3" json:"listen_port,omitempty"`
|
||||||
|
Peers []*WireGuardPeer `protobuf:"bytes,4,rep,name=peers,proto3" json:"peers,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *InspectWireGuardNetworkResponse) Reset() {
|
||||||
|
*x = InspectWireGuardNetworkResponse{}
|
||||||
|
if protoimpl.UnsafeEnabled {
|
||||||
|
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[13]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *InspectWireGuardNetworkResponse) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*InspectWireGuardNetworkResponse) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *InspectWireGuardNetworkResponse) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[13]
|
||||||
|
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 InspectWireGuardNetworkResponse.ProtoReflect.Descriptor instead.
|
||||||
|
func (*InspectWireGuardNetworkResponse) Descriptor() ([]byte, []int) {
|
||||||
|
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{13}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *InspectWireGuardNetworkResponse) GetInterfaceName() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.InterfaceName
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *InspectWireGuardNetworkResponse) GetPublicKey() []byte {
|
||||||
|
if x != nil {
|
||||||
|
return x.PublicKey
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *InspectWireGuardNetworkResponse) GetListenPort() int32 {
|
||||||
|
if x != nil {
|
||||||
|
return x.ListenPort
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *InspectWireGuardNetworkResponse) GetPeers() []*WireGuardPeer {
|
||||||
|
if x != nil {
|
||||||
|
return x.Peers
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type WireGuardPeer struct {
|
||||||
|
state protoimpl.MessageState
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
|
||||||
|
PublicKey []byte `protobuf:"bytes,1,opt,name=public_key,json=publicKey,proto3" json:"public_key,omitempty"`
|
||||||
|
Endpoint string `protobuf:"bytes,2,opt,name=endpoint,proto3" json:"endpoint,omitempty"`
|
||||||
|
LastHandshakeTime *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=last_handshake_time,json=lastHandshakeTime,proto3" json:"last_handshake_time,omitempty"`
|
||||||
|
ReceiveBytes int64 `protobuf:"varint,4,opt,name=receive_bytes,json=receiveBytes,proto3" json:"receive_bytes,omitempty"`
|
||||||
|
TransmitBytes int64 `protobuf:"varint,5,opt,name=transmit_bytes,json=transmitBytes,proto3" json:"transmit_bytes,omitempty"`
|
||||||
|
AllowedIps []string `protobuf:"bytes,6,rep,name=allowed_ips,json=allowedIps,proto3" json:"allowed_ips,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *WireGuardPeer) Reset() {
|
||||||
|
*x = WireGuardPeer{}
|
||||||
|
if protoimpl.UnsafeEnabled {
|
||||||
|
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[14]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *WireGuardPeer) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*WireGuardPeer) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *WireGuardPeer) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[14]
|
||||||
|
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 WireGuardPeer.ProtoReflect.Descriptor instead.
|
||||||
|
func (*WireGuardPeer) Descriptor() ([]byte, []int) {
|
||||||
|
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{14}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *WireGuardPeer) GetPublicKey() []byte {
|
||||||
|
if x != nil {
|
||||||
|
return x.PublicKey
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *WireGuardPeer) GetEndpoint() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Endpoint
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *WireGuardPeer) GetLastHandshakeTime() *timestamppb.Timestamp {
|
||||||
|
if x != nil {
|
||||||
|
return x.LastHandshakeTime
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *WireGuardPeer) GetReceiveBytes() int64 {
|
||||||
|
if x != nil {
|
||||||
|
return x.ReceiveBytes
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *WireGuardPeer) GetTransmitBytes() int64 {
|
||||||
|
if x != nil {
|
||||||
|
return x.TransmitBytes
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *WireGuardPeer) GetAllowedIps() []string {
|
||||||
|
if x != nil {
|
||||||
|
return x.AllowedIps
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
type Service_Container struct {
|
type Service_Container struct {
|
||||||
state protoimpl.MessageState
|
state protoimpl.MessageState
|
||||||
sizeCache protoimpl.SizeCache
|
sizeCache protoimpl.SizeCache
|
||||||
@@ -682,7 +971,7 @@ type Service_Container struct {
|
|||||||
func (x *Service_Container) Reset() {
|
func (x *Service_Container) Reset() {
|
||||||
*x = Service_Container{}
|
*x = Service_Container{}
|
||||||
if protoimpl.UnsafeEnabled {
|
if protoimpl.UnsafeEnabled {
|
||||||
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[11]
|
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[15]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@@ -695,7 +984,7 @@ func (x *Service_Container) String() string {
|
|||||||
func (*Service_Container) ProtoMessage() {}
|
func (*Service_Container) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *Service_Container) ProtoReflect() protoreflect.Message {
|
func (x *Service_Container) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[11]
|
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[15]
|
||||||
if protoimpl.UnsafeEnabled && x != nil {
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
@@ -708,7 +997,7 @@ func (x *Service_Container) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use Service_Container.ProtoReflect.Descriptor instead.
|
// Deprecated: Use Service_Container.ProtoReflect.Descriptor instead.
|
||||||
func (*Service_Container) Descriptor() ([]byte, []int) {
|
func (*Service_Container) Descriptor() ([]byte, []int) {
|
||||||
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{8, 0}
|
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{10, 0}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *Service_Container) GetMachineId() string {
|
func (x *Service_Container) GetMachineId() string {
|
||||||
@@ -732,58 +1021,81 @@ var file_internal_machine_api_pb_machine_proto_rawDesc = []byte{
|
|||||||
0x6e, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x62, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e,
|
0x6e, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x62, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e,
|
||||||
0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x03, 0x61, 0x70, 0x69, 0x1a, 0x1b, 0x67, 0x6f,
|
0x65, 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, 0x24, 0x69, 0x6e, 0x74, 0x65, 0x72,
|
0x70, 0x74, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c,
|
||||||
0x6e, 0x61, 0x6c, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f,
|
0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73,
|
||||||
0x70, 0x62, 0x2f, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22,
|
0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x24, 0x69, 0x6e, 0x74, 0x65,
|
||||||
0x85, 0x01, 0x0a, 0x0b, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x12,
|
0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x2f, 0x61, 0x70, 0x69,
|
||||||
0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12,
|
0x2f, 0x70, 0x62, 0x2f, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f,
|
||||||
0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e,
|
0x22, 0x85, 0x01, 0x0a, 0x0b, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f,
|
||||||
0x61, 0x6d, 0x65, 0x12, 0x2c, 0x0a, 0x07, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x18, 0x03,
|
0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64,
|
||||||
0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4e, 0x65, 0x74, 0x77, 0x6f,
|
0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04,
|
||||||
0x72, 0x6b, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x07, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72,
|
0x6e, 0x61, 0x6d, 0x65, 0x12, 0x2c, 0x0a, 0x07, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x18,
|
||||||
0x6b, 0x12, 0x24, 0x0a, 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x69, 0x70, 0x18, 0x04,
|
0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4e, 0x65, 0x74, 0x77,
|
||||||
0x20, 0x01, 0x28, 0x0b, 0x32, 0x07, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x50, 0x52, 0x08, 0x70,
|
0x6f, 0x72, 0x6b, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x07, 0x6e, 0x65, 0x74, 0x77, 0x6f,
|
||||||
0x75, 0x62, 0x6c, 0x69, 0x63, 0x49, 0x70, 0x22, 0xae, 0x01, 0x0a, 0x0d, 0x4e, 0x65, 0x74, 0x77,
|
0x72, 0x6b, 0x12, 0x24, 0x0a, 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x69, 0x70, 0x18,
|
||||||
0x6f, 0x72, 0x6b, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x25, 0x0a, 0x06, 0x73, 0x75, 0x62,
|
0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x07, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x50, 0x52, 0x08,
|
||||||
0x6e, 0x65, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x61, 0x70, 0x69, 0x2e,
|
0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x49, 0x70, 0x22, 0xae, 0x01, 0x0a, 0x0d, 0x4e, 0x65, 0x74,
|
||||||
0x49, 0x50, 0x50, 0x72, 0x65, 0x66, 0x69, 0x78, 0x52, 0x06, 0x73, 0x75, 0x62, 0x6e, 0x65, 0x74,
|
0x77, 0x6f, 0x72, 0x6b, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x25, 0x0a, 0x06, 0x73, 0x75,
|
||||||
0x12, 0x2c, 0x0a, 0x0d, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x5f, 0x69,
|
0x62, 0x6e, 0x65, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x61, 0x70, 0x69,
|
||||||
0x70, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x07, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x50,
|
0x2e, 0x49, 0x50, 0x50, 0x72, 0x65, 0x66, 0x69, 0x78, 0x52, 0x06, 0x73, 0x75, 0x62, 0x6e, 0x65,
|
||||||
0x52, 0x0c, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x49, 0x70, 0x12, 0x29,
|
0x74, 0x12, 0x2c, 0x0a, 0x0d, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x5f,
|
||||||
0x0a, 0x09, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28,
|
0x69, 0x70, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x07, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49,
|
||||||
0x0b, 0x32, 0x0b, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x50, 0x50, 0x6f, 0x72, 0x74, 0x52, 0x09,
|
0x50, 0x52, 0x0c, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x49, 0x70, 0x12,
|
||||||
0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x70, 0x75, 0x62,
|
0x29, 0x0a, 0x09, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x18, 0x03, 0x20, 0x03,
|
||||||
0x6c, 0x69, 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x70,
|
0x28, 0x0b, 0x32, 0x0b, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x50, 0x50, 0x6f, 0x72, 0x74, 0x52,
|
||||||
0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x22, 0x50, 0x0a, 0x1a, 0x43, 0x68, 0x65, 0x63,
|
0x09, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x70, 0x75,
|
||||||
0x6b, 0x50, 0x72, 0x65, 0x72, 0x65, 0x71, 0x75, 0x69, 0x73, 0x69, 0x74, 0x65, 0x73, 0x52, 0x65,
|
0x62, 0x6c, 0x69, 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09,
|
||||||
0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x61, 0x74, 0x69, 0x73, 0x66,
|
0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x22, 0x50, 0x0a, 0x1a, 0x43, 0x68, 0x65,
|
||||||
0x69, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x73, 0x61, 0x74, 0x69, 0x73,
|
0x63, 0x6b, 0x50, 0x72, 0x65, 0x72, 0x65, 0x71, 0x75, 0x69, 0x73, 0x69, 0x74, 0x65, 0x73, 0x52,
|
||||||
0x66, 0x69, 0x65, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x02, 0x20,
|
0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x61, 0x74, 0x69, 0x73,
|
||||||
0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x22, 0xc3, 0x01, 0x0a, 0x12, 0x49,
|
0x66, 0x69, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x73, 0x61, 0x74, 0x69,
|
||||||
0x6e, 0x69, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
|
0x73, 0x66, 0x69, 0x65, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x02,
|
||||||
0x74, 0x12, 0x20, 0x0a, 0x0b, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x4e, 0x61, 0x6d, 0x65,
|
0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x22, 0x81, 0x02, 0x0a, 0x12,
|
||||||
0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x4e,
|
0x49, 0x6e, 0x69, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65,
|
||||||
0x61, 0x6d, 0x65, 0x12, 0x27, 0x0a, 0x07, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x18, 0x02,
|
0x73, 0x74, 0x12, 0x20, 0x0a, 0x0b, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x4e, 0x61, 0x6d,
|
||||||
0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x50, 0x50, 0x72, 0x65,
|
0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65,
|
||||||
0x66, 0x69, 0x78, 0x52, 0x07, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x12, 0x26, 0x0a, 0x09,
|
0x4e, 0x61, 0x6d, 0x65, 0x12, 0x27, 0x0a, 0x07, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x18,
|
||||||
0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x69, 0x70, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32,
|
0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x50, 0x50, 0x72,
|
||||||
0x07, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x50, 0x48, 0x00, 0x52, 0x08, 0x70, 0x75, 0x62, 0x6c,
|
0x65, 0x66, 0x69, 0x78, 0x52, 0x07, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x12, 0x26, 0x0a,
|
||||||
0x69, 0x63, 0x49, 0x70, 0x12, 0x26, 0x0a, 0x0e, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x69,
|
0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x69, 0x70, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b,
|
||||||
0x70, 0x5f, 0x61, 0x75, 0x74, 0x6f, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x48, 0x00, 0x52, 0x0c,
|
0x32, 0x07, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x50, 0x48, 0x00, 0x52, 0x08, 0x70, 0x75, 0x62,
|
||||||
0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x49, 0x70, 0x41, 0x75, 0x74, 0x6f, 0x42, 0x12, 0x0a, 0x10,
|
0x6c, 0x69, 0x63, 0x49, 0x70, 0x12, 0x26, 0x0a, 0x0e, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f,
|
||||||
0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x69, 0x70, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67,
|
0x69, 0x70, 0x5f, 0x61, 0x75, 0x74, 0x6f, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x48, 0x00, 0x52,
|
||||||
0x22, 0x41, 0x0a, 0x13, 0x49, 0x6e, 0x69, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x52,
|
0x0c, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x49, 0x70, 0x41, 0x75, 0x74, 0x6f, 0x12, 0x3c, 0x0a,
|
||||||
0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2a, 0x0a, 0x07, 0x6d, 0x61, 0x63, 0x68, 0x69,
|
0x13, 0x77, 0x69, 0x72, 0x65, 0x67, 0x75, 0x61, 0x72, 0x64, 0x5f, 0x65, 0x6e, 0x64, 0x70, 0x6f,
|
||||||
0x6e, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d,
|
0x69, 0x6e, 0x74, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0b, 0x2e, 0x61, 0x70, 0x69,
|
||||||
0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x07, 0x6d, 0x61, 0x63, 0x68,
|
0x2e, 0x49, 0x50, 0x50, 0x6f, 0x72, 0x74, 0x52, 0x12, 0x77, 0x69, 0x72, 0x65, 0x67, 0x75, 0x61,
|
||||||
0x69, 0x6e, 0x65, 0x22, 0x79, 0x0a, 0x12, 0x4a, 0x6f, 0x69, 0x6e, 0x43, 0x6c, 0x75, 0x73, 0x74,
|
0x72, 0x64, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x42, 0x12, 0x0a, 0x10, 0x70,
|
||||||
|
0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x69, 0x70, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22,
|
||||||
|
0x41, 0x0a, 0x13, 0x49, 0x6e, 0x69, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x52, 0x65,
|
||||||
|
0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2a, 0x0a, 0x07, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e,
|
||||||
|
0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d, 0x61,
|
||||||
|
0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x07, 0x6d, 0x61, 0x63, 0x68, 0x69,
|
||||||
|
0x6e, 0x65, 0x22, 0xaa, 0x01, 0x0a, 0x12, 0x4a, 0x6f, 0x69, 0x6e, 0x43, 0x6c, 0x75, 0x73, 0x74,
|
||||||
0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2a, 0x0a, 0x07, 0x6d, 0x61, 0x63,
|
0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2a, 0x0a, 0x07, 0x6d, 0x61, 0x63,
|
||||||
0x68, 0x69, 0x6e, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x61, 0x70, 0x69,
|
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,
|
0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x07, 0x6d, 0x61,
|
||||||
0x63, 0x68, 0x69, 0x6e, 0x65, 0x12, 0x37, 0x0a, 0x0e, 0x6f, 0x74, 0x68, 0x65, 0x72, 0x5f, 0x6d,
|
0x63, 0x68, 0x69, 0x6e, 0x65, 0x12, 0x37, 0x0a, 0x0e, 0x6f, 0x74, 0x68, 0x65, 0x72, 0x5f, 0x6d,
|
||||||
0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x10, 0x2e,
|
0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x10, 0x2e,
|
||||||
0x61, 0x70, 0x69, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52,
|
0x61, 0x70, 0x69, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52,
|
||||||
0x0d, 0x6f, 0x74, 0x68, 0x65, 0x72, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x22, 0x25,
|
0x0d, 0x6f, 0x74, 0x68, 0x65, 0x72, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x12, 0x2f,
|
||||||
|
0x0a, 0x14, 0x6d, 0x69, 0x6e, 0x5f, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x5f, 0x64, 0x62, 0x5f, 0x76,
|
||||||
|
0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x11, 0x6d, 0x69,
|
||||||
|
0x6e, 0x53, 0x74, 0x6f, 0x72, 0x65, 0x44, 0x62, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22,
|
||||||
|
0x49, 0x0a, 0x16, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e,
|
||||||
|
0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2f, 0x0a, 0x08, 0x6d, 0x61, 0x63,
|
||||||
|
0x68, 0x69, 0x6e, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x61, 0x70,
|
||||||
|
0x69, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73,
|
||||||
|
0x52, 0x08, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x22, 0x91, 0x01, 0x0a, 0x0e, 0x4d,
|
||||||
|
0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x12, 0x29, 0x0a,
|
||||||
|
0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32,
|
||||||
|
0x0d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x08,
|
||||||
|
0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x2a, 0x0a, 0x07, 0x6d, 0x61, 0x63, 0x68,
|
||||||
|
0x69, 0x6e, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x61, 0x70, 0x69, 0x2e,
|
||||||
|
0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x07, 0x6d, 0x61, 0x63,
|
||||||
|
0x68, 0x69, 0x6e, 0x65, 0x12, 0x28, 0x0a, 0x10, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x5f, 0x64, 0x62,
|
||||||
|
0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0e,
|
||||||
|
0x73, 0x74, 0x6f, 0x72, 0x65, 0x44, 0x62, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x25,
|
||||||
0x0a, 0x0d, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12,
|
0x0a, 0x0d, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12,
|
||||||
0x14, 0x0a, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05,
|
0x14, 0x0a, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05,
|
||||||
0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0x0e, 0x0a, 0x0c, 0x52, 0x65, 0x73, 0x65, 0x74, 0x52, 0x65,
|
0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0x0e, 0x0a, 0x0c, 0x52, 0x65, 0x73, 0x65, 0x74, 0x52, 0x65,
|
||||||
@@ -806,39 +1118,77 @@ var file_internal_machine_api_pb_machine_proto_rawDesc = []byte{
|
|||||||
0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x26,
|
0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x26,
|
||||||
0x0a, 0x07, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32,
|
0x0a, 0x07, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32,
|
||||||
0x0c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x07, 0x73,
|
0x0c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x07, 0x73,
|
||||||
0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x32, 0xc3, 0x03, 0x0a, 0x07, 0x4d, 0x61, 0x63, 0x68, 0x69,
|
0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x22, 0xb2, 0x01, 0x0a, 0x1f, 0x49, 0x6e, 0x73, 0x70, 0x65,
|
||||||
0x6e, 0x65, 0x12, 0x4d, 0x0a, 0x12, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x50, 0x72, 0x65, 0x72, 0x65,
|
0x63, 0x74, 0x57, 0x69, 0x72, 0x65, 0x47, 0x75, 0x61, 0x72, 0x64, 0x4e, 0x65, 0x74, 0x77, 0x6f,
|
||||||
0x71, 0x75, 0x69, 0x73, 0x69, 0x74, 0x65, 0x73, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c,
|
0x72, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x25, 0x0a, 0x0e, 0x69, 0x6e,
|
||||||
0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79,
|
0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01,
|
||||||
0x1a, 0x1f, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x50, 0x72, 0x65, 0x72,
|
0x28, 0x09, 0x52, 0x0d, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x4e, 0x61, 0x6d,
|
||||||
0x65, 0x71, 0x75, 0x69, 0x73, 0x69, 0x74, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
|
0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x18,
|
||||||
0x65, 0x12, 0x40, 0x0a, 0x0b, 0x49, 0x6e, 0x69, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72,
|
0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79,
|
||||||
0x12, 0x17, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x69, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74,
|
0x12, 0x1f, 0x0a, 0x0b, 0x6c, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x5f, 0x70, 0x6f, 0x72, 0x74, 0x18,
|
||||||
0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x61, 0x70, 0x69, 0x2e,
|
0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0a, 0x6c, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x50, 0x6f, 0x72,
|
||||||
0x49, 0x6e, 0x69, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f,
|
0x74, 0x12, 0x28, 0x0a, 0x05, 0x70, 0x65, 0x65, 0x72, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b,
|
||||||
0x6e, 0x73, 0x65, 0x12, 0x3e, 0x0a, 0x0b, 0x4a, 0x6f, 0x69, 0x6e, 0x43, 0x6c, 0x75, 0x73, 0x74,
|
0x32, 0x12, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x57, 0x69, 0x72, 0x65, 0x47, 0x75, 0x61, 0x72, 0x64,
|
||||||
0x65, 0x72, 0x12, 0x17, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4a, 0x6f, 0x69, 0x6e, 0x43, 0x6c, 0x75,
|
0x50, 0x65, 0x65, 0x72, 0x52, 0x05, 0x70, 0x65, 0x65, 0x72, 0x73, 0x22, 0x83, 0x02, 0x0a, 0x0d,
|
||||||
0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f,
|
0x57, 0x69, 0x72, 0x65, 0x47, 0x75, 0x61, 0x72, 0x64, 0x50, 0x65, 0x65, 0x72, 0x12, 0x1d, 0x0a,
|
||||||
0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d,
|
0x0a, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28,
|
||||||
0x70, 0x74, 0x79, 0x12, 0x33, 0x0a, 0x05, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x16, 0x2e, 0x67,
|
0x0c, 0x52, 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x12, 0x1a, 0x0a, 0x08,
|
||||||
0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45,
|
0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08,
|
||||||
0x6d, 0x70, 0x74, 0x79, 0x1a, 0x12, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x54, 0x6f, 0x6b, 0x65, 0x6e,
|
0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x4a, 0x0a, 0x13, 0x6c, 0x61, 0x73, 0x74,
|
||||||
0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x33, 0x0a, 0x07, 0x49, 0x6e, 0x73, 0x70,
|
0x5f, 0x68, 0x61, 0x6e, 0x64, 0x73, 0x68, 0x61, 0x6b, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18,
|
||||||
0x65, 0x63, 0x74, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f,
|
0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70,
|
||||||
0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x10, 0x2e, 0x61, 0x70,
|
0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d,
|
||||||
0x69, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x32, 0x0a,
|
0x70, 0x52, 0x11, 0x6c, 0x61, 0x73, 0x74, 0x48, 0x61, 0x6e, 0x64, 0x73, 0x68, 0x61, 0x6b, 0x65,
|
||||||
0x05, 0x52, 0x65, 0x73, 0x65, 0x74, 0x12, 0x11, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x52, 0x65, 0x73,
|
0x54, 0x69, 0x6d, 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x65, 0x63, 0x65, 0x69, 0x76, 0x65, 0x5f,
|
||||||
0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67,
|
0x62, 0x79, 0x74, 0x65, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x72, 0x65, 0x63,
|
||||||
0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74,
|
0x65, 0x69, 0x76, 0x65, 0x42, 0x79, 0x74, 0x65, 0x73, 0x12, 0x25, 0x0a, 0x0e, 0x74, 0x72, 0x61,
|
||||||
0x79, 0x12, 0x49, 0x0a, 0x0e, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72, 0x76,
|
0x6e, 0x73, 0x6d, 0x69, 0x74, 0x5f, 0x62, 0x79, 0x74, 0x65, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28,
|
||||||
0x69, 0x63, 0x65, 0x12, 0x1a, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63,
|
0x03, 0x52, 0x0d, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x6d, 0x69, 0x74, 0x42, 0x79, 0x74, 0x65, 0x73,
|
||||||
0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a,
|
0x12, 0x1f, 0x0a, 0x0b, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x5f, 0x69, 0x70, 0x73, 0x18,
|
||||||
0x1b, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72,
|
0x06, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0a, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x49, 0x70,
|
||||||
0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x37, 0x5a, 0x35,
|
0x73, 0x32, 0xe3, 0x04, 0x0a, 0x07, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x12, 0x4d, 0x0a,
|
||||||
0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x70, 0x73, 0x76, 0x69, 0x64,
|
0x12, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x50, 0x72, 0x65, 0x72, 0x65, 0x71, 0x75, 0x69, 0x73, 0x69,
|
||||||
0x65, 0x72, 0x73, 0x6b, 0x69, 0x2f, 0x75, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2f, 0x69, 0x6e,
|
0x74, 0x65, 0x73, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f,
|
||||||
0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x2f, 0x61,
|
0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1f, 0x2e, 0x61, 0x70,
|
||||||
0x70, 0x69, 0x2f, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
0x69, 0x2e, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x50, 0x72, 0x65, 0x72, 0x65, 0x71, 0x75, 0x69, 0x73,
|
||||||
|
0x69, 0x74, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x40, 0x0a, 0x0b,
|
||||||
|
0x49, 0x6e, 0x69, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x12, 0x17, 0x2e, 0x61, 0x70,
|
||||||
|
0x69, 0x2e, 0x49, 0x6e, 0x69, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x71,
|
||||||
|
0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x69, 0x74, 0x43,
|
||||||
|
0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3e,
|
||||||
|
0x0a, 0x0b, 0x4a, 0x6f, 0x69, 0x6e, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x12, 0x17, 0x2e,
|
||||||
|
0x61, 0x70, 0x69, 0x2e, 0x4a, 0x6f, 0x69, 0x6e, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x52,
|
||||||
|
0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e,
|
||||||
|
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x33,
|
||||||
|
0x0a, 0x05, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65,
|
||||||
|
0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a,
|
||||||
|
0x12, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f,
|
||||||
|
0x6e, 0x73, 0x65, 0x12, 0x33, 0x0a, 0x07, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x12, 0x16,
|
||||||
|
0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66,
|
||||||
|
0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x10, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d, 0x61, 0x63,
|
||||||
|
0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x45, 0x0a, 0x0e, 0x49, 0x6e, 0x73, 0x70,
|
||||||
|
0x65, 0x63, 0x74, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 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, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74,
|
||||||
|
0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12,
|
||||||
|
0x57, 0x0a, 0x17, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x57, 0x69, 0x72, 0x65, 0x47, 0x75,
|
||||||
|
0x61, 0x72, 0x64, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 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, 0x24, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74,
|
||||||
|
0x57, 0x69, 0x72, 0x65, 0x47, 0x75, 0x61, 0x72, 0x64, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b,
|
||||||
|
0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x32, 0x0a, 0x05, 0x52, 0x65, 0x73, 0x65,
|
||||||
|
0x74, 0x12, 0x11, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x52, 0x65, 0x73, 0x65, 0x74, 0x52, 0x65, 0x71,
|
||||||
|
0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72,
|
||||||
|
0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x49, 0x0a, 0x0e,
|
||||||
|
0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x1a,
|
||||||
|
0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72, 0x76,
|
||||||
|
0x69, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x61, 0x70, 0x69,
|
||||||
|
0x2e, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52,
|
||||||
|
0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x37, 0x5a, 0x35, 0x67, 0x69, 0x74, 0x68, 0x75,
|
||||||
|
0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x70, 0x73, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x6b, 0x69,
|
||||||
|
0x2f, 0x75, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61,
|
||||||
|
0x6c, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x62,
|
||||||
|
0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -853,57 +1203,73 @@ func file_internal_machine_api_pb_machine_proto_rawDescGZIP() []byte {
|
|||||||
return file_internal_machine_api_pb_machine_proto_rawDescData
|
return file_internal_machine_api_pb_machine_proto_rawDescData
|
||||||
}
|
}
|
||||||
|
|
||||||
var file_internal_machine_api_pb_machine_proto_msgTypes = make([]protoimpl.MessageInfo, 12)
|
var file_internal_machine_api_pb_machine_proto_msgTypes = make([]protoimpl.MessageInfo, 16)
|
||||||
var file_internal_machine_api_pb_machine_proto_goTypes = []any{
|
var file_internal_machine_api_pb_machine_proto_goTypes = []any{
|
||||||
(*MachineInfo)(nil), // 0: api.MachineInfo
|
(*MachineInfo)(nil), // 0: api.MachineInfo
|
||||||
(*NetworkConfig)(nil), // 1: api.NetworkConfig
|
(*NetworkConfig)(nil), // 1: api.NetworkConfig
|
||||||
(*CheckPrerequisitesResponse)(nil), // 2: api.CheckPrerequisitesResponse
|
(*CheckPrerequisitesResponse)(nil), // 2: api.CheckPrerequisitesResponse
|
||||||
(*InitClusterRequest)(nil), // 3: api.InitClusterRequest
|
(*InitClusterRequest)(nil), // 3: api.InitClusterRequest
|
||||||
(*InitClusterResponse)(nil), // 4: api.InitClusterResponse
|
(*InitClusterResponse)(nil), // 4: api.InitClusterResponse
|
||||||
(*JoinClusterRequest)(nil), // 5: api.JoinClusterRequest
|
(*JoinClusterRequest)(nil), // 5: api.JoinClusterRequest
|
||||||
(*TokenResponse)(nil), // 6: api.TokenResponse
|
(*InspectMachineResponse)(nil), // 6: api.InspectMachineResponse
|
||||||
(*ResetRequest)(nil), // 7: api.ResetRequest
|
(*MachineDetails)(nil), // 7: api.MachineDetails
|
||||||
(*Service)(nil), // 8: api.Service
|
(*TokenResponse)(nil), // 8: api.TokenResponse
|
||||||
(*InspectServiceRequest)(nil), // 9: api.InspectServiceRequest
|
(*ResetRequest)(nil), // 9: api.ResetRequest
|
||||||
(*InspectServiceResponse)(nil), // 10: api.InspectServiceResponse
|
(*Service)(nil), // 10: api.Service
|
||||||
(*Service_Container)(nil), // 11: api.Service.Container
|
(*InspectServiceRequest)(nil), // 11: api.InspectServiceRequest
|
||||||
(*IP)(nil), // 12: api.IP
|
(*InspectServiceResponse)(nil), // 12: api.InspectServiceResponse
|
||||||
(*IPPrefix)(nil), // 13: api.IPPrefix
|
(*InspectWireGuardNetworkResponse)(nil), // 13: api.InspectWireGuardNetworkResponse
|
||||||
(*IPPort)(nil), // 14: api.IPPort
|
(*WireGuardPeer)(nil), // 14: api.WireGuardPeer
|
||||||
(*emptypb.Empty)(nil), // 15: google.protobuf.Empty
|
(*Service_Container)(nil), // 15: api.Service.Container
|
||||||
|
(*IP)(nil), // 16: api.IP
|
||||||
|
(*IPPrefix)(nil), // 17: api.IPPrefix
|
||||||
|
(*IPPort)(nil), // 18: api.IPPort
|
||||||
|
(*Metadata)(nil), // 19: api.Metadata
|
||||||
|
(*timestamppb.Timestamp)(nil), // 20: google.protobuf.Timestamp
|
||||||
|
(*emptypb.Empty)(nil), // 21: google.protobuf.Empty
|
||||||
}
|
}
|
||||||
var file_internal_machine_api_pb_machine_proto_depIdxs = []int32{
|
var file_internal_machine_api_pb_machine_proto_depIdxs = []int32{
|
||||||
1, // 0: api.MachineInfo.network:type_name -> api.NetworkConfig
|
1, // 0: api.MachineInfo.network:type_name -> api.NetworkConfig
|
||||||
12, // 1: api.MachineInfo.public_ip:type_name -> api.IP
|
16, // 1: api.MachineInfo.public_ip:type_name -> api.IP
|
||||||
13, // 2: api.NetworkConfig.subnet:type_name -> api.IPPrefix
|
17, // 2: api.NetworkConfig.subnet:type_name -> api.IPPrefix
|
||||||
12, // 3: api.NetworkConfig.management_ip:type_name -> api.IP
|
16, // 3: api.NetworkConfig.management_ip:type_name -> api.IP
|
||||||
14, // 4: api.NetworkConfig.endpoints:type_name -> api.IPPort
|
18, // 4: api.NetworkConfig.endpoints:type_name -> api.IPPort
|
||||||
13, // 5: api.InitClusterRequest.network:type_name -> api.IPPrefix
|
17, // 5: api.InitClusterRequest.network:type_name -> api.IPPrefix
|
||||||
12, // 6: api.InitClusterRequest.public_ip:type_name -> api.IP
|
16, // 6: api.InitClusterRequest.public_ip:type_name -> api.IP
|
||||||
0, // 7: api.InitClusterResponse.machine:type_name -> api.MachineInfo
|
18, // 7: api.InitClusterRequest.wireguard_endpoints:type_name -> api.IPPort
|
||||||
0, // 8: api.JoinClusterRequest.machine:type_name -> api.MachineInfo
|
0, // 8: api.InitClusterResponse.machine:type_name -> api.MachineInfo
|
||||||
0, // 9: api.JoinClusterRequest.other_machines:type_name -> api.MachineInfo
|
0, // 9: api.JoinClusterRequest.machine:type_name -> api.MachineInfo
|
||||||
11, // 10: api.Service.containers:type_name -> api.Service.Container
|
0, // 10: api.JoinClusterRequest.other_machines:type_name -> api.MachineInfo
|
||||||
8, // 11: api.InspectServiceResponse.service:type_name -> api.Service
|
7, // 11: api.InspectMachineResponse.machines:type_name -> api.MachineDetails
|
||||||
15, // 12: api.Machine.CheckPrerequisites:input_type -> google.protobuf.Empty
|
19, // 12: api.MachineDetails.metadata:type_name -> api.Metadata
|
||||||
3, // 13: api.Machine.InitCluster:input_type -> api.InitClusterRequest
|
0, // 13: api.MachineDetails.machine:type_name -> api.MachineInfo
|
||||||
5, // 14: api.Machine.JoinCluster:input_type -> api.JoinClusterRequest
|
15, // 14: api.Service.containers:type_name -> api.Service.Container
|
||||||
15, // 15: api.Machine.Token:input_type -> google.protobuf.Empty
|
10, // 15: api.InspectServiceResponse.service:type_name -> api.Service
|
||||||
15, // 16: api.Machine.Inspect:input_type -> google.protobuf.Empty
|
14, // 16: api.InspectWireGuardNetworkResponse.peers:type_name -> api.WireGuardPeer
|
||||||
7, // 17: api.Machine.Reset:input_type -> api.ResetRequest
|
20, // 17: api.WireGuardPeer.last_handshake_time:type_name -> google.protobuf.Timestamp
|
||||||
9, // 18: api.Machine.InspectService:input_type -> api.InspectServiceRequest
|
21, // 18: api.Machine.CheckPrerequisites:input_type -> google.protobuf.Empty
|
||||||
2, // 19: api.Machine.CheckPrerequisites:output_type -> api.CheckPrerequisitesResponse
|
3, // 19: api.Machine.InitCluster:input_type -> api.InitClusterRequest
|
||||||
4, // 20: api.Machine.InitCluster:output_type -> api.InitClusterResponse
|
5, // 20: api.Machine.JoinCluster:input_type -> api.JoinClusterRequest
|
||||||
15, // 21: api.Machine.JoinCluster:output_type -> google.protobuf.Empty
|
21, // 21: api.Machine.Token:input_type -> google.protobuf.Empty
|
||||||
6, // 22: api.Machine.Token:output_type -> api.TokenResponse
|
21, // 22: api.Machine.Inspect:input_type -> google.protobuf.Empty
|
||||||
0, // 23: api.Machine.Inspect:output_type -> api.MachineInfo
|
21, // 23: api.Machine.InspectMachine:input_type -> google.protobuf.Empty
|
||||||
15, // 24: api.Machine.Reset:output_type -> google.protobuf.Empty
|
21, // 24: api.Machine.InspectWireGuardNetwork:input_type -> google.protobuf.Empty
|
||||||
10, // 25: api.Machine.InspectService:output_type -> api.InspectServiceResponse
|
9, // 25: api.Machine.Reset:input_type -> api.ResetRequest
|
||||||
19, // [19:26] is the sub-list for method output_type
|
11, // 26: api.Machine.InspectService:input_type -> api.InspectServiceRequest
|
||||||
12, // [12:19] is the sub-list for method input_type
|
2, // 27: api.Machine.CheckPrerequisites:output_type -> api.CheckPrerequisitesResponse
|
||||||
12, // [12:12] is the sub-list for extension type_name
|
4, // 28: api.Machine.InitCluster:output_type -> api.InitClusterResponse
|
||||||
12, // [12:12] is the sub-list for extension extendee
|
21, // 29: api.Machine.JoinCluster:output_type -> google.protobuf.Empty
|
||||||
0, // [0:12] is the sub-list for field type_name
|
8, // 30: api.Machine.Token:output_type -> api.TokenResponse
|
||||||
|
0, // 31: api.Machine.Inspect:output_type -> api.MachineInfo
|
||||||
|
6, // 32: api.Machine.InspectMachine:output_type -> api.InspectMachineResponse
|
||||||
|
13, // 33: api.Machine.InspectWireGuardNetwork:output_type -> api.InspectWireGuardNetworkResponse
|
||||||
|
21, // 34: api.Machine.Reset:output_type -> google.protobuf.Empty
|
||||||
|
12, // 35: api.Machine.InspectService:output_type -> api.InspectServiceResponse
|
||||||
|
27, // [27:36] is the sub-list for method output_type
|
||||||
|
18, // [18:27] is the sub-list for method input_type
|
||||||
|
18, // [18:18] is the sub-list for extension type_name
|
||||||
|
18, // [18:18] is the sub-list for extension extendee
|
||||||
|
0, // [0:18] is the sub-list for field type_name
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() { file_internal_machine_api_pb_machine_proto_init() }
|
func init() { file_internal_machine_api_pb_machine_proto_init() }
|
||||||
@@ -986,7 +1352,7 @@ func file_internal_machine_api_pb_machine_proto_init() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
file_internal_machine_api_pb_machine_proto_msgTypes[6].Exporter = func(v any, i int) any {
|
file_internal_machine_api_pb_machine_proto_msgTypes[6].Exporter = func(v any, i int) any {
|
||||||
switch v := v.(*TokenResponse); i {
|
switch v := v.(*InspectMachineResponse); i {
|
||||||
case 0:
|
case 0:
|
||||||
return &v.state
|
return &v.state
|
||||||
case 1:
|
case 1:
|
||||||
@@ -998,7 +1364,7 @@ func file_internal_machine_api_pb_machine_proto_init() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
file_internal_machine_api_pb_machine_proto_msgTypes[7].Exporter = func(v any, i int) any {
|
file_internal_machine_api_pb_machine_proto_msgTypes[7].Exporter = func(v any, i int) any {
|
||||||
switch v := v.(*ResetRequest); i {
|
switch v := v.(*MachineDetails); i {
|
||||||
case 0:
|
case 0:
|
||||||
return &v.state
|
return &v.state
|
||||||
case 1:
|
case 1:
|
||||||
@@ -1010,7 +1376,7 @@ func file_internal_machine_api_pb_machine_proto_init() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
file_internal_machine_api_pb_machine_proto_msgTypes[8].Exporter = func(v any, i int) any {
|
file_internal_machine_api_pb_machine_proto_msgTypes[8].Exporter = func(v any, i int) any {
|
||||||
switch v := v.(*Service); i {
|
switch v := v.(*TokenResponse); i {
|
||||||
case 0:
|
case 0:
|
||||||
return &v.state
|
return &v.state
|
||||||
case 1:
|
case 1:
|
||||||
@@ -1022,7 +1388,7 @@ func file_internal_machine_api_pb_machine_proto_init() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
file_internal_machine_api_pb_machine_proto_msgTypes[9].Exporter = func(v any, i int) any {
|
file_internal_machine_api_pb_machine_proto_msgTypes[9].Exporter = func(v any, i int) any {
|
||||||
switch v := v.(*InspectServiceRequest); i {
|
switch v := v.(*ResetRequest); i {
|
||||||
case 0:
|
case 0:
|
||||||
return &v.state
|
return &v.state
|
||||||
case 1:
|
case 1:
|
||||||
@@ -1034,7 +1400,7 @@ func file_internal_machine_api_pb_machine_proto_init() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
file_internal_machine_api_pb_machine_proto_msgTypes[10].Exporter = func(v any, i int) any {
|
file_internal_machine_api_pb_machine_proto_msgTypes[10].Exporter = func(v any, i int) any {
|
||||||
switch v := v.(*InspectServiceResponse); i {
|
switch v := v.(*Service); i {
|
||||||
case 0:
|
case 0:
|
||||||
return &v.state
|
return &v.state
|
||||||
case 1:
|
case 1:
|
||||||
@@ -1046,6 +1412,54 @@ func file_internal_machine_api_pb_machine_proto_init() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
file_internal_machine_api_pb_machine_proto_msgTypes[11].Exporter = func(v any, i int) any {
|
file_internal_machine_api_pb_machine_proto_msgTypes[11].Exporter = func(v any, i int) any {
|
||||||
|
switch v := v.(*InspectServiceRequest); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file_internal_machine_api_pb_machine_proto_msgTypes[12].Exporter = func(v any, i int) any {
|
||||||
|
switch v := v.(*InspectServiceResponse); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file_internal_machine_api_pb_machine_proto_msgTypes[13].Exporter = func(v any, i int) any {
|
||||||
|
switch v := v.(*InspectWireGuardNetworkResponse); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file_internal_machine_api_pb_machine_proto_msgTypes[14].Exporter = func(v any, i int) any {
|
||||||
|
switch v := v.(*WireGuardPeer); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file_internal_machine_api_pb_machine_proto_msgTypes[15].Exporter = func(v any, i int) any {
|
||||||
switch v := v.(*Service_Container); i {
|
switch v := v.(*Service_Container); i {
|
||||||
case 0:
|
case 0:
|
||||||
return &v.state
|
return &v.state
|
||||||
@@ -1068,7 +1482,7 @@ func file_internal_machine_api_pb_machine_proto_init() {
|
|||||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||||
RawDescriptor: file_internal_machine_api_pb_machine_proto_rawDesc,
|
RawDescriptor: file_internal_machine_api_pb_machine_proto_rawDesc,
|
||||||
NumEnums: 0,
|
NumEnums: 0,
|
||||||
NumMessages: 12,
|
NumMessages: 16,
|
||||||
NumExtensions: 0,
|
NumExtensions: 0,
|
||||||
NumServices: 1,
|
NumServices: 1,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ 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 "google/protobuf/timestamp.proto";
|
||||||
import "internal/machine/api/pb/common.proto";
|
import "internal/machine/api/pb/common.proto";
|
||||||
|
|
||||||
service Machine {
|
service Machine {
|
||||||
@@ -13,7 +14,12 @@ service Machine {
|
|||||||
rpc InitCluster(InitClusterRequest) returns (InitClusterResponse);
|
rpc InitCluster(InitClusterRequest) returns (InitClusterResponse);
|
||||||
rpc JoinCluster(JoinClusterRequest) returns (google.protobuf.Empty);
|
rpc JoinCluster(JoinClusterRequest) returns (google.protobuf.Empty);
|
||||||
rpc Token(google.protobuf.Empty) returns (TokenResponse);
|
rpc Token(google.protobuf.Empty) returns (TokenResponse);
|
||||||
|
// Deprecated: use InspectMachine instead.
|
||||||
rpc Inspect(google.protobuf.Empty) returns (MachineInfo);
|
rpc Inspect(google.protobuf.Empty) returns (MachineInfo);
|
||||||
|
// InspectMachine retrieves detailed information about the machine. Supports broadcasting to multiple machines.
|
||||||
|
rpc InspectMachine(google.protobuf.Empty) returns (InspectMachineResponse);
|
||||||
|
// InspectWireGuardNetwork retrieves the current WireGuard network configuration and peer status.
|
||||||
|
rpc InspectWireGuardNetwork(google.protobuf.Empty) returns (InspectWireGuardNetworkResponse);
|
||||||
// Reset restores the machine to a clean state, removing all cluster-related configuration and data.
|
// Reset restores the machine to a clean state, removing all cluster-related configuration and data.
|
||||||
rpc Reset(ResetRequest) returns (google.protobuf.Empty);
|
rpc Reset(ResetRequest) returns (google.protobuf.Empty);
|
||||||
|
|
||||||
@@ -49,6 +55,9 @@ message InitClusterRequest {
|
|||||||
IP public_ip = 3;
|
IP public_ip = 3;
|
||||||
bool public_ip_auto = 4;
|
bool public_ip_auto = 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Optional WireGuard endpoints other machines will use to connect to this machine instead of auto-discovered ones.
|
||||||
|
repeated IPPort wireguard_endpoints = 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
message InitClusterResponse {
|
message InitClusterResponse {
|
||||||
@@ -58,6 +67,20 @@ message InitClusterResponse {
|
|||||||
message JoinClusterRequest {
|
message JoinClusterRequest {
|
||||||
MachineInfo machine = 1;
|
MachineInfo machine = 1;
|
||||||
repeated MachineInfo other_machines = 3;
|
repeated MachineInfo other_machines = 3;
|
||||||
|
// Minimum store database version the new machine should sync to before starting cluster operations.
|
||||||
|
int64 min_store_db_version = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message InspectMachineResponse {
|
||||||
|
// Must contain only one repeated messages field to allow broadcasting InspectMachine requests to multiple machines.
|
||||||
|
repeated MachineDetails machines = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message MachineDetails {
|
||||||
|
Metadata metadata = 1;
|
||||||
|
MachineInfo machine = 2;
|
||||||
|
// Current Corrosion cr-sqlite database version (Lamport timestamp) of the cluster store.
|
||||||
|
int64 store_db_version = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
message TokenResponse {
|
message TokenResponse {
|
||||||
@@ -88,3 +111,19 @@ message InspectServiceRequest {
|
|||||||
message InspectServiceResponse {
|
message InspectServiceResponse {
|
||||||
Service service = 1;
|
Service service = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message InspectWireGuardNetworkResponse {
|
||||||
|
string interface_name = 1;
|
||||||
|
bytes public_key = 2;
|
||||||
|
int32 listen_port = 3;
|
||||||
|
repeated WireGuardPeer peers = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message WireGuardPeer {
|
||||||
|
bytes public_key = 1;
|
||||||
|
string endpoint = 2;
|
||||||
|
google.protobuf.Timestamp last_handshake_time = 3;
|
||||||
|
int64 receive_bytes = 4;
|
||||||
|
int64 transmit_bytes = 5;
|
||||||
|
repeated string allowed_ips = 6;
|
||||||
|
}
|
||||||
|
|||||||
@@ -20,13 +20,15 @@ import (
|
|||||||
const _ = grpc.SupportPackageIsVersion9
|
const _ = grpc.SupportPackageIsVersion9
|
||||||
|
|
||||||
const (
|
const (
|
||||||
Machine_CheckPrerequisites_FullMethodName = "/api.Machine/CheckPrerequisites"
|
Machine_CheckPrerequisites_FullMethodName = "/api.Machine/CheckPrerequisites"
|
||||||
Machine_InitCluster_FullMethodName = "/api.Machine/InitCluster"
|
Machine_InitCluster_FullMethodName = "/api.Machine/InitCluster"
|
||||||
Machine_JoinCluster_FullMethodName = "/api.Machine/JoinCluster"
|
Machine_JoinCluster_FullMethodName = "/api.Machine/JoinCluster"
|
||||||
Machine_Token_FullMethodName = "/api.Machine/Token"
|
Machine_Token_FullMethodName = "/api.Machine/Token"
|
||||||
Machine_Inspect_FullMethodName = "/api.Machine/Inspect"
|
Machine_Inspect_FullMethodName = "/api.Machine/Inspect"
|
||||||
Machine_Reset_FullMethodName = "/api.Machine/Reset"
|
Machine_InspectMachine_FullMethodName = "/api.Machine/InspectMachine"
|
||||||
Machine_InspectService_FullMethodName = "/api.Machine/InspectService"
|
Machine_InspectWireGuardNetwork_FullMethodName = "/api.Machine/InspectWireGuardNetwork"
|
||||||
|
Machine_Reset_FullMethodName = "/api.Machine/Reset"
|
||||||
|
Machine_InspectService_FullMethodName = "/api.Machine/InspectService"
|
||||||
)
|
)
|
||||||
|
|
||||||
// MachineClient is the client API for Machine service.
|
// MachineClient is the client API for Machine service.
|
||||||
@@ -38,7 +40,12 @@ type MachineClient interface {
|
|||||||
InitCluster(ctx context.Context, in *InitClusterRequest, opts ...grpc.CallOption) (*InitClusterResponse, error)
|
InitCluster(ctx context.Context, in *InitClusterRequest, opts ...grpc.CallOption) (*InitClusterResponse, error)
|
||||||
JoinCluster(ctx context.Context, in *JoinClusterRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
|
JoinCluster(ctx context.Context, in *JoinClusterRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
|
||||||
Token(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*TokenResponse, error)
|
Token(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*TokenResponse, error)
|
||||||
|
// Deprecated: use InspectMachine instead.
|
||||||
Inspect(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*MachineInfo, error)
|
Inspect(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*MachineInfo, error)
|
||||||
|
// InspectMachine retrieves detailed information about the machine. Supports broadcasting to multiple machines.
|
||||||
|
InspectMachine(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*InspectMachineResponse, error)
|
||||||
|
// InspectWireGuardNetwork retrieves the current WireGuard network configuration and peer status.
|
||||||
|
InspectWireGuardNetwork(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*InspectWireGuardNetworkResponse, error)
|
||||||
// Reset restores the machine to a clean state, removing all cluster-related configuration and data.
|
// Reset restores the machine to a clean state, removing all cluster-related configuration and data.
|
||||||
Reset(ctx context.Context, in *ResetRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
|
Reset(ctx context.Context, in *ResetRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
|
||||||
InspectService(ctx context.Context, in *InspectServiceRequest, opts ...grpc.CallOption) (*InspectServiceResponse, error)
|
InspectService(ctx context.Context, in *InspectServiceRequest, opts ...grpc.CallOption) (*InspectServiceResponse, error)
|
||||||
@@ -102,6 +109,26 @@ func (c *machineClient) Inspect(ctx context.Context, in *emptypb.Empty, opts ...
|
|||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *machineClient) InspectMachine(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*InspectMachineResponse, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(InspectMachineResponse)
|
||||||
|
err := c.cc.Invoke(ctx, Machine_InspectMachine_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *machineClient) InspectWireGuardNetwork(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*InspectWireGuardNetworkResponse, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(InspectWireGuardNetworkResponse)
|
||||||
|
err := c.cc.Invoke(ctx, Machine_InspectWireGuardNetwork_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (c *machineClient) Reset(ctx context.Context, in *ResetRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) {
|
func (c *machineClient) Reset(ctx context.Context, in *ResetRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) {
|
||||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
out := new(emptypb.Empty)
|
out := new(emptypb.Empty)
|
||||||
@@ -131,7 +158,12 @@ type MachineServer interface {
|
|||||||
InitCluster(context.Context, *InitClusterRequest) (*InitClusterResponse, error)
|
InitCluster(context.Context, *InitClusterRequest) (*InitClusterResponse, error)
|
||||||
JoinCluster(context.Context, *JoinClusterRequest) (*emptypb.Empty, error)
|
JoinCluster(context.Context, *JoinClusterRequest) (*emptypb.Empty, error)
|
||||||
Token(context.Context, *emptypb.Empty) (*TokenResponse, error)
|
Token(context.Context, *emptypb.Empty) (*TokenResponse, error)
|
||||||
|
// Deprecated: use InspectMachine instead.
|
||||||
Inspect(context.Context, *emptypb.Empty) (*MachineInfo, error)
|
Inspect(context.Context, *emptypb.Empty) (*MachineInfo, error)
|
||||||
|
// InspectMachine retrieves detailed information about the machine. Supports broadcasting to multiple machines.
|
||||||
|
InspectMachine(context.Context, *emptypb.Empty) (*InspectMachineResponse, error)
|
||||||
|
// InspectWireGuardNetwork retrieves the current WireGuard network configuration and peer status.
|
||||||
|
InspectWireGuardNetwork(context.Context, *emptypb.Empty) (*InspectWireGuardNetworkResponse, error)
|
||||||
// Reset restores the machine to a clean state, removing all cluster-related configuration and data.
|
// Reset restores the machine to a clean state, removing all cluster-related configuration and data.
|
||||||
Reset(context.Context, *ResetRequest) (*emptypb.Empty, error)
|
Reset(context.Context, *ResetRequest) (*emptypb.Empty, error)
|
||||||
InspectService(context.Context, *InspectServiceRequest) (*InspectServiceResponse, error)
|
InspectService(context.Context, *InspectServiceRequest) (*InspectServiceResponse, error)
|
||||||
@@ -160,6 +192,12 @@ func (UnimplementedMachineServer) Token(context.Context, *emptypb.Empty) (*Token
|
|||||||
func (UnimplementedMachineServer) Inspect(context.Context, *emptypb.Empty) (*MachineInfo, error) {
|
func (UnimplementedMachineServer) Inspect(context.Context, *emptypb.Empty) (*MachineInfo, error) {
|
||||||
return nil, status.Errorf(codes.Unimplemented, "method Inspect not implemented")
|
return nil, status.Errorf(codes.Unimplemented, "method Inspect not implemented")
|
||||||
}
|
}
|
||||||
|
func (UnimplementedMachineServer) InspectMachine(context.Context, *emptypb.Empty) (*InspectMachineResponse, error) {
|
||||||
|
return nil, status.Errorf(codes.Unimplemented, "method InspectMachine not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedMachineServer) InspectWireGuardNetwork(context.Context, *emptypb.Empty) (*InspectWireGuardNetworkResponse, error) {
|
||||||
|
return nil, status.Errorf(codes.Unimplemented, "method InspectWireGuardNetwork not implemented")
|
||||||
|
}
|
||||||
func (UnimplementedMachineServer) Reset(context.Context, *ResetRequest) (*emptypb.Empty, error) {
|
func (UnimplementedMachineServer) Reset(context.Context, *ResetRequest) (*emptypb.Empty, error) {
|
||||||
return nil, status.Errorf(codes.Unimplemented, "method Reset not implemented")
|
return nil, status.Errorf(codes.Unimplemented, "method Reset not implemented")
|
||||||
}
|
}
|
||||||
@@ -277,6 +315,42 @@ func _Machine_Inspect_Handler(srv interface{}, ctx context.Context, dec func(int
|
|||||||
return interceptor(ctx, in, info, handler)
|
return interceptor(ctx, in, info, handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func _Machine_InspectMachine_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(emptypb.Empty)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(MachineServer).InspectMachine(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: Machine_InspectMachine_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(MachineServer).InspectMachine(ctx, req.(*emptypb.Empty))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _Machine_InspectWireGuardNetwork_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(emptypb.Empty)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(MachineServer).InspectWireGuardNetwork(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: Machine_InspectWireGuardNetwork_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(MachineServer).InspectWireGuardNetwork(ctx, req.(*emptypb.Empty))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
func _Machine_Reset_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
func _Machine_Reset_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
in := new(ResetRequest)
|
in := new(ResetRequest)
|
||||||
if err := dec(in); err != nil {
|
if err := dec(in); err != nil {
|
||||||
@@ -340,6 +414,14 @@ var Machine_ServiceDesc = grpc.ServiceDesc{
|
|||||||
MethodName: "Inspect",
|
MethodName: "Inspect",
|
||||||
Handler: _Machine_Inspect_Handler,
|
Handler: _Machine_Inspect_Handler,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
MethodName: "InspectMachine",
|
||||||
|
Handler: _Machine_InspectMachine_Handler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MethodName: "InspectWireGuardNetwork",
|
||||||
|
Handler: _Machine_InspectWireGuardNetwork_Handler,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
MethodName: "Reset",
|
MethodName: "Reset",
|
||||||
Handler: _Machine_Reset_Handler,
|
Handler: _Machine_Reset_Handler,
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ func (d *Director) remoteBackend(addr string) (*RemoteBackend, error) {
|
|||||||
|
|
||||||
// FlushRemoteBackends closes all remote backend connections and removes them from the cache.
|
// FlushRemoteBackends closes all remote backend connections and removes them from the cache.
|
||||||
func (d *Director) FlushRemoteBackends() {
|
func (d *Director) FlushRemoteBackends() {
|
||||||
d.remoteBackends.Range(func(key, value interface{}) bool {
|
d.remoteBackends.Range(func(key, value any) bool {
|
||||||
backend, ok := value.(*RemoteBackend)
|
backend, ok := value.(*RemoteBackend)
|
||||||
if !ok {
|
if !ok {
|
||||||
return true
|
return true
|
||||||
|
|||||||
@@ -80,6 +80,8 @@ func (b *RemoteBackend) GetConnection(ctx context.Context, _ string) (context.Co
|
|||||||
backoffConfig.MaxDelay = 15 * time.Second
|
backoffConfig.MaxDelay = 15 * time.Second
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
|
// This client keeps retrying connection in the background indefinitely even after the first connection fails
|
||||||
|
// and the Unavailable error is returned to the caller.
|
||||||
b.conn, err = grpc.NewClient(
|
b.conn, err = grpc.NewClient(
|
||||||
b.target,
|
b.target,
|
||||||
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
||||||
@@ -88,7 +90,7 @@ func (b *RemoteBackend) GetConnection(ctx context.Context, _ string) (context.Co
|
|||||||
// Not published as a constant in gRPC library.
|
// Not published as a constant in gRPC library.
|
||||||
// See: https://github.com/grpc/grpc-go/blob/d5dee5fdbdeb52f6ea10b37b2cc7ce37814642d7/clientconn.go#L55-L56
|
// See: https://github.com/grpc/grpc-go/blob/d5dee5fdbdeb52f6ea10b37b2cc7ce37814642d7/clientconn.go#L55-L56
|
||||||
// Each connection attempt can take up to MinConnectTimeout.
|
// Each connection attempt can take up to MinConnectTimeout.
|
||||||
MinConnectTimeout: 20 * time.Second,
|
MinConnectTimeout: 10 * time.Second,
|
||||||
}),
|
}),
|
||||||
grpc.WithDefaultCallOptions(
|
grpc.WithDefaultCallOptions(
|
||||||
grpc.ForceCodecV2(proxy.Codec()),
|
grpc.ForceCodecV2(proxy.Codec()),
|
||||||
|
|||||||
@@ -12,14 +12,16 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"text/template"
|
"text/template"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/psviderski/uncloud/internal/machine/store"
|
"github.com/psviderski/uncloud/internal/machine/store"
|
||||||
"github.com/psviderski/uncloud/pkg/api"
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
caddyfileHeader = `# This file is autogenerated by Uncloud based on the configuration of running services.
|
caddyfileHeaderFmt = `# Caddyfile autogenerated by Uncloud (DO NOT EDIT): %s
|
||||||
# Do not edit manually. Any manual changes will be overwritten on the next update.
|
# Automatically updated on service or health status changes.
|
||||||
|
# Docs: https://uncloud.run/docs/concepts/ingress/overview
|
||||||
`
|
`
|
||||||
caddyfileTemplate = `# Health check endpoint to verify Caddy reachability on this machine.
|
caddyfileTemplate = `# Health check endpoint to verify Caddy reachability on this machine.
|
||||||
http:// {
|
http:// {
|
||||||
@@ -56,7 +58,9 @@ https://{{$hostname}} {
|
|||||||
}{{end}}
|
}{{end}}
|
||||||
`
|
`
|
||||||
caddyfileUnavailabeFooter = `# NOTE: User-defined configs for services were skipped because Caddy is not running on this machine
|
caddyfileUnavailabeFooter = `# NOTE: User-defined configs for services were skipped because Caddy is not running on this machine
|
||||||
# or the latest generated config is invalid. Please check the Caddy logs if it's running.
|
# (not accessible via the shared admin socket /run/uncloud/caddy/admin.sock) or the latest
|
||||||
|
# generated config is invalid. Please check the service 'caddy' is running (uc inspect caddy)
|
||||||
|
# and its logs for more details (uc logs caddy).
|
||||||
`
|
`
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -120,6 +124,7 @@ func (g *CaddyfileGenerator) Generate(
|
|||||||
return "", fmt.Errorf("generate base Caddyfile from service ports: %w", err)
|
return "", fmt.Errorf("generate base Caddyfile from service ports: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
caddyfileHeader := fmt.Sprintf(caddyfileHeaderFmt, time.Now().UTC().Format(time.RFC3339))
|
||||||
if !includeCustom {
|
if !includeCustom {
|
||||||
return fmt.Sprintf("%s\n%s\n%s", caddyfileHeader, caddyfile, caddyfileUnavailabeFooter), nil
|
return fmt.Sprintf("%s\n%s\n%s", caddyfileHeader, caddyfile, caddyfileUnavailabeFooter), nil
|
||||||
}
|
}
|
||||||
@@ -221,12 +226,13 @@ func (g *CaddyfileGenerator) Generate(
|
|||||||
|
|
||||||
// Append error summary as comment if there were any invalid configs.
|
// Append error summary as comment if there were any invalid configs.
|
||||||
if len(configErrors) > 0 {
|
if len(configErrors) > 0 {
|
||||||
errorsComment := "# Skipped invalid user-defined configs:\n"
|
var errorsComment strings.Builder
|
||||||
|
errorsComment.WriteString("# Skipped invalid user-defined configs:\n")
|
||||||
for _, e := range configErrors {
|
for _, e := range configErrors {
|
||||||
errorsComment += fmt.Sprintf("# - %s\n", e)
|
errorsComment.WriteString(fmt.Sprintf("# - %s\n", e))
|
||||||
}
|
}
|
||||||
|
|
||||||
caddyfile += "\n" + errorsComment
|
caddyfile += "\n" + errorsComment.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
return caddyfileHeader + "\n" + caddyfile, nil
|
return caddyfileHeader + "\n" + caddyfile, nil
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package caddyconfig
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@@ -17,8 +18,17 @@ import (
|
|||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
const testCaddyfileHeader = `# This file is autogenerated by Uncloud based on the configuration of running services.
|
// generatedTimestampRegex matches the "(DO NOT EDIT): <RFC3339 timestamp>" part of the header.
|
||||||
# Do not edit manually. Any manual changes will be overwritten on the next update.
|
var generatedTimestampRegex = regexp.MustCompile(`\(DO NOT EDIT\): \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z`)
|
||||||
|
|
||||||
|
// normaliseGeneratedTimestamp replaces dynamic timestamps with a placeholder for test comparison.
|
||||||
|
func normaliseGeneratedTimestamp(caddyfile string) string {
|
||||||
|
return generatedTimestampRegex.ReplaceAllString(caddyfile, "(DO NOT EDIT): TIMESTAMP_PLACEHOLDER")
|
||||||
|
}
|
||||||
|
|
||||||
|
const testCaddyfileHeader = `# Caddyfile autogenerated by Uncloud (DO NOT EDIT): TIMESTAMP_PLACEHOLDER
|
||||||
|
# Automatically updated on service or health status changes.
|
||||||
|
# Docs: https://uncloud.run/docs/concepts/ingress/overview
|
||||||
|
|
||||||
# Health check endpoint to verify Caddy reachability on this machine.
|
# Health check endpoint to verify Caddy reachability on this machine.
|
||||||
http:// {
|
http:// {
|
||||||
@@ -190,7 +200,7 @@ https://secure.example.com {
|
|||||||
}
|
}
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.Equal(t, tt.want, config, "Generated Caddyfile doesn't match")
|
assert.Equal(t, tt.want, normaliseGeneratedTimestamp(config), "Generated Caddyfile doesn't match")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -216,8 +226,9 @@ func TestCaddyfileGeneratorWithCustomConfigs(t *testing.T) {
|
|||||||
time.Now(),
|
time.Now(),
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
want: `# This file is autogenerated by Uncloud based on the configuration of running services.
|
want: `# Caddyfile autogenerated by Uncloud (DO NOT EDIT): TIMESTAMP_PLACEHOLDER
|
||||||
# Do not edit manually. Any manual changes will be overwritten on the next update.
|
# Automatically updated on service or health status changes.
|
||||||
|
# Docs: https://uncloud.run/docs/concepts/ingress/overview
|
||||||
|
|
||||||
# User-defined global config from service 'caddy'.
|
# User-defined global config from service 'caddy'.
|
||||||
# Global Caddy configuration
|
# Global Caddy configuration
|
||||||
@@ -412,8 +423,9 @@ web.example.com {
|
|||||||
time.Now(),
|
time.Now(),
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
want: `# This file is autogenerated by Uncloud based on the configuration of running services.
|
want: `# Caddyfile autogenerated by Uncloud (DO NOT EDIT): TIMESTAMP_PLACEHOLDER
|
||||||
# Do not edit manually. Any manual changes will be overwritten on the next update.
|
# Automatically updated on service or health status changes.
|
||||||
|
# Docs: https://uncloud.run/docs/concepts/ingress/overview
|
||||||
|
|
||||||
# User-defined global config from service 'caddy'.
|
# User-defined global config from service 'caddy'.
|
||||||
# Global config
|
# Global config
|
||||||
@@ -649,8 +661,9 @@ badconfig.com {
|
|||||||
time.Now(),
|
time.Now(),
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
want: `# This file is autogenerated by Uncloud based on the configuration of running services.
|
want: `# Caddyfile autogenerated by Uncloud (DO NOT EDIT): TIMESTAMP_PLACEHOLDER
|
||||||
# Do not edit manually. Any manual changes will be overwritten on the next update.
|
# Automatically updated on service or health status changes.
|
||||||
|
# Docs: https://uncloud.run/docs/concepts/ingress/overview
|
||||||
|
|
||||||
# User-defined global config from service 'caddy'.
|
# User-defined global config from service 'caddy'.
|
||||||
# Global config from test machine
|
# Global config from test machine
|
||||||
@@ -831,7 +844,7 @@ valid.example.com {
|
|||||||
}
|
}
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.Equal(t, tt.want, config, "Generated Caddyfile doesn't match")
|
assert.Equal(t, tt.want, normaliseGeneratedTimestamp(config), "Generated Caddyfile doesn't match")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -926,7 +939,9 @@ http://api.example.com {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# NOTE: User-defined configs for services were skipped because Caddy is not running on this machine
|
# NOTE: User-defined configs for services were skipped because Caddy is not running on this machine
|
||||||
# or the latest generated config is invalid. Please check the Caddy logs if it's running.
|
# (not accessible via the shared admin socket /run/uncloud/caddy/admin.sock) or the latest
|
||||||
|
# generated config is invalid. Please check the service 'caddy' is running (uc inspect caddy)
|
||||||
|
# and its logs for more details (uc logs caddy).
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -950,7 +965,9 @@ http://api.example.com {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# NOTE: User-defined configs for services were skipped because Caddy is not running on this machine
|
# NOTE: User-defined configs for services were skipped because Caddy is not running on this machine
|
||||||
# or the latest generated config is invalid. Please check the Caddy logs if it's running.
|
# (not accessible via the shared admin socket /run/uncloud/caddy/admin.sock) or the latest
|
||||||
|
# generated config is invalid. Please check the service 'caddy' is running (uc inspect caddy)
|
||||||
|
# and its logs for more details (uc logs caddy).
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -964,7 +981,7 @@ http://api.example.com {
|
|||||||
config, err := generator.Generate(ctx, tt.containers, false)
|
config, err := generator.Generate(ctx, tt.containers, false)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.Equal(t, tt.want, config, "Generated Caddyfile doesn't match")
|
assert.Equal(t, tt.want, normaliseGeneratedTimestamp(config), "Generated Caddyfile doesn't match")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+217
-52
@@ -40,7 +40,9 @@ type clusterController struct {
|
|||||||
corroService corroservice.Service
|
corroService corroservice.Service
|
||||||
dockerCtrl *docker.Controller
|
dockerCtrl *docker.Controller
|
||||||
// dockerReady is signalled when Docker is configured and ready for containers.
|
// dockerReady is signalled when Docker is configured and ready for containers.
|
||||||
dockerReady chan<- struct{}
|
dockerReady chan<- struct{}
|
||||||
|
// clusterReady is signalled when the cluster controller has finished initializing all components.
|
||||||
|
clusterReady chan<- struct{}
|
||||||
caddyconfigCtrl *caddyconfig.Controller
|
caddyconfigCtrl *caddyconfig.Controller
|
||||||
|
|
||||||
// dnsServer is the embedded internal DNS server for the cluster listening on the machine IP.
|
// dnsServer is the embedded internal DNS server for the cluster listening on the machine IP.
|
||||||
@@ -60,6 +62,7 @@ func newClusterController(
|
|||||||
corroService corroservice.Service,
|
corroService corroservice.Service,
|
||||||
dockerService *docker.Service,
|
dockerService *docker.Service,
|
||||||
dockerReady chan<- struct{},
|
dockerReady chan<- struct{},
|
||||||
|
clusterReady chan<- struct{},
|
||||||
caddyfileCtrl *caddyconfig.Controller,
|
caddyfileCtrl *caddyconfig.Controller,
|
||||||
dnsServer *dns.Server,
|
dnsServer *dns.Server,
|
||||||
dnsResolver *dns.ClusterResolver,
|
dnsResolver *dns.ClusterResolver,
|
||||||
@@ -81,6 +84,7 @@ func newClusterController(
|
|||||||
corroService: corroService,
|
corroService: corroService,
|
||||||
dockerCtrl: docker.NewController(state.ID, dockerService, store),
|
dockerCtrl: docker.NewController(state.ID, dockerService, store),
|
||||||
dockerReady: dockerReady,
|
dockerReady: dockerReady,
|
||||||
|
clusterReady: clusterReady,
|
||||||
caddyconfigCtrl: caddyfileCtrl,
|
caddyconfigCtrl: caddyfileCtrl,
|
||||||
dnsServer: dnsServer,
|
dnsServer: dnsServer,
|
||||||
dnsResolver: dnsResolver,
|
dnsResolver: dnsResolver,
|
||||||
@@ -123,6 +127,21 @@ func (cc *clusterController) Run(ctx context.Context) error {
|
|||||||
|
|
||||||
errGroup, ctx := errgroup.WithContext(ctx)
|
errGroup, ctx := errgroup.WithContext(ctx)
|
||||||
|
|
||||||
|
// Start the WireGuard control loop before waiting for store sync. This ensures endpoint rotation happens
|
||||||
|
// while waiting, allowing Corrosion to connect to peers.
|
||||||
|
errGroup.Go(func() error {
|
||||||
|
if err := cc.wgnet.Run(ctx); err != nil {
|
||||||
|
return fmt.Errorf("WireGuard network failed: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
// Watch for WireGuard peer endpoint changes and update the machine state accordingly.
|
||||||
|
errGroup.Go(func() error {
|
||||||
|
cc.handleEndpointChanges(ctx)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
// Start the network API server. Assume the management IP can't be changed when the network is running.
|
// Start the network API server. Assume the management IP can't be changed when the network is running.
|
||||||
apiAddr := net.JoinHostPort(cc.state.Network.ManagementIP.String(), strconv.Itoa(constants.MachineAPIPort))
|
apiAddr := net.JoinHostPort(cc.state.Network.ManagementIP.String(), strconv.Itoa(constants.MachineAPIPort))
|
||||||
listener, err := net.Listen("tcp", apiAddr)
|
listener, err := net.Listen("tcp", apiAddr)
|
||||||
@@ -137,6 +156,22 @@ func (cc *clusterController) Run(ctx context.Context) error {
|
|||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Wait for the store database to sync to the minimum version before starting store-dependent components.
|
||||||
|
// This prevents issues with using partially replicated data when the machine just joined the cluster,
|
||||||
|
// e.g., an empty machine list causing WireGuard peer misconfiguration.
|
||||||
|
cc.waitStoreSync(ctx)
|
||||||
|
|
||||||
|
// Check if waitStoreSync exited because the context was cancelled. Return early in that case.
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
cc.stopAPIServer()
|
||||||
|
|
||||||
|
err := errGroup.Wait()
|
||||||
|
if corroErr := cc.stopCorrosion(); corroErr != nil {
|
||||||
|
err = errors.Join(err, corroErr)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
errGroup.Go(func() error {
|
errGroup.Go(func() error {
|
||||||
slog.Info("Starting embedded DNS resolver.")
|
slog.Info("Starting embedded DNS resolver.")
|
||||||
if err := cc.dnsResolver.Run(ctx); err != nil {
|
if err := cc.dnsResolver.Run(ctx); err != nil {
|
||||||
@@ -169,44 +204,6 @@ func (cc *clusterController) Run(ctx context.Context) error {
|
|||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
// Watch for WireGuard peer endpoint changes and update the machine state accordingly.
|
|
||||||
errGroup.Go(func() error {
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case e, ok := <-cc.endpointChanges:
|
|
||||||
if !ok {
|
|
||||||
// The channel was closed, stop watching for changes.
|
|
||||||
cc.endpointChanges = nil
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
cc.state.mu.Lock()
|
|
||||||
for i := range cc.state.Network.Peers {
|
|
||||||
if cc.state.Network.Peers[i].PublicKey.Equal(e.PublicKey) {
|
|
||||||
cc.state.Network.Peers[i].Endpoint = &e.Endpoint
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err := cc.state.Save(); err != nil {
|
|
||||||
slog.Error("Failed to save machine state.", "err", err)
|
|
||||||
}
|
|
||||||
cc.state.mu.Unlock()
|
|
||||||
|
|
||||||
slog.Debug("Preserved endpoint change in the machine state.",
|
|
||||||
"public_key", e.PublicKey, "endpoint", e.Endpoint)
|
|
||||||
case <-ctx.Done():
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
errGroup.Go(func() error {
|
|
||||||
if err := cc.wgnet.Run(ctx); err != nil {
|
|
||||||
return fmt.Errorf("WireGuard network failed: %w", err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
|
|
||||||
errGroup.Go(func() error {
|
errGroup.Go(func() error {
|
||||||
slog.Info("Starting caddyconfig controller.")
|
slog.Info("Starting caddyconfig controller.")
|
||||||
if err := cc.caddyconfigCtrl.Run(ctx); err != nil {
|
if err := cc.caddyconfigCtrl.Run(ctx); err != nil {
|
||||||
@@ -225,12 +222,13 @@ func (cc *clusterController) Run(ctx context.Context) error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait for the context to be done and stop the network API server.
|
// Signal that the cluster controller has finished starting all components.
|
||||||
|
close(cc.clusterReady)
|
||||||
|
slog.Info("Cluster controller finished starting all components.")
|
||||||
|
// Wait for the context to be done and stop all servers and controllers.
|
||||||
<-ctx.Done()
|
<-ctx.Done()
|
||||||
slog.Info("Stopping network API server.")
|
|
||||||
// TODO: implement timeout for graceful shutdown.
|
cc.stopAPIServer()
|
||||||
cc.server.GracefulStop()
|
|
||||||
slog.Info("Network API server stopped.")
|
|
||||||
|
|
||||||
// Stop the unregistry server with a timeout if it was started.
|
// Stop the unregistry server with a timeout if it was started.
|
||||||
if cc.unregistry != nil {
|
if cc.unregistry != nil {
|
||||||
@@ -248,19 +246,50 @@ func (cc *clusterController) Run(ctx context.Context) error {
|
|||||||
// Wait for all controllers to finish.
|
// Wait for all controllers to finish.
|
||||||
err = errGroup.Wait()
|
err = errGroup.Wait()
|
||||||
|
|
||||||
// It's safe to stop the Corrosion service after the controllers depending on it and API server are stopped.
|
// Stop Corrosion after all controllers depending on it and API server are stopped.
|
||||||
// Use a new context with a timeout as the current context is already canceled.
|
if corroErr := cc.stopCorrosion(); corroErr != nil {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
err = errors.Join(err, corroErr)
|
||||||
defer cancel()
|
|
||||||
if corroErr := cc.corroService.Stop(ctx); corroErr != nil {
|
|
||||||
err = errors.Join(err, fmt.Errorf("stop corrosion service: %w", corroErr))
|
|
||||||
} else {
|
|
||||||
slog.Info("Corrosion service stopped.")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// stopAPIServer gracefully stops the network API server with a timeout.
|
||||||
|
func (cc *clusterController) stopAPIServer() {
|
||||||
|
timeout := 10 * time.Second
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
stopped := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
slog.Info("Stopping network API server.")
|
||||||
|
cc.server.GracefulStop()
|
||||||
|
close(stopped)
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
slog.Warn("Network API server graceful stop timed out, forcing stop.", "timeout", timeout)
|
||||||
|
cc.server.Stop()
|
||||||
|
case <-stopped:
|
||||||
|
}
|
||||||
|
|
||||||
|
slog.Info("Network API server stopped.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// stopCorrosion stops the Corrosion service with a timeout.
|
||||||
|
func (cc *clusterController) stopCorrosion() error {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
if err := cc.corroService.Stop(ctx); err != nil {
|
||||||
|
return fmt.Errorf("stop corrosion service: %w", err)
|
||||||
|
}
|
||||||
|
slog.Info("Corrosion service stopped.")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// ensureDockerNetwork ensures that the Docker network is configured and ready for containers.
|
// ensureDockerNetwork ensures that the Docker network is configured and ready for containers.
|
||||||
func (cc *clusterController) ensureDockerNetwork(ctx context.Context) error {
|
func (cc *clusterController) ensureDockerNetwork(ctx context.Context) error {
|
||||||
if err := cc.dockerCtrl.WaitDaemonReady(ctx); err != nil {
|
if err := cc.dockerCtrl.WaitDaemonReady(ctx); err != nil {
|
||||||
@@ -281,6 +310,140 @@ func (cc *clusterController) ensureDockerNetwork(ctx context.Context) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// handleEndpointChanges watches for WireGuard peer endpoint changes and persists them to the machine state.
|
||||||
|
func (cc *clusterController) handleEndpointChanges(ctx context.Context) {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case e, ok := <-cc.endpointChanges:
|
||||||
|
if !ok {
|
||||||
|
// The channel was closed, stop watching for changes.
|
||||||
|
cc.endpointChanges = nil
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cc.state.mu.Lock()
|
||||||
|
for i := range cc.state.Network.Peers {
|
||||||
|
if cc.state.Network.Peers[i].PublicKey.Equal(e.PublicKey) {
|
||||||
|
cc.state.Network.Peers[i].Endpoint = &e.Endpoint
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := cc.state.Save(); err != nil {
|
||||||
|
slog.Error("Failed to save machine state.", "err", err)
|
||||||
|
}
|
||||||
|
cc.state.mu.Unlock()
|
||||||
|
|
||||||
|
slog.Debug("Preserved endpoint change in the machine state.",
|
||||||
|
"public_key", e.PublicKey, "endpoint", e.Endpoint)
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// waitStoreSync waits for the store database to sync to the minimum required DB version if set in the machine state.
|
||||||
|
// Blocks until synced or context is cancelled.
|
||||||
|
func (cc *clusterController) waitStoreSync(ctx context.Context) {
|
||||||
|
minVersion := cc.state.MinStoreDBVersion
|
||||||
|
if minVersion == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
slog.Info("Waiting for the initial cluster store sync.", "min_version", minVersion)
|
||||||
|
|
||||||
|
ticker := time.NewTicker(500 * time.Millisecond)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
var (
|
||||||
|
lastVersion int64
|
||||||
|
lastLogTime time.Time
|
||||||
|
lastErrLogTime time.Time
|
||||||
|
)
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
case <-ticker.C:
|
||||||
|
version, err := cc.store.DBVersion(ctx)
|
||||||
|
if err != nil {
|
||||||
|
// Log errors at most once every 5 seconds.
|
||||||
|
if time.Since(lastErrLogTime) >= 5*time.Second {
|
||||||
|
slog.Error("Failed to get the cluster store DB version, retrying.", "err", err)
|
||||||
|
lastErrLogTime = time.Now()
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if version >= minVersion {
|
||||||
|
// Clear MinStoreDBVersion so next restart doesn't wait for sync.
|
||||||
|
cc.state.mu.Lock()
|
||||||
|
cc.state.MinStoreDBVersion = 0
|
||||||
|
if err := cc.state.Save(); err != nil {
|
||||||
|
slog.Error("Failed to save machine state after the initial cluster store sync.", "err", err)
|
||||||
|
}
|
||||||
|
cc.state.mu.Unlock()
|
||||||
|
|
||||||
|
// Wait for all known missing changes to be synced before returning.
|
||||||
|
// TODO: reevaluate if this is necessary after migrating to the latest Corrosion version:
|
||||||
|
// https://github.com/psviderski/uncloud/issues/172
|
||||||
|
// This works on the best effort basis as the missing changes may not be yet known when we start
|
||||||
|
// checking it after reaching the minimum version.
|
||||||
|
// Reaching the minimum version doesn't guarantee that the store is actually synced to
|
||||||
|
// the state we observed on the source node. db_version is a machine-local Lamport clock.
|
||||||
|
// When changes are received from a remote machine, the local db_version is set to
|
||||||
|
// max(local_db_version, incoming_db_version) + 1 for each applied transaction. This means
|
||||||
|
// the new machine's db_version can jump well past the source machine's db_version on the very
|
||||||
|
// first batch of replicated changes, without having received all changes from all machines
|
||||||
|
// in the cluster.
|
||||||
|
cc.waitKnownMissingChanges(ctx)
|
||||||
|
|
||||||
|
if ver, verErr := cc.store.DBVersion(ctx); verErr == nil {
|
||||||
|
version = ver
|
||||||
|
}
|
||||||
|
slog.Info("Cluster store completed the initial sync.", "version", version, "min_version", minVersion)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log progress only once a second.
|
||||||
|
if version != lastVersion && time.Since(lastLogTime) >= 1*time.Second {
|
||||||
|
slog.Info("Syncing cluster store.", "version", version, "min_version", minVersion)
|
||||||
|
lastLogTime = time.Now()
|
||||||
|
lastVersion = version
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// waitKnownMissingChanges polls the store until all known missing changes have been synced.
|
||||||
|
func (cc *clusterController) waitKnownMissingChanges(ctx context.Context) {
|
||||||
|
ticker := time.NewTicker(1 * time.Second)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
case <-ticker.C:
|
||||||
|
changes, err := cc.store.KnownMissingChanges(ctx)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("Failed to get known missing changes from the cluster store, skipping check.",
|
||||||
|
"err", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(changes) == 0 {
|
||||||
|
slog.Debug("All known missing changes have been synced to the cluster store.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
slog.Debug("Waiting for known missing changes to be synced to the cluster store.", "remaining",
|
||||||
|
len(changes))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// syncDockerContainers watches local Docker containers and syncs them to the cluster store.
|
// syncDockerContainers watches local Docker containers and syncs them to the cluster store.
|
||||||
// TODO: move this to the Docker controller.
|
// TODO: move this to the Docker controller.
|
||||||
func (cc *clusterController) syncDockerContainers(ctx context.Context) error {
|
func (cc *clusterController) syncDockerContainers(ctx context.Context) error {
|
||||||
@@ -340,6 +503,8 @@ func (cc *clusterController) handleMachineChanges(ctx context.Context) error {
|
|||||||
|
|
||||||
// The machine store may be empty when a machine first joins the cluster, before store synchronization
|
// The machine store may be empty when a machine first joins the cluster, before store synchronization
|
||||||
// completes. Skip configuration now and apply it when the store changes are received.
|
// completes. Skip configuration now and apply it when the store changes are received.
|
||||||
|
// TODO: remove this check after ensuring the store is actually synced to the latest known state at this point.
|
||||||
|
// See TODO in waitStoreSync.
|
||||||
if len(machines) > 0 {
|
if len(machines) > 0 {
|
||||||
slog.Info("Reconfiguring network peers with the current machines.", "machines", len(machines))
|
slog.Info("Reconfiguring network peers with the current machines.", "machines", len(machines))
|
||||||
if err = cc.configurePeers(machines); err != nil {
|
if err = cc.configurePeers(machines); err != nil {
|
||||||
|
|||||||
@@ -26,12 +26,19 @@ type Cluster struct {
|
|||||||
corroAdmin *corrosion.AdminClient
|
corroAdmin *corrosion.AdminClient
|
||||||
// machineID is the ID of the current machine that is running the cluster service.
|
// machineID is the ID of the current machine that is running the cluster service.
|
||||||
machineID string
|
machineID string
|
||||||
|
// initialised is closed when the machine is configured as a member of a cluster.
|
||||||
|
initialised <-chan struct{}
|
||||||
|
// ready is closed when the cluster controller has finished starting all components
|
||||||
|
// and the machine is ready to serve cluster requests.
|
||||||
|
ready <-chan struct{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewCluster(store *store.Store, corroAdmin *corrosion.AdminClient) *Cluster {
|
func NewCluster(store *store.Store, corroAdmin *corrosion.AdminClient, initialised, ready <-chan struct{}) *Cluster {
|
||||||
return &Cluster{
|
return &Cluster{
|
||||||
store: store,
|
store: store,
|
||||||
corroAdmin: corroAdmin,
|
corroAdmin: corroAdmin,
|
||||||
|
initialised: initialised,
|
||||||
|
ready: ready,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,67 +48,45 @@ func (c *Cluster) UpdateMachineID(mid string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *Cluster) Init(ctx context.Context, network netip.Prefix) error {
|
func (c *Cluster) Init(ctx context.Context, network netip.Prefix) error {
|
||||||
initialised, err := c.Initialised(ctx)
|
select {
|
||||||
if err != nil {
|
case <-c.initialised:
|
||||||
return err
|
return fmt.Errorf("cluster is already initialised on this machine")
|
||||||
}
|
default:
|
||||||
if initialised {
|
|
||||||
return fmt.Errorf("cluster is already initialised")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = c.store.Put(ctx, "network", network.String()); err != nil {
|
if err := c.store.Put(ctx, "network", network.String()); err != nil {
|
||||||
return fmt.Errorf("put network to store: %w", err)
|
return fmt.Errorf("put network to store: %w", err)
|
||||||
}
|
}
|
||||||
if err = c.store.Put(ctx, "created_at", time.Now().UTC().Format(time.RFC3339)); err != nil {
|
if err := c.store.Put(ctx, "created_at", time.Now().UTC().Format(time.RFC3339)); err != nil {
|
||||||
return fmt.Errorf("put created_at to store: %w", err)
|
return fmt.Errorf("put created_at to store: %w", err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Cluster) Initialised(ctx context.Context) (bool, error) {
|
// checkReady checks if the machine is ready to serve cluster requests (store synced, cluster components started).
|
||||||
var createdAt string
|
func (c *Cluster) checkReady() error {
|
||||||
if err := c.store.Get(ctx, "created_at", &createdAt); err != nil {
|
select {
|
||||||
if errors.Is(err, store.ErrKeyNotFound) {
|
case <-c.ready:
|
||||||
return false, nil
|
return nil
|
||||||
}
|
default:
|
||||||
return false, status.Errorf(codes.Internal, "get created_at from store: %v", err)
|
return status.Error(codes.Unavailable, "machine is not ready to serve cluster requests")
|
||||||
}
|
}
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Cluster) checkInitialised(ctx context.Context) error {
|
|
||||||
initialised, err := c.Initialised(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if !initialised {
|
|
||||||
return status.Error(codes.FailedPrecondition, "cluster is not initialised")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Cluster) Network(ctx context.Context) (netip.Prefix, error) {
|
|
||||||
if err := c.checkInitialised(ctx); err != nil {
|
|
||||||
return netip.Prefix{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var net string
|
|
||||||
if err := c.store.Get(ctx, "network", &net); err != nil {
|
|
||||||
return netip.Prefix{}, status.Errorf(codes.Internal, "get network from store: %v", err)
|
|
||||||
}
|
|
||||||
prefix, err := netip.ParsePrefix(net)
|
|
||||||
if err != nil {
|
|
||||||
return netip.Prefix{}, status.Errorf(codes.Internal, "parse network prefix: %v", err)
|
|
||||||
}
|
|
||||||
return prefix, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddMachine adds a machine to the cluster.
|
// AddMachine adds a machine to the cluster.
|
||||||
func (c *Cluster) AddMachine(ctx context.Context, req *pb.AddMachineRequest) (*pb.AddMachineResponse, error) {
|
func (c *Cluster) AddMachine(ctx context.Context, req *pb.AddMachineRequest) (*pb.AddMachineResponse, error) {
|
||||||
if err := c.checkInitialised(ctx); err != nil {
|
if err := c.checkReady(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return c.AddMachineWithoutReadyCheck(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddMachineWithoutReadyCheck adds a machine to the cluster without checking if the cluster is ready.
|
||||||
|
// This is used internally during cluster initialisation to add the first machine.
|
||||||
|
func (c *Cluster) AddMachineWithoutReadyCheck(
|
||||||
|
ctx context.Context, req *pb.AddMachineRequest,
|
||||||
|
) (*pb.AddMachineResponse, error) {
|
||||||
if req.Network == nil {
|
if req.Network == nil {
|
||||||
return nil, status.Error(codes.InvalidArgument, "network not set")
|
return nil, status.Error(codes.InvalidArgument, "network not set")
|
||||||
}
|
}
|
||||||
@@ -162,7 +147,7 @@ func (c *Cluster) AddMachine(ctx context.Context, req *pb.AddMachineRequest) (*p
|
|||||||
manageIP = pb.NewIP(network.ManagementIP(req.Network.PublicKey))
|
manageIP = pb.NewIP(network.ManagementIP(req.Network.PublicKey))
|
||||||
}
|
}
|
||||||
// Allocate a subnet for the machine from the cluster network.
|
// Allocate a subnet for the machine from the cluster network.
|
||||||
clusterNetwork, err := c.Network(ctx)
|
clusterNetwork, err := c.network(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, status.Errorf(codes.Internal, "get cluster network: %v", err)
|
return nil, status.Errorf(codes.Internal, "get cluster network: %v", err)
|
||||||
}
|
}
|
||||||
@@ -198,9 +183,21 @@ func (c *Cluster) AddMachine(ctx context.Context, req *pb.AddMachineRequest) (*p
|
|||||||
return resp, nil
|
return resp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Cluster) network(ctx context.Context) (netip.Prefix, error) {
|
||||||
|
var net string
|
||||||
|
if err := c.store.Get(ctx, "network", &net); err != nil {
|
||||||
|
return netip.Prefix{}, status.Errorf(codes.Internal, "get network from store: %v", err)
|
||||||
|
}
|
||||||
|
prefix, err := netip.ParsePrefix(net)
|
||||||
|
if err != nil {
|
||||||
|
return netip.Prefix{}, status.Errorf(codes.Internal, "parse network prefix: %v", err)
|
||||||
|
}
|
||||||
|
return prefix, nil
|
||||||
|
}
|
||||||
|
|
||||||
// UpdateMachine updates machine configuration in the cluster.
|
// UpdateMachine updates machine configuration in the cluster.
|
||||||
func (c *Cluster) UpdateMachine(ctx context.Context, req *pb.UpdateMachineRequest) (*pb.UpdateMachineResponse, error) {
|
func (c *Cluster) UpdateMachine(ctx context.Context, req *pb.UpdateMachineRequest) (*pb.UpdateMachineResponse, error) {
|
||||||
if err := c.checkInitialised(ctx); err != nil {
|
if err := c.checkReady(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -262,7 +259,7 @@ func (c *Cluster) UpdateMachine(ctx context.Context, req *pb.UpdateMachineReques
|
|||||||
updatedMachine.PublicIp = req.PublicIp
|
updatedMachine.PublicIp = req.PublicIp
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if req.Endpoints != nil {
|
if len(req.Endpoints) > 0 {
|
||||||
updatedMachine.Network.Endpoints = req.Endpoints
|
updatedMachine.Network.Endpoints = req.Endpoints
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -283,7 +280,7 @@ func (c *Cluster) UpdateMachine(ctx context.Context, req *pb.UpdateMachineReques
|
|||||||
|
|
||||||
// ListMachines lists all machines in the cluster including their membership states.
|
// ListMachines lists all machines in the cluster including their membership states.
|
||||||
func (c *Cluster) ListMachines(ctx context.Context, _ *emptypb.Empty) (*pb.ListMachinesResponse, error) {
|
func (c *Cluster) ListMachines(ctx context.Context, _ *emptypb.Empty) (*pb.ListMachinesResponse, error) {
|
||||||
if err := c.checkInitialised(ctx); err != nil {
|
if err := c.checkReady(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -329,7 +326,7 @@ func (c *Cluster) ListMachines(ctx context.Context, _ *emptypb.Empty) (*pb.ListM
|
|||||||
|
|
||||||
// RemoveMachine removes a machine from the cluster.
|
// RemoveMachine removes a machine from the cluster.
|
||||||
func (c *Cluster) RemoveMachine(ctx context.Context, req *pb.RemoveMachineRequest) (*emptypb.Empty, error) {
|
func (c *Cluster) RemoveMachine(ctx context.Context, req *pb.RemoveMachineRequest) (*emptypb.Empty, error) {
|
||||||
if err := c.checkInitialised(ctx); err != nil {
|
if err := c.checkReady(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ type uncloudDNSDomain struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *Cluster) ReserveDomain(ctx context.Context, req *pb.ReserveDomainRequest) (*pb.Domain, error) {
|
func (c *Cluster) ReserveDomain(ctx context.Context, req *pb.ReserveDomainRequest) (*pb.Domain, error) {
|
||||||
if err := c.checkInitialised(ctx); err != nil {
|
if err := c.checkReady(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,7 +64,7 @@ func (c *Cluster) ReserveDomain(ctx context.Context, req *pb.ReserveDomainReques
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *Cluster) GetDomain(ctx context.Context, _ *emptypb.Empty) (*pb.Domain, error) {
|
func (c *Cluster) GetDomain(ctx context.Context, _ *emptypb.Empty) (*pb.Domain, error) {
|
||||||
if err := c.checkInitialised(ctx); err != nil {
|
if err := c.checkReady(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,7 +95,7 @@ func (c *Cluster) storedDomain(ctx context.Context) (uncloudDNSDomain, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *Cluster) ReleaseDomain(ctx context.Context, _ *emptypb.Empty) (*pb.Domain, error) {
|
func (c *Cluster) ReleaseDomain(ctx context.Context, _ *emptypb.Empty) (*pb.Domain, error) {
|
||||||
if err := c.checkInitialised(ctx); err != nil {
|
if err := c.checkReady(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,7 +115,7 @@ func (c *Cluster) ReleaseDomain(ctx context.Context, _ *emptypb.Empty) (*pb.Doma
|
|||||||
func (c *Cluster) CreateDomainRecords(
|
func (c *Cluster) CreateDomainRecords(
|
||||||
ctx context.Context, req *pb.CreateDomainRecordsRequest,
|
ctx context.Context, req *pb.CreateDomainRecordsRequest,
|
||||||
) (*pb.CreateDomainRecordsResponse, error) {
|
) (*pb.CreateDomainRecordsResponse, error) {
|
||||||
if err := c.checkInitialised(ctx); err != nil {
|
if err := c.checkReady(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import (
|
|||||||
"github.com/docker/docker/client"
|
"github.com/docker/docker/client"
|
||||||
"github.com/docker/docker/pkg/stdcopy"
|
"github.com/docker/docker/pkg/stdcopy"
|
||||||
"github.com/docker/go-connections/nat"
|
"github.com/docker/go-connections/nat"
|
||||||
|
"github.com/docker/go-units"
|
||||||
"github.com/google/go-containerregistry/pkg/authn"
|
"github.com/google/go-containerregistry/pkg/authn"
|
||||||
"github.com/google/go-containerregistry/pkg/name"
|
"github.com/google/go-containerregistry/pkg/name"
|
||||||
"github.com/google/go-containerregistry/pkg/v1/remote"
|
"github.com/google/go-containerregistry/pkg/v1/remote"
|
||||||
@@ -566,6 +567,22 @@ func (s *Server) CreateServiceContainer(
|
|||||||
if spec.Mode == "" {
|
if spec.Mode == "" {
|
||||||
config.Labels[api.LabelServiceMode] = api.ServiceModeReplicated
|
config.Labels[api.LabelServiceMode] = api.ServiceModeReplicated
|
||||||
}
|
}
|
||||||
|
if hc := spec.Container.Healthcheck; hc != nil {
|
||||||
|
if hc.Disable {
|
||||||
|
config.Healthcheck = &container.HealthConfig{
|
||||||
|
Test: []string{"NONE"},
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
config.Healthcheck = &container.HealthConfig{
|
||||||
|
Test: hc.Test,
|
||||||
|
Interval: hc.Interval,
|
||||||
|
Timeout: hc.Timeout,
|
||||||
|
StartPeriod: hc.StartPeriod,
|
||||||
|
StartInterval: hc.StartInterval,
|
||||||
|
Retries: int(hc.Retries),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: do not set the ports as container labels once migrated to retrieve them from the spec in DB.
|
// TODO: do not set the ports as container labels once migrated to retrieve them from the spec in DB.
|
||||||
var err error
|
var err error
|
||||||
@@ -605,22 +622,29 @@ func (s *Server) CreateServiceContainer(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
hostConfig := &container.HostConfig{
|
hostConfig := &container.HostConfig{
|
||||||
|
CapAdd: spec.Container.CapAdd,
|
||||||
|
CapDrop: spec.Container.CapDrop,
|
||||||
Binds: spec.Container.Volumes,
|
Binds: spec.Container.Volumes,
|
||||||
Init: spec.Container.Init,
|
Init: spec.Container.Init,
|
||||||
Mounts: mounts,
|
Mounts: mounts,
|
||||||
|
PidMode: container.PidMode(spec.Container.PidMode),
|
||||||
PortBindings: portBindings,
|
PortBindings: portBindings,
|
||||||
Privileged: spec.Container.Privileged,
|
Privileged: spec.Container.Privileged,
|
||||||
Resources: container.Resources{
|
Resources: container.Resources{
|
||||||
NanoCPUs: spec.Container.Resources.CPU,
|
NanoCPUs: spec.Container.Resources.CPU,
|
||||||
Memory: spec.Container.Resources.Memory,
|
Memory: spec.Container.Resources.Memory,
|
||||||
MemoryReservation: spec.Container.Resources.MemoryReservation,
|
MemoryReservation: spec.Container.Resources.MemoryReservation,
|
||||||
|
Devices: toDockerDevices(spec.Container.Resources.Devices),
|
||||||
DeviceRequests: spec.Container.Resources.DeviceReservations,
|
DeviceRequests: spec.Container.Resources.DeviceReservations,
|
||||||
|
Ulimits: toDockerUlimits(spec.Container.Resources.Ulimits),
|
||||||
},
|
},
|
||||||
// Restart service containers if they exit or a machine restarts unless they are explicitly stopped.
|
// Restart service containers if they exit or a machine restarts unless they are explicitly stopped.
|
||||||
// For one-off containers and batch jobs we plan to use a different service type/mode.
|
// For one-off containers and batch jobs we plan to use a different service type/mode.
|
||||||
RestartPolicy: container.RestartPolicy{
|
RestartPolicy: container.RestartPolicy{
|
||||||
Name: container.RestartPolicyUnlessStopped,
|
Name: container.RestartPolicyUnlessStopped,
|
||||||
},
|
},
|
||||||
|
ShmSize: spec.Container.Resources.SharedMemory,
|
||||||
|
Sysctls: spec.Container.Sysctls,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Configure the container to use the internal DNS server if it's available.
|
// Configure the container to use the internal DNS server if it's available.
|
||||||
@@ -801,14 +825,17 @@ func (s *Server) injectConfigs(ctx context.Context, containerID string, configs
|
|||||||
}
|
}
|
||||||
|
|
||||||
// copyContentToContainer copies content directly to a file in the container using Docker's CopyToContainer API.
|
// copyContentToContainer copies content directly to a file in the container using Docker's CopyToContainer API.
|
||||||
|
// It will create any intermediate directories in the target path that don't exist.
|
||||||
func (s *Server) copyContentToContainer(ctx context.Context, containerID string, content []byte, targetPath string, uid *uint64, gid *uint64, fileMode os.FileMode) error {
|
func (s *Server) copyContentToContainer(ctx context.Context, containerID string, content []byte, targetPath string, uid *uint64, gid *uint64, fileMode os.FileMode) error {
|
||||||
// Create a tar archive containing the file
|
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
tw := tar.NewWriter(&buf)
|
tw := tar.NewWriter(&buf)
|
||||||
|
|
||||||
// Create tar header
|
// Trim leading slash(es) to avoid double slashes in the tar path
|
||||||
|
tarPath := strings.TrimPrefix(targetPath, "/")
|
||||||
|
|
||||||
|
// Create tar header with full path
|
||||||
header := &tar.Header{
|
header := &tar.Header{
|
||||||
Name: filepath.Base(targetPath),
|
Name: tarPath,
|
||||||
Size: int64(len(content)),
|
Size: int64(len(content)),
|
||||||
Mode: int64(fileMode),
|
Mode: int64(fileMode),
|
||||||
ModTime: time.Now(),
|
ModTime: time.Now(),
|
||||||
@@ -834,16 +861,12 @@ func (s *Server) copyContentToContainer(ctx context.Context, containerID string,
|
|||||||
return fmt.Errorf("close tar writer: %w", err)
|
return fmt.Errorf("close tar writer: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copy the tar archive to the container
|
// Always extract to root. The tar archive contains the full path, so tar will
|
||||||
targetDir := filepath.Dir(targetPath)
|
// automatically create any intermediate directories that don't exist in the container.
|
||||||
if targetDir == "." {
|
|
||||||
targetDir = "/"
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := s.client.CopyToContainer(
|
if err := s.client.CopyToContainer(
|
||||||
ctx,
|
ctx,
|
||||||
containerID,
|
containerID,
|
||||||
targetDir,
|
"/",
|
||||||
&buf,
|
&buf,
|
||||||
container.CopyToContainerOptions{CopyUIDGID: true},
|
container.CopyToContainerOptions{CopyUIDGID: true},
|
||||||
); err != nil {
|
); err != nil {
|
||||||
@@ -875,6 +898,40 @@ func toDockerBindOptions(opts *api.BindOptions) *mount.BindOptions {
|
|||||||
return dockerOpts
|
return dockerOpts
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func toDockerUlimits(ulimits map[string]api.Ulimit) []*units.Ulimit {
|
||||||
|
if len(ulimits) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
dockerUlimits := make([]*units.Ulimit, 0, len(ulimits))
|
||||||
|
for name, u := range ulimits {
|
||||||
|
dockerUlimits = append(dockerUlimits, &units.Ulimit{
|
||||||
|
Name: name,
|
||||||
|
Soft: u.Soft,
|
||||||
|
Hard: u.Hard,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return dockerUlimits
|
||||||
|
}
|
||||||
|
|
||||||
|
func toDockerDevices(devices []api.DeviceMapping) []container.DeviceMapping {
|
||||||
|
if len(devices) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
dockerDevices := make([]container.DeviceMapping, 0, len(devices))
|
||||||
|
for _, d := range devices {
|
||||||
|
dockerDevices = append(dockerDevices, container.DeviceMapping{
|
||||||
|
PathOnHost: d.HostPath,
|
||||||
|
PathInContainer: d.ContainerPath,
|
||||||
|
CgroupPermissions: d.CgroupPermissions,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return dockerDevices
|
||||||
|
}
|
||||||
|
|
||||||
// verifyDockerVolumesExist checks if the Docker named volumes referenced in the mounts exist on the machine.
|
// verifyDockerVolumesExist checks if the Docker named volumes referenced in the mounts exist on the machine.
|
||||||
func (s *Server) verifyDockerVolumesExist(ctx context.Context, mounts []mount.Mount) error {
|
func (s *Server) verifyDockerVolumesExist(ctx context.Context, mounts []mount.Mount) error {
|
||||||
for _, m := range mounts {
|
for _, m := range mounts {
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ func createIptablesChains() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
firstRejectRuleNum := 0
|
firstRejectRuleNum := 0
|
||||||
for _, line := range strings.Split(string(out), "\n") {
|
for line := range strings.SplitSeq(string(out), "\n") {
|
||||||
fields := strings.Fields(line)
|
fields := strings.Fields(line)
|
||||||
if len(fields) < 2 {
|
if len(fields) < 2 {
|
||||||
continue
|
continue
|
||||||
|
|||||||
+119
-21
@@ -33,10 +33,12 @@ import (
|
|||||||
"github.com/psviderski/unregistry"
|
"github.com/psviderski/unregistry"
|
||||||
"github.com/siderolabs/grpc-proxy/proxy"
|
"github.com/siderolabs/grpc-proxy/proxy"
|
||||||
"golang.org/x/sync/errgroup"
|
"golang.org/x/sync/errgroup"
|
||||||
|
"golang.zx2c4.com/wireguard/wgctrl"
|
||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
"google.golang.org/grpc/codes"
|
"google.golang.org/grpc/codes"
|
||||||
"google.golang.org/grpc/status"
|
"google.golang.org/grpc/status"
|
||||||
"google.golang.org/protobuf/types/known/emptypb"
|
"google.golang.org/protobuf/types/known/emptypb"
|
||||||
|
"google.golang.org/protobuf/types/known/timestamppb"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -174,10 +176,13 @@ type Machine struct {
|
|||||||
state *State
|
state *State
|
||||||
// started is closed when the machine is ready to serve requests on the local API server.
|
// started is closed when the machine is ready to serve requests on the local API server.
|
||||||
started chan struct{}
|
started chan struct{}
|
||||||
// initialised is signalled when the machine is configured as a member of a cluster.
|
// initialised is closed when the machine is configured as a member of a cluster.
|
||||||
initialised chan struct{}
|
initialised chan struct{}
|
||||||
// networkReady is signalled when the Docker network is configured and ready for containers.
|
// networkReady is closed when the Docker network is configured and ready for containers.
|
||||||
networkReady chan struct{}
|
networkReady chan struct{}
|
||||||
|
// clusterReady is closed when the cluster controller has finished starting all components
|
||||||
|
// and the machine is ready to serve cluster requests.
|
||||||
|
clusterReady chan struct{}
|
||||||
// resetting is true when the machine is being reset.
|
// resetting is true when the machine is being reset.
|
||||||
resetting bool
|
resetting bool
|
||||||
// stop cancels the Run method context to stop the machine gracefully.
|
// stop cancels the Run method context to stop the machine gracefully.
|
||||||
@@ -246,7 +251,10 @@ func NewMachine(config *Config) (*Machine, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("create corrosion admin client: %w", err)
|
return nil, fmt.Errorf("create corrosion admin client: %w", err)
|
||||||
}
|
}
|
||||||
c := cluster.NewCluster(corroStore, corroAdmin)
|
|
||||||
|
initialised := make(chan struct{})
|
||||||
|
clusterReady := make(chan struct{})
|
||||||
|
c := cluster.NewCluster(corroStore, corroAdmin, initialised, clusterReady)
|
||||||
|
|
||||||
// Init dependencies for a gRPC Docker server that proxies requests to the local Docker daemon.
|
// Init dependencies for a gRPC Docker server that proxies requests to the local Docker daemon.
|
||||||
dbFilePath := filepath.Join(config.DataDir, DBFileName)
|
dbFilePath := filepath.Join(config.DataDir, DBFileName)
|
||||||
@@ -269,8 +277,9 @@ func NewMachine(config *Config) (*Machine, error) {
|
|||||||
config: *config,
|
config: *config,
|
||||||
state: state,
|
state: state,
|
||||||
started: make(chan struct{}),
|
started: make(chan struct{}),
|
||||||
initialised: make(chan struct{}, 1),
|
initialised: initialised,
|
||||||
networkReady: make(chan struct{}),
|
networkReady: make(chan struct{}),
|
||||||
|
clusterReady: clusterReady,
|
||||||
store: corroStore,
|
store: corroStore,
|
||||||
cluster: c,
|
cluster: c,
|
||||||
dockerService: dockerService,
|
dockerService: dockerService,
|
||||||
@@ -294,7 +303,7 @@ func NewMachine(config *Config) (*Machine, error) {
|
|||||||
m.localMachineServer = newGRPCServer(m, c, m.dockerServer, caddyServer)
|
m.localMachineServer = newGRPCServer(m, c, m.dockerServer, caddyServer)
|
||||||
|
|
||||||
if m.Initialised() {
|
if m.Initialised() {
|
||||||
m.initialised <- struct{}{}
|
close(m.initialised)
|
||||||
}
|
}
|
||||||
|
|
||||||
return m, nil
|
return m, nil
|
||||||
@@ -475,6 +484,7 @@ func (m *Machine) Run(ctx context.Context) error {
|
|||||||
m.config.CorrosionService,
|
m.config.CorrosionService,
|
||||||
m.dockerService,
|
m.dockerService,
|
||||||
m.networkReady,
|
m.networkReady,
|
||||||
|
m.clusterReady,
|
||||||
caddyconfigCtrl,
|
caddyconfigCtrl,
|
||||||
dnsServer,
|
dnsServer,
|
||||||
dnsResolver,
|
dnsResolver,
|
||||||
@@ -691,20 +701,29 @@ func (m *Machine) InitCluster(ctx context.Context, req *pb.InitClusterRequest) (
|
|||||||
return nil, status.Errorf(codes.Internal, "generate machine name: %v", err)
|
return nil, status.Errorf(codes.Internal, "generate machine name: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Use the public and all routable IPs as endpoints.
|
|
||||||
ips, err := network.ListRoutableIPs()
|
// Use explicitly provided WireGuard endpoints, or use the routable IPs on the machine and its public IP
|
||||||
if err != nil {
|
// if not provided. The IPs are auto-detected.
|
||||||
return nil, status.Errorf(codes.Internal, "list routable IPs: %v", err)
|
var endpoints []*pb.IPPort
|
||||||
}
|
|
||||||
publicIP, pubIPErr := network.GetPublicIP()
|
publicIP, pubIPErr := network.GetPublicIP()
|
||||||
// Ignore the error if failed to get the public IP using API services.
|
|
||||||
if pubIPErr == nil && !slices.Contains(ips, publicIP) {
|
if len(req.WireguardEndpoints) > 0 {
|
||||||
ips = append(ips, publicIP)
|
endpoints = req.WireguardEndpoints
|
||||||
}
|
} else {
|
||||||
endpoints := make([]*pb.IPPort, len(ips))
|
ips, err := network.ListRoutableIPs()
|
||||||
for i, addr := range ips {
|
if err != nil {
|
||||||
addrPort := netip.AddrPortFrom(addr, network.WireGuardPort)
|
return nil, status.Errorf(codes.Internal, "list routable IPs: %v", err)
|
||||||
endpoints[i] = pb.NewIPPort(addrPort)
|
}
|
||||||
|
// Ignore the error if failed to get the public IP using API services.
|
||||||
|
if pubIPErr == nil && !slices.Contains(ips, publicIP) {
|
||||||
|
ips = append(ips, publicIP)
|
||||||
|
}
|
||||||
|
|
||||||
|
endpoints = make([]*pb.IPPort, len(ips))
|
||||||
|
for i, addr := range ips {
|
||||||
|
addrPort := netip.AddrPortFrom(addr, network.WireGuardPort)
|
||||||
|
endpoints[i] = pb.NewIPPort(addrPort)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register the new machine in the cluster to populate the state and get its ID and subnet.
|
// Register the new machine in the cluster to populate the state and get its ID and subnet.
|
||||||
@@ -722,7 +741,7 @@ func (m *Machine) InitCluster(ctx context.Context, req *pb.InitClusterRequest) (
|
|||||||
addReq.PublicIp = pb.NewIP(publicIP)
|
addReq.PublicIp = pb.NewIP(publicIP)
|
||||||
}
|
}
|
||||||
|
|
||||||
addResp, err := m.cluster.AddMachine(ctx, addReq)
|
addResp, err := m.cluster.AddMachineWithoutReadyCheck(ctx, addReq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, status.Errorf(codes.Internal, "add machine to cluster: %v", err)
|
return nil, status.Errorf(codes.Internal, "add machine to cluster: %v", err)
|
||||||
}
|
}
|
||||||
@@ -749,7 +768,7 @@ func (m *Machine) InitCluster(ctx context.Context, req *pb.InitClusterRequest) (
|
|||||||
}
|
}
|
||||||
slog.Info("Cluster initialised with machine.", "id", m.state.ID, "machine", m.state.Name)
|
slog.Info("Cluster initialised with machine.", "id", m.state.ID, "machine", m.state.Name)
|
||||||
// Signal that the machine is initialised as a member of a cluster.
|
// Signal that the machine is initialised as a member of a cluster.
|
||||||
m.initialised <- struct{}{}
|
close(m.initialised)
|
||||||
|
|
||||||
resp := &pb.InitClusterResponse{
|
resp := &pb.InitClusterResponse{
|
||||||
Machine: addResp.Machine,
|
Machine: addResp.Machine,
|
||||||
@@ -792,6 +811,7 @@ func (m *Machine) JoinCluster(_ context.Context, req *pb.JoinClusterRequest) (*e
|
|||||||
PrivateKey: m.state.Network.PrivateKey,
|
PrivateKey: m.state.Network.PrivateKey,
|
||||||
PublicKey: m.state.Network.PublicKey,
|
PublicKey: m.state.Network.PublicKey,
|
||||||
}
|
}
|
||||||
|
m.state.MinStoreDBVersion = req.MinStoreDbVersion
|
||||||
|
|
||||||
// Build a peers config from other cluster machines.
|
// Build a peers config from other cluster machines.
|
||||||
m.state.Network.Peers = make([]network.PeerConfig, 0, len(req.OtherMachines))
|
m.state.Network.Peers = make([]network.PeerConfig, 0, len(req.OtherMachines))
|
||||||
@@ -830,7 +850,7 @@ func (m *Machine) JoinCluster(_ context.Context, req *pb.JoinClusterRequest) (*e
|
|||||||
"peers", len(m.state.Network.Peers),
|
"peers", len(m.state.Network.Peers),
|
||||||
)
|
)
|
||||||
// Signal that the machine is initialised as a member of a cluster.
|
// Signal that the machine is initialised as a member of a cluster.
|
||||||
m.initialised <- struct{}{}
|
close(m.initialised)
|
||||||
|
|
||||||
return &emptypb.Empty{}, nil
|
return &emptypb.Empty{}, nil
|
||||||
}
|
}
|
||||||
@@ -864,6 +884,7 @@ func (m *Machine) Token(_ context.Context, _ *emptypb.Empty) (*pb.TokenResponse,
|
|||||||
return &pb.TokenResponse{Token: tokenStr}, nil
|
return &pb.TokenResponse{Token: tokenStr}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Deprecated: use InspectMachine instead.
|
||||||
func (m *Machine) Inspect(_ context.Context, _ *emptypb.Empty) (*pb.MachineInfo, error) {
|
func (m *Machine) Inspect(_ context.Context, _ *emptypb.Empty) (*pb.MachineInfo, error) {
|
||||||
return &pb.MachineInfo{
|
return &pb.MachineInfo{
|
||||||
Id: m.state.ID,
|
Id: m.state.ID,
|
||||||
@@ -876,6 +897,31 @@ func (m *Machine) Inspect(_ context.Context, _ *emptypb.Empty) (*pb.MachineInfo,
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *Machine) InspectMachine(ctx context.Context, _ *emptypb.Empty) (*pb.InspectMachineResponse, error) {
|
||||||
|
dbVersion, err := m.store.DBVersion(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, status.Errorf(codes.Internal, "get database version of the cluster store: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &pb.InspectMachineResponse{
|
||||||
|
Machines: []*pb.MachineDetails{
|
||||||
|
{
|
||||||
|
// Metadata is injected by the gRPC proxy.
|
||||||
|
Machine: &pb.MachineInfo{
|
||||||
|
Id: m.state.ID,
|
||||||
|
Name: m.state.Name,
|
||||||
|
Network: &pb.NetworkConfig{
|
||||||
|
Subnet: pb.NewIPPrefix(m.state.Network.Subnet),
|
||||||
|
ManagementIp: pb.NewIP(m.state.Network.ManagementIP),
|
||||||
|
PublicKey: m.state.Network.PublicKey,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
StoreDbVersion: dbVersion,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
// IsNetworkReady returns true if the Docker network is ready for containers.
|
// IsNetworkReady returns true if the Docker network is ready for containers.
|
||||||
func (m *Machine) IsNetworkReady() bool {
|
func (m *Machine) IsNetworkReady() bool {
|
||||||
if !m.Initialised() {
|
if !m.Initialised() {
|
||||||
@@ -909,6 +955,58 @@ func (m *Machine) WaitForNetworkReady(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// InspectWireGuardNetwork retrieves the current WireGuard network configuration and peer status.
|
||||||
|
func (m *Machine) InspectWireGuardNetwork(
|
||||||
|
_ context.Context, _ *emptypb.Empty,
|
||||||
|
) (*pb.InspectWireGuardNetworkResponse, error) {
|
||||||
|
deviceName := network.WireGuardInterfaceName
|
||||||
|
|
||||||
|
wg, err := wgctrl.New()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("create WireGuard client: %w", err)
|
||||||
|
}
|
||||||
|
defer wg.Close()
|
||||||
|
|
||||||
|
dev, err := wg.Device(deviceName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("get WireGuard device '%s': %w", deviceName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
peers := make([]*pb.WireGuardPeer, len(dev.Peers))
|
||||||
|
for i, p := range dev.Peers {
|
||||||
|
var lastHandshake *timestamppb.Timestamp
|
||||||
|
if !p.LastHandshakeTime.IsZero() {
|
||||||
|
lastHandshake = timestamppb.New(p.LastHandshakeTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
allowedIPs := make([]string, len(p.AllowedIPs))
|
||||||
|
for j, ip := range p.AllowedIPs {
|
||||||
|
allowedIPs[j] = ip.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
var endpoint string
|
||||||
|
if p.Endpoint != nil {
|
||||||
|
endpoint = p.Endpoint.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
peers[i] = &pb.WireGuardPeer{
|
||||||
|
PublicKey: p.PublicKey[:],
|
||||||
|
Endpoint: endpoint,
|
||||||
|
LastHandshakeTime: lastHandshake,
|
||||||
|
ReceiveBytes: p.ReceiveBytes,
|
||||||
|
TransmitBytes: p.TransmitBytes,
|
||||||
|
AllowedIps: allowedIPs,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &pb.InspectWireGuardNetworkResponse{
|
||||||
|
InterfaceName: dev.Name,
|
||||||
|
PublicKey: dev.PublicKey[:],
|
||||||
|
ListenPort: int32(dev.ListenPort),
|
||||||
|
Peers: peers,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
// Reset restores the machine to a clean state, scheduling a graceful shutdown and removing all cluster-related
|
// Reset restores the machine to a clean state, scheduling a graceful shutdown and removing all cluster-related
|
||||||
// configuration and resource. The uncloud daemon will restart the machine if managed by systemd.
|
// configuration and resource. The uncloud daemon will restart the machine if managed by systemd.
|
||||||
func (m *Machine) Reset(_ context.Context, _ *pb.ResetRequest) (*emptypb.Empty, error) {
|
func (m *Machine) Reset(_ context.Context, _ *pb.ResetRequest) (*emptypb.Empty, error) {
|
||||||
|
|||||||
@@ -31,6 +31,12 @@ func ListRoutableIPs() ([]netip.Addr, error) {
|
|||||||
// Skip Docker bridge interfaces.
|
// Skip Docker bridge interfaces.
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if iface.Name == "tailscale0" {
|
||||||
|
// Skip Tailscale interface as we don't want double-tunneling. Also, MTU=1280 on tailscale0 is too small
|
||||||
|
// for tunneling IPv6 used for uncloud management traffic.
|
||||||
|
// See Discord thread: https://discord.com/channels/1371726032104587335/1477823130670993408
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
if iface.Flags&net.FlagUp == 0 || iface.Flags&net.FlagRunning == 0 || iface.Flags&net.FlagLoopback != 0 {
|
if iface.Flags&net.FlagUp == 0 || iface.Flags&net.FlagRunning == 0 || iface.Flags&net.FlagLoopback != 0 {
|
||||||
// Skip interfaces:
|
// Skip interfaces:
|
||||||
|
|||||||
@@ -24,6 +24,9 @@ type State struct {
|
|||||||
Name string
|
Name string
|
||||||
// Network specifies the network configuration for this machine.
|
// Network specifies the network configuration for this machine.
|
||||||
Network *network.Config
|
Network *network.Config
|
||||||
|
// MinStoreDBVersion is the latest database version of one of the existing cluster machines at the time this machine
|
||||||
|
// joined the cluster. The machine should sync to at least this version before starting any cluster operations.
|
||||||
|
MinStoreDBVersion int64 `json:",omitempty"`
|
||||||
|
|
||||||
// path is the file path config is read from and saved to.
|
// path is the file path config is read from and saved to.
|
||||||
path string
|
path string
|
||||||
@@ -66,7 +69,7 @@ func (c *State) Encode() ([]byte, error) {
|
|||||||
return data, nil
|
return data, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save writes the state data to the file at the given path.
|
// Save atomically writes the state data to the file at the configured path.
|
||||||
func (c *State) Save() error {
|
func (c *State) Save() error {
|
||||||
if c.path == "" {
|
if c.path == "" {
|
||||||
return fmt.Errorf("state path not set")
|
return fmt.Errorf("state path not set")
|
||||||
@@ -80,5 +83,37 @@ func (c *State) Save() error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return os.WriteFile(c.path, data, 0o600)
|
|
||||||
|
// Write to a temporary file and rename for atomic save. CreateTemp creates a file with mode 0o600.
|
||||||
|
tmpFile, err := os.CreateTemp(dir, ".machine.json.*")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("create temp file: %w", err)
|
||||||
|
}
|
||||||
|
tmpPath := tmpFile.Name()
|
||||||
|
|
||||||
|
// Clean up temp file on error.
|
||||||
|
defer func() {
|
||||||
|
if err != nil {
|
||||||
|
os.Remove(tmpPath)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
if _, err = tmpFile.Write(data); err != nil {
|
||||||
|
tmpFile.Close()
|
||||||
|
return fmt.Errorf("write temp file: %w", err)
|
||||||
|
}
|
||||||
|
// Without fsync, the data may only be in the OS buffer cache. If the system crashes before the OS flushes
|
||||||
|
// it to disk, the file could be empty or corrupted even after rename.
|
||||||
|
if err = tmpFile.Sync(); err != nil {
|
||||||
|
tmpFile.Close()
|
||||||
|
return fmt.Errorf("sync temp file: %w", err)
|
||||||
|
}
|
||||||
|
if err = tmpFile.Close(); err != nil {
|
||||||
|
return fmt.Errorf("close temp file: %w", err)
|
||||||
|
}
|
||||||
|
if err = os.Rename(tmpPath, c.path); err != nil {
|
||||||
|
return fmt.Errorf("rename temp file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ func (s *Store) CreateOrUpdateContainer(ctx context.Context, ctr api.ServiceCont
|
|||||||
|
|
||||||
// ListContainers returns a list of container records from the store database that match the given options.
|
// ListContainers returns a list of container records from the store database that match the given options.
|
||||||
func (s *Store) ListContainers(ctx context.Context, opts ListOptions) ([]ContainerRecord, error) {
|
func (s *Store) ListContainers(ctx context.Context, opts ListOptions) ([]ContainerRecord, error) {
|
||||||
q := sq.Select("container", "machine_id", "sync_status", "updated_at").From("containers").
|
q := sq.Select("id", "container", "machine_id", "sync_status", "updated_at").From("containers").
|
||||||
Where(sq.Eq{"sync_status": SyncStatusSynced})
|
Where(sq.Eq{"sync_status": SyncStatusSynced})
|
||||||
|
|
||||||
if len(opts.MachineIDs) > 0 {
|
if len(opts.MachineIDs) > 0 {
|
||||||
@@ -112,14 +112,23 @@ func (s *Store) ListContainers(ctx context.Context, opts ListOptions) ([]Contain
|
|||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
|
|
||||||
var containers []ContainerRecord
|
var containers []ContainerRecord
|
||||||
var cJSON, machineID, syncStatus, updatedAtStr string
|
var id, cJSON, machineID, syncStatus, updatedAtStr string
|
||||||
var updatedAt time.Time
|
var updatedAt time.Time
|
||||||
|
skipped := 0
|
||||||
|
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
if err = rows.Scan(&cJSON, &machineID, &syncStatus, &updatedAtStr); err != nil {
|
if err = rows.Scan(&id, &cJSON, &machineID, &syncStatus, &updatedAtStr); err != nil {
|
||||||
return nil, fmt.Errorf("scan container record: %w", err)
|
return nil, fmt.Errorf("scan container record: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Skip containers with empty JSON data. This can happen during partial replication
|
||||||
|
// when cr-sqlite has created the row but the container column hasn't been synced yet.
|
||||||
|
if cJSON == "" || cJSON == "{}" {
|
||||||
|
slog.Debug("Skipping container with empty data in the store (partial replication?).", "id", id)
|
||||||
|
skipped++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
var c api.ServiceContainer
|
var c api.ServiceContainer
|
||||||
if err = json.Unmarshal([]byte(cJSON), &c); err != nil {
|
if err = json.Unmarshal([]byte(cJSON), &c); err != nil {
|
||||||
return nil, fmt.Errorf("unmarshal container: %w", err)
|
return nil, fmt.Errorf("unmarshal container: %w", err)
|
||||||
@@ -135,6 +144,11 @@ func (s *Store) ListContainers(ctx context.Context, opts ListOptions) ([]Contain
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if skipped > 0 {
|
||||||
|
slog.Warn("Listing containers from the store skipped empty records (possibly due to partial replication).",
|
||||||
|
"skipped", skipped, "valid", len(containers))
|
||||||
|
}
|
||||||
|
|
||||||
return containers, nil
|
return containers, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,7 +180,7 @@ func (s *Store) DeleteContainers(ctx context.Context, opts DeleteOptions) error
|
|||||||
// receive any values, it just signals when a container(s) has been added, updated, or deleted in the database.
|
// receive any values, it just signals when a container(s) has been added, updated, or deleted in the database.
|
||||||
func (s *Store) SubscribeContainers(ctx context.Context) ([]ContainerRecord, <-chan struct{}, error) {
|
func (s *Store) SubscribeContainers(ctx context.Context) ([]ContainerRecord, <-chan struct{}, error) {
|
||||||
// TODO: figure out whether we need sync_status at all (not used at the moment).
|
// TODO: figure out whether we need sync_status at all (not used at the moment).
|
||||||
q := sq.Select("container", "machine_id", "sync_status", "updated_at").From("containers").
|
q := sq.Select("id", "container", "machine_id", "sync_status", "updated_at").From("containers").
|
||||||
Where(sq.Eq{"sync_status": SyncStatusSynced})
|
Where(sq.Eq{"sync_status": SyncStatusSynced})
|
||||||
query, args, err := q.ToSql()
|
query, args, err := q.ToSql()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -179,15 +193,24 @@ func (s *Store) SubscribeContainers(ctx context.Context) ([]ContainerRecord, <-c
|
|||||||
}
|
}
|
||||||
|
|
||||||
var containers []ContainerRecord
|
var containers []ContainerRecord
|
||||||
var cJSON, updatedAtStr string
|
var id, cJSON, updatedAtStr string
|
||||||
|
skipped := 0
|
||||||
|
|
||||||
rows := sub.Rows()
|
rows := sub.Rows()
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var cr ContainerRecord
|
var cr ContainerRecord
|
||||||
if err = rows.Scan(&cJSON, &cr.MachineID, &cr.SyncStatus, &updatedAtStr); err != nil {
|
if err = rows.Scan(&id, &cJSON, &cr.MachineID, &cr.SyncStatus, &updatedAtStr); err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Skip containers with empty JSON data. This can happen during partial replication
|
||||||
|
// when cr-sqlite has created the row but the container column hasn't been synced yet.
|
||||||
|
if cJSON == "" || cJSON == "{}" {
|
||||||
|
slog.Debug("Skipping container with empty data in the store (partial replication?).", "id", id)
|
||||||
|
skipped++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
if err = json.Unmarshal([]byte(cJSON), &cr.Container); err != nil {
|
if err = json.Unmarshal([]byte(cJSON), &cr.Container); err != nil {
|
||||||
return nil, nil, fmt.Errorf("unmarshal container: %w", err)
|
return nil, nil, fmt.Errorf("unmarshal container: %w", err)
|
||||||
}
|
}
|
||||||
@@ -196,6 +219,12 @@ func (s *Store) SubscribeContainers(ctx context.Context) ([]ContainerRecord, <-c
|
|||||||
}
|
}
|
||||||
containers = append(containers, cr)
|
containers = append(containers, cr)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if skipped > 0 {
|
||||||
|
slog.Warn("Container subscription skipped empty records in the store (possibly due to partial replication).",
|
||||||
|
"skipped", skipped, "valid", len(containers))
|
||||||
|
}
|
||||||
|
|
||||||
events, err := sub.Changes()
|
events, err := sub.Changes()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, fmt.Errorf("get subscription changes: %w", err)
|
return nil, nil, fmt.Errorf("get subscription changes: %w", err)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package store
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
_ "embed"
|
_ "embed"
|
||||||
|
"encoding/hex"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
@@ -56,6 +57,54 @@ func (s *Store) Delete(ctx context.Context, key string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DBVersion returns the current cr-sqlite database version (Lamport timestamp).
|
||||||
|
func (s *Store) DBVersion(ctx context.Context) (int64, error) {
|
||||||
|
rows, err := s.corro.QueryContext(ctx, "SELECT crsql_db_version()")
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("query crsql_db_version(): %w", err)
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
if !rows.Next() {
|
||||||
|
return 0, fmt.Errorf("no result from crsql_db_version()")
|
||||||
|
}
|
||||||
|
|
||||||
|
var version int64
|
||||||
|
if err = rows.Scan(&version); err != nil {
|
||||||
|
return 0, fmt.Errorf("scan db version: %w", err)
|
||||||
|
}
|
||||||
|
return version, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type MissingChange struct {
|
||||||
|
ActorID string
|
||||||
|
StartVersion int64
|
||||||
|
EndVersion int64
|
||||||
|
}
|
||||||
|
|
||||||
|
// KnownMissingChanges returns a list of currently known missing changes in the Corrosion database.
|
||||||
|
func (s *Store) KnownMissingChanges(ctx context.Context) ([]MissingChange, error) {
|
||||||
|
rows, err := s.corro.QueryContext(ctx, "SELECT actor_id, start, end FROM __corro_bookkeeping_gaps")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("query missing changes: %w", err)
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var changes []MissingChange
|
||||||
|
for rows.Next() {
|
||||||
|
var c MissingChange
|
||||||
|
var actorBytes []byte
|
||||||
|
if err = rows.Scan(&actorBytes, &c.StartVersion, &c.EndVersion); err != nil {
|
||||||
|
return nil, fmt.Errorf("scan missing change: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.ActorID = hex.EncodeToString(actorBytes)
|
||||||
|
changes = append(changes, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
return changes, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Store) CreateMachine(ctx context.Context, m *pb.MachineInfo) error {
|
func (s *Store) CreateMachine(ctx context.Context, m *pb.MachineInfo) error {
|
||||||
mJSON, err := protojson.Marshal(m)
|
mJSON, err := protojson.Marshal(m)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -116,19 +165,29 @@ func (s *Store) GetMachine(ctx context.Context, machineID string) (*pb.MachineIn
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) ListMachines(ctx context.Context) ([]*pb.MachineInfo, error) {
|
func (s *Store) ListMachines(ctx context.Context) ([]*pb.MachineInfo, error) {
|
||||||
rows, err := s.corro.QueryContext(ctx, "SELECT info FROM machines ORDER BY name")
|
rows, err := s.corro.QueryContext(ctx, "SELECT id, info FROM machines ORDER BY name")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
|
|
||||||
var machines []*pb.MachineInfo
|
var machines []*pb.MachineInfo
|
||||||
|
skipped := 0
|
||||||
|
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var mJSON string
|
var id, mJSON string
|
||||||
if err = rows.Scan(&mJSON); err != nil {
|
if err = rows.Scan(&id, &mJSON); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Skip machines with empty JSON data. This can happen during partial replication
|
||||||
|
// when cr-sqlite has created the row but the info column hasn't been synced yet.
|
||||||
|
if mJSON == "" || mJSON == "{}" {
|
||||||
|
slog.Debug("Skipping machine with empty data in the store (partial replication?).", "id", id)
|
||||||
|
skipped++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
protojsonParser := protojson.UnmarshalOptions{DiscardUnknown: true}
|
protojsonParser := protojson.UnmarshalOptions{DiscardUnknown: true}
|
||||||
var m pb.MachineInfo
|
var m pb.MachineInfo
|
||||||
if err = protojsonParser.Unmarshal([]byte(mJSON), &m); err != nil {
|
if err = protojsonParser.Unmarshal([]byte(mJSON), &m); err != nil {
|
||||||
@@ -141,6 +200,12 @@ func (s *Store) ListMachines(ctx context.Context) ([]*pb.MachineInfo, error) {
|
|||||||
}
|
}
|
||||||
machines = append(machines, &m)
|
machines = append(machines, &m)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if skipped > 0 {
|
||||||
|
slog.Warn("Listing machines from the store skipped empty records (possibly due to partial replication).",
|
||||||
|
"skipped", skipped, "valid", len(machines))
|
||||||
|
}
|
||||||
|
|
||||||
return machines, nil
|
return machines, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -186,24 +251,41 @@ func (s *Store) DeleteMachine(ctx context.Context, id string) error {
|
|||||||
// SubscribeMachines returns a list of machines and a channel that signals changes to the list. The channel doesn't
|
// SubscribeMachines returns a list of machines and a channel that signals changes to the list. The channel doesn't
|
||||||
// receive any values, it just signals when a machine has been added, updated, or deleted in the database.
|
// receive any values, it just signals when a machine has been added, updated, or deleted in the database.
|
||||||
func (s *Store) SubscribeMachines(ctx context.Context) ([]*pb.MachineInfo, <-chan struct{}, error) {
|
func (s *Store) SubscribeMachines(ctx context.Context) ([]*pb.MachineInfo, <-chan struct{}, error) {
|
||||||
sub, err := s.corro.SubscribeContext(ctx, "SELECT info FROM machines ORDER BY name", nil, false)
|
sub, err := s.corro.SubscribeContext(ctx, "SELECT id, info FROM machines ORDER BY name", nil, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
rows := sub.Rows()
|
rows := sub.Rows()
|
||||||
var machines []*pb.MachineInfo
|
var machines []*pb.MachineInfo
|
||||||
|
skipped := 0
|
||||||
|
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var mJSON string
|
var id, mJSON string
|
||||||
if err = rows.Scan(&mJSON); err != nil {
|
if err = rows.Scan(&id, &mJSON); err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Skip machines with empty JSON data. This can happen during partial replication
|
||||||
|
// when cr-sqlite has created the row but the info column hasn't been synced yet.
|
||||||
|
if mJSON == "" || mJSON == "{}" {
|
||||||
|
slog.Debug("Skipping machine with empty data in the store (partial replication?).", "id", id)
|
||||||
|
skipped++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
var m pb.MachineInfo
|
var m pb.MachineInfo
|
||||||
if err = protojson.Unmarshal([]byte(mJSON), &m); err != nil {
|
if err = protojson.Unmarshal([]byte(mJSON), &m); err != nil {
|
||||||
return nil, nil, fmt.Errorf("unmarshal machine info: %w", err)
|
return nil, nil, fmt.Errorf("unmarshal machine info: %w", err)
|
||||||
}
|
}
|
||||||
machines = append(machines, &m)
|
machines = append(machines, &m)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if skipped > 0 {
|
||||||
|
slog.Warn("Machine subscription skipped empty records in the store (possibly due to partial replication).",
|
||||||
|
"skipped", skipped, "valid", len(machines))
|
||||||
|
}
|
||||||
|
|
||||||
events, err := sub.Changes()
|
events, err := sub.Changes()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, fmt.Errorf("get subscription changes: %w", err)
|
return nil, nil, fmt.Errorf("get subscription changes: %w", err)
|
||||||
|
|||||||
@@ -116,7 +116,7 @@ func (p *Proxy) handleConnection(ctx context.Context, localConn net.Conn) {
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
// Wait for both copies to complete or context cancel.
|
// Wait for both copies to complete or context cancel.
|
||||||
for i := 0; i < 2; i++ {
|
for range 2 {
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
// Close connections to abort ongoing copies.
|
// Close connections to abort ongoing copies.
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
|
osuser "os/user"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -13,7 +14,17 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func Connect(user, host string, port int, sshKeyPath string) (*ssh.Client, error) {
|
func Connect(user, host string, port int, sshKeyPath string) (*ssh.Client, error) {
|
||||||
|
// Use the current OS user if no user is specified to be make it consistent with ssh CLI behavior.
|
||||||
|
if user == "" {
|
||||||
|
if u, err := osuser.Current(); err == nil {
|
||||||
|
user = u.Username
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if port == 0 {
|
||||||
|
port = 22
|
||||||
|
}
|
||||||
addr := net.JoinHostPort(host, strconv.Itoa(port))
|
addr := net.JoinHostPort(host, strconv.Itoa(port))
|
||||||
|
|
||||||
// Try to connect using SSH agent only.
|
// Try to connect using SSH agent only.
|
||||||
agentAuth, agentClose, agentErr := sshAgentAuth()
|
agentAuth, agentClose, agentErr := sshAgentAuth()
|
||||||
if agentErr == nil {
|
if agentErr == nil {
|
||||||
|
|||||||
+39
-25
@@ -1,12 +1,15 @@
|
|||||||
package sshexec
|
package sshexec
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type SSHCLIRemote struct {
|
type SSHCLIRemote struct {
|
||||||
@@ -25,48 +28,59 @@ func NewSSHCLIRemote(user, host string, port int, keyPath string) *SSHCLIRemote
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Refactor and reuse this with buildDialArgs and buildSSHArgs from
|
// newSSHCommand creates an exec.Cmd for ssh that sends SIGINT on context cancellation, giving the
|
||||||
// SSHCLI Connector.
|
// remote process a chance to exit gracefully before being killed.
|
||||||
func (r *SSHCLIRemote) buildSSHArgs() []string {
|
func (r *SSHCLIRemote) newSSHCommand(ctx context.Context, cmd string) *exec.Cmd {
|
||||||
args := []string{"-o", "ConnectTimeout=5"}
|
args := []string{
|
||||||
|
"-o", "ConnectTimeout=5",
|
||||||
if r.port != 0 && r.port != 22 {
|
// Disable pseudo-terminal allocation to prevent SSH from executing as a login shell.
|
||||||
args = append(args, "-p", strconv.Itoa(r.port))
|
"-T",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if r.port != 0 {
|
||||||
|
args = append(args, "-p", strconv.Itoa(r.port))
|
||||||
|
}
|
||||||
if r.keyPath != "" {
|
if r.keyPath != "" {
|
||||||
args = append(args, "-i", r.keyPath)
|
args = append(args, "-i", r.keyPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
args = append(args, r.user+"@"+r.host)
|
dst := r.host
|
||||||
return args
|
if r.user != "" {
|
||||||
|
dst = fmt.Sprintf("%s@%s", r.user, dst)
|
||||||
|
}
|
||||||
|
args = append(args, dst, cmd)
|
||||||
|
|
||||||
|
execCmd := exec.CommandContext(ctx, "ssh", args...)
|
||||||
|
execCmd.Cancel = func() error {
|
||||||
|
return execCmd.Process.Signal(os.Interrupt)
|
||||||
|
}
|
||||||
|
execCmd.WaitDelay = 5 * time.Second
|
||||||
|
|
||||||
|
return execCmd
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *SSHCLIRemote) Run(ctx context.Context, cmd string) (string, error) {
|
func (r *SSHCLIRemote) Run(ctx context.Context, cmd string) (string, error) {
|
||||||
args := r.buildSSHArgs()
|
var stdout, stderr bytes.Buffer
|
||||||
args = append(args, cmd)
|
err := r.Stream(ctx, cmd, &stdout, &stderr)
|
||||||
|
out := strings.TrimSpace(stdout.String())
|
||||||
execCmd := exec.CommandContext(ctx, "ssh", args...)
|
|
||||||
output, err := execCmd.CombinedOutput()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return strings.TrimSpace(string(output)),
|
return out, fmt.Errorf("%w: %s", err, stderr.String())
|
||||||
fmt.Errorf("run command on remote host: %w: %s", err, string(output))
|
|
||||||
}
|
}
|
||||||
return strings.TrimSpace(string(output)), nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *SSHCLIRemote) Stream(ctx context.Context, cmd string, stdout, stderr io.Writer) error {
|
func (r *SSHCLIRemote) Stream(ctx context.Context, cmd string, stdout, stderr io.Writer) error {
|
||||||
args := r.buildSSHArgs()
|
sshCmd := r.newSSHCommand(ctx, cmd)
|
||||||
args = append(args, cmd)
|
sshCmd.Stdout = stdout
|
||||||
|
sshCmd.Stderr = stderr
|
||||||
|
|
||||||
execCmd := exec.CommandContext(ctx, "ssh", args...)
|
if err := sshCmd.Run(); err != nil {
|
||||||
execCmd.Stdout = stdout
|
return fmt.Errorf("run command on remote host: %w", err)
|
||||||
execCmd.Stderr = stderr
|
}
|
||||||
|
return nil
|
||||||
return execCmd.Run()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// no-op as there is no persistent connection.
|
// Close is no-op as there is no persistent connection.
|
||||||
func (r *SSHCLIRemote) Close() error {
|
func (r *SSHCLIRemote) Close() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
package sshexec
|
package sshexec
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestSSHCLIRemote_buildSSHArgs(t *testing.T) {
|
func TestSSHCLIRemote_newSSHCommand(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
@@ -15,47 +16,85 @@ func TestSSHCLIRemote_buildSSHArgs(t *testing.T) {
|
|||||||
host string
|
host string
|
||||||
port int
|
port int
|
||||||
keyPath string
|
keyPath string
|
||||||
|
cmd string
|
||||||
expected []string
|
expected []string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "default port, no key",
|
name: "host only",
|
||||||
user: "root",
|
host: "example.com",
|
||||||
host: "example.com",
|
cmd: "whoami",
|
||||||
port: 22,
|
expected: []string{
|
||||||
keyPath: "",
|
"ssh", "-o", "ConnectTimeout=5", "-T", "example.com", "whoami",
|
||||||
expected: []string{"-o", "ConnectTimeout=5", "root@example.com"},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "custom port",
|
name: "with user",
|
||||||
user: "ubuntu",
|
user: "root",
|
||||||
host: "192.168.1.10",
|
host: "example.com",
|
||||||
port: 2222,
|
cmd: "whoami",
|
||||||
keyPath: "",
|
expected: []string{
|
||||||
expected: []string{"-o", "ConnectTimeout=5", "-p", "2222", "ubuntu@192.168.1.10"},
|
"ssh", "-o", "ConnectTimeout=5", "-T", "root@example.com", "whoami",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "with key path",
|
name: "with port",
|
||||||
user: "root",
|
host: "example.com",
|
||||||
host: "example.com",
|
port: 2222,
|
||||||
port: 22,
|
cmd: "whoami",
|
||||||
keyPath: "/path/to/key",
|
expected: []string{
|
||||||
expected: []string{"-o", "ConnectTimeout=5", "-i", "/path/to/key", "root@example.com"},
|
"ssh", "-o", "ConnectTimeout=5", "-T", "-p", "2222", "example.com", "whoami",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "all options",
|
name: "with key",
|
||||||
user: "admin",
|
host: "example.com",
|
||||||
host: "server.local",
|
keyPath: "/path/to/key",
|
||||||
port: 2222,
|
cmd: "whoami",
|
||||||
keyPath: "~/.ssh/id_rsa",
|
expected: []string{
|
||||||
expected: []string{"-o", "ConnectTimeout=5", "-p", "2222", "-i", "~/.ssh/id_rsa", "admin@server.local"},
|
"ssh", "-o", "ConnectTimeout=5", "-T", "-i", "/path/to/key", "example.com", "whoami",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "port zero (default)",
|
name: "user and port",
|
||||||
user: "root",
|
user: "ubuntu",
|
||||||
host: "example.com",
|
host: "192.168.1.10",
|
||||||
port: 0,
|
port: 2222,
|
||||||
keyPath: "",
|
cmd: "ls -la",
|
||||||
expected: []string{"-o", "ConnectTimeout=5", "root@example.com"},
|
expected: []string{
|
||||||
|
"ssh", "-o", "ConnectTimeout=5", "-T", "-p", "2222", "ubuntu@192.168.1.10", "ls -la",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "user and key",
|
||||||
|
user: "root",
|
||||||
|
host: "example.com",
|
||||||
|
keyPath: "/path/to/key",
|
||||||
|
cmd: "whoami",
|
||||||
|
expected: []string{
|
||||||
|
"ssh", "-o", "ConnectTimeout=5", "-T", "-i", "/path/to/key", "root@example.com", "whoami",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "port and key",
|
||||||
|
host: "example.com",
|
||||||
|
port: 22,
|
||||||
|
keyPath: "~/.ssh/id_rsa",
|
||||||
|
cmd: "whoami",
|
||||||
|
expected: []string{
|
||||||
|
"ssh", "-o", "ConnectTimeout=5", "-T", "-p", "22", "-i", "~/.ssh/id_rsa", "example.com", "whoami",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "all options",
|
||||||
|
user: "admin",
|
||||||
|
host: "server.local",
|
||||||
|
port: 2222,
|
||||||
|
keyPath: "~/.ssh/id_rsa",
|
||||||
|
cmd: "sudo bash -c 'echo hello'",
|
||||||
|
expected: []string{
|
||||||
|
"ssh", "-o", "ConnectTimeout=5", "-T", "-p", "2222", "-i", "~/.ssh/id_rsa",
|
||||||
|
"admin@server.local", "sudo bash -c 'echo hello'",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,8 +108,9 @@ func TestSSHCLIRemote_buildSSHArgs(t *testing.T) {
|
|||||||
port: tt.port,
|
port: tt.port,
|
||||||
keyPath: tt.keyPath,
|
keyPath: tt.keyPath,
|
||||||
}
|
}
|
||||||
result := remote.buildSSHArgs()
|
execCmd := remote.newSSHCommand(context.Background(), tt.cmd)
|
||||||
assert.Equal(t, tt.expected, result)
|
assert.Equal(t, tt.expected, execCmd.Args)
|
||||||
|
assert.NotNil(t, execCmd.Cancel, "Cancel should be set for graceful shutdown")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+46
-16
@@ -119,17 +119,16 @@ func (p *Provisioner) CreateCluster(ctx context.Context, name string, opts Creat
|
|||||||
func (p *Provisioner) initCluster(ctx context.Context, machines []Machine) error {
|
func (p *Provisioner) initCluster(ctx context.Context, machines []Machine) error {
|
||||||
// Init a new cluster on the first machine.
|
// Init a new cluster on the first machine.
|
||||||
initMachine := machines[0]
|
initMachine := machines[0]
|
||||||
|
|
||||||
if err := WaitMachineReady(ctx, initMachine, 30*time.Second); err != nil {
|
|
||||||
return fmt.Errorf("wait for machine %q to be ready: %w", initMachine.Name, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
initClient, err := initMachine.Connect(ctx)
|
initClient, err := initMachine.Connect(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("create machine client over TCP '%s': %w", initMachine.APIAddress, err)
|
return fmt.Errorf("create machine client over TCP '%s': %w", initMachine.APIAddress, err)
|
||||||
}
|
}
|
||||||
defer initClient.Close()
|
defer initClient.Close()
|
||||||
|
|
||||||
|
if err := initClient.WaitMachineReady(ctx, 30*time.Second); err != nil {
|
||||||
|
return fmt.Errorf("wait for machine %q to be ready: %w", initMachine.Name, err)
|
||||||
|
}
|
||||||
|
|
||||||
req := &pb.InitClusterRequest{
|
req := &pb.InitClusterRequest{
|
||||||
MachineName: initMachine.Name,
|
MachineName: initMachine.Name,
|
||||||
Network: pb.NewIPPrefix(cluster.DefaultNetwork),
|
Network: pb.NewIPPrefix(cluster.DefaultNetwork),
|
||||||
@@ -138,14 +137,22 @@ func (p *Provisioner) initCluster(ctx context.Context, machines []Machine) error
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("init cluster: %w", err)
|
return fmt.Errorf("init cluster: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("Cluster %q initialised with machine %q\n", initMachine.ClusterName, initResp.Machine.Name)
|
fmt.Printf("Cluster %q initialised with machine %q\n", initMachine.ClusterName, initResp.Machine.Name)
|
||||||
|
fmt.Printf("Waiting for cluster to be ready...")
|
||||||
|
if err = initClient.WaitClusterReady(ctx, 30*time.Second); err != nil {
|
||||||
|
return fmt.Errorf("wait for cluster to be ready: %w", err)
|
||||||
|
}
|
||||||
|
fmt.Println(" done.")
|
||||||
|
|
||||||
|
// Get the current store DB version from the init machine to pass to the join requests.
|
||||||
|
inspectResp, err := initClient.MachineClient.InspectMachine(ctx, &emptypb.Empty{})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("inspect init machine: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
// Join the rest of the machines to the cluster.
|
// Join the rest of the machines to the cluster.
|
||||||
for _, m := range machines[1:] {
|
for _, m := range machines[1:] {
|
||||||
if err = WaitMachineReady(ctx, m, 30*time.Second); err != nil {
|
|
||||||
return fmt.Errorf("wait for machine %q to be ready: %w", m.Name, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
cli, err := m.Connect(ctx)
|
cli, err := m.Connect(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("create machine client over TCP '%s': %w", m.APIAddress, err)
|
return fmt.Errorf("create machine client over TCP '%s': %w", m.APIAddress, err)
|
||||||
@@ -153,6 +160,10 @@ func (p *Provisioner) initCluster(ctx context.Context, machines []Machine) error
|
|||||||
//goland:noinspection GoDeferInLoop
|
//goland:noinspection GoDeferInLoop
|
||||||
defer cli.Close()
|
defer cli.Close()
|
||||||
|
|
||||||
|
if err := cli.WaitMachineReady(ctx, 30*time.Second); err != nil {
|
||||||
|
return fmt.Errorf("wait for machine %q to be ready: %w", m.Name, err)
|
||||||
|
}
|
||||||
|
|
||||||
tokenResp, err := cli.Token(ctx, &emptypb.Empty{})
|
tokenResp, err := cli.Token(ctx, &emptypb.Empty{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("get machine token: %w", err)
|
return fmt.Errorf("get machine token: %w", err)
|
||||||
@@ -181,8 +192,9 @@ func (p *Provisioner) initCluster(ctx context.Context, machines []Machine) error
|
|||||||
|
|
||||||
// Configure the machine to join the cluster.
|
// Configure the machine to join the cluster.
|
||||||
joinReq := &pb.JoinClusterRequest{
|
joinReq := &pb.JoinClusterRequest{
|
||||||
Machine: addResp.Machine,
|
Machine: addResp.Machine,
|
||||||
OtherMachines: []*pb.MachineInfo{initResp.Machine},
|
OtherMachines: []*pb.MachineInfo{initResp.Machine},
|
||||||
|
MinStoreDbVersion: inspectResp.Machines[0].StoreDbVersion,
|
||||||
}
|
}
|
||||||
if _, err = cli.JoinCluster(ctx, joinReq); err != nil {
|
if _, err = cli.JoinCluster(ctx, joinReq); err != nil {
|
||||||
return fmt.Errorf("join cluster: %w", err)
|
return fmt.Errorf("join cluster: %w", err)
|
||||||
@@ -256,16 +268,16 @@ func (p *Provisioner) InspectCluster(ctx context.Context, name string) (Cluster,
|
|||||||
// WaitClusterReady waits for all machines in the cluster to be ready and UP.
|
// WaitClusterReady waits for all machines in the cluster to be ready and UP.
|
||||||
func (p *Provisioner) WaitClusterReady(ctx context.Context, c Cluster, timeout time.Duration) error {
|
func (p *Provisioner) WaitClusterReady(ctx context.Context, c Cluster, timeout time.Duration) error {
|
||||||
firstMachine := c.Machines[0]
|
firstMachine := c.Machines[0]
|
||||||
if err := WaitMachineReady(ctx, firstMachine, timeout); err != nil {
|
|
||||||
return fmt.Errorf("wait for machine '%s' to be ready: %w", firstMachine.Name, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
cli, err := firstMachine.Connect(ctx)
|
cli, err := firstMachine.Connect(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("connect to machine over TCP '%s': %w", firstMachine.APIAddress, err)
|
return fmt.Errorf("connect to machine over TCP '%s': %w", firstMachine.APIAddress, err)
|
||||||
}
|
}
|
||||||
defer cli.Close()
|
defer cli.Close()
|
||||||
|
|
||||||
|
if err = cli.WaitClusterReady(ctx, timeout); err != nil {
|
||||||
|
return fmt.Errorf("wait for cluster to be ready: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
boff := backoff.WithContext(backoff.NewExponentialBackOff(
|
boff := backoff.WithContext(backoff.NewExponentialBackOff(
|
||||||
backoff.WithInitialInterval(100*time.Millisecond),
|
backoff.WithInitialInterval(100*time.Millisecond),
|
||||||
backoff.WithMaxInterval(1*time.Second),
|
backoff.WithMaxInterval(1*time.Second),
|
||||||
@@ -289,7 +301,25 @@ func (p *Provisioner) WaitClusterReady(ctx context.Context, c Cluster, timeout t
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return backoff.Retry(checkMachinesUp, boff)
|
if err = backoff.Retry(checkMachinesUp, boff); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for each machine to sync the cluster store and be ready to serve cluster requests.
|
||||||
|
for _, m := range c.Machines[1:] {
|
||||||
|
mcli, err := m.Connect(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("connect to machine over TCP '%s': %w", m.APIAddress, err)
|
||||||
|
}
|
||||||
|
//goland:noinspection GoDeferInLoop
|
||||||
|
defer mcli.Close()
|
||||||
|
|
||||||
|
if err = mcli.WaitClusterReady(ctx, timeout); err != nil {
|
||||||
|
return fmt.Errorf("wait for cluster to be ready on machine '%s': %w", m.Name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Provisioner) RemoveCluster(ctx context.Context, name string) error {
|
func (p *Provisioner) RemoveCluster(ctx context.Context, name string) error {
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import (
|
|||||||
"net/netip"
|
"net/netip"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/cenkalti/backoff/v4"
|
|
||||||
"github.com/containerd/errdefs"
|
"github.com/containerd/errdefs"
|
||||||
"github.com/docker/docker/api/types/container"
|
"github.com/docker/docker/api/types/container"
|
||||||
"github.com/docker/docker/api/types/image"
|
"github.com/docker/docker/api/types/image"
|
||||||
@@ -17,7 +16,6 @@ import (
|
|||||||
"github.com/psviderski/uncloud/internal/secret"
|
"github.com/psviderski/uncloud/internal/secret"
|
||||||
"github.com/psviderski/uncloud/pkg/client"
|
"github.com/psviderski/uncloud/pkg/client"
|
||||||
"github.com/psviderski/uncloud/pkg/client/connector"
|
"github.com/psviderski/uncloud/pkg/client/connector"
|
||||||
"google.golang.org/protobuf/types/known/emptypb"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -194,26 +192,3 @@ func randomMachineName() (string, error) {
|
|||||||
}
|
}
|
||||||
return "machine-" + suffix, nil
|
return "machine-" + suffix, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// WaitMachineReady waits for the machine API to respond.
|
|
||||||
func WaitMachineReady(ctx context.Context, m Machine, timeout time.Duration) error {
|
|
||||||
cli, err := m.Connect(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("connect to machine over TCP '%s': %w", m.APIAddress, err)
|
|
||||||
}
|
|
||||||
defer cli.Close()
|
|
||||||
|
|
||||||
boff := backoff.WithContext(backoff.NewExponentialBackOff(
|
|
||||||
backoff.WithInitialInterval(100*time.Millisecond),
|
|
||||||
backoff.WithMaxInterval(10*time.Second),
|
|
||||||
backoff.WithMaxElapsedTime(timeout),
|
|
||||||
), ctx)
|
|
||||||
|
|
||||||
inspect := func() error {
|
|
||||||
if _, err := cli.Inspect(ctx, &emptypb.Empty{}); err != nil {
|
|
||||||
return fmt.Errorf("inspect machine: %w", err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return backoff.Retry(inspect, boff)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
# @generated - this file is auto-generated by `mise lock` https://mise.jdx.dev/dev-tools/mise-lock.html
|
||||||
|
|
||||||
|
[[tools."aqua:vektra/mockery"]]
|
||||||
|
version = "3.5.3"
|
||||||
|
backend = "aqua:vektra/mockery"
|
||||||
|
|
||||||
|
[tools."aqua:vektra/mockery"."platforms.linux-x64"]
|
||||||
|
checksum = "sha256:ebce416b0175338525246c376885a1579ca6cd4d4015140ba0c70e6b5339a39c"
|
||||||
|
url = "https://github.com/vektra/mockery/releases/download/v3.5.3/mockery_3.5.3_Linux_x86_64.tar.gz"
|
||||||
|
|
||||||
|
[tools."aqua:vektra/mockery"."platforms.macos-arm64"]
|
||||||
|
checksum = "sha256:a3a94b14c7414e148f2252199ffc4a0108d311358f3d336cbe05bb73cb203704"
|
||||||
|
url = "https://github.com/vektra/mockery/releases/download/v3.5.3/mockery_3.5.3_Darwin_arm64.tar.gz"
|
||||||
|
|
||||||
|
[[tools.go]]
|
||||||
|
version = "1.26.1"
|
||||||
|
backend = "core:go"
|
||||||
|
|
||||||
|
[tools.go."platforms.linux-x64"]
|
||||||
|
checksum = "sha256:031f088e5d955bab8657ede27ad4e3bc5b7c1ba281f05f245bcc304f327c987a"
|
||||||
|
url = "https://dl.google.com/go/go1.26.1.linux-amd64.tar.gz"
|
||||||
|
|
||||||
|
[tools.go."platforms.macos-arm64"]
|
||||||
|
checksum = "sha256:353df43a7811ce284c8938b5f3c7df40b7bfb6f56cb165b150bc40b5e2dd541f"
|
||||||
|
url = "https://dl.google.com/go/go1.26.1.darwin-arm64.tar.gz"
|
||||||
|
|
||||||
|
[[tools.golangci-lint]]
|
||||||
|
version = "2.11.2"
|
||||||
|
backend = "aqua:golangci/golangci-lint"
|
||||||
|
|
||||||
|
[tools.golangci-lint."platforms.linux-x64"]
|
||||||
|
checksum = "sha256:852741ea769f4543ab4b6cf86b313cb7173f2e8d2d39f00eb0cd5ef5d4d5eda9"
|
||||||
|
url = "https://github.com/golangci/golangci-lint/releases/download/v2.11.2/golangci-lint-2.11.2-linux-amd64.tar.gz"
|
||||||
|
|
||||||
|
[tools.golangci-lint."platforms.macos-arm64"]
|
||||||
|
checksum = "sha256:11e9549b955bae4bfb71aa3b5c2762e446e2d3f975fe963d6d767b8f745d9344"
|
||||||
|
url = "https://github.com/golangci/golangci-lint/releases/download/v2.11.2/golangci-lint-2.11.2-darwin-arm64.tar.gz"
|
||||||
|
|
||||||
|
[[tools.node]]
|
||||||
|
version = "22.20.0"
|
||||||
|
backend = "core:node"
|
||||||
|
|
||||||
|
[tools.node."platforms.linux-x64"]
|
||||||
|
checksum = "sha256:eeaccb0378b79406f2208e8b37a62479c70595e20be6b659125eb77dd1ab2a29"
|
||||||
|
url = "https://nodejs.org/dist/v22.20.0/node-v22.20.0-linux-x64.tar.gz"
|
||||||
|
|
||||||
|
[tools.node."platforms.macos-arm64"]
|
||||||
|
checksum = "sha256:cc04a76a09f79290194c0646f48fec40354d88969bec467789a5d55dd097f949"
|
||||||
|
url = "https://nodejs.org/dist/v22.20.0/node-v22.20.0-darwin-arm64.tar.gz"
|
||||||
|
|
||||||
|
[[tools.protoc]]
|
||||||
|
version = "27.3"
|
||||||
|
backend = "aqua:protocolbuffers/protobuf/protoc"
|
||||||
|
|
||||||
|
[[tools.protoc-gen-go]]
|
||||||
|
version = "1.34.2"
|
||||||
|
backend = "aqua:protocolbuffers/protobuf-go/protoc-gen-go"
|
||||||
|
|
||||||
|
[tools.protoc-gen-go."platforms.linux-x64"]
|
||||||
|
url = "https://github.com/protocolbuffers/protobuf-go/releases/download/v1.34.2/protoc-gen-go.v1.34.2.linux.amd64.tar.gz"
|
||||||
|
|
||||||
|
[tools.protoc-gen-go."platforms.macos-arm64"]
|
||||||
|
url = "https://github.com/protocolbuffers/protobuf-go/releases/download/v1.34.2/protoc-gen-go.v1.34.2.darwin.arm64.tar.gz"
|
||||||
|
|
||||||
|
[[tools.protoc-gen-go-grpc]]
|
||||||
|
version = "1.5.1"
|
||||||
|
backend = "aqua:grpc/grpc-go/protoc-gen-go-grpc"
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
[settings]
|
||||||
|
experimental = true
|
||||||
|
|
||||||
|
[tools]
|
||||||
|
"aqua:vektra/mockery" = "3.5.3"
|
||||||
|
go = "1.26.1"
|
||||||
|
golangci-lint = "2.11.2"
|
||||||
|
node = "22.20.0"
|
||||||
|
protoc = "27.3"
|
||||||
|
protoc-gen-go = "1.34.2"
|
||||||
|
protoc-gen-go-grpc = "1.5.1"
|
||||||
|
|
||||||
|
[vars]
|
||||||
|
ucind_image = "ghcr.io/psviderski/ucind:latest"
|
||||||
|
|
||||||
|
[env]
|
||||||
|
_.file = ".env"
|
||||||
|
|
||||||
|
[tasks."build:uncloudd-amd64"]
|
||||||
|
description = "Build uncloudd for linux/amd64"
|
||||||
|
run = """
|
||||||
|
VERSION=$(git describe --tags --always --dirty) \
|
||||||
|
GOOS=linux GOARCH=amd64 go build -ldflags "-X github.com/psviderski/uncloud/internal/version.version=$VERSION" \
|
||||||
|
-o uncloudd-linux-amd64 ./cmd/uncloudd
|
||||||
|
"""
|
||||||
|
outputs = ["uncloudd-linux-amd64"]
|
||||||
|
|
||||||
|
[tasks."build:uncloudd-arm64"]
|
||||||
|
description = "Build uncloudd for linux/arm64"
|
||||||
|
run = """
|
||||||
|
VERSION=$(git describe --tags --always --dirty) \
|
||||||
|
GOOS=linux GOARCH=arm64 go build -ldflags "-X github.com/psviderski/uncloud/internal/version.version=$VERSION" \
|
||||||
|
-o uncloudd-linux-arm64 ./cmd/uncloudd
|
||||||
|
"""
|
||||||
|
outputs = ["uncloudd-linux-arm64"]
|
||||||
|
|
||||||
|
[tasks."mise:lock"]
|
||||||
|
description = "Update mise.lock with hashes for all platforms"
|
||||||
|
run = "mise lock --platform linux-x64,macos-arm64"
|
||||||
|
|
||||||
|
[tasks.proto]
|
||||||
|
description = "Regenerate gRPC API code from .proto files"
|
||||||
|
run = """
|
||||||
|
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
|
||||||
|
"""
|
||||||
|
|
||||||
|
[tasks.uc]
|
||||||
|
description = "Run the Uncloud CLI"
|
||||||
|
run = "go run ./cmd/uncloud"
|
||||||
|
|
||||||
|
[tasks.ucind]
|
||||||
|
description = "Run ucind CLI for managing local Docker-based clusters"
|
||||||
|
run = "go run ./cmd/ucind"
|
||||||
|
|
||||||
|
[tasks."ucind:image"]
|
||||||
|
description = "Build the ucind Docker image"
|
||||||
|
run = "docker build -t {{vars.ucind_image}} --target ucind ."
|
||||||
|
|
||||||
|
[tasks."ucind:cleanup"]
|
||||||
|
description = "Remove ucind managed containers and networks"
|
||||||
|
run = """
|
||||||
|
echo "==> Removing ucind managed containers..."
|
||||||
|
docker ps --filter "label=ucind.managed" -q | xargs docker rm -f
|
||||||
|
echo "==> Removing ucind managed networks..."
|
||||||
|
docker network ls --filter "label=ucind.managed" -q | xargs docker network rm -f
|
||||||
|
"""
|
||||||
+12
-2
@@ -21,11 +21,14 @@ type ContainerClient interface {
|
|||||||
CreateContainer(
|
CreateContainer(
|
||||||
ctx context.Context, serviceID string, spec ServiceSpec, machineID string,
|
ctx context.Context, serviceID string, spec ServiceSpec, machineID string,
|
||||||
) (container.CreateResponse, error)
|
) (container.CreateResponse, error)
|
||||||
|
ExecContainer(ctx context.Context, serviceNameOrID, containerNameOrID string, config ExecOptions) (int, error)
|
||||||
InspectContainer(ctx context.Context, serviceNameOrID, containerNameOrID string) (MachineServiceContainer, error)
|
InspectContainer(ctx context.Context, serviceNameOrID, containerNameOrID string) (MachineServiceContainer, error)
|
||||||
RemoveContainer(ctx context.Context, serviceNameOrID, containerNameOrID string, opts container.RemoveOptions) error
|
|
||||||
StartContainer(ctx context.Context, serviceNameOrID, containerNameOrID string) error
|
StartContainer(ctx context.Context, serviceNameOrID, containerNameOrID string) error
|
||||||
StopContainer(ctx context.Context, serviceNameOrID, containerNameOrID string, opts container.StopOptions) error
|
StopContainer(ctx context.Context, serviceNameOrID, containerNameOrID string, opts container.StopOptions) error
|
||||||
ExecContainer(ctx context.Context, serviceNameOrID, containerNameOrID string, config ExecOptions) (int, error)
|
RemoveContainer(ctx context.Context, serviceNameOrID, containerNameOrID string, opts container.RemoveOptions) error
|
||||||
|
WaitContainerHealthy(
|
||||||
|
ctx context.Context, serviceNameOrID, containerNameOrID string, opts WaitContainerHealthyOptions,
|
||||||
|
) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type DNSClient interface {
|
type DNSClient interface {
|
||||||
@@ -57,3 +60,10 @@ type VolumeClient interface {
|
|||||||
ListVolumes(ctx context.Context, filter *VolumeFilter) ([]MachineVolume, error)
|
ListVolumes(ctx context.Context, filter *VolumeFilter) ([]MachineVolume, error)
|
||||||
RemoveVolume(ctx context.Context, machineNameOrID, volumeName string, force bool) error
|
RemoveVolume(ctx context.Context, machineNameOrID, volumeName string, force bool) error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AsPtr returns a pointer to the given value. Useful for optional fields in API structs.
|
||||||
|
//
|
||||||
|
//go:fix inline
|
||||||
|
func AsPtr[T any](v T) *T {
|
||||||
|
return new(v)
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -98,6 +99,76 @@ func (c *ConfigMount) Validate() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Compare compares this ConfigMount with another.
|
||||||
|
// Returns:
|
||||||
|
//
|
||||||
|
// -1 if c < other
|
||||||
|
// 0 if c == other
|
||||||
|
// +1 if c > other
|
||||||
|
func (c *ConfigMount) Compare(other *ConfigMount) int {
|
||||||
|
if c.ConfigName != other.ConfigName {
|
||||||
|
if c.ConfigName < other.ConfigName {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
if c.ContainerPath != other.ContainerPath {
|
||||||
|
if c.ContainerPath < other.ContainerPath {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
if c.Uid != other.Uid {
|
||||||
|
if c.Uid < other.Uid {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
if c.Gid != other.Gid {
|
||||||
|
if c.Gid < other.Gid {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
// Compare Mode (handle nil cases)
|
||||||
|
if c.Mode == nil && other.Mode != nil {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
if c.Mode != nil && other.Mode == nil {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
if c.Mode != nil && other.Mode != nil {
|
||||||
|
if *c.Mode < *other.Mode {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
if *c.Mode > *other.Mode {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Equals compares two ConfigMount instances for equality.
|
||||||
|
func (c *ConfigMount) Equals(other *ConfigMount) bool {
|
||||||
|
return c.Compare(other) == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ConfigMount) Clone() ConfigMount {
|
||||||
|
clone := *c
|
||||||
|
if c.Mode != nil {
|
||||||
|
mode := *c.Mode
|
||||||
|
clone.Mode = &mode
|
||||||
|
}
|
||||||
|
return clone
|
||||||
|
}
|
||||||
|
|
||||||
|
// sortConfigMounts sorts a slice of ConfigMount instances.
|
||||||
|
func sortConfigMounts(mounts []ConfigMount) {
|
||||||
|
sort.Slice(mounts, func(i, j int) bool {
|
||||||
|
return mounts[i].Compare(&mounts[j]) < 0
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// ValidateConfigsAndMounts takes config specs and config mounts and validates that all mounts refer to existing specs
|
// ValidateConfigsAndMounts takes config specs and config mounts and validates that all mounts refer to existing specs
|
||||||
func ValidateConfigsAndMounts(configs []ConfigSpec, mounts []ConfigMount) error {
|
func ValidateConfigsAndMounts(configs []ConfigSpec, mounts []ConfigMount) error {
|
||||||
configMap := make(map[string]struct{})
|
configMap := make(map[string]struct{})
|
||||||
|
|||||||
@@ -8,11 +8,6 @@ import (
|
|||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
// uint64Ptr is a convenience function to create a pointer to a uint64 value
|
|
||||||
func uint64Ptr(v uint64) *uint64 {
|
|
||||||
return &v
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestConfigMount_GetNumericUid(t *testing.T) {
|
func TestConfigMount_GetNumericUid(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
@@ -30,12 +25,12 @@ func TestConfigMount_GetNumericUid(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "valid numeric uid",
|
name: "valid numeric uid",
|
||||||
uid: "1000",
|
uid: "1000",
|
||||||
expected: uint64Ptr(1000),
|
expected: new(uint64(1000)),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "zero uid",
|
name: "zero uid",
|
||||||
uid: "0",
|
uid: "0",
|
||||||
expected: uint64Ptr(0),
|
expected: new(uint64(0)),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "invalid non-numeric uid",
|
name: "invalid non-numeric uid",
|
||||||
@@ -96,12 +91,12 @@ func TestConfigMount_GetNumericGid(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "valid numeric gid",
|
name: "valid numeric gid",
|
||||||
gid: "1000",
|
gid: "1000",
|
||||||
expected: uint64Ptr(1000),
|
expected: new(uint64(1000)),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "zero gid",
|
name: "zero gid",
|
||||||
gid: "0",
|
gid: "0",
|
||||||
expected: uint64Ptr(0),
|
expected: new(uint64(0)),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "invalid non-numeric gid",
|
name: "invalid non-numeric gid",
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ package api
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -41,6 +43,19 @@ func (c *Container) CreatedTime() time.Time {
|
|||||||
return c.created
|
return c.created
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HasHealthcheck returns true if the container has a health check configured.
|
||||||
|
func (c *Container) HasHealthcheck() bool {
|
||||||
|
hc := c.Config.Healthcheck
|
||||||
|
if hc == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(hc.Test) > 0 && hc.Test[0] == "NONE" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
// Healthy determines if the container is running and healthy.
|
// Healthy determines if the container is running and healthy.
|
||||||
// A running container with no health check configured is considered healthy.
|
// A running container with no health check configured is considered healthy.
|
||||||
func (c *Container) Healthy() bool {
|
func (c *Container) Healthy() bool {
|
||||||
@@ -135,6 +150,10 @@ func (c *Container) UnmarshalJSON(data []byte) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
*c = Container(temp)
|
*c = Container(temp)
|
||||||
|
if c.ContainerJSONBase == nil {
|
||||||
|
return fmt.Errorf("container data is missing mandatory base fields: %s", data)
|
||||||
|
}
|
||||||
|
|
||||||
c.Name = strings.TrimPrefix(c.Name, "/")
|
c.Name = strings.TrimPrefix(c.Name, "/")
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -240,3 +259,61 @@ func (c *ServiceContainer) UnmarshalJSON(data []byte) error {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DefaultHealthMonitorPeriod is the default duration (5 seconds) to wait before checking that the container is still
|
||||||
|
// running and not restarting. Can be overridden with the UNCLOUD_HEALTH_MONITOR_PERIOD environment variable
|
||||||
|
// (e.g. "10s" or "0").
|
||||||
|
var DefaultHealthMonitorPeriod = defaultHealthMonitorPeriod()
|
||||||
|
|
||||||
|
func defaultHealthMonitorPeriod() time.Duration {
|
||||||
|
if v, ok := os.LookupEnv("UNCLOUD_HEALTH_MONITOR_PERIOD"); ok {
|
||||||
|
if d, err := time.ParseDuration(v); err == nil {
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 5 * time.Second
|
||||||
|
}
|
||||||
|
|
||||||
|
// WaitContainerHealthyOptions configures the behaviour of WaitContainerHealthy.
|
||||||
|
type WaitContainerHealthyOptions struct {
|
||||||
|
// MonitorPeriod is how long to wait before checking that the container is still running and not restarting.
|
||||||
|
// Containers with a health check that become healthy before the period ends succeed early.
|
||||||
|
// nil means use the default DefaultHealthMonitorPeriod.
|
||||||
|
// Zero skips the monitoring and checks the container's health immediately after starting.
|
||||||
|
MonitorPeriod *time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExecOptions contains configuration for executing a command in a container.
|
||||||
|
type ExecOptions struct {
|
||||||
|
// Command is the command to run in the container.
|
||||||
|
Command []string
|
||||||
|
// AttachStdin attaches the stdin stream to the exec session.
|
||||||
|
AttachStdin bool
|
||||||
|
// AttachStdout attaches the stdout stream to the exec session.
|
||||||
|
AttachStdout bool
|
||||||
|
// AttachStderr attaches the stderr stream to the exec session.
|
||||||
|
AttachStderr bool
|
||||||
|
// Tty allocates a pseudo-TTY for the exec session.
|
||||||
|
Tty bool
|
||||||
|
// Detach runs the command in the background without attaching to streams.
|
||||||
|
Detach bool
|
||||||
|
|
||||||
|
//// Not yet implemented fields
|
||||||
|
// User specifies the user to run the command as.
|
||||||
|
User string
|
||||||
|
// Privileged runs the command in privileged mode.
|
||||||
|
Privileged bool
|
||||||
|
// WorkingDir sets the working directory for the command.
|
||||||
|
WorkingDir string
|
||||||
|
// Env sets environment variables for the command.
|
||||||
|
Env []string
|
||||||
|
|
||||||
|
// Client-side only fields (not serialized, not sent to server)
|
||||||
|
// Stdin is the input stream. Defaults to os.Stdin if nil.
|
||||||
|
Stdin io.Reader `json:"-"`
|
||||||
|
// Stdout is the output stream. Defaults to os.Stdout if nil.
|
||||||
|
Stdout io.Writer `json:"-"`
|
||||||
|
// Stderr is the error stream. Defaults to os.Stderr if nil.
|
||||||
|
Stderr io.Writer `json:"-"`
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,37 +0,0 @@
|
|||||||
package api
|
|
||||||
|
|
||||||
import "io"
|
|
||||||
|
|
||||||
// ExecOptions contains configuration for executing a command in a container.
|
|
||||||
type ExecOptions struct {
|
|
||||||
// Command is the command to run in the container.
|
|
||||||
Command []string
|
|
||||||
// AttachStdin attaches the stdin stream to the exec session.
|
|
||||||
AttachStdin bool
|
|
||||||
// AttachStdout attaches the stdout stream to the exec session.
|
|
||||||
AttachStdout bool
|
|
||||||
// AttachStderr attaches the stderr stream to the exec session.
|
|
||||||
AttachStderr bool
|
|
||||||
// Tty allocates a pseudo-TTY for the exec session.
|
|
||||||
Tty bool
|
|
||||||
// Detach runs the command in the background without attaching to streams.
|
|
||||||
Detach bool
|
|
||||||
|
|
||||||
//// Not yet implemented fields
|
|
||||||
// User specifies the user to run the command as.
|
|
||||||
User string
|
|
||||||
// Privileged runs the command in privileged mode.
|
|
||||||
Privileged bool
|
|
||||||
// WorkingDir sets the working directory for the command.
|
|
||||||
WorkingDir string
|
|
||||||
// Env sets environment variables for the command.
|
|
||||||
Env []string
|
|
||||||
|
|
||||||
// Client-side only fields (not serialized, not sent to server)
|
|
||||||
// Stdin is the input stream. Defaults to os.Stdin if nil.
|
|
||||||
Stdin io.Reader `json:"-"`
|
|
||||||
// Stdout is the output stream. Defaults to os.Stdout if nil.
|
|
||||||
Stdout io.Writer `json:"-"`
|
|
||||||
// Stderr is the error stream. Defaults to os.Stderr if nil.
|
|
||||||
Stderr io.Writer `json:"-"`
|
|
||||||
}
|
|
||||||
+22
-1
@@ -17,6 +17,27 @@ type ContainerResources struct {
|
|||||||
// MemoryReservation is the minimum amount of memory (in bytes) the container needs to run efficiently.
|
// MemoryReservation is the minimum amount of memory (in bytes) the container needs to run efficiently.
|
||||||
// TODO: implement a placement constraint that checks available memory on machines.
|
// TODO: implement a placement constraint that checks available memory on machines.
|
||||||
MemoryReservation int64
|
MemoryReservation int64
|
||||||
// Device reservations/requests for access to things like GPUs
|
// Devices provides direct access to host devices.
|
||||||
|
Devices []DeviceMapping
|
||||||
|
// DeviceReservations requests for access to things like GPUs.
|
||||||
DeviceReservations []container.DeviceRequest
|
DeviceReservations []container.DeviceRequest
|
||||||
|
// SharedMemory is the size of the shared memory (in bytes) mounted at /dev/shm in the container.
|
||||||
|
SharedMemory int64
|
||||||
|
// Ulimits defines the resource limits for the container.
|
||||||
|
Ulimits map[string]Ulimit
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeviceMapping represents a device mapping between host and container.
|
||||||
|
type DeviceMapping struct {
|
||||||
|
// HostPath is the path to the device on the host.
|
||||||
|
HostPath string
|
||||||
|
// ContainerPath is the path to the device in the container.
|
||||||
|
ContainerPath string
|
||||||
|
// CgroupPermissions is the cgroup permissions for the device (e.g., "rwm").
|
||||||
|
CgroupPermissions string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Ulimit struct {
|
||||||
|
Soft int64
|
||||||
|
Hard int64
|
||||||
}
|
}
|
||||||
|
|||||||
+103
-4
@@ -7,6 +7,7 @@ import (
|
|||||||
"regexp"
|
"regexp"
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
mapset "github.com/deckarep/golang-set/v2"
|
mapset "github.com/deckarep/golang-set/v2"
|
||||||
"github.com/distribution/reference"
|
"github.com/distribution/reference"
|
||||||
@@ -19,6 +20,13 @@ const (
|
|||||||
ServiceModeReplicated = "replicated"
|
ServiceModeReplicated = "replicated"
|
||||||
ServiceModeGlobal = "global"
|
ServiceModeGlobal = "global"
|
||||||
|
|
||||||
|
// UpdateOrderStartFirst starts the new container before stopping the old one.
|
||||||
|
// This minimizes downtime but briefly runs both containers.
|
||||||
|
UpdateOrderStartFirst = "start-first"
|
||||||
|
// UpdateOrderStopFirst stops the old container before starting the new one.
|
||||||
|
// This prevents data corruption for stateful services but causes brief downtime.
|
||||||
|
UpdateOrderStopFirst = "stop-first"
|
||||||
|
|
||||||
// PullPolicyAlways means the image is always pulled from the registry.
|
// PullPolicyAlways means the image is always pulled from the registry.
|
||||||
PullPolicyAlways = "always"
|
PullPolicyAlways = "always"
|
||||||
// PullPolicyMissing means the image is pulled from the registry only if it's not available on the machine where
|
// PullPolicyMissing means the image is pulled from the registry only if it's not available on the machine where
|
||||||
@@ -47,6 +55,8 @@ type ServiceSpec struct {
|
|||||||
// Caddy is the optional Caddy reverse proxy configuration for the service.
|
// Caddy is the optional Caddy reverse proxy configuration for the service.
|
||||||
// Caddy and Ports cannot be specified simultaneously.
|
// Caddy and Ports cannot be specified simultaneously.
|
||||||
Caddy *CaddySpec `json:",omitempty"`
|
Caddy *CaddySpec `json:",omitempty"`
|
||||||
|
// Configs is list of configuration objects that can be mounted into the container.
|
||||||
|
Configs []ConfigSpec
|
||||||
// Container defines the desired state of each container in the service.
|
// Container defines the desired state of each container in the service.
|
||||||
Container ContainerSpec
|
Container ContainerSpec
|
||||||
// Mode is the replication mode of the service. Default is ServiceModeReplicated if empty.
|
// Mode is the replication mode of the service. Default is ServiceModeReplicated if empty.
|
||||||
@@ -59,10 +69,10 @@ type ServiceSpec struct {
|
|||||||
Ports []PortSpec
|
Ports []PortSpec
|
||||||
// Replicas is the number of containers to run for the service. Only valid for a replicated service.
|
// Replicas is the number of containers to run for the service. Only valid for a replicated service.
|
||||||
Replicas uint `json:",omitempty"`
|
Replicas uint `json:",omitempty"`
|
||||||
|
// UpdateConfig configures how the service is updated during a deployment.
|
||||||
|
UpdateConfig UpdateConfig
|
||||||
// Volumes is list of data volumes that can be mounted into the container.
|
// Volumes is list of data volumes that can be mounted into the container.
|
||||||
Volumes []VolumeSpec
|
Volumes []VolumeSpec
|
||||||
// Configs is list of configuration objects that can be mounted into the container.
|
|
||||||
Configs []ConfigSpec
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// CaddyConfig returns the Caddy reverse proxy configuration for the service or an empty string if it's not defined.
|
// CaddyConfig returns the Caddy reverse proxy configuration for the service or an empty string if it's not defined.
|
||||||
@@ -225,17 +235,26 @@ func (s *ServiceSpec) Clone() ServiceSpec {
|
|||||||
// ContainerSpec defines the desired state of a container in a service.
|
// ContainerSpec defines the desired state of a container in a service.
|
||||||
// ATTENTION: after changing this struct, verify if deploy.EvalContainerSpecChange needs to be updated.
|
// ATTENTION: after changing this struct, verify if deploy.EvalContainerSpecChange needs to be updated.
|
||||||
type ContainerSpec struct {
|
type ContainerSpec struct {
|
||||||
|
// Specifies which additional capabilities should be added for the container.
|
||||||
|
CapAdd []string
|
||||||
|
// Specifies which capabilities should be dropped from the container.
|
||||||
|
CapDrop []string
|
||||||
// Command overrides the default CMD of the image to be executed when running a container.
|
// Command overrides the default CMD of the image to be executed when running a container.
|
||||||
Command []string
|
Command []string
|
||||||
// Entrypoint overrides the default ENTRYPOINT of the image.
|
// Entrypoint overrides the default ENTRYPOINT of the image.
|
||||||
Entrypoint []string
|
Entrypoint []string
|
||||||
// Env defines the environment variables to set inside the container.
|
// Env defines the environment variables to set inside the container.
|
||||||
Env EnvVars
|
Env EnvVars
|
||||||
Image string
|
// Healthcheck defines the health check configuration for the container or overrides the health check options
|
||||||
|
// defined in the image. If nil, the image's default health check is used.
|
||||||
|
Healthcheck *HealthcheckSpec `json:",omitempty"`
|
||||||
|
Image string
|
||||||
// Run a custom init inside the container. If nil, use the daemon's configured settings.
|
// Run a custom init inside the container. If nil, use the daemon's configured settings.
|
||||||
Init *bool
|
Init *bool
|
||||||
// LogDriver overrides the default logging driver for the container. Each Docker daemon can have its own default.
|
// LogDriver overrides the default logging driver for the container. Each Docker daemon can have its own default.
|
||||||
LogDriver *LogDriver
|
LogDriver *LogDriver
|
||||||
|
// PidMode sets the PID namespace mode for the container. Currently only "" or "host" is supported.
|
||||||
|
PidMode string
|
||||||
// Privileged gives extended privileges to the container. This is a security risk and should be used with caution.
|
// Privileged gives extended privileges to the container. This is a security risk and should be used with caution.
|
||||||
Privileged bool
|
Privileged bool
|
||||||
// PullPolicy determines when to pull the image from the registry or use the image already available in the cluster.
|
// PullPolicy determines when to pull the image from the registry or use the image already available in the cluster.
|
||||||
@@ -243,6 +262,11 @@ type ContainerSpec struct {
|
|||||||
PullPolicy string
|
PullPolicy string
|
||||||
// Resource allocation for the container.
|
// Resource allocation for the container.
|
||||||
Resources ContainerResources
|
Resources ContainerResources
|
||||||
|
// StopGracePeriod is how long to wait after SIGTERM before sending SIGKILL when stopping the container.
|
||||||
|
// Default is 10 seconds if not specified.
|
||||||
|
StopGracePeriod *time.Duration `json:",omitempty"`
|
||||||
|
// Namespaced kernel parameters to be set in the container.
|
||||||
|
Sysctls map[string]string
|
||||||
// User overrides the default user of the image used to run the container. Format: user|UID[:group|GID].
|
// User overrides the default user of the image used to run the container. Format: user|UID[:group|GID].
|
||||||
User string
|
User string
|
||||||
// VolumeMounts specifies how volumes are mounted into the container filesystem.
|
// VolumeMounts specifies how volumes are mounted into the container filesystem.
|
||||||
@@ -290,18 +314,32 @@ func (s *ContainerSpec) Equals(spec ContainerSpec) bool {
|
|||||||
orig := s.SetDefaults()
|
orig := s.SetDefaults()
|
||||||
spec = spec.SetDefaults()
|
spec = spec.SetDefaults()
|
||||||
|
|
||||||
|
// Volumes
|
||||||
slices.Sort(orig.Volumes)
|
slices.Sort(orig.Volumes)
|
||||||
slices.Sort(spec.Volumes)
|
slices.Sort(spec.Volumes)
|
||||||
|
|
||||||
|
// Volume mounts
|
||||||
sortVolumeMounts(orig.VolumeMounts)
|
sortVolumeMounts(orig.VolumeMounts)
|
||||||
sortVolumeMounts(spec.VolumeMounts)
|
sortVolumeMounts(spec.VolumeMounts)
|
||||||
|
|
||||||
|
// Config mounts
|
||||||
|
sortConfigMounts(orig.ConfigMounts)
|
||||||
|
sortConfigMounts(spec.ConfigMounts)
|
||||||
|
|
||||||
return cmp.Equal(orig, spec, cmpopts.EquateEmpty())
|
return cmp.Equal(orig, spec, cmpopts.EquateEmpty())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ContainerSpec) Clone() ContainerSpec {
|
func (s *ContainerSpec) Clone() ContainerSpec {
|
||||||
spec := *s
|
spec := *s
|
||||||
|
|
||||||
|
if s.CapAdd != nil {
|
||||||
|
spec.CapAdd = make([]string, len(s.CapAdd))
|
||||||
|
copy(spec.CapAdd, s.CapAdd)
|
||||||
|
}
|
||||||
|
if s.CapDrop != nil {
|
||||||
|
spec.CapDrop = make([]string, len(s.CapDrop))
|
||||||
|
copy(spec.CapDrop, s.CapDrop)
|
||||||
|
}
|
||||||
if s.Command != nil {
|
if s.Command != nil {
|
||||||
spec.Command = make([]string, len(s.Command))
|
spec.Command = make([]string, len(s.Command))
|
||||||
copy(spec.Command, s.Command)
|
copy(spec.Command, s.Command)
|
||||||
@@ -310,6 +348,15 @@ func (s *ContainerSpec) Clone() ContainerSpec {
|
|||||||
spec.Entrypoint = make([]string, len(s.Entrypoint))
|
spec.Entrypoint = make([]string, len(s.Entrypoint))
|
||||||
copy(spec.Entrypoint, s.Entrypoint)
|
copy(spec.Entrypoint, s.Entrypoint)
|
||||||
}
|
}
|
||||||
|
if s.Env != nil {
|
||||||
|
spec.Env = make(EnvVars, len(s.Env))
|
||||||
|
maps.Copy(spec.Env, s.Env)
|
||||||
|
}
|
||||||
|
if s.Healthcheck != nil {
|
||||||
|
hc := *s.Healthcheck
|
||||||
|
hc.Test = slices.Clone(s.Healthcheck.Test)
|
||||||
|
spec.Healthcheck = &hc
|
||||||
|
}
|
||||||
if s.LogDriver != nil {
|
if s.LogDriver != nil {
|
||||||
logDriver := *s.LogDriver
|
logDriver := *s.LogDriver
|
||||||
if s.LogDriver.Options != nil {
|
if s.LogDriver.Options != nil {
|
||||||
@@ -325,6 +372,25 @@ func (s *ContainerSpec) Clone() ContainerSpec {
|
|||||||
spec.VolumeMounts = make([]VolumeMount, len(s.VolumeMounts))
|
spec.VolumeMounts = make([]VolumeMount, len(s.VolumeMounts))
|
||||||
copy(spec.VolumeMounts, s.VolumeMounts)
|
copy(spec.VolumeMounts, s.VolumeMounts)
|
||||||
}
|
}
|
||||||
|
if s.ConfigMounts != nil {
|
||||||
|
spec.ConfigMounts = make([]ConfigMount, len(s.ConfigMounts))
|
||||||
|
for i, cm := range s.ConfigMounts {
|
||||||
|
spec.ConfigMounts[i] = cm.Clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if s.Sysctls != nil {
|
||||||
|
spec.Sysctls = make(map[string]string, len(s.Sysctls))
|
||||||
|
maps.Copy(spec.Sysctls, s.Sysctls)
|
||||||
|
}
|
||||||
|
if s.Resources.Ulimits != nil {
|
||||||
|
spec.Resources.Ulimits = maps.Clone(s.Resources.Ulimits)
|
||||||
|
}
|
||||||
|
if s.Resources.Devices != nil {
|
||||||
|
spec.Resources.Devices = slices.Clone(s.Resources.Devices)
|
||||||
|
}
|
||||||
|
if s.Resources.DeviceReservations != nil {
|
||||||
|
spec.Resources.DeviceReservations = slices.Clone(s.Resources.DeviceReservations)
|
||||||
|
}
|
||||||
|
|
||||||
return spec
|
return spec
|
||||||
}
|
}
|
||||||
@@ -343,6 +409,26 @@ func (e EnvVars) ToSlice() []string {
|
|||||||
return env
|
return env
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HealthcheckSpec defines the health check configuration for a container.
|
||||||
|
type HealthcheckSpec struct {
|
||||||
|
// Test is the command used to check health.
|
||||||
|
// Formats: ["CMD", args...], ["CMD-SHELL", "command"], or ["NONE"] to disable.
|
||||||
|
Test []string `json:",omitempty"`
|
||||||
|
// Interval is the time between health checks.
|
||||||
|
// Zero means to inherit the value from the image or use the Docker default (30s) if not defined in the image.
|
||||||
|
Interval time.Duration `json:",omitempty"`
|
||||||
|
// Timeout is how long to wait before considering the checck to have hung.
|
||||||
|
Timeout time.Duration `json:",omitempty"`
|
||||||
|
// StartPeriod is the initialisation time for a container before the retries start to count down.
|
||||||
|
StartPeriod time.Duration `json:",omitempty"`
|
||||||
|
// StartInterval is the time between health checks during the start period.
|
||||||
|
StartInterval time.Duration `json:",omitempty"`
|
||||||
|
// Retries is the number of consecutive failures needed to consider a container unhealthy.
|
||||||
|
Retries uint `json:",omitempty"`
|
||||||
|
// Disable disables the health check defined in the image. true is equivalent to setting Test to ["NONE"].
|
||||||
|
Disable bool `json:",omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
type LogDriver struct {
|
type LogDriver struct {
|
||||||
// Name of the logging driver to use.
|
// Name of the logging driver to use.
|
||||||
Name string
|
Name string
|
||||||
@@ -350,6 +436,19 @@ type LogDriver struct {
|
|||||||
Options map[string]string
|
Options map[string]string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UpdateConfig configures how a service is updated during a deployment.
|
||||||
|
type UpdateConfig struct {
|
||||||
|
// Order specifies the order of operations during an update.
|
||||||
|
// Valid values are "start-first" (default for stateless services) and "stop-first" (default for services with
|
||||||
|
// volumes). Empty value means the strategy will determine the order based on service characteristics.
|
||||||
|
Order string `json:",omitempty"`
|
||||||
|
// MonitorPeriod is how long to wait after starting a container before checking that it's still running
|
||||||
|
// and not restarting. Containers with a health check that become healthy before the period ends succeed early.
|
||||||
|
// nil means use the default api.DefaultHealthMonitorPeriod.
|
||||||
|
// Zero skips the monitoring and checks the container's health immediately after starting.
|
||||||
|
MonitorPeriod *time.Duration `json:",omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
type RunServiceResponse struct {
|
type RunServiceResponse struct {
|
||||||
ID string
|
ID string
|
||||||
Name string
|
Name string
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/docker/docker/api/types/container"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -197,3 +200,102 @@ func TestServiceSpec_Validate_CaddyAndPorts(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestContainerSpec_Clone(t *testing.T) {
|
||||||
|
mode := os.FileMode(0o644)
|
||||||
|
original := ContainerSpec{
|
||||||
|
CapAdd: []string{"NET_ADMIN"},
|
||||||
|
CapDrop: []string{"ALL"},
|
||||||
|
PidMode: "host",
|
||||||
|
Command: []string{"sh", "-c", "echo hello"},
|
||||||
|
Entrypoint: []string{"/bin/bash"},
|
||||||
|
Env: EnvVars{
|
||||||
|
"FOO": "bar",
|
||||||
|
"BAZ": "qux",
|
||||||
|
},
|
||||||
|
Image: "nginx:latest",
|
||||||
|
Init: new(true),
|
||||||
|
LogDriver: &LogDriver{
|
||||||
|
Name: "json-file",
|
||||||
|
Options: map[string]string{
|
||||||
|
"max-size": "10m",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Privileged: true,
|
||||||
|
PullPolicy: PullPolicyAlways,
|
||||||
|
Resources: ContainerResources{
|
||||||
|
CPU: 1234,
|
||||||
|
Memory: 2345,
|
||||||
|
MemoryReservation: 3456,
|
||||||
|
Devices: []DeviceMapping{
|
||||||
|
{HostPath: "/dev/sda", ContainerPath: "/dev/xvda", CgroupPermissions: "rwm"},
|
||||||
|
},
|
||||||
|
DeviceReservations: []container.DeviceRequest{
|
||||||
|
{Count: 1, Capabilities: [][]string{{"gpu"}}, Driver: "nvidia"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Sysctls: map[string]string{
|
||||||
|
"net.ipv4.ip_forward": "1",
|
||||||
|
},
|
||||||
|
User: "1000:1000",
|
||||||
|
Volumes: []string{"/data", "/config"},
|
||||||
|
VolumeMounts: []VolumeMount{
|
||||||
|
{VolumeName: "data", ContainerPath: "/data"},
|
||||||
|
},
|
||||||
|
ConfigMounts: []ConfigMount{
|
||||||
|
{ConfigName: "app-config", ContainerPath: "/etc/config", Mode: &mode},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
cloned := original.Clone()
|
||||||
|
|
||||||
|
// Check ContainerSpec equality
|
||||||
|
assert.True(t, original.Equals(cloned))
|
||||||
|
|
||||||
|
// Verify deep copy by modifying the original
|
||||||
|
stringModified := "modified"
|
||||||
|
original.CapAdd[0] = stringModified
|
||||||
|
original.CapDrop[0] = stringModified
|
||||||
|
original.Command[0] = stringModified
|
||||||
|
original.Entrypoint[0] = stringModified
|
||||||
|
original.Env["FOO"] = stringModified
|
||||||
|
original.LogDriver.Options["max-size"] = stringModified
|
||||||
|
original.Volumes[0] = stringModified
|
||||||
|
original.VolumeMounts[0].ContainerPath = stringModified
|
||||||
|
original.ConfigMounts[0].ContainerPath = stringModified
|
||||||
|
*original.ConfigMounts[0].Mode = 0o755 // Modify the Mode pointer value
|
||||||
|
original.Sysctls["net.ipv4.ip_forward"] = stringModified
|
||||||
|
original.Resources.Devices[0].HostPath = stringModified
|
||||||
|
original.Resources.DeviceReservations[0].Count = 2
|
||||||
|
original.Resources.DeviceReservations[0].Driver = stringModified
|
||||||
|
|
||||||
|
assert.False(t, original.Equals(cloned))
|
||||||
|
// Assert cloned values are unchanged
|
||||||
|
assert.Equal(t, "NET_ADMIN", cloned.CapAdd[0])
|
||||||
|
assert.Equal(t, "ALL", cloned.CapDrop[0])
|
||||||
|
assert.Equal(t, "sh", cloned.Command[0])
|
||||||
|
assert.Equal(t, "/bin/bash", cloned.Entrypoint[0])
|
||||||
|
assert.Equal(t, "bar", cloned.Env["FOO"])
|
||||||
|
assert.Equal(t, "qux", cloned.Env["BAZ"])
|
||||||
|
assert.Equal(t, "nginx:latest", cloned.Image)
|
||||||
|
assert.NotNil(t, cloned.Init)
|
||||||
|
assert.Equal(t, true, *cloned.Init)
|
||||||
|
assert.NotNil(t, cloned.LogDriver)
|
||||||
|
assert.Equal(t, "json-file", cloned.LogDriver.Name)
|
||||||
|
assert.Equal(t, "10m", cloned.LogDriver.Options["max-size"])
|
||||||
|
assert.Equal(t, true, cloned.Privileged)
|
||||||
|
assert.Equal(t, PullPolicyAlways, cloned.PullPolicy)
|
||||||
|
assert.Equal(t, int64(1234), cloned.Resources.CPU)
|
||||||
|
assert.Equal(t, int64(2345), cloned.Resources.Memory)
|
||||||
|
assert.Equal(t, int64(3456), cloned.Resources.MemoryReservation)
|
||||||
|
assert.Equal(t, "/dev/sda", cloned.Resources.Devices[0].HostPath)
|
||||||
|
assert.Equal(t, 1, cloned.Resources.DeviceReservations[0].Count)
|
||||||
|
assert.Equal(t, "nvidia", cloned.Resources.DeviceReservations[0].Driver)
|
||||||
|
assert.Equal(t, "1000:1000", cloned.User)
|
||||||
|
assert.Equal(t, "/data", cloned.Volumes[0])
|
||||||
|
assert.Equal(t, "/data", cloned.VolumeMounts[0].ContainerPath)
|
||||||
|
assert.Equal(t, "/etc/config", cloned.ConfigMounts[0].ContainerPath)
|
||||||
|
assert.NotNil(t, cloned.ConfigMounts[0].Mode)
|
||||||
|
assert.Equal(t, os.FileMode(0o644), *cloned.ConfigMounts[0].Mode, "Mode should be deep copied")
|
||||||
|
assert.Equal(t, "1", cloned.Sysctls["net.ipv4.ip_forward"])
|
||||||
|
}
|
||||||
|
|||||||
+3
-6
@@ -2,6 +2,7 @@ package api
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"maps"
|
||||||
"reflect"
|
"reflect"
|
||||||
"slices"
|
"slices"
|
||||||
"sort"
|
"sort"
|
||||||
@@ -165,18 +166,14 @@ func (v *VolumeSpec) Clone() VolumeSpec {
|
|||||||
driver := *v.VolumeOptions.Driver
|
driver := *v.VolumeOptions.Driver
|
||||||
if driver.Options != nil {
|
if driver.Options != nil {
|
||||||
driver.Options = make(map[string]string, len(v.VolumeOptions.Driver.Options))
|
driver.Options = make(map[string]string, len(v.VolumeOptions.Driver.Options))
|
||||||
for k, val := range v.VolumeOptions.Driver.Options {
|
maps.Copy(driver.Options, v.VolumeOptions.Driver.Options)
|
||||||
driver.Options[k] = val
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
opts.Driver = &driver
|
opts.Driver = &driver
|
||||||
}
|
}
|
||||||
|
|
||||||
if opts.Labels != nil {
|
if opts.Labels != nil {
|
||||||
opts.Labels = make(map[string]string, len(v.VolumeOptions.Labels))
|
opts.Labels = make(map[string]string, len(v.VolumeOptions.Labels))
|
||||||
for k, val := range v.VolumeOptions.Labels {
|
maps.Copy(opts.Labels, v.VolumeOptions.Labels)
|
||||||
opts.Labels[k] = val
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
spec.VolumeOptions = &opts
|
spec.VolumeOptions = &opts
|
||||||
|
|||||||
@@ -13,18 +13,27 @@ import (
|
|||||||
func configSpecsFromCompose(
|
func configSpecsFromCompose(
|
||||||
configs types.Configs, serviceConfigs []types.ServiceConfigObjConfig, workingDir string,
|
configs types.Configs, serviceConfigs []types.ServiceConfigObjConfig, workingDir string,
|
||||||
) ([]api.ConfigSpec, []api.ConfigMount, error) {
|
) ([]api.ConfigSpec, []api.ConfigMount, error) {
|
||||||
var configSpecs []api.ConfigSpec
|
|
||||||
var configMounts []api.ConfigMount
|
var configMounts []api.ConfigMount
|
||||||
|
|
||||||
|
// Temporary map to hold config specs
|
||||||
|
configSpecsMap := make(map[string]api.ConfigSpec)
|
||||||
|
|
||||||
|
// We iterate over all service config objects (config mounts)
|
||||||
for _, serviceConfig := range serviceConfigs {
|
for _, serviceConfig := range serviceConfigs {
|
||||||
var spec api.ConfigSpec
|
var spec api.ConfigSpec
|
||||||
|
|
||||||
if projectConfig, exists := configs[serviceConfig.Source]; exists {
|
projectConfig, exists := configs[serviceConfig.Source]
|
||||||
if projectConfig.External {
|
if !exists {
|
||||||
return nil, nil, fmt.Errorf("external configs are not supported: %s",
|
return nil, nil, fmt.Errorf("config '%s' not found in project configs", serviceConfig.Source)
|
||||||
serviceConfig.Source)
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
if projectConfig.External {
|
||||||
|
return nil, nil, fmt.Errorf("external configs are not supported: %s",
|
||||||
|
serviceConfig.Source)
|
||||||
|
}
|
||||||
|
|
||||||
|
spec, exists = configSpecsMap[serviceConfig.Source]
|
||||||
|
if !exists {
|
||||||
spec = api.ConfigSpec{
|
spec = api.ConfigSpec{
|
||||||
Name: serviceConfig.Source,
|
Name: serviceConfig.Source,
|
||||||
Content: []byte(projectConfig.Content),
|
Content: []byte(projectConfig.Content),
|
||||||
@@ -44,11 +53,9 @@ func configSpecsFromCompose(
|
|||||||
}
|
}
|
||||||
spec.Content = fileContent
|
spec.Content = fileContent
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
return nil, nil, fmt.Errorf("config '%s' not found in project configs", serviceConfig.Source)
|
|
||||||
}
|
|
||||||
|
|
||||||
configSpecs = append(configSpecs, spec)
|
configSpecsMap[serviceConfig.Source] = spec
|
||||||
|
}
|
||||||
|
|
||||||
// Create config mount
|
// Create config mount
|
||||||
target := serviceConfig.Target
|
target := serviceConfig.Target
|
||||||
@@ -71,5 +78,11 @@ func configSpecsFromCompose(
|
|||||||
configMounts = append(configMounts, mount)
|
configMounts = append(configMounts, mount)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var configSpecs []api.ConfigSpec
|
||||||
|
// Convert config spec map to slice
|
||||||
|
for _, spec := range configSpecsMap {
|
||||||
|
configSpecs = append(configSpecs, spec)
|
||||||
|
}
|
||||||
|
|
||||||
return configSpecs, configMounts, nil
|
return configSpecs, configMounts, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,6 +77,147 @@ func TestConfigSpecsFromCompose(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "same source mounted to different targets",
|
||||||
|
configs: types.Configs{
|
||||||
|
"shared-config": types.ConfigObjConfig{
|
||||||
|
File: "testdata/config1.txt",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
serviceConfigs: []types.ServiceConfigObjConfig{
|
||||||
|
{
|
||||||
|
Source: "shared-config",
|
||||||
|
Target: "/app/config.json",
|
||||||
|
UID: "1000",
|
||||||
|
GID: "1000",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Source: "shared-config",
|
||||||
|
Target: "/backup/config.json",
|
||||||
|
UID: "1001",
|
||||||
|
GID: "1001",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedSpecs: []api.ConfigSpec{
|
||||||
|
{
|
||||||
|
Name: "shared-config",
|
||||||
|
Content: []byte("test config content\n"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedMounts: []api.ConfigMount{
|
||||||
|
{
|
||||||
|
ConfigName: "shared-config",
|
||||||
|
ContainerPath: "/app/config.json",
|
||||||
|
Uid: "1000",
|
||||||
|
Gid: "1000",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ConfigName: "shared-config",
|
||||||
|
ContainerPath: "/backup/config.json",
|
||||||
|
Uid: "1001",
|
||||||
|
Gid: "1001",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "config with default target path",
|
||||||
|
configs: types.Configs{
|
||||||
|
"default-config": types.ConfigObjConfig{
|
||||||
|
Content: "inline config content",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
serviceConfigs: []types.ServiceConfigObjConfig{
|
||||||
|
{
|
||||||
|
Source: "default-config",
|
||||||
|
// No Target specified - should use default
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedSpecs: []api.ConfigSpec{
|
||||||
|
{
|
||||||
|
Name: "default-config",
|
||||||
|
Content: []byte("inline config content"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedMounts: []api.ConfigMount{
|
||||||
|
{
|
||||||
|
ConfigName: "default-config",
|
||||||
|
ContainerPath: "/default-config",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "config with inline content",
|
||||||
|
configs: types.Configs{
|
||||||
|
"inline-config": types.ConfigObjConfig{
|
||||||
|
Content: "server {\n listen 80;\n}",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
serviceConfigs: []types.ServiceConfigObjConfig{
|
||||||
|
{
|
||||||
|
Source: "inline-config",
|
||||||
|
Target: "/etc/nginx/sites-available/default",
|
||||||
|
Mode: func() *types.FileMode { m := types.FileMode(0o755); return &m }(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedSpecs: []api.ConfigSpec{
|
||||||
|
{
|
||||||
|
Name: "inline-config",
|
||||||
|
Content: []byte("server {\n listen 80;\n}"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedMounts: []api.ConfigMount{
|
||||||
|
{
|
||||||
|
ConfigName: "inline-config",
|
||||||
|
ContainerPath: "/etc/nginx/sites-available/default",
|
||||||
|
Mode: func() *os.FileMode { m := os.FileMode(0o755); return &m }(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "config not found error",
|
||||||
|
configs: types.Configs{
|
||||||
|
"existing-config": types.ConfigObjConfig{
|
||||||
|
Content: "some content",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
serviceConfigs: []types.ServiceConfigObjConfig{
|
||||||
|
{
|
||||||
|
Source: "missing-config",
|
||||||
|
Target: "/app/config.json",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "external config error",
|
||||||
|
configs: types.Configs{
|
||||||
|
"external-config": types.ConfigObjConfig{
|
||||||
|
External: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
serviceConfigs: []types.ServiceConfigObjConfig{
|
||||||
|
{
|
||||||
|
Source: "external-config",
|
||||||
|
Target: "/app/config.json",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "file not found error",
|
||||||
|
configs: types.Configs{
|
||||||
|
"missing-file-config": types.ConfigObjConfig{
|
||||||
|
File: "testdata/nonexistent.txt",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
serviceConfigs: []types.ServiceConfigObjConfig{
|
||||||
|
{
|
||||||
|
Source: "missing-file-config",
|
||||||
|
Target: "/app/config.json",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package compose
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/compose-spec/compose-go/v2/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ContextExtensionKey is the top-level Compose extension key for specifying the cluster context.
|
||||||
|
const ContextExtensionKey = "x-context"
|
||||||
|
|
||||||
|
// ClusterContext extracts the x-context value from the project's top-level extensions.
|
||||||
|
// Returns an empty string if x-context is not set.
|
||||||
|
func ClusterContext(project *types.Project) string {
|
||||||
|
v, ok := project.Extensions[ContextExtensionKey]
|
||||||
|
if !ok {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
s, ok := v.(string)
|
||||||
|
if !ok {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
package compose
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestClusterContext(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
content string
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "no x-context",
|
||||||
|
content: `
|
||||||
|
services:
|
||||||
|
web:
|
||||||
|
image: nginx
|
||||||
|
`,
|
||||||
|
want: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "x-context set",
|
||||||
|
content: `
|
||||||
|
x-context: prod
|
||||||
|
services:
|
||||||
|
web:
|
||||||
|
image: nginx
|
||||||
|
`,
|
||||||
|
want: "prod",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
project, err := LoadProjectFromContent(context.Background(), tt.content)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, tt.want, ClusterContext(project))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
"github.com/docker/docker/api/types/volume"
|
"github.com/docker/docker/api/types/volume"
|
||||||
"github.com/psviderski/uncloud/pkg/api"
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
"github.com/psviderski/uncloud/pkg/client/deploy"
|
"github.com/psviderski/uncloud/pkg/client/deploy"
|
||||||
|
"github.com/psviderski/uncloud/pkg/client/deploy/operation"
|
||||||
"github.com/psviderski/uncloud/pkg/client/deploy/scheduler"
|
"github.com/psviderski/uncloud/pkg/client/deploy/scheduler"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -27,7 +28,7 @@ type Deployment struct {
|
|||||||
SpecResolver *deploy.ServiceSpecResolver
|
SpecResolver *deploy.ServiceSpecResolver
|
||||||
Strategy deploy.Strategy
|
Strategy deploy.Strategy
|
||||||
state *scheduler.ClusterState
|
state *scheduler.ClusterState
|
||||||
plan *deploy.SequenceOperation
|
plan *Plan
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewDeployment(ctx context.Context, cli Client, project *types.Project) (*Deployment, error) {
|
func NewDeployment(ctx context.Context, cli Client, project *types.Project) (*Deployment, error) {
|
||||||
@@ -58,11 +59,11 @@ func NewDeploymentWithStrategy(ctx context.Context, cli Client, project *types.P
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Deployment) Plan(ctx context.Context) (deploy.SequenceOperation, error) {
|
func (d *Deployment) Plan(ctx context.Context) (Plan, error) {
|
||||||
if d.plan != nil {
|
if d.plan != nil {
|
||||||
return *d.plan, nil
|
return *d.plan, nil
|
||||||
}
|
}
|
||||||
plan := deploy.SequenceOperation{}
|
var plan Plan
|
||||||
|
|
||||||
// Generate service specs for all services in the project.
|
// Generate service specs for all services in the project.
|
||||||
var serviceSpecs []api.ServiceSpec
|
var serviceSpecs []api.ServiceSpec
|
||||||
@@ -89,9 +90,7 @@ func (d *Deployment) Plan(ctx context.Context) (deploy.SequenceOperation, error)
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return plan, err
|
return plan, err
|
||||||
}
|
}
|
||||||
for _, op := range volumeOps {
|
plan.Volumes = volumeOps
|
||||||
plan.Operations = append(plan.Operations, op)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, spec := range serviceSpecs {
|
for _, spec := range serviceSpecs {
|
||||||
// TODO: properly handle depends_on conditions in the service deployment plan as the first operation.
|
// TODO: properly handle depends_on conditions in the service deployment plan as the first operation.
|
||||||
@@ -104,7 +103,7 @@ func (d *Deployment) Plan(ctx context.Context) (deploy.SequenceOperation, error)
|
|||||||
|
|
||||||
// Skip no-op (up-to-date) service plans.
|
// Skip no-op (up-to-date) service plans.
|
||||||
if len(servicePlan.Operations) > 0 {
|
if len(servicePlan.Operations) > 0 {
|
||||||
plan.Operations = append(plan.Operations, &servicePlan)
|
plan.Services = append(plan.Services, &servicePlan)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,7 +122,7 @@ func (d *Deployment) ServiceSpec(name string) (api.ServiceSpec, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// PlanVolumes checks if the external volumes exist and plans the creation of missing volumes.
|
// PlanVolumes checks if the external volumes exist and plans the creation of missing volumes.
|
||||||
func (d *Deployment) planVolumes(serviceSpecs []api.ServiceSpec) ([]*deploy.CreateVolumeOperation, error) {
|
func (d *Deployment) planVolumes(serviceSpecs []api.ServiceSpec) ([]*operation.CreateVolumeOperation, error) {
|
||||||
if len(d.Project.Volumes) == 0 {
|
if len(d.Project.Volumes) == 0 {
|
||||||
// No volumes to check or create.
|
// No volumes to check or create.
|
||||||
return nil, nil
|
return nil, nil
|
||||||
@@ -145,7 +144,7 @@ func (d *Deployment) planVolumes(serviceSpecs []api.ServiceSpec) ([]*deploy.Crea
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Generate operations to create scheduled missing volumes.
|
// Generate operations to create scheduled missing volumes.
|
||||||
var ops []*deploy.CreateVolumeOperation
|
var ops []*operation.CreateVolumeOperation
|
||||||
for machineID, volumes := range scheduledVolumes {
|
for machineID, volumes := range scheduledVolumes {
|
||||||
for _, v := range volumes {
|
for _, v := range volumes {
|
||||||
machineName := machineID
|
machineName := machineID
|
||||||
@@ -153,7 +152,7 @@ func (d *Deployment) planVolumes(serviceSpecs []api.ServiceSpec) ([]*deploy.Crea
|
|||||||
machineName = m.Info.Name
|
machineName = m.Info.Name
|
||||||
}
|
}
|
||||||
|
|
||||||
ops = append(ops, &deploy.CreateVolumeOperation{
|
ops = append(ops, &operation.CreateVolumeOperation{
|
||||||
MachineID: machineID,
|
MachineID: machineID,
|
||||||
MachineName: machineName,
|
MachineName: machineName,
|
||||||
VolumeSpec: v,
|
VolumeSpec: v,
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ const MachinesExtensionKey = "x-machines"
|
|||||||
type MachinesSource []string
|
type MachinesSource []string
|
||||||
|
|
||||||
// DecodeMapstructure implements custom decoding for multiple input types
|
// DecodeMapstructure implements custom decoding for multiple input types
|
||||||
func (m *MachinesSource) DecodeMapstructure(value interface{}) error {
|
func (m *MachinesSource) DecodeMapstructure(value any) error {
|
||||||
switch v := value.(type) {
|
switch v := value.(type) {
|
||||||
case *MachinesSource:
|
case *MachinesSource:
|
||||||
// Handle case where compose-go passes a pointer to an already created instance
|
// Handle case where compose-go passes a pointer to an already created instance
|
||||||
@@ -38,7 +38,7 @@ func (m *MachinesSource) DecodeMapstructure(value interface{}) error {
|
|||||||
}
|
}
|
||||||
*m = MachinesSource(machines)
|
*m = MachinesSource(machines)
|
||||||
return nil
|
return nil
|
||||||
case []interface{}:
|
case []any:
|
||||||
// Support interface array that may come from YAML parsing
|
// Support interface array that may come from YAML parsing
|
||||||
machineNames := make([]string, 0, len(v))
|
machineNames := make([]string, 0, len(v))
|
||||||
for i, machine := range v {
|
for i, machine := range v {
|
||||||
|
|||||||
@@ -0,0 +1,130 @@
|
|||||||
|
package compose
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"charm.land/lipgloss/v2"
|
||||||
|
"github.com/psviderski/uncloud/internal/cli/tui"
|
||||||
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
|
"github.com/psviderski/uncloud/pkg/client/deploy"
|
||||||
|
"github.com/psviderski/uncloud/pkg/client/deploy/operation"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Plan holds the compose-level deployment plan with volume and service operations.
|
||||||
|
type Plan struct {
|
||||||
|
Volumes []*operation.CreateVolumeOperation
|
||||||
|
Services []*deploy.ServicePlan
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsEmpty returns true if the plan has no volume or service operations.
|
||||||
|
func (p *Plan) IsEmpty() bool {
|
||||||
|
return len(p.Volumes) == 0 && len(p.Services) == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format renders the entire deployment plan as a styled tree with a summary footer.
|
||||||
|
func (p *Plan) Format() string {
|
||||||
|
var out strings.Builder
|
||||||
|
|
||||||
|
// Format volume operations.
|
||||||
|
for _, op := range p.Volumes {
|
||||||
|
out.WriteString(op.Format())
|
||||||
|
out.WriteString("\n")
|
||||||
|
}
|
||||||
|
if len(p.Volumes) > 0 {
|
||||||
|
out.WriteString("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format service plans.
|
||||||
|
for _, svcPlan := range p.Services {
|
||||||
|
out.WriteString(svcPlan.Format())
|
||||||
|
out.WriteString("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format summary footer.
|
||||||
|
summary := p.formatSummary()
|
||||||
|
out.WriteString(tui.Faint.Render(strings.Repeat("─", lipgloss.Width(summary))))
|
||||||
|
out.WriteString("\n")
|
||||||
|
out.WriteString(summary)
|
||||||
|
out.WriteString("\n")
|
||||||
|
|
||||||
|
return out.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// formatSummary counts all operations across the plan and renders the summary footer.
|
||||||
|
func (p *Plan) formatSummary() string {
|
||||||
|
var createCount, startFirstCount, stopFirstCount, removeCount int
|
||||||
|
machines := make(map[string]struct{})
|
||||||
|
|
||||||
|
for _, op := range p.Volumes {
|
||||||
|
machines[op.MachineID] = struct{}{}
|
||||||
|
createCount++
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, svcPlan := range p.Services {
|
||||||
|
for _, op := range svcPlan.Operations {
|
||||||
|
switch o := op.(type) {
|
||||||
|
case *operation.RunContainerOperation:
|
||||||
|
machines[o.MachineID] = struct{}{}
|
||||||
|
createCount++
|
||||||
|
case *operation.ReplaceContainerOperation:
|
||||||
|
machines[o.MachineID] = struct{}{}
|
||||||
|
if o.Order == api.UpdateOrderStopFirst {
|
||||||
|
stopFirstCount++
|
||||||
|
} else {
|
||||||
|
startFirstCount++
|
||||||
|
}
|
||||||
|
case *operation.RemoveContainerOperation:
|
||||||
|
machines[o.MachineID] = struct{}{}
|
||||||
|
removeCount++
|
||||||
|
case *operation.StopContainerOperation:
|
||||||
|
machines[o.MachineID] = struct{}{}
|
||||||
|
removeCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var parts []string
|
||||||
|
if createCount > 0 {
|
||||||
|
parts = append(parts,
|
||||||
|
tui.BoldGreen.Render(strconv.Itoa(createCount))+" "+tui.Green.Render("create"))
|
||||||
|
}
|
||||||
|
if startFirstCount > 0 {
|
||||||
|
parts = append(parts,
|
||||||
|
tui.BoldGreen.Render(strconv.Itoa(startFirstCount))+" "+tui.Green.Render("replace (start-first)"))
|
||||||
|
}
|
||||||
|
if stopFirstCount > 0 {
|
||||||
|
parts = append(parts,
|
||||||
|
tui.BoldYellow.Render(strconv.Itoa(stopFirstCount))+" "+tui.Yellow.Render("replace (stop-first)"))
|
||||||
|
}
|
||||||
|
if removeCount > 0 {
|
||||||
|
parts = append(parts,
|
||||||
|
tui.BoldRed.Render(strconv.Itoa(removeCount))+" "+tui.Red.Render("remove"))
|
||||||
|
}
|
||||||
|
|
||||||
|
machinesWord := "machines"
|
||||||
|
if len(machines) == 1 {
|
||||||
|
machinesWord = "machine"
|
||||||
|
}
|
||||||
|
parts = append(parts, fmt.Sprintf("across %s %s", tui.Bold.Render(strconv.Itoa(len(machines))), machinesWord))
|
||||||
|
|
||||||
|
sep := " " + tui.Faint.Render("·") + " "
|
||||||
|
return strings.Join(parts, sep)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute runs all volume operations followed by all service operations.
|
||||||
|
func (p *Plan) Execute(ctx context.Context, cli operation.Client) error {
|
||||||
|
for _, op := range p.Volumes {
|
||||||
|
if err := op.Execute(ctx, cli); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, sp := range p.Services {
|
||||||
|
if err := sp.Execute(ctx, cli); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -6,13 +6,23 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
composecli "github.com/compose-spec/compose-go/v2/cli"
|
composecli "github.com/compose-spec/compose-go/v2/cli"
|
||||||
|
"github.com/compose-spec/compose-go/v2/transform"
|
||||||
|
"github.com/compose-spec/compose-go/v2/tree"
|
||||||
"github.com/compose-spec/compose-go/v2/types"
|
"github.com/compose-spec/compose-go/v2/types"
|
||||||
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var registerComposeOverrides sync.Once
|
||||||
|
|
||||||
// LoadProject loads a Compose project from the default locations or the given paths.
|
// LoadProject loads a Compose project from the default locations or the given paths.
|
||||||
func LoadProject(ctx context.Context, paths []string, opts ...composecli.ProjectOptionsFn) (*types.Project, error) {
|
func LoadProject(ctx context.Context, paths []string, opts ...composecli.ProjectOptionsFn) (*types.Project, error) {
|
||||||
|
registerComposeOverrides.Do(func() {
|
||||||
|
transform.RegisterDefaultValue("services.*.deploy.update_config", setUpdateConfigDefaults)
|
||||||
|
})
|
||||||
|
|
||||||
defaultOpts := []composecli.ProjectOptionsFn{
|
defaultOpts := []composecli.ProjectOptionsFn{
|
||||||
// First apply os.Environment, always wins.
|
// First apply os.Environment, always wins.
|
||||||
composecli.WithOsEnv,
|
composecli.WithOsEnv,
|
||||||
@@ -92,3 +102,14 @@ func removeProjectPrefixFromNames(project *types.Project) {
|
|||||||
project.Volumes[name] = vol
|
project.Volumes[name] = vol
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// setUpdateConfigDefaults sets default values for deploy.update_config attributes when not specified in the compose file.
|
||||||
|
func setUpdateConfigDefaults(data any, _ tree.Path, _ bool) (any, error) {
|
||||||
|
switch v := data.(type) {
|
||||||
|
case map[string]any:
|
||||||
|
if _, ok := v["monitor"]; !ok {
|
||||||
|
v["monitor"] = api.DefaultHealthMonitorPeriod.String()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
|||||||
+116
-12
@@ -5,12 +5,14 @@ import (
|
|||||||
"maps"
|
"maps"
|
||||||
"os"
|
"os"
|
||||||
"slices"
|
"slices"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/compose-spec/compose-go/v2/types"
|
"github.com/compose-spec/compose-go/v2/types"
|
||||||
"github.com/docker/docker/api/types/container"
|
"github.com/docker/docker/api/types/container"
|
||||||
"github.com/docker/docker/api/types/mount"
|
"github.com/docker/docker/api/types/mount"
|
||||||
"github.com/opencontainers/go-digest"
|
"github.com/opencontainers/go-digest"
|
||||||
"github.com/psviderski/uncloud/pkg/api"
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
|
cdi "tags.cncf.io/container-device-interface/pkg/parser"
|
||||||
)
|
)
|
||||||
|
|
||||||
func ServiceSpecFromCompose(project *types.Project, serviceName string) (api.ServiceSpec, error) {
|
func ServiceSpecFromCompose(project *types.Project, serviceName string) (api.ServiceSpec, error) {
|
||||||
@@ -43,15 +45,20 @@ func ServiceSpecFromCompose(project *types.Project, serviceName string) (api.Ser
|
|||||||
|
|
||||||
spec := api.ServiceSpec{
|
spec := api.ServiceSpec{
|
||||||
Container: api.ContainerSpec{
|
Container: api.ContainerSpec{
|
||||||
Command: service.Command,
|
CapAdd: service.CapAdd,
|
||||||
Entrypoint: service.Entrypoint,
|
CapDrop: service.CapDrop,
|
||||||
Env: env,
|
Command: service.Command,
|
||||||
Image: service.Image,
|
Entrypoint: service.Entrypoint,
|
||||||
Init: service.Init,
|
Env: env,
|
||||||
Privileged: service.Privileged,
|
Healthcheck: healthcheckFromCompose(service.HealthCheck),
|
||||||
PullPolicy: pullPolicy,
|
Image: service.Image,
|
||||||
Resources: resourcesFromCompose(service),
|
Init: service.Init,
|
||||||
User: service.User,
|
PidMode: service.Pid,
|
||||||
|
Privileged: service.Privileged,
|
||||||
|
PullPolicy: pullPolicy,
|
||||||
|
Resources: resourcesFromCompose(service),
|
||||||
|
Sysctls: service.Sysctls,
|
||||||
|
User: service.User,
|
||||||
},
|
},
|
||||||
Name: serviceName,
|
Name: serviceName,
|
||||||
Mode: api.ServiceModeReplicated,
|
Mode: api.ServiceModeReplicated,
|
||||||
@@ -79,6 +86,11 @@ func ServiceSpecFromCompose(project *types.Project, serviceName string) (api.Ser
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if service.StopGracePeriod != nil {
|
||||||
|
d := time.Duration(*service.StopGracePeriod)
|
||||||
|
spec.Container.StopGracePeriod = &d
|
||||||
|
}
|
||||||
|
|
||||||
if service.Scale != nil {
|
if service.Scale != nil {
|
||||||
spec.Replicas = uint(*service.Scale)
|
spec.Replicas = uint(*service.Scale)
|
||||||
}
|
}
|
||||||
@@ -94,6 +106,22 @@ func ServiceSpecFromCompose(project *types.Project, serviceName string) (api.Ser
|
|||||||
default:
|
default:
|
||||||
return spec, fmt.Errorf("unsupported deploy mode: '%s'", service.Deploy.Mode)
|
return spec, fmt.Errorf("unsupported deploy mode: '%s'", service.Deploy.Mode)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if cfg := service.Deploy.UpdateConfig; cfg != nil {
|
||||||
|
switch cfg.Order {
|
||||||
|
case "":
|
||||||
|
// No order specified, use default behavior.
|
||||||
|
case "start-first":
|
||||||
|
spec.UpdateConfig.Order = api.UpdateOrderStartFirst
|
||||||
|
case "stop-first":
|
||||||
|
spec.UpdateConfig.Order = api.UpdateOrderStopFirst
|
||||||
|
default:
|
||||||
|
return spec, fmt.Errorf("unsupported deploy.update_config.order: '%s'", cfg.Order)
|
||||||
|
}
|
||||||
|
|
||||||
|
d := time.Duration(cfg.Monitor)
|
||||||
|
spec.UpdateConfig.MonitorPeriod = &d
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: can service.tmpfs be handled as tmpfs volume mounts as well?
|
// TODO: can service.tmpfs be handled as tmpfs volume mounts as well?
|
||||||
@@ -117,11 +145,62 @@ func ServiceSpecFromCompose(project *types.Project, serviceName string) (api.Ser
|
|||||||
return spec, nil
|
return spec, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func healthcheckFromCompose(hc *types.HealthCheckConfig) *api.HealthcheckSpec {
|
||||||
|
if hc == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if hc.Disable {
|
||||||
|
return &api.HealthcheckSpec{Disable: true}
|
||||||
|
}
|
||||||
|
|
||||||
|
spec := &api.HealthcheckSpec{Test: hc.Test}
|
||||||
|
if hc.Interval != nil {
|
||||||
|
spec.Interval = time.Duration(*hc.Interval)
|
||||||
|
}
|
||||||
|
if hc.Timeout != nil {
|
||||||
|
spec.Timeout = time.Duration(*hc.Timeout)
|
||||||
|
}
|
||||||
|
if hc.StartPeriod != nil {
|
||||||
|
spec.StartPeriod = time.Duration(*hc.StartPeriod)
|
||||||
|
}
|
||||||
|
if hc.StartInterval != nil {
|
||||||
|
spec.StartInterval = time.Duration(*hc.StartInterval)
|
||||||
|
}
|
||||||
|
if hc.Retries != nil {
|
||||||
|
spec.Retries = uint(*hc.Retries)
|
||||||
|
}
|
||||||
|
|
||||||
|
return spec
|
||||||
|
}
|
||||||
|
|
||||||
func resourcesFromCompose(service types.ServiceConfig) api.ContainerResources {
|
func resourcesFromCompose(service types.ServiceConfig) api.ContainerResources {
|
||||||
resources := api.ContainerResources{
|
resources := api.ContainerResources{
|
||||||
CPU: int64(service.CPUS * 1e9),
|
CPU: int64(service.CPUS * 1e9),
|
||||||
Memory: int64(service.MemLimit),
|
Memory: int64(service.MemLimit),
|
||||||
MemoryReservation: int64(service.MemReservation),
|
MemoryReservation: int64(service.MemReservation),
|
||||||
|
SharedMemory: int64(service.ShmSize),
|
||||||
|
Ulimits: ulimitsFromCompose(service.Ulimits),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert device mappings, separating CDI devices from regular device mappings.
|
||||||
|
// CDI devices are identified when Source == Target and the source is a qualified CDI name.
|
||||||
|
var cdiDeviceNames []string
|
||||||
|
for _, dev := range service.Devices {
|
||||||
|
if dev.Source == dev.Target && cdi.IsQualifiedName(dev.Source) {
|
||||||
|
cdiDeviceNames = append(cdiDeviceNames, dev.Source)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
resources.Devices = append(resources.Devices, api.DeviceMapping{
|
||||||
|
HostPath: dev.Source,
|
||||||
|
ContainerPath: dev.Target,
|
||||||
|
CgroupPermissions: dev.Permissions,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if len(cdiDeviceNames) > 0 {
|
||||||
|
resources.DeviceReservations = append(resources.DeviceReservations, container.DeviceRequest{
|
||||||
|
Driver: "cdi",
|
||||||
|
DeviceIDs: cdiDeviceNames,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert GPU device requests from compose format, appending "gpu" capability.
|
// Convert GPU device requests from compose format, appending "gpu" capability.
|
||||||
@@ -268,9 +347,7 @@ func dockerVolumeSpecFromCompose(serviceVolume types.ServiceVolumeConfig, volume
|
|||||||
func mergeLabels(labels ...types.Labels) types.Labels {
|
func mergeLabels(labels ...types.Labels) types.Labels {
|
||||||
merged := types.Labels{}
|
merged := types.Labels{}
|
||||||
for _, l := range labels {
|
for _, l := range labels {
|
||||||
for k, v := range l {
|
maps.Copy(merged, l)
|
||||||
merged[k] = v
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return merged
|
return merged
|
||||||
}
|
}
|
||||||
@@ -290,6 +367,33 @@ func tmpfsVolumeSpecFromCompose(serviceVolume types.ServiceVolumeConfig) api.Vol
|
|||||||
return spec
|
return spec
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ulimitsFromCompose(ulimits map[string]*types.UlimitsConfig) map[string]api.Ulimit {
|
||||||
|
if len(ulimits) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
res := make(map[string]api.Ulimit, len(ulimits))
|
||||||
|
for name, u := range ulimits {
|
||||||
|
soft := u.Soft
|
||||||
|
hard := u.Hard
|
||||||
|
if u.Single != 0 {
|
||||||
|
if soft == 0 {
|
||||||
|
soft = u.Single
|
||||||
|
}
|
||||||
|
if hard == 0 {
|
||||||
|
hard = u.Single
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
res[name] = api.Ulimit{
|
||||||
|
Soft: int64(soft),
|
||||||
|
Hard: int64(hard),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
// validateServicesExtensions validates extension combinations across all services in the project.
|
// validateServicesExtensions validates extension combinations across all services in the project.
|
||||||
func validateServicesExtensions(project *types.Project) error {
|
func validateServicesExtensions(project *types.Project) error {
|
||||||
for _, service := range project.Services {
|
for _, service := range project.Services {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
composecli "github.com/compose-spec/compose-go/v2/cli"
|
composecli "github.com/compose-spec/compose-go/v2/cli"
|
||||||
"github.com/docker/docker/api/types/container"
|
"github.com/docker/docker/api/types/container"
|
||||||
@@ -93,13 +94,15 @@ func TestServiceSpecFromCompose(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "full-spec",
|
name: "full spec",
|
||||||
filename: "compose-full-spec.yaml",
|
filename: "compose-full-spec.yaml",
|
||||||
want: map[string]api.ServiceSpec{
|
want: map[string]api.ServiceSpec{
|
||||||
"test": {
|
"test": {
|
||||||
Name: "test",
|
Name: "test",
|
||||||
Mode: api.ServiceModeReplicated,
|
Mode: api.ServiceModeReplicated,
|
||||||
Container: api.ContainerSpec{
|
Container: api.ContainerSpec{
|
||||||
|
CapAdd: []string{"NET_ADMIN"},
|
||||||
|
CapDrop: []string{"ALL"},
|
||||||
Command: []string{"nginx", "updated", "command"},
|
Command: []string{"nginx", "updated", "command"},
|
||||||
Entrypoint: []string{"/updated-docker-entrypoint.sh"},
|
Entrypoint: []string{"/updated-docker-entrypoint.sh"},
|
||||||
Env: map[string]string{
|
Env: map[string]string{
|
||||||
@@ -107,6 +110,14 @@ func TestServiceSpecFromCompose(t *testing.T) {
|
|||||||
"EMPTY": "",
|
"EMPTY": "",
|
||||||
"VAR": "value",
|
"VAR": "value",
|
||||||
},
|
},
|
||||||
|
Healthcheck: &api.HealthcheckSpec{
|
||||||
|
Test: []string{"CMD", "curl", "-f", "http://localhost"},
|
||||||
|
Interval: 1*time.Minute + 30*time.Second,
|
||||||
|
Timeout: 10 * time.Second,
|
||||||
|
Retries: 5,
|
||||||
|
StartPeriod: 15 * time.Second,
|
||||||
|
StartInterval: 2 * time.Second,
|
||||||
|
},
|
||||||
Image: "nginx:latest",
|
Image: "nginx:latest",
|
||||||
Init: &initTrue,
|
Init: &initTrue,
|
||||||
LogDriver: &api.LogDriver{
|
LogDriver: &api.LogDriver{
|
||||||
@@ -116,12 +127,29 @@ func TestServiceSpecFromCompose(t *testing.T) {
|
|||||||
"max-file": "3",
|
"max-file": "3",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
PidMode: "host",
|
||||||
Privileged: true,
|
Privileged: true,
|
||||||
PullPolicy: api.PullPolicyAlways,
|
PullPolicy: api.PullPolicyAlways,
|
||||||
Resources: api.ContainerResources{
|
Resources: api.ContainerResources{
|
||||||
CPU: 0.5 * api.Core,
|
CPU: 0.5 * api.Core,
|
||||||
Memory: 100 * units.MiB,
|
Memory: 100 * units.MiB,
|
||||||
MemoryReservation: 50 * units.MiB,
|
MemoryReservation: 50 * units.MiB,
|
||||||
|
SharedMemory: 256 * units.MiB,
|
||||||
|
Ulimits: map[string]api.Ulimit{
|
||||||
|
"nofile": {Soft: 20000, Hard: 40000},
|
||||||
|
"nproc": {Soft: 65535, Hard: 65535},
|
||||||
|
},
|
||||||
|
Devices: []api.DeviceMapping{
|
||||||
|
{HostPath: "/dev/ttyUSB0", ContainerPath: "/dev/ttyUSB0", CgroupPermissions: "rw"},
|
||||||
|
{HostPath: "/dev/sda", ContainerPath: "/dev/xvda", CgroupPermissions: "rwm"},
|
||||||
|
},
|
||||||
|
DeviceReservations: []container.DeviceRequest{
|
||||||
|
{Count: -1, Capabilities: [][]string{{"gpu"}}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
StopGracePeriod: new(30 * time.Second),
|
||||||
|
Sysctls: map[string]string{
|
||||||
|
"net.ipv4.ip_forward": "1",
|
||||||
},
|
},
|
||||||
User: "nginx:nginx",
|
User: "nginx:nginx",
|
||||||
VolumeMounts: []api.VolumeMount{
|
VolumeMounts: []api.VolumeMount{
|
||||||
@@ -134,6 +162,15 @@ func TestServiceSpecFromCompose(t *testing.T) {
|
|||||||
VolumeName: "data1",
|
VolumeName: "data1",
|
||||||
ContainerPath: "/data1",
|
ContainerPath: "/data1",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
VolumeName: "bind-53f1acbf1de61e9e608c93effca23791674e463d02bb7aaca7c625804aef1926",
|
||||||
|
ContainerPath: "/path/in/container",
|
||||||
|
ReadOnly: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
VolumeName: "data3-labeled",
|
||||||
|
ContainerPath: "/data3",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
VolumeName: "data2-alias",
|
VolumeName: "data2-alias",
|
||||||
ContainerPath: "/data2/long/syntax",
|
ContainerPath: "/data2/long/syntax",
|
||||||
@@ -168,8 +205,24 @@ func TestServiceSpecFromCompose(t *testing.T) {
|
|||||||
Mode: api.PortModeHost,
|
Mode: api.PortModeHost,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
Placement: api.Placement{
|
||||||
|
Machines: []string{"machine-1", "machine-2"},
|
||||||
|
},
|
||||||
Replicas: 3,
|
Replicas: 3,
|
||||||
|
UpdateConfig: api.UpdateConfig{
|
||||||
|
Order: api.UpdateOrderStopFirst,
|
||||||
|
MonitorPeriod: &api.DefaultHealthMonitorPeriod,
|
||||||
|
},
|
||||||
Volumes: []api.VolumeSpec{
|
Volumes: []api.VolumeSpec{
|
||||||
|
{
|
||||||
|
Name: "bind-53f1acbf1de61e9e608c93effca23791674e463d02bb7aaca7c625804aef1926",
|
||||||
|
Type: api.VolumeTypeBind,
|
||||||
|
BindOptions: &api.BindOptions{
|
||||||
|
HostPath: "/path/on/host",
|
||||||
|
CreateHostPath: true,
|
||||||
|
Propagation: mount.Propagation("rprivate"),
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
Name: "bind-bb6aed1683cea1e0a1ae5cd227aacd0734f2f87f7a78fcf1baeff978ce300b90",
|
Name: "bind-bb6aed1683cea1e0a1ae5cd227aacd0734f2f87f7a78fcf1baeff978ce300b90",
|
||||||
Type: api.VolumeTypeBind,
|
Type: api.VolumeTypeBind,
|
||||||
@@ -202,11 +255,22 @@ func TestServiceSpecFromCompose(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: "data3-labeled",
|
||||||
|
Type: api.VolumeTypeVolume,
|
||||||
|
VolumeOptions: &api.VolumeOptions{
|
||||||
|
Name: "data3-labeled",
|
||||||
|
NoCopy: true,
|
||||||
|
SubPath: "app/data",
|
||||||
|
Labels: map[string]string{"env": "test"},
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
Name: "tmpfs-efa57ba8b6a1779674ac438de3af8729e2d55900b79eb929431cf9c5b0179542",
|
Name: "tmpfs-efa57ba8b6a1779674ac438de3af8729e2d55900b79eb929431cf9c5b0179542",
|
||||||
Type: api.VolumeTypeTmpfs,
|
Type: api.VolumeTypeTmpfs,
|
||||||
TmpfsOptions: &mount.TmpfsOptions{
|
TmpfsOptions: &mount.TmpfsOptions{
|
||||||
SizeBytes: 10 * units.MiB,
|
SizeBytes: 10 * units.MiB,
|
||||||
|
Mode: os.FileMode(1770),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -252,6 +316,8 @@ func TestServiceSpecFromCompose(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestServiceSpecFromCompose_Caddy(t *testing.T) {
|
func TestServiceSpecFromCompose_Caddy(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
composeYAML string
|
composeYAML string
|
||||||
@@ -400,6 +466,8 @@ services:
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestServiceSpecFromCompose_GPUs(t *testing.T) {
|
func TestServiceSpecFromCompose_GPUs(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
composeYAML string
|
composeYAML string
|
||||||
@@ -587,6 +655,8 @@ services:
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestServiceSpecFromCompose_VolumeDriverOpts(t *testing.T) {
|
func TestServiceSpecFromCompose_VolumeDriverOpts(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
composeYAML string
|
composeYAML string
|
||||||
@@ -694,7 +764,269 @@ volumes:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestServiceSpecFromCompose_Ulimits(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
composeYAML string
|
||||||
|
expected map[string]api.Ulimit
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "single ulimit with soft and hard limits",
|
||||||
|
composeYAML: `
|
||||||
|
services:
|
||||||
|
db:
|
||||||
|
image: postgres
|
||||||
|
ulimits:
|
||||||
|
nofile:
|
||||||
|
soft: 20000
|
||||||
|
hard: 40000
|
||||||
|
`,
|
||||||
|
expected: map[string]api.Ulimit{
|
||||||
|
"nofile": {
|
||||||
|
Soft: 20000,
|
||||||
|
Hard: 40000,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "single ulimit with single value (soft=hard)",
|
||||||
|
composeYAML: `
|
||||||
|
services:
|
||||||
|
db:
|
||||||
|
image: postgres
|
||||||
|
ulimits:
|
||||||
|
nproc: 65535
|
||||||
|
`,
|
||||||
|
expected: map[string]api.Ulimit{
|
||||||
|
"nproc": {
|
||||||
|
Soft: 65535,
|
||||||
|
Hard: 65535,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "multiple ulimits",
|
||||||
|
composeYAML: `
|
||||||
|
services:
|
||||||
|
db:
|
||||||
|
image: postgres
|
||||||
|
ulimits:
|
||||||
|
nofile:
|
||||||
|
soft: 20000
|
||||||
|
hard: 40000
|
||||||
|
nproc: 65535
|
||||||
|
`,
|
||||||
|
expected: map[string]api.Ulimit{
|
||||||
|
"nofile": {
|
||||||
|
Soft: 20000,
|
||||||
|
Hard: 40000,
|
||||||
|
},
|
||||||
|
"nproc": {
|
||||||
|
Soft: 65535,
|
||||||
|
Hard: 65535,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty ulimits",
|
||||||
|
composeYAML: `
|
||||||
|
services:
|
||||||
|
db:
|
||||||
|
image: postgres
|
||||||
|
ulimits: {}
|
||||||
|
`,
|
||||||
|
expected: nil,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
project, err := LoadProjectFromContent(context.Background(), tt.composeYAML)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
spec, err := ServiceSpecFromCompose(project, "db")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, tt.expected, spec.Container.Resources.Ulimits)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestServiceSpecFromCompose_UpdateConfig(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
composeYAML string
|
||||||
|
expected api.UpdateConfig
|
||||||
|
expectError bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "no update_config",
|
||||||
|
composeYAML: `
|
||||||
|
services:
|
||||||
|
test:
|
||||||
|
image: nginx
|
||||||
|
`,
|
||||||
|
expected: api.UpdateConfig{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty update_config",
|
||||||
|
composeYAML: `
|
||||||
|
services:
|
||||||
|
test:
|
||||||
|
image: nginx
|
||||||
|
deploy:
|
||||||
|
update_config: {}
|
||||||
|
`,
|
||||||
|
expected: api.UpdateConfig{
|
||||||
|
MonitorPeriod: &api.DefaultHealthMonitorPeriod,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "update_config with unsupported attributes ignored",
|
||||||
|
composeYAML: `
|
||||||
|
services:
|
||||||
|
test:
|
||||||
|
image: nginx
|
||||||
|
deploy:
|
||||||
|
update_config:
|
||||||
|
parallelism: 1
|
||||||
|
`,
|
||||||
|
expected: api.UpdateConfig{
|
||||||
|
MonitorPeriod: &api.DefaultHealthMonitorPeriod,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "update_config with stop-first order",
|
||||||
|
composeYAML: `
|
||||||
|
services:
|
||||||
|
test:
|
||||||
|
image: postgres
|
||||||
|
deploy:
|
||||||
|
update_config:
|
||||||
|
order: stop-first
|
||||||
|
`,
|
||||||
|
expected: api.UpdateConfig{
|
||||||
|
Order: api.UpdateOrderStopFirst,
|
||||||
|
MonitorPeriod: &api.DefaultHealthMonitorPeriod,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "update_config with start-first order",
|
||||||
|
composeYAML: `
|
||||||
|
services:
|
||||||
|
test:
|
||||||
|
image: nginx
|
||||||
|
deploy:
|
||||||
|
update_config:
|
||||||
|
order: start-first
|
||||||
|
`,
|
||||||
|
expected: api.UpdateConfig{
|
||||||
|
Order: api.UpdateOrderStartFirst,
|
||||||
|
MonitorPeriod: &api.DefaultHealthMonitorPeriod,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "update_config with invalid order",
|
||||||
|
composeYAML: `
|
||||||
|
services:
|
||||||
|
test:
|
||||||
|
image: nginx
|
||||||
|
deploy:
|
||||||
|
update_config:
|
||||||
|
order: invalid-order
|
||||||
|
`,
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "update_config with replicas and order",
|
||||||
|
composeYAML: `
|
||||||
|
services:
|
||||||
|
test:
|
||||||
|
image: nginx
|
||||||
|
deploy:
|
||||||
|
replicas: 3
|
||||||
|
update_config:
|
||||||
|
order: stop-first
|
||||||
|
`,
|
||||||
|
expected: api.UpdateConfig{
|
||||||
|
Order: api.UpdateOrderStopFirst,
|
||||||
|
MonitorPeriod: &api.DefaultHealthMonitorPeriod,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "update_config with custom monitor",
|
||||||
|
composeYAML: `
|
||||||
|
services:
|
||||||
|
test:
|
||||||
|
image: nginx
|
||||||
|
deploy:
|
||||||
|
update_config:
|
||||||
|
monitor: 10s
|
||||||
|
`,
|
||||||
|
expected: api.UpdateConfig{
|
||||||
|
MonitorPeriod: api.AsPtr(10 * time.Second),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "update_config with monitor and order",
|
||||||
|
composeYAML: `
|
||||||
|
services:
|
||||||
|
test:
|
||||||
|
image: nginx
|
||||||
|
deploy:
|
||||||
|
update_config:
|
||||||
|
order: start-first
|
||||||
|
monitor: 30s
|
||||||
|
`,
|
||||||
|
expected: api.UpdateConfig{
|
||||||
|
Order: api.UpdateOrderStartFirst,
|
||||||
|
MonitorPeriod: api.AsPtr(30 * time.Second),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "update_config with zero monitor skips monitoring",
|
||||||
|
composeYAML: `
|
||||||
|
services:
|
||||||
|
test:
|
||||||
|
image: nginx
|
||||||
|
deploy:
|
||||||
|
update_config:
|
||||||
|
monitor: 0s
|
||||||
|
`,
|
||||||
|
expected: api.UpdateConfig{
|
||||||
|
MonitorPeriod: api.AsPtr(time.Duration(0)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
project, err := LoadProjectFromContent(context.Background(), tt.composeYAML)
|
||||||
|
if tt.expectError {
|
||||||
|
require.Error(t, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
spec, err := ServiceSpecFromCompose(project, "test")
|
||||||
|
if tt.expectError {
|
||||||
|
assert.Error(t, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, tt.expected, spec.UpdateConfig)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestServiceSpecFromCompose_XMachinesPlacement(t *testing.T) {
|
func TestServiceSpecFromCompose_XMachinesPlacement(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
composeYAML string
|
composeYAML string
|
||||||
@@ -851,3 +1183,104 @@ services:
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestServiceSpecFromCompose_Devices(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
composeYAML string
|
||||||
|
expectedDevices []api.DeviceMapping
|
||||||
|
expectedReservations []container.DeviceRequest
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "simple device",
|
||||||
|
composeYAML: `
|
||||||
|
services:
|
||||||
|
test:
|
||||||
|
image: nginx
|
||||||
|
devices:
|
||||||
|
- /dev/dri
|
||||||
|
`,
|
||||||
|
expectedDevices: []api.DeviceMapping{
|
||||||
|
{HostPath: "/dev/dri", ContainerPath: "/dev/dri", CgroupPermissions: "rwm"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "device with target and permissions",
|
||||||
|
composeYAML: `
|
||||||
|
services:
|
||||||
|
test:
|
||||||
|
image: nginx
|
||||||
|
devices:
|
||||||
|
- /dev/sda:/dev/xvda:r
|
||||||
|
`,
|
||||||
|
expectedDevices: []api.DeviceMapping{
|
||||||
|
{HostPath: "/dev/sda", ContainerPath: "/dev/xvda", CgroupPermissions: "r"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "multiple devices",
|
||||||
|
composeYAML: `
|
||||||
|
services:
|
||||||
|
test:
|
||||||
|
image: nginx
|
||||||
|
devices:
|
||||||
|
- "/dev/ttyUSB0:/dev/ttyUSB0:rw"
|
||||||
|
- /dev/sda:/dev/xvda
|
||||||
|
- "/dev/dri"
|
||||||
|
`,
|
||||||
|
expectedDevices: []api.DeviceMapping{
|
||||||
|
{HostPath: "/dev/ttyUSB0", ContainerPath: "/dev/ttyUSB0", CgroupPermissions: "rw"},
|
||||||
|
{HostPath: "/dev/sda", ContainerPath: "/dev/xvda", CgroupPermissions: "rwm"},
|
||||||
|
{HostPath: "/dev/dri", ContainerPath: "/dev/dri", CgroupPermissions: "rwm"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "CDI device",
|
||||||
|
composeYAML: `
|
||||||
|
services:
|
||||||
|
test:
|
||||||
|
image: nginx
|
||||||
|
devices:
|
||||||
|
- vendor.com/class=device1
|
||||||
|
`,
|
||||||
|
expectedReservations: []container.DeviceRequest{
|
||||||
|
{Driver: "cdi", DeviceIDs: []string{"vendor.com/class=device1"}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "mixed CDI and regular devices",
|
||||||
|
composeYAML: `
|
||||||
|
services:
|
||||||
|
test:
|
||||||
|
image: nginx
|
||||||
|
devices:
|
||||||
|
- /dev/dri
|
||||||
|
- vendor.com/class=device1
|
||||||
|
- nvidia.com/gpu=0
|
||||||
|
- /dev/sda:/dev/xvda:r
|
||||||
|
`,
|
||||||
|
expectedDevices: []api.DeviceMapping{
|
||||||
|
{HostPath: "/dev/dri", ContainerPath: "/dev/dri", CgroupPermissions: "rwm"},
|
||||||
|
{HostPath: "/dev/sda", ContainerPath: "/dev/xvda", CgroupPermissions: "r"},
|
||||||
|
},
|
||||||
|
expectedReservations: []container.DeviceRequest{
|
||||||
|
{Driver: "cdi", DeviceIDs: []string{"vendor.com/class=device1", "nvidia.com/gpu=0"}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
project, err := LoadProjectFromContent(context.Background(), tt.composeYAML)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
spec, err := ServiceSpecFromCompose(project, "test")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, tt.expectedDevices, spec.Container.Resources.Devices)
|
||||||
|
assert.Equal(t, tt.expectedReservations, spec.Container.Resources.DeviceReservations)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,12 +1,31 @@
|
|||||||
services:
|
services:
|
||||||
test:
|
test:
|
||||||
|
cap_add:
|
||||||
|
- NET_ADMIN
|
||||||
|
cap_drop:
|
||||||
|
- ALL
|
||||||
command: ["nginx", "updated", "command"]
|
command: ["nginx", "updated", "command"]
|
||||||
cpus: 0.5
|
cpus: 0.5
|
||||||
|
pid: host
|
||||||
|
deploy:
|
||||||
|
update_config:
|
||||||
|
order: stop-first
|
||||||
|
devices:
|
||||||
|
- /dev/ttyUSB0:/dev/ttyUSB0:rw
|
||||||
|
- /dev/sda:/dev/xvda
|
||||||
entrypoint: ["/updated-docker-entrypoint.sh"]
|
entrypoint: ["/updated-docker-entrypoint.sh"]
|
||||||
environment:
|
environment:
|
||||||
BOOL: "true"
|
BOOL: "true"
|
||||||
EMPTY: ""
|
EMPTY: ""
|
||||||
VAR: value
|
VAR: value
|
||||||
|
gpus: all
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-f", "http://localhost"]
|
||||||
|
interval: 1m30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 5
|
||||||
|
start_period: 15s
|
||||||
|
start_interval: 2s
|
||||||
image: nginx:latest
|
image: nginx:latest
|
||||||
init: true
|
init: true
|
||||||
logging:
|
logging:
|
||||||
@@ -16,13 +35,35 @@ services:
|
|||||||
max-file: 3
|
max-file: 3
|
||||||
mem_limit: 100M
|
mem_limit: 100M
|
||||||
mem_reservation: 50M
|
mem_reservation: 50M
|
||||||
|
shm_size: 268435456
|
||||||
privileged: true
|
privileged: true
|
||||||
pull_policy: always
|
pull_policy: always
|
||||||
scale: 3
|
scale: 3
|
||||||
|
stop_grace_period: 30s
|
||||||
|
sysctls:
|
||||||
|
- net.ipv4.ip_forward=1
|
||||||
|
ulimits:
|
||||||
|
nofile:
|
||||||
|
soft: 20000
|
||||||
|
hard: 40000
|
||||||
|
nproc: 65535
|
||||||
user: nginx:nginx
|
user: nginx:nginx
|
||||||
volumes:
|
volumes:
|
||||||
- /etc/passwd:/host/etc/passwd:ro
|
- /etc/passwd:/host/etc/passwd:ro
|
||||||
- data1:/data1
|
- data1:/data1
|
||||||
|
- type: bind
|
||||||
|
source: /path/on/host
|
||||||
|
target: /path/in/container
|
||||||
|
read_only: true
|
||||||
|
bind:
|
||||||
|
create_host_path: true
|
||||||
|
propagation: rprivate
|
||||||
|
- type: volume
|
||||||
|
source: data3-labeled
|
||||||
|
target: /data3
|
||||||
|
volume:
|
||||||
|
nocopy: true
|
||||||
|
subpath: app/data
|
||||||
- type: volume
|
- type: volume
|
||||||
source: data2-alias
|
source: data2-alias
|
||||||
target: /data2/long/syntax
|
target: /data2/long/syntax
|
||||||
@@ -31,6 +72,8 @@ services:
|
|||||||
target: /tmpfs
|
target: /tmpfs
|
||||||
tmpfs:
|
tmpfs:
|
||||||
size: 10485760
|
size: 10485760
|
||||||
|
mode: 1770
|
||||||
|
x-machines: ["machine-1", "machine-2"]
|
||||||
x-ports:
|
x-ports:
|
||||||
- test.example.com:80/https
|
- test.example.com:80/https
|
||||||
- 8000/http
|
- 8000/http
|
||||||
@@ -49,5 +92,8 @@ volumes:
|
|||||||
data2-alias:
|
data2-alias:
|
||||||
name: data2
|
name: data2
|
||||||
driver: local
|
driver: local
|
||||||
|
data3-labeled:
|
||||||
|
labels:
|
||||||
|
env: test
|
||||||
data-external:
|
data-external:
|
||||||
external: true
|
external: true
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package connector
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// defaultServiceConfig defines the default gRPC service configuration including retry policy for transient failures.
|
||||||
|
var defaultServiceConfig = mustMarshalJSON(map[string]any{
|
||||||
|
"methodConfig": []map[string]any{
|
||||||
|
{
|
||||||
|
"name": []map[string]string{{"service": ""}},
|
||||||
|
"retryPolicy": map[string]any{
|
||||||
|
"maxAttempts": 5, // 5 is the maximum allowed by gRPC
|
||||||
|
"initialBackoff": "0.5s",
|
||||||
|
"maxBackoff": "5s",
|
||||||
|
"backoffMultiplier": 2,
|
||||||
|
"retryableStatusCodes": []string{"UNAVAILABLE"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
func mustMarshalJSON(v any) string {
|
||||||
|
b, err := json.Marshal(v)
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Sprintf("failed to marshal service config: %v", err))
|
||||||
|
}
|
||||||
|
return string(b)
|
||||||
|
}
|
||||||
@@ -24,6 +24,16 @@ type SSHConnectorConfig struct {
|
|||||||
SockPath string
|
SockPath string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Destination returns the destination string that can be passed to ssh CLI in the format [user@]host.
|
||||||
|
func (cfg *SSHConnectorConfig) Destination() string {
|
||||||
|
dst := cfg.Host
|
||||||
|
if cfg.User != "" {
|
||||||
|
dst = fmt.Sprintf("%s@%s", cfg.User, dst)
|
||||||
|
}
|
||||||
|
|
||||||
|
return dst
|
||||||
|
}
|
||||||
|
|
||||||
// SSHConnector establishes a connection to the machine API through an SSH tunnel to the machine.
|
// SSHConnector establishes a connection to the machine API through an SSH tunnel to the machine.
|
||||||
type SSHConnector struct {
|
type SSHConnector struct {
|
||||||
config SSHConnectorConfig
|
config SSHConnectorConfig
|
||||||
@@ -48,7 +58,11 @@ func (c *SSHConnector) Connect(ctx context.Context) (*grpc.ClientConn, error) {
|
|||||||
var err error
|
var err error
|
||||||
c.client, err = sshexec.Connect(c.config.User, c.config.Host, c.config.Port, c.config.KeyPath)
|
c.client, err = sshexec.Connect(c.config.User, c.config.Host, c.config.Port, c.config.KeyPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("SSH login to %s@%s:%d: %w", c.config.User, c.config.Host, c.config.Port, err)
|
dst := c.config.Destination()
|
||||||
|
if c.config.Port != 0 {
|
||||||
|
dst = fmt.Sprintf("%s:%d", dst, c.config.Port)
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("SSH login to %s: %w", dst, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,6 +73,7 @@ func (c *SSHConnector) Connect(ctx context.Context) (*grpc.ClientConn, error) {
|
|||||||
conn, err := grpc.NewClient(
|
conn, err := grpc.NewClient(
|
||||||
"unix://"+sockPath,
|
"unix://"+sockPath,
|
||||||
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
||||||
|
grpc.WithDefaultServiceConfig(defaultServiceConfig),
|
||||||
grpc.WithContextDialer(
|
grpc.WithContextDialer(
|
||||||
func(ctx context.Context, addr string) (net.Conn, error) {
|
func(ctx context.Context, addr string) (net.Conn, error) {
|
||||||
addr = strings.TrimPrefix(addr, "unix://")
|
addr = strings.TrimPrefix(addr, "unix://")
|
||||||
|
|||||||
+128
-71
@@ -4,6 +4,8 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/docker/cli/cli/connhelper/commandconn"
|
"github.com/docker/cli/cli/connhelper/commandconn"
|
||||||
@@ -17,100 +19,96 @@ import (
|
|||||||
// and running `uncloudd dial-stdio` on the remote machine.
|
// and running `uncloudd dial-stdio` on the remote machine.
|
||||||
type SSHCLIConnector struct {
|
type SSHCLIConnector struct {
|
||||||
config SSHConnectorConfig
|
config SSHConnectorConfig
|
||||||
conn net.Conn
|
// Path to SSH control socket for connection reuse.
|
||||||
|
controlSockPath string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewSSHCLIConnector(cfg *SSHConnectorConfig) *SSHCLIConnector {
|
func NewSSHCLIConnector(cfg *SSHConnectorConfig) *SSHCLIConnector {
|
||||||
return &SSHCLIConnector{config: *cfg}
|
return &SSHCLIConnector{
|
||||||
|
config: *cfg,
|
||||||
|
controlSockPath: controlSocketPath(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// sshCLIDialer implements proxy.ContextDialer by spawning SSH processes with -W flag.
|
// controlSocketPath returns a unique control socket path for the SSH connection.
|
||||||
type sshCLIDialer struct {
|
// Returns an empty string if unable to find or create a suitable path.
|
||||||
config SSHConnectorConfig
|
func controlSocketPath() string {
|
||||||
}
|
// %C is expanded by `ssh` to a hash of user, local and remote hostnames, port, and the contents
|
||||||
|
// of the ProxyJump option. This ensures that shared connections are uniquely identified.
|
||||||
|
sockName := fmt.Sprintf("uc_control_%%C.sock")
|
||||||
|
|
||||||
// buildDialArgs constructs SSH command arguments for -W flag dialing.
|
// Prefer XDG_RUNTIME_DIR if set, fall back to ~/.ssh if it exists.
|
||||||
func (d *sshCLIDialer) buildDialArgs(address string) []string {
|
if dir := os.Getenv("XDG_RUNTIME_DIR"); dir != "" {
|
||||||
args := []string{}
|
return filepath.Join(dir, sockName)
|
||||||
|
}
|
||||||
// Add connection timeout to fail fast when node is down.
|
if home, err := os.UserHomeDir(); err == nil {
|
||||||
args = append(args, "-o", "ConnectTimeout=5")
|
sshDir := filepath.Join(home, ".ssh")
|
||||||
|
if fi, sErr := os.Stat(sshDir); sErr == nil && fi.IsDir() {
|
||||||
// Add port if non-standard.
|
return filepath.Join(sshDir, sockName)
|
||||||
if d.config.Port != 0 && d.config.Port != 22 {
|
}
|
||||||
args = append(args, "-p", strconv.Itoa(d.config.Port))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add identity file if specified.
|
// Last resort: create a subdirectory in temp with restricted permissions.
|
||||||
if d.config.KeyPath != "" {
|
tmpDir := filepath.Join(os.TempDir(), fmt.Sprintf("uncloud-%d", os.Getuid()))
|
||||||
args = append(args, "-i", d.config.KeyPath)
|
path := filepath.Join(tmpDir, sockName)
|
||||||
|
if len(path)-2+40 < 104 { // 40 chars for %C hash, 104 is typical UNIX socket path limit
|
||||||
|
if err := os.MkdirAll(tmpDir, 0o700); err == nil {
|
||||||
|
return path
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add -W flag for stdin/stdout forwarding to target address.
|
return ""
|
||||||
args = append(args, "-W", address)
|
|
||||||
|
|
||||||
// Add user@host.
|
|
||||||
args = append(args, d.config.User+"@"+d.config.Host)
|
|
||||||
|
|
||||||
return args
|
|
||||||
}
|
|
||||||
|
|
||||||
// DialContext establishes a connection to the target address through an SSH tunnel using -W flag.
|
|
||||||
func (d *sshCLIDialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) {
|
|
||||||
// Only support TCP connections.
|
|
||||||
if network != "tcp" {
|
|
||||||
return nil, fmt.Errorf("unsupported network type: %s", network)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build SSH command arguments.
|
|
||||||
args := d.buildDialArgs(address)
|
|
||||||
|
|
||||||
// Create connection using commandconn.
|
|
||||||
conn, err := commandconn.New(ctx, "ssh", args...)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("SSH connection to %s@%s for dialing %s: %w", d.config.User, d.config.Host, address, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return conn, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *SSHCLIConnector) Connect(ctx context.Context) (*grpc.ClientConn, error) {
|
func (c *SSHCLIConnector) Connect(ctx context.Context) (*grpc.ClientConn, error) {
|
||||||
// Build SSH command arguments.
|
// Create gRPC client with a dialer that spawns a new SSH connection on demand.
|
||||||
args := c.buildSSHArgs()
|
// Each dial attempt runs `ssh ... uncloudd dial-stdio`, reusing the control socket if available.
|
||||||
|
|
||||||
// Create connection using commandconn.
|
|
||||||
conn, err := commandconn.New(ctx, "ssh", args...)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("SSH connection to %s@%s: %w", c.config.User, c.config.Host, err)
|
|
||||||
}
|
|
||||||
c.conn = conn
|
|
||||||
|
|
||||||
// Create gRPC client over the connection.
|
|
||||||
// Use a custom dialer that returns our existing connection.
|
|
||||||
grpcConn, err := grpc.NewClient(
|
grpcConn, err := grpc.NewClient(
|
||||||
"passthrough:///", // Dummy target since we're using a custom dialer.
|
"passthrough:///", // Dummy target since we're using a custom dialer.
|
||||||
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
||||||
|
grpc.WithDefaultServiceConfig(defaultServiceConfig),
|
||||||
grpc.WithContextDialer(func(ctx context.Context, _ string) (net.Conn, error) {
|
grpc.WithContextDialer(func(ctx context.Context, _ string) (net.Conn, error) {
|
||||||
return c.conn, nil
|
args := c.buildSSHArgs()
|
||||||
|
conn, err := commandconn.New(ctx, "ssh", args...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("SSH connection to %s: %w", c.config.Destination(), err)
|
||||||
|
}
|
||||||
|
return conn, nil
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.conn.Close()
|
|
||||||
return nil, fmt.Errorf("create machine API client: %w", err)
|
return nil, fmt.Errorf("create machine API client: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return grpcConn, nil
|
return grpcConn, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// buildSSHArgs constructs the SSH command arguments.
|
// buildSSHArgs constructs the SSH command arguments to run `uncloudd dial-stdio` on the remote machine reusing
|
||||||
|
// the established connection via control socket.
|
||||||
func (c *SSHCLIConnector) buildSSHArgs() []string {
|
func (c *SSHCLIConnector) buildSSHArgs() []string {
|
||||||
args := []string{}
|
var args []string
|
||||||
|
|
||||||
|
// Add control socket options for connection reuse if available.
|
||||||
|
if c.controlSockPath != "" {
|
||||||
|
args = append(args, "-o", "ControlMaster=auto")
|
||||||
|
args = append(args, "-o", "ControlPath="+c.controlSockPath)
|
||||||
|
|
||||||
|
// Keep the established connection alive for a short duration after the last session closes to allow reuse.
|
||||||
|
controlPersist := "10m"
|
||||||
|
// Override the default duration with the UNCLOUD_SSH_CONTROL_PERSIST env variable.
|
||||||
|
if v := os.Getenv("UNCLOUD_SSH_CONTROL_PERSIST"); v != "" {
|
||||||
|
controlPersist = v
|
||||||
|
}
|
||||||
|
args = append(args, "-o", "ControlPersist="+controlPersist)
|
||||||
|
}
|
||||||
|
|
||||||
// Add connection timeout to fail fast when node is down.
|
// Add connection timeout to fail fast when node is down.
|
||||||
args = append(args, "-o", "ConnectTimeout=5")
|
args = append(args, "-o", "ConnectTimeout=5")
|
||||||
|
// Disable pseudo-terminal allocation to prevent SSH from executing as a login shell.
|
||||||
|
args = append(args, "-T")
|
||||||
|
|
||||||
// Add port if non-standard.
|
// Add port if specified.
|
||||||
if c.config.Port != 0 && c.config.Port != 22 {
|
if c.config.Port != 0 {
|
||||||
args = append(args, "-p", strconv.Itoa(c.config.Port))
|
args = append(args, "-p", strconv.Itoa(c.config.Port))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,8 +117,8 @@ func (c *SSHCLIConnector) buildSSHArgs() []string {
|
|||||||
args = append(args, "-i", c.config.KeyPath)
|
args = append(args, "-i", c.config.KeyPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add user@host.
|
// Add [user@]host destination.
|
||||||
args = append(args, c.config.User+"@"+c.config.Host)
|
args = append(args, c.config.Destination())
|
||||||
|
|
||||||
// Add remote command: uncloudd dial-stdio
|
// Add remote command: uncloudd dial-stdio
|
||||||
args = append(args, "uncloudd", "dial-stdio")
|
args = append(args, "uncloudd", "dial-stdio")
|
||||||
@@ -140,15 +138,74 @@ func (c *SSHCLIConnector) Dialer() (proxy.ContextDialer, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return &sshCLIDialer{
|
return &sshCLIDialer{
|
||||||
config: c.config,
|
config: c.config,
|
||||||
|
controlSockPath: c.controlSockPath,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *SSHCLIConnector) Close() error {
|
func (c *SSHCLIConnector) Close() error {
|
||||||
if c.conn != nil {
|
// Individual connections are managed by gRPC and closed when the gRPC connection closes.
|
||||||
err := c.conn.Close()
|
// The SSH control socket may persist for connection reuse across CLI invocations.
|
||||||
c.conn = nil
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// sshCLIDialer implements proxy.ContextDialer by spawning SSH processes with -W flag.
|
||||||
|
type sshCLIDialer struct {
|
||||||
|
config SSHConnectorConfig
|
||||||
|
// Shared control socket path from SSHCLIConnector for connection reuse.
|
||||||
|
controlSockPath string
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildDialArgs constructs SSH command arguments for -W flag dialing.
|
||||||
|
func (d *sshCLIDialer) buildDialArgs(address string) []string {
|
||||||
|
var args []string
|
||||||
|
|
||||||
|
if d.controlSockPath != "" {
|
||||||
|
// Try to reuse the existing control connection without initiating a new one.
|
||||||
|
// Falls back to direct connection if the control socket is not available.
|
||||||
|
args = append(args, "-o", "ControlMaster=no")
|
||||||
|
args = append(args, "-o", "ControlPath="+d.controlSockPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add connection timeout to fail fast when node is down.
|
||||||
|
args = append(args, "-o", "ConnectTimeout=5")
|
||||||
|
// Disable pseudo-terminal allocation to prevent SSH from executing as a login shell.
|
||||||
|
args = append(args, "-T")
|
||||||
|
|
||||||
|
// Add port if specified.
|
||||||
|
if d.config.Port != 0 {
|
||||||
|
args = append(args, "-p", strconv.Itoa(d.config.Port))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add identity file if specified.
|
||||||
|
if d.config.KeyPath != "" {
|
||||||
|
args = append(args, "-i", d.config.KeyPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add -W flag for stdin/stdout forwarding to target address.
|
||||||
|
args = append(args, "-W", address)
|
||||||
|
|
||||||
|
// Add [user@]host destination.
|
||||||
|
args = append(args, d.config.Destination())
|
||||||
|
|
||||||
|
return args
|
||||||
|
}
|
||||||
|
|
||||||
|
// DialContext establishes a connection to the target address through an SSH tunnel using -W flag.
|
||||||
|
func (d *sshCLIDialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) {
|
||||||
|
// Only support TCP connections.
|
||||||
|
if network != "tcp" {
|
||||||
|
return nil, fmt.Errorf("unsupported network type: %s", network)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build SSH command arguments.
|
||||||
|
args := d.buildDialArgs(address)
|
||||||
|
|
||||||
|
// Create connection using docker's commandconn.
|
||||||
|
conn, err := commandconn.New(ctx, "ssh", args...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("SSH connection to %s for dialing %s: %w", d.config.Destination(), address, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return conn, nil
|
||||||
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user