mirror of
https://github.com/psviderski/uncloud.git
synced 2026-08-26 11:03:34 +00:00
Compare commits
212
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
59b96cc01f | ||
|
|
4c77fe2cfd | ||
|
|
3b89af4a11 | ||
|
|
1abadaef50 | ||
|
|
3b1ce42dd7 | ||
|
|
1640b4c433 | ||
|
|
44549c00fd | ||
|
|
26f34df3e6 | ||
|
|
89fb9efcfa | ||
|
|
638f320959 | ||
|
|
3f2e60ea74 | ||
|
|
8e0a24e781 | ||
|
|
e3e39dde32 | ||
|
|
fc61e2fc07 | ||
|
|
2a0d31cec0 | ||
|
|
2f55af0cab | ||
|
|
971b21d6c1 | ||
|
|
b651d95183 | ||
|
|
d45c1b6869 | ||
|
|
a5fe9fbfb2 | ||
|
|
4590a516ad | ||
|
|
5f9ed6dacf | ||
|
|
e97497d737 | ||
|
|
b4bf1d17d0 | ||
|
|
cfb4693286 | ||
|
|
fd3facb6b0 | ||
|
|
bd03bb9bfa | ||
|
|
eec728620b | ||
|
|
d2dd5e3c89 | ||
|
|
2bfcce7803 | ||
|
|
1ec5184f21 | ||
|
|
23dfe45b2f | ||
|
|
d9e906a77e | ||
|
|
51f7ebf3bf | ||
|
|
ae04dea808 | ||
|
|
009c0e1b4c | ||
|
|
18d38e96b4 | ||
|
|
12c86f1dd4 | ||
|
|
4df1820f96 | ||
|
|
6f9ac1fcc7 | ||
|
|
6a98acbe79 | ||
|
|
b0059af9b1 | ||
|
|
7edd8f2976 | ||
|
|
735e36012a | ||
|
|
69ebce57e9 | ||
|
|
365a5fc0b5 | ||
|
|
624caf037b | ||
|
|
ff1a8fbcf9 | ||
|
|
3aa8fead76 | ||
|
|
4075f85b72 | ||
|
|
59f6fa75ca | ||
|
|
8b66a11394 | ||
|
|
fa3d992dbc | ||
|
|
a7d8afb052 | ||
|
|
0d820bc75a | ||
|
|
6a4e63dbd4 | ||
|
|
7699b530c2 | ||
|
|
f1abbbea6d | ||
|
|
683ad22358 | ||
|
|
8f00e0082e | ||
|
|
c34542a9d8 | ||
|
|
88db1cb6a9 | ||
|
|
dfa6e001c5 | ||
|
|
f49ec465d0 | ||
|
|
1ffe9acdd7 | ||
|
|
e35e69b4be | ||
|
|
11c2e641f1 | ||
|
|
bb97537df7 | ||
|
|
9f4b8179b5 | ||
|
|
f7b079c4f2 | ||
|
|
3648187219 | ||
|
|
93346301fb | ||
|
|
cc86c8660c | ||
|
|
2963436538 | ||
|
|
6e326277bb | ||
|
|
469d6fadcd | ||
|
|
5f214247ca | ||
|
|
0b8495e964 | ||
|
|
6068e24f4f | ||
|
|
7861d56dff | ||
|
|
7bc69b9a9e | ||
|
|
38800924ed | ||
|
|
cf7e333579 | ||
|
|
769f5e47c3 | ||
|
|
6e1165ec57 | ||
|
|
8d16da596c | ||
|
|
d6c06828b3 | ||
|
|
fe93d91894 | ||
|
|
3f70018e7c | ||
|
|
70e9385032 | ||
|
|
db3016fe89 | ||
|
|
c56385602e | ||
|
|
fbbf839258 | ||
|
|
eee28e2ead | ||
|
|
ef57b62625 | ||
|
|
e854bbb3e1 | ||
|
|
938e7d05c0 | ||
|
|
6c2759f315 | ||
|
|
72bf34632f | ||
|
|
7db8bba6ea | ||
|
|
badb8ecd3c | ||
|
|
9413f1c0b1 | ||
|
|
d8818ba770 | ||
|
|
440215b811 | ||
|
|
02a418aafb | ||
|
|
33550be65e | ||
|
|
edad2429c5 | ||
|
|
b605a64fbe | ||
|
|
16c876d83f | ||
|
|
d6ff62159d | ||
|
|
fcf164dda1 | ||
|
|
e5957d9d6b | ||
|
|
143e68cc95 | ||
|
|
835c310987 | ||
|
|
5fce84d33c | ||
|
|
45497fd20c | ||
|
|
f08d9fe405 | ||
|
|
565a8d8366 | ||
|
|
343357adf9 | ||
|
|
8ee551ca8f | ||
|
|
948201cb6f | ||
|
|
f71fc57f34 | ||
|
|
1acb0ff110 | ||
|
|
03c2681792 | ||
|
|
f3cb6657ae | ||
|
|
4532b985d4 | ||
|
|
000a8a1d3e | ||
|
|
7705db2c4f | ||
|
|
590a1b231f | ||
|
|
8709ac0ed5 | ||
|
|
f8ebd94a79 | ||
|
|
87331a9265 | ||
|
|
2f5a2c6344 | ||
|
|
97e032cb7b | ||
|
|
410a95ec88 | ||
|
|
df4ab40e12 | ||
|
|
c6205d4f57 | ||
|
|
13dea92311 | ||
|
|
9100c22e7f | ||
|
|
6125553a97 | ||
|
|
c9452f1509 | ||
|
|
25b173128a | ||
|
|
4e40a93991 | ||
|
|
9fc54c9d64 | ||
|
|
cacfca7a88 | ||
|
|
b3885b0888 | ||
|
|
3ada89c4f8 | ||
|
|
20b2ca2ea5 | ||
|
|
1bbefdd0d2 | ||
|
|
f467fad84c | ||
|
|
0e80f2e5b2 | ||
|
|
64f6a4f3d3 | ||
|
|
0975cbab66 | ||
|
|
615022c9ac | ||
|
|
ecbd4378e4 | ||
|
|
1becd033e6 | ||
|
|
1cba1eef0a | ||
|
|
3893005866 | ||
|
|
1911e03c82 | ||
|
|
641b11a92d | ||
|
|
7c7530f977 | ||
|
|
6e7fe405ef | ||
|
|
03c5c054e1 | ||
|
|
c42968c812 | ||
|
|
ffef41081b | ||
|
|
58b51b974c | ||
|
|
2d923380a3 | ||
|
|
3910101a88 | ||
|
|
7a9d0cf9db | ||
|
|
042dd594e0 | ||
|
|
1471a94162 | ||
|
|
b1abd07dde | ||
|
|
3757813b20 | ||
|
|
f1a150821c | ||
|
|
874af1ba10 | ||
|
|
2ad098c612 | ||
|
|
4172cb826f | ||
|
|
041d51b74f | ||
|
|
fb01604d21 | ||
|
|
bd8bcbd2f9 | ||
|
|
07501813c8 | ||
|
|
1bbcfa8519 | ||
|
|
e8715fce38 | ||
|
|
a0a66f34c4 | ||
|
|
b799b7518e | ||
|
|
2571498ed2 | ||
|
|
5bcbfc332d | ||
|
|
34574e41fc | ||
|
|
c7355afa3d | ||
|
|
a907fdb1af | ||
|
|
c9ba4adccb | ||
|
|
a0551d2d64 | ||
|
|
76129ea056 | ||
|
|
6ffd35f596 | ||
|
|
0ef776e7f1 | ||
|
|
0b22b2136a | ||
|
|
e1530bb6f3 | ||
|
|
7fc07ce4bd | ||
|
|
eb42082955 | ||
|
|
8719e37856 | ||
|
|
e1cc39b0d5 | ||
|
|
efe646f54c | ||
|
|
e130d803e4 | ||
|
|
56e7e8baa2 | ||
|
|
b963ef39d1 | ||
|
|
1df0fa290f | ||
|
|
c3bb0c9f49 | ||
|
|
cf65cecd9e | ||
|
|
fff17dbb60 | ||
|
|
586a4fa129 | ||
|
|
2a82f75cad | ||
|
|
805afd0d28 |
@@ -4,6 +4,7 @@
|
||||
# Allow files and directories.
|
||||
!cmd/
|
||||
!internal/
|
||||
!pkg/
|
||||
!scripts/docker/
|
||||
!go.*
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
github: [psviderski]
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 525 KiB |
@@ -0,0 +1,67 @@
|
||||
name: Go Tests
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "main"
|
||||
- "test/**"
|
||||
- "release/**"
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- ".github/**"
|
||||
- "**.go"
|
||||
- "**.proto"
|
||||
- "go.*"
|
||||
- "Makefile"
|
||||
- "scripts/**"
|
||||
- "test/**"
|
||||
permissions:
|
||||
contents: read
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
|
||||
with:
|
||||
go-version: "1.23.2"
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
go mod tidy
|
||||
git diff --exit-code ||
|
||||
(echo "go.mod or go.sum has changed. Please run 'go mod tidy' and commit the changes." && exit 1)
|
||||
|
||||
- name: Run tests
|
||||
run: make test
|
||||
timeout-minutes: 10
|
||||
|
||||
check-protobuf:
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 10
|
||||
strategy:
|
||||
matrix:
|
||||
# Run on more platforms so that mise lockfile checksums are checked/generated for all platforms.
|
||||
# See https://github.com/jdx/mise/issues/4276 for details.
|
||||
os: [ubuntu-latest, macos-latest]
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Install Mise
|
||||
uses: jdx/mise-action@13abe502c30c1559a5c37dff303831bab82c9402 # v2.2.3
|
||||
with:
|
||||
version: "2025.6.5"
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
|
||||
- name: Generate Protobuf files
|
||||
run: |
|
||||
make proto
|
||||
# check if the generated files are up to date
|
||||
git diff --exit-code ||
|
||||
(echo "Generated protobuf files are not up to date. Please run 'make proto' and commit the changes." && exit 1)
|
||||
+4
-2
@@ -27,8 +27,10 @@ go.work.sum
|
||||
.env
|
||||
.idea/
|
||||
|
||||
data
|
||||
dist/
|
||||
/data/
|
||||
/dist/
|
||||
/uc
|
||||
/tmp
|
||||
|
||||
# Web
|
||||
node_modules/
|
||||
|
||||
@@ -24,6 +24,8 @@ builds:
|
||||
goarch:
|
||||
- amd64
|
||||
- arm64
|
||||
ldflags:
|
||||
- -s -w -X github.com/psviderski/uncloud/internal/version.version={{ .Version }}
|
||||
|
||||
- id: uncloudd
|
||||
main: ./cmd/uncloudd
|
||||
@@ -35,6 +37,8 @@ builds:
|
||||
goarch:
|
||||
- amd64
|
||||
- arm64
|
||||
ldflags:
|
||||
- -s -w -X github.com/psviderski/uncloud/internal/version.version={{ .Version }}
|
||||
|
||||
archives:
|
||||
- id: uncloud
|
||||
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
[tools.go]
|
||||
version = "1.23.10"
|
||||
backend = "core:go"
|
||||
|
||||
[tools.go.checksums]
|
||||
"go1.23.10.darwin-arm64.tar.gz" = "sha256:25c64bfa8a8fd8e7f62fb54afa4354af8409a4bb2358c2699a1003b733e6fce5"
|
||||
"go1.23.10.linux-amd64.tar.gz" = "sha256:535f9f81802499f2a7dbfa70abb8fda3793725fcc29460f719815f6e10b5fd60"
|
||||
|
||||
[tools.protoc]
|
||||
version = "27.3"
|
||||
backend = "aqua:protocolbuffers/protobuf/protoc"
|
||||
|
||||
[tools.protoc.checksums]
|
||||
"protoc-27.3-linux-x86_64.zip" = "sha256:6dab2adab83f915126cab53540d48957c40e9e9023969c3e84d44bfb936c7741"
|
||||
"protoc-27.3-osx-aarch_64.zip" = "sha256:b22116bd97cdbd7ea25346abe635a9df268515fe5ef5afa93cd9a68fc2513f84"
|
||||
|
||||
[tools.protoc-gen-go]
|
||||
version = "1.34.2"
|
||||
backend = "aqua:protocolbuffers/protobuf-go/protoc-gen-go"
|
||||
|
||||
[tools.protoc-gen-go.checksums]
|
||||
"protoc-gen-go.v1.34.2.darwin.arm64.tar.gz" = "sha256:17aca7f948dbb624049030cf841e35895cf34183ba006e721247fdeb95ff2780"
|
||||
"protoc-gen-go.v1.34.2.linux.amd64.tar.gz" = "sha256:b87bc134dee55576a842141bf0ed27761c635d746780fce5dee038c6dd16554f"
|
||||
|
||||
[tools.protoc-gen-go-grpc]
|
||||
version = "1.5.1"
|
||||
backend = "aqua:grpc/grpc-go/protoc-gen-go-grpc"
|
||||
|
||||
[tools.protoc-gen-go-grpc.checksums]
|
||||
"protoc-gen-go-grpc.v1.5.1.darwin.arm64.tar.gz" = "sha256:d6083feb51dcfe59f26793e99ef01ef5eac68b64294ec2546711f614ac5878f3"
|
||||
"protoc-gen-go-grpc.v1.5.1.linux.amd64.tar.gz" = "sha256:a6cac4ea731e54aea304ad44d704a69d1cdc82997084b33637e21a89dc9229d6"
|
||||
@@ -1,5 +1,11 @@
|
||||
[settings]
|
||||
experimental = true
|
||||
|
||||
[tools]
|
||||
go = "1.23"
|
||||
protoc = "27.3"
|
||||
protoc-gen-go = "1.34.2"
|
||||
protoc-gen-go-grpc = "1.5.1"
|
||||
|
||||
[env]
|
||||
_.file = ".env"
|
||||
|
||||
+12
-3
@@ -17,13 +17,22 @@ RUN GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -o uncloudd cmd/uncloudd/main
|
||||
|
||||
|
||||
FROM alpine:${ALPINE_VERSION} AS corrosion-download
|
||||
RUN wget -q -O /tmp/corrosion.tar.gz \
|
||||
https://github.com/psviderski/corrosion/releases/latest/download/corrosion-aarch64-unknown-linux-gnu.tar.gz \
|
||||
ARG TARGETARCH
|
||||
|
||||
RUN CORROSION_ARCH=$(case "${TARGETARCH}" in \
|
||||
"amd64") echo "x86_64" ;; \
|
||||
"arm64") echo "aarch64" ;; \
|
||||
*) echo "Architecture '${TARGETARCH}' not supported" >&2 && exit 1 ;; \
|
||||
esac) \
|
||||
&& wget -q -O /tmp/corrosion.tar.gz \
|
||||
"https://github.com/psviderski/corrosion/releases/latest/download/corrosion-${CORROSION_ARCH}-unknown-linux-gnu.tar.gz" \
|
||||
&& tar -xzf /tmp/corrosion.tar.gz -C /tmp \
|
||||
&& install /tmp/corrosion /usr/local/bin/corrosion \
|
||||
&& rm /tmp/corrosion.tar.gz /tmp/corrosion
|
||||
|
||||
FROM chainguard/wolfi-base:latest AS corrosion
|
||||
# Beware that more modern images like chainguard/wolfi-base build glibc with flags that require newer CPU features,
|
||||
# e.g. x86-64-v2. So such image may fail with "Fatal glibc error: CPU does not support x86-64-v2" on older CPUs.
|
||||
FROM gcr.io/distroless/cc-debian12:latest AS corrosion
|
||||
COPY --from=corrosion-download /usr/local/bin/corrosion /usr/local/bin/corrosion
|
||||
CMD ["corrosion", "agent"]
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
CORROSION_IMAGE ?= ghcr.io/psviderski/corrosion:latest
|
||||
UCIND_IMAGE ?= ghcr.io/psviderski/ucind:latest
|
||||
DOCS_IMAGE ?= ghcr.io/psviderski/uncloud-docs:latest
|
||||
|
||||
update-dev:
|
||||
GOOS=linux GOARCH=amd64 go build -o uncloudd-linux-amd64 ./cmd/uncloudd && \
|
||||
@@ -30,6 +31,16 @@ reset-dev:
|
||||
ssh spy@192.168.40.176 "sudo systemctl stop uncloud && sudo rm -rf /var/lib/uncloud"
|
||||
ssh ubuntu@152.67.101.197 "sudo systemctl stop uncloud && sudo rm -rf /var/lib/uncloud"
|
||||
|
||||
demo-reset:
|
||||
rm -fv ~/.config/uncloud/config.yaml
|
||||
ssh ubuntu@152.67.101.197 "AUTO_CONFIRM=true sudo -E uncloud-uninstall && docker rmi caddy:2.9.1"
|
||||
ssh root@5.223.45.199 "AUTO_CONFIRM=true sudo -E uncloud-uninstall"
|
||||
ssh spy@192.168.40.243 "AUTO_CONFIRM=true sudo -E uncloud-uninstall"
|
||||
|
||||
.PHONY: ucind-cluster
|
||||
ucind-cluster:
|
||||
go run ./cmd/ucind cluster rm && go run ./cmd/ucind cluster create -m 3
|
||||
|
||||
.PHONY: proto
|
||||
proto:
|
||||
protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative \
|
||||
@@ -39,14 +50,45 @@ proto:
|
||||
corrosion-image:
|
||||
docker build -t "$(CORROSION_IMAGE)" --target corrosion .
|
||||
|
||||
.PHONY: corrosion-image-push
|
||||
corrosion-image-push: corrosion-image
|
||||
docker push "$(CORROSION_IMAGE)"
|
||||
.PHONY: corrosion-multiarch-image-push
|
||||
corrosion-multiarch-image-push:
|
||||
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-image-push
|
||||
ucind-image-push: ucind-image
|
||||
docker push "$(UCIND_IMAGE)"
|
||||
.PHONY: ucind-multiarch-image-push
|
||||
ucind-multiarch-image-push:
|
||||
docker buildx build --push --platform linux/amd64,linux/arm64 -t "$(UCIND_IMAGE)" --target ucind .
|
||||
|
||||
.PHONY: test
|
||||
test:
|
||||
ifeq ($(TEST_NAME),)
|
||||
go test -count=1 -v ./...
|
||||
else
|
||||
go test -count=1 -v -run ^$(TEST_NAME)$$ ./...
|
||||
endif
|
||||
|
||||
.PHONY: test-clean
|
||||
test-clean:
|
||||
@CONTAINERS=$$(docker ps --filter "name=ucind-test" -q); \
|
||||
if [ -n "$$CONTAINERS" ]; then \
|
||||
echo "Killing containers..."; \
|
||||
docker kill $$CONTAINERS; \
|
||||
fi; \
|
||||
CONTAINERS_STOPPED=$$(docker ps -a --filter "name=ucind-test" -q); \
|
||||
if [ -n "$$CONTAINERS_STOPPED" ]; then \
|
||||
echo "Removing stopped containers..."; \
|
||||
docker rm $$CONTAINERS_STOPPED; \
|
||||
else \
|
||||
echo "Nothing to clean."; \
|
||||
fi
|
||||
|
||||
.PHONY: vet
|
||||
vet:
|
||||
go vet ./...
|
||||
|
||||
.PHONY: docs-image-push
|
||||
docs-image:
|
||||
docker buildx build --push --platform linux/amd64,linux/arm64 -t "$(DOCS_IMAGE)" ./docs
|
||||
|
||||
@@ -2,47 +2,70 @@
|
||||
<img src="./website/images/logo.svg" height="100" alt="Uncloud logo"/>
|
||||
<h1>Uncloud</h1>
|
||||
<p><strong>Docker simplicity. Multi-machine power.</strong></p>
|
||||
|
||||
<p>
|
||||
<a href="https://docs.uncloud.run"><img src="https://img.shields.io/badge/Docs-blue.svg?style=for-the-badge&logo=gitbook&logoColor=white" alt="Documentation"></a>
|
||||
<a href="https://discord.gg/eR35KQJhPu"><img src="https://img.shields.io/badge/discord-5865F2.svg?style=for-the-badge&logo=discord&logoColor=white" alt="Join Discord"></a>
|
||||
<a href="https://x.com/psviderski"><img src="https://img.shields.io/badge/follow-black?style=for-the-badge&logo=X&logoColor=while" alt="Follow on X"></a>
|
||||
<a href="https://github.com/sponsors/psviderski"><img src="https://img.shields.io/badge/Donate-EA4AAA.svg?style=for-the-badge&logo=githubsponsors&logoColor=white" alt="Donate"></a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
Uncloud is a lightweight clustering and container orchestration tool that lets you deploy and manage web apps across
|
||||
cloud VMs and bare metal with minimized cluster management overhead. It creates a secure WireGuard mesh network between
|
||||
cloud VMs and bare metal with minimised cluster management overhead. It creates a secure WireGuard mesh network between
|
||||
your Docker hosts and provides automatic service discovery, load balancing, ingress with HTTPS, and simple CLI commands
|
||||
to manage your apps.
|
||||
|
||||
Unlike traditional orchestrators, there's no central control plane and quorum to maintain. Each machine maintains a
|
||||
synchronized copy of the cluster state through peer-to-peer communication, keeping cluster operations functional even if
|
||||
synchronised copy of the cluster state through peer-to-peer communication, keeping cluster operations functional even if
|
||||
some machines go offline.
|
||||
|
||||
Uncloud aims to be the solution for developers who want the flexibility of self-hosted infrastructure without the
|
||||
operational complexity of Kubernetes.
|
||||
Uncloud is the solution for developers who want the flexibility of self-hosted infrastructure without the operational
|
||||
complexity of Kubernetes.
|
||||
|
||||
## ✨ Features
|
||||
|
||||
* **Deploy anywhere**: Combine cloud VMs, dedicated servers, and bare metal into a unified computing environment —
|
||||
regardless of location or provider.
|
||||
* **Docker Compose**: Familiar [Docker Compose](https://compose-spec.io/) format for defining services and volumes. No
|
||||
need to learn a new bespoke DSL.
|
||||
* **Zero-downtime deployments**: Rolling updates without service interruption. Automatic rollback on failure is coming
|
||||
soon.
|
||||
* **Service discovery**: Built-in DNS server resolves service names to container IPs.
|
||||
* **Persistent storage**: Run stateful services with Docker volumes managed across machines.
|
||||
* **Zero-config private network**: Automatic WireGuard mesh with peer discovery and NAT traversal. Containers get unique
|
||||
IPs for direct cross-machine communication.
|
||||
* **No control plane**: Fully decentralized design eliminates single points of failure and reduces operational overhead.
|
||||
* **No control plane**: Fully decentralised design eliminates single points of failure and reduces operational overhead.
|
||||
* **Imperative over declarative**: Favoring imperative operations over state reconciliation simplifies both the mental
|
||||
model and troubleshooting.
|
||||
* **Service discovery**: Built-in DNS server resolves service names to container IPs.
|
||||
* **Managed DNS**: Automatic DNS records `*.<id>.cluster.uncloud.run` for services with public access via managed
|
||||
[Uncloud DNS](https://github.com/psviderski/uncloud-dns) service.
|
||||
* **Automatic HTTPS**: Built-in Caddy reverse proxy handles TLS certificate provisioning and renewal using Let's
|
||||
Encrypt.
|
||||
* **Docker-like CLI**: Familiar commands for managing both infrastructure and applications.
|
||||
* **Remote management**: Control your entire infrastructure through SSH access to any single machine in the cluster.
|
||||
|
||||
Coming soon:
|
||||
### 🚀 Coming soon
|
||||
|
||||
* Project isolation through environments/namespaces
|
||||
* Infrastructure as Code using Docker Compose format
|
||||
* Persistent volumes and secrets management
|
||||
* Monitoring and log aggregation
|
||||
* Database deployment and management
|
||||
* Curated application catalog
|
||||
* **[Unregistry](https://github.com/psviderski/unregistry) integration**: Push your Docker images directly to your
|
||||
machines without an external registry. It will transfer only the missing layers, making it fast and efficient.
|
||||
|
||||
Here is a diagram of an Uncloud multi-provider cluster of 3 machines:
|
||||
## 🎬 Quick demo
|
||||
|
||||

|
||||
The screenshot below demonstrates how I use Uncloud to deploy the [Uncloud Documentation](https://docs.uncloud.run)
|
||||
website to 2 remote machines (why not?) from the [`compose.yaml`](docs/compose.yaml) file on my local machine.
|
||||
|
||||
It exposes the container port `8000/tcp` as HTTPS on the domain `docs.uncloud.run`, served by the Caddy reverse proxy on
|
||||
the remote machines. All managed by Uncloud.
|
||||
|
||||

|
||||
|
||||
Here is a more advanced use case. Deploy a highly available web app with automatic HTTPS across multiple regions and
|
||||
on-premises in just a couple minutes.
|
||||
|
||||
<a href="https://uncloud.wistia.com/medias/k47uwt9uau?wvideo=k47uwt9uau">
|
||||
<img src="https://embed-ssl.wistia.com/deliveries/3cf7014a48b93afc556444bed3e39a8c.jpg?image_crop_resized=900x526&image_play_button_rounded=true&image_play_button_size=2x&image_play_button_color=18181Be0" alt="Uncloud demo" width="450" height="263" />
|
||||
</a>
|
||||
|
||||
## 💫 Why Uncloud?
|
||||
|
||||
@@ -52,60 +75,111 @@ believe there's a sweet spot in between — a pragmatic solution for the majorit
|
||||
scale. You should be able to:
|
||||
|
||||
* **Own your infrastructure and data**: Whether driven by costs, compliance, or flexibility, run applications on any
|
||||
combination of cloud VMs and personal hardware while maintaining the cloud-like experience you love.
|
||||
* **Stay simple**: Don't worry about control planes, highly-available clusters, or complex YAML configurations for
|
||||
common use cases.
|
||||
combination of cloud VMs and personal hardware while controlling your data and maintaining the cloud-like experience
|
||||
you love.
|
||||
* **Stay simple as you grow**: Start with a single machine and add more whenever you need without changing your
|
||||
workflow. No worrying about highly-available control planes or complex YAML configurations.
|
||||
* **Build with proven primitives**: Get production-grade networking, deployment primitives, service discovery, load
|
||||
balancing, and ingress with HTTPS out of the box without becoming a distributed systems expert.
|
||||
* **Support sustainable computing** 🌿: Minimize system overhead to maximize resources available for your applications.
|
||||
* **Support sustainable computing** 🌿: Minimise system overhead to maximise resources available for your applications.
|
||||
|
||||
Uncloud's goal is to make deployment and management of containerized applications feel as seamless as using a cloud
|
||||
Uncloud's goal is to make deployment and management of containerised applications feel as seamless as using a cloud
|
||||
platform, whether you're running on a $5 VPS, a spare Mac mini, or a rack of bare metal servers.
|
||||
|
||||
## 🚀 Quick start
|
||||
|
||||
1. Install Uncloud CLI:
|
||||
|
||||
```bash
|
||||
brew install psviderski/tap/uncloud
|
||||
```bash
|
||||
brew install psviderski/tap/uncloud
|
||||
|
||||
# or using curl (macOS/Linux)
|
||||
curl -fsS https://get.uncloud.run/install.sh | sh
|
||||
```
|
||||
# or using curl (macOS/Linux)
|
||||
curl -fsS https://get.uncloud.run/install.sh | sh
|
||||
```
|
||||
|
||||
2. Initialize your first machine:
|
||||
2. Initialise your first machine:
|
||||
|
||||
```bash
|
||||
uc machine init root@your-server-ip
|
||||
```
|
||||
```bash
|
||||
uc machine init root@your-server-ip
|
||||
```
|
||||
|
||||
3. Create a DNS A record in your DNS provider (Cloudflare, Namecheap, etc.) that points `app.example.com` to your
|
||||
3. Deploy your app from a Docker image and publish its container port 8000 as HTTPS using `app.example.com` domain:
|
||||
|
||||
```bash
|
||||
uc run -p app.example.com:8000/https image/my-app
|
||||
```
|
||||
|
||||
4. Create a DNS A record in your DNS provider (Cloudflare, Namecheap, etc.) that points `app.example.com` to your
|
||||
server's IP address. Allow a few minutes for DNS propagation.
|
||||
4. Deploy your app from a Docker image:
|
||||
|
||||
```bash
|
||||
uc run -p app.example.com:8000/https my-app-image
|
||||
```
|
||||
|
||||
That's it! Your app is now running and accessible at https://app.example.com ✨
|
||||
That's it! Your app is now running and accessible at https://app.example.com ✨
|
||||
|
||||
5. Clean up when you're done:
|
||||
|
||||
```bash
|
||||
uc ls
|
||||
# Copy the service ID from the output and remove it:
|
||||
uc rm my-app-id
|
||||
```
|
||||
```bash
|
||||
uc ls
|
||||
# Copy the service name from the output and run the rm command:
|
||||
uc rm my-app-name
|
||||
```
|
||||
|
||||
If you want to fully uninstall Uncloud on a machine, run:
|
||||
|
||||
```bash
|
||||
uncloud-uninstall
|
||||
```
|
||||
|
||||
View the [Documentation](https://docs.uncloud.run) for more information.
|
||||
|
||||
## ⚙️ How it works
|
||||
|
||||
Check out the [design document](docs/design.md) to understand Uncloud's design philosophy and goals. Here, let's peek
|
||||
under the hood to see what happens when you run certain commands.
|
||||
Check out the [design document](docs/design.md) to understand Uncloud's design philosophy and goals.
|
||||
|
||||
**When you initialize a new cluster on a machine:**
|
||||
Here is a diagram of an Uncloud multi-provider cluster of 3 machines:
|
||||
|
||||

|
||||
|
||||
<details>
|
||||
<summary>Peek under the hood to see what happens when you run certain commands.</summary>
|
||||
|
||||
**When you initialise a new cluster on a machine:**
|
||||
|
||||
```bash
|
||||
uc machine init root@your-server-ip
|
||||
$ uc machine init --name oracle-vm ubuntu@152.67.101.197
|
||||
|
||||
Downloading Uncloud install script: https://raw.githubusercontent.com/psviderski/uncloud/refs/heads/main/scripts/install.sh
|
||||
⏳ Running Uncloud install script...
|
||||
✓ Docker is already installed.
|
||||
⏳ Installing Docker...
|
||||
...
|
||||
✓ Docker installed successfully.
|
||||
✓ Linux user and group 'uncloud' created.
|
||||
✓ Linux user 'ubuntu' added to group 'uncloud'.
|
||||
⏳ Installing Uncloud binaries...
|
||||
⏳ Downloading uncloudd binary: https://github.com/psviderski/uncloud/releases/latest/download/uncloudd_linux_arm64.tar.gz
|
||||
✓ uncloudd binary installed: /usr/local/bin/uncloudd
|
||||
⏳ Downloading uninstall script: https://raw.githubusercontent.com/psviderski/uncloud/refs/heads/main/scripts/uninstall.sh
|
||||
✓ uncloud-uninstall script installed: /usr/local/bin/uncloud-uninstall
|
||||
✓ Systemd unit file created: /etc/systemd/system/uncloud.service
|
||||
Created symlink /etc/systemd/system/multi-user.target.wants/uncloud.service → /etc/systemd/system/uncloud.service.
|
||||
⏳ Downloading uncloud-corrosion binary: https://github.com/psviderski/corrosion/releases/latest/download/corrosion-aarch64-unknown-linux-gnu.tar.gz
|
||||
✓ uncloud-corrosion binary installed: /usr/local/bin/uncloud-corrosion
|
||||
✓ Systemd unit file created: /etc/systemd/system/uncloud-corrosion.service
|
||||
⏳ Starting Uncloud machine daemon (uncloud.service)...
|
||||
✓ Uncloud machine daemon started.
|
||||
✓ Uncloud installed on the machine successfully! 🎉
|
||||
Cluster "default" initialised with machine "oracle-vm"
|
||||
Waiting for the machine to be ready...
|
||||
|
||||
Reserved cluster domain: xuw3xd.cluster.uncloud.run
|
||||
[+] Deploying service caddy 1/1
|
||||
✔ Container caddy-c47x on oracle-vm Started 0.9s
|
||||
|
||||
Updating cluster domain records in Uncloud DNS to point to machines running caddy service...
|
||||
[+] Verifying internet access to caddy service 1/1
|
||||
✔ Machine oracle-vm (152.67.101.197) Reachable 0.1s
|
||||
|
||||
DNS records updated to use only the internet-reachable machines running caddy service:
|
||||
*.xuw3xd.cluster.uncloud.run A → 152.67.101.197
|
||||
```
|
||||
|
||||
1. The CLI SSHs into the machine and installs Docker, the `uncloudd` machine daemon and
|
||||
@@ -121,7 +195,42 @@ uc machine init root@your-server-ip
|
||||
**When you add another machine:**
|
||||
|
||||
```bash
|
||||
uc machine add ubuntu@second-server-ip
|
||||
$ uc machine add --name hetzner-server root@5.223.45.199
|
||||
Downloading Uncloud install script: https://raw.githubusercontent.com/psviderski/uncloud/refs/heads/main/scripts/install.sh
|
||||
⏳ Running Uncloud install script...
|
||||
✓ Docker is already installed.
|
||||
✓ Linux user and group 'uncloud' created.
|
||||
⏳ Installing Uncloud binaries...
|
||||
⏳ Downloading uncloudd binary: https://github.com/psviderski/uncloud/releases/latest/download/uncloudd_linux_amd64.tar.gz
|
||||
✓ uncloudd binary installed: /usr/local/bin/uncloudd
|
||||
⏳ Downloading uninstall script: https://raw.githubusercontent.com/psviderski/uncloud/refs/heads/main/scripts/uninstall.sh
|
||||
✓ uncloud-uninstall script installed: /usr/local/bin/uncloud-uninstall
|
||||
✓ Systemd unit file created: /etc/systemd/system/uncloud.service
|
||||
Created symlink /etc/systemd/system/multi-user.target.wants/uncloud.service → /etc/systemd/system/uncloud.service.
|
||||
⏳ Downloading uncloud-corrosion binary: https://github.com/psviderski/corrosion/releases/latest/download/corrosion-x86_64-unknown-linux-gnu.tar.gz
|
||||
✓ uncloud-corrosion binary installed: /usr/local/bin/uncloud-corrosion
|
||||
✓ Systemd unit file created: /etc/systemd/system/uncloud-corrosion.service
|
||||
⏳ Starting Uncloud machine daemon (uncloud.service)...
|
||||
✓ Uncloud machine daemon started.
|
||||
✓ Uncloud installed on the machine successfully! 🎉
|
||||
Machine "hetzner-server" added to cluster
|
||||
Waiting for the machine to be ready...
|
||||
|
||||
[+] Deploying service caddy 1/1
|
||||
✔ Container caddy-d36c on hetzner-server Started 1.0s
|
||||
|
||||
Updating cluster domain records in Uncloud DNS to point to machines running caddy service...
|
||||
[+] Verifying internet access to caddy service 2/2
|
||||
✔ Machine hetzner-server (5.223.45.199) Reachable 0.2s
|
||||
✔ Machine oracle-vm (152.67.101.197) Reachable 0.1s
|
||||
|
||||
DNS records updated to use only the internet-reachable machines running caddy service:
|
||||
*.xuw3xd.cluster.uncloud.run A → 152.67.101.197, 5.223.45.199
|
||||
|
||||
$ uc machine ls
|
||||
NAME STATE ADDRESS PUBLIC IP WIREGUARD ENDPOINTS
|
||||
oracle-vm Up 10.210.0.1/24 152.67.101.197 10.0.0.95:51820, 152.67.101.197:51820
|
||||
hetzner-server Up 10.210.1.1/24 5.223.45.199 5.223.45.199:51820, [2a01:4ff:2f0:128b::1]:51820
|
||||
```
|
||||
|
||||
1. The second machine gets provisioned just like the first. A non-root SSH user will need `sudo` access.
|
||||
@@ -140,7 +249,14 @@ and automatically establish a WireGuard tunnel with it.
|
||||
**When you run a service:**
|
||||
|
||||
```bash
|
||||
uc run -p app.example.com:8000/https my-app-image
|
||||
$ uc run -p app.example.com:8000/https image/my-app
|
||||
|
||||
[+] Running service my-app-1b3b (replicated mode) 1/1
|
||||
✔ Container my-app-1b3b-tcex on oracle-vm Started
|
||||
|
||||
my-app-1b3b endpoints:
|
||||
• https://app.example.com → :8000
|
||||
• https://my-app-1b3b.xuw3xd.cluster.uncloud.run → :8000
|
||||
```
|
||||
|
||||
1. CLI picks a machine to run your container.
|
||||
@@ -158,23 +274,52 @@ Look ma, no control plane or master nodes to maintain! Just a simple overlay net
|
||||
sync that lets machines work together. Want to check on things or make changes? Connect to any machine either implicitly
|
||||
using the CLI or directly over SSH. They all have the complete cluster state and can control everything. It's like each
|
||||
machine is a full backup of your control plane.
|
||||
</details>
|
||||
|
||||
## 🏗 Project status
|
||||
|
||||
Uncloud is currently in active development and is **not ready for production use**. Features may change significantly
|
||||
and there may be breaking changes between releases.
|
||||
|
||||
I'd love your input! Here's how you can contribute:
|
||||
We'd love your input! Here's how you can contribute:
|
||||
|
||||
* 🐛 Found a bug? [Open an issue](https://github.com/psviderski/uncloud/issues)
|
||||
* 💡 Have ideas? [Join the discussion](https://github.com/psviderski/uncloud/discussions)
|
||||
* 💡 Have questions, ideas, or need help?
|
||||
* Start a discussion or join an existing one in
|
||||
the [Discussions](https://github.com/psviderski/uncloud/discussions).
|
||||
* Join our [Discord community](https://discord.gg/eR35KQJhPu) where we discuss features, roadmap, implementation
|
||||
details, and help each other out.
|
||||
|
||||
## 🙏 Inspiration & Acknowledgements
|
||||
|
||||
I'm grateful to the following projects that inspired Uncloud's design and implementation:
|
||||
|
||||
* [Kamal](https://kamal-deploy.org/) — for proving that even in the declarative era of Kubernetes there is a place for
|
||||
simple deployment tools that use imperative commands without complex orchestration. Kamal powers the multi-billion
|
||||
dollar company [37signals](https://37signals.com/) where it was created, and that's truly inspiring!
|
||||
* [Fly.io](https://fly.io/) — for inspiring my vision for what self-hosted infrastructure should feel like, proving that
|
||||
developer experience and powerful infrastructure can coexist beautifully.
|
||||
* [Tailscale](https://tailscale.com/) — for pioneering the vision of decentralised flat mesh networking with an amazing
|
||||
user experience that feels like magic.
|
||||
* [Talos Linux](https://github.com/siderolabs/talos)
|
||||
and [KubeSpan](https://www.talos.dev/v1.10/talos-guides/network/kubespan/) — for the machine API design using
|
||||
[grpc-proxy](https://github.com/siderolabs/grpc-proxy) and for its elegant approach to secure WireGuard-based overlay
|
||||
networking with zero configuration.
|
||||
* [Docker Swarm Classic](https://github.com/docker-archive/classicswarm) and
|
||||
[Rancher 1.x](http://rancher-com-website-main-elb-elb-1798790864.us-west-2.elb.amazonaws.com/docs/rancher/v1.6/en/)
|
||||
— for showing the power of simplicity and pragmatism in container orchestration and that not every problem needs the
|
||||
complexity of Kubernetes.
|
||||
|
||||
Special thanks to the [Corrosion](https://github.com/superfly/corrosion) project by Fly.io for providing the distributed
|
||||
SQLite database used to share Uncloud's cluster state.
|
||||
|
||||
## 📫 Stay updated
|
||||
|
||||
* [Subscribe](https://uncloud.run/#subscribe) to my newsletter to follow the progress, get early insights into new
|
||||
* Join our [Discord server](https://discord.gg/eR35KQJhPu) for real-time discussions, support, and updates.
|
||||
* Follow [@psviderski](https://x.com/psviderski) on X/Twitter.
|
||||
* Subscribe to [my newsletter](https://uncloud.run/#subscribe) to follow the progress, get early insights into new
|
||||
features, and be the first to know when it's ready for production use.
|
||||
* Watch this repository for releases.
|
||||
* Follow [@psviderski](https://github.com/psviderski) on GitHub.
|
||||
|
||||
## ❤️ Contributors
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ package cluster
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/spf13/cobra"
|
||||
"uncloud/internal/ucind"
|
||||
"github.com/psviderski/uncloud/internal/ucind"
|
||||
)
|
||||
|
||||
func NewCreateCommand() *cobra.Command {
|
||||
|
||||
@@ -3,7 +3,7 @@ package cluster
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/spf13/cobra"
|
||||
"uncloud/internal/ucind"
|
||||
"github.com/psviderski/uncloud/internal/ucind"
|
||||
)
|
||||
|
||||
func NewRemoveCommand() *cobra.Command {
|
||||
|
||||
+7
-6
@@ -3,12 +3,13 @@ package main
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/spf13/cobra"
|
||||
"os"
|
||||
"strings"
|
||||
"uncloud/cmd/ucind/cluster"
|
||||
"uncloud/internal/ucind"
|
||||
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/psviderski/uncloud/cmd/ucind/cluster"
|
||||
"github.com/psviderski/uncloud/internal/ucind"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func main() {
|
||||
@@ -44,9 +45,9 @@ func main() {
|
||||
}
|
||||
|
||||
// TODO: allow to override using UNCLOUD_CONFIG env var.
|
||||
cmd.PersistentFlags().StringVar(&configPath, "uncloud-config", "~/.config/uncloud/config.toml",
|
||||
cmd.PersistentFlags().StringVar(&configPath, "uncloud-config", "~/.config/uncloud/config.yaml",
|
||||
"path to the Uncloud configuration file.")
|
||||
_ = cmd.MarkPersistentFlagFilename("uncloud-config", "toml")
|
||||
_ = cmd.MarkPersistentFlagFilename("uncloud-config", "yaml", "yml")
|
||||
|
||||
cmd.AddCommand(
|
||||
cluster.NewRootCommand(),
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
composecli "github.com/compose-spec/compose-go/v2/cli"
|
||||
"github.com/psviderski/uncloud/internal/cli"
|
||||
"github.com/psviderski/uncloud/pkg/client/compose"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// NewBuildCommand creates a new command to build services from a Compose file.
|
||||
func NewBuildCommand() *cobra.Command {
|
||||
opts := cli.BuildOptions{}
|
||||
cmd := &cobra.Command{
|
||||
Use: "build [FLAGS] [SERVICE...]",
|
||||
Short: "Build services from a Compose file.",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
uncli := cmd.Context().Value("cli").(*cli.CLI)
|
||||
|
||||
if len(args) > 0 {
|
||||
opts.Services = args
|
||||
}
|
||||
|
||||
return runBuild(cmd.Context(), uncli, opts)
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().StringSliceVarP(&opts.Files, "file", "f", nil,
|
||||
"One or more Compose files to build (default compose.yaml)")
|
||||
cmd.Flags().StringSliceVarP(&opts.Profiles, "profile", "p", nil,
|
||||
"One or more Compose profiles to enable.")
|
||||
cmd.Flags().BoolVarP(&opts.Push, "push", "P", false,
|
||||
"Push built images to the registry after building. (default false)")
|
||||
cmd.Flags().BoolVarP(&opts.NoCache, "no-cache", "n", false,
|
||||
"Do not use cache when building images. (default false)")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
// TODO: deduplicate with a similar functino for deploy options
|
||||
// projectOpts returns the project options for the Compose file(s).
|
||||
func projectOptsFromBuildOpts(opts cli.BuildOptions) []composecli.ProjectOptionsFn {
|
||||
projectOpts := []composecli.ProjectOptionsFn{}
|
||||
|
||||
if len(opts.Profiles) > 0 {
|
||||
projectOpts = append(projectOpts, composecli.WithDefaultProfiles(opts.Profiles...))
|
||||
}
|
||||
|
||||
return projectOpts
|
||||
}
|
||||
|
||||
// runBuild parses the Compose file(s), builds the services, and pushes them if requested.
|
||||
func runBuild(ctx context.Context, uncli *cli.CLI, opts cli.BuildOptions) error {
|
||||
projectOpts := projectOptsFromBuildOpts(opts)
|
||||
project, err := compose.LoadProject(ctx, opts.Files, projectOpts...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("load compose file(s): %w", err)
|
||||
}
|
||||
|
||||
if len(opts.Services) > 0 {
|
||||
project, err = project.WithSelectedServices(opts.Services)
|
||||
if err != nil {
|
||||
return fmt.Errorf("select services: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
servicesToBuild := cli.GetServicesThatNeedBuild(project)
|
||||
|
||||
if len(servicesToBuild) == 0 {
|
||||
fmt.Println("No services to build.")
|
||||
return nil
|
||||
}
|
||||
|
||||
return cli.BuildServices(ctx, servicesToBuild, opts)
|
||||
}
|
||||
+45
-98
@@ -4,22 +4,23 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/charmbracelet/huh"
|
||||
"github.com/docker/cli/cli/streams"
|
||||
"github.com/docker/compose/v2/pkg/progress"
|
||||
"github.com/spf13/cobra"
|
||||
"maps"
|
||||
"slices"
|
||||
"strings"
|
||||
"uncloud/internal/cli"
|
||||
"uncloud/internal/cli/client"
|
||||
"uncloud/internal/machine/api/pb"
|
||||
|
||||
"github.com/docker/cli/cli/streams"
|
||||
"github.com/docker/compose/v2/pkg/progress"
|
||||
"github.com/psviderski/uncloud/internal/cli"
|
||||
"github.com/psviderski/uncloud/internal/machine/api/pb"
|
||||
"github.com/psviderski/uncloud/pkg/api"
|
||||
"github.com/psviderski/uncloud/pkg/client"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type deployOptions struct {
|
||||
image string
|
||||
machine string
|
||||
cluster string
|
||||
image string
|
||||
machines []string
|
||||
context string
|
||||
}
|
||||
|
||||
func NewDeployCommand() *cobra.Command {
|
||||
@@ -32,24 +33,25 @@ func NewDeployCommand() *cobra.Command {
|
||||
"A rolling update is performed when updating existing containers to minimise disruption.",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
uncli := cmd.Context().Value("cli").(*cli.CLI)
|
||||
return deploy(cmd.Context(), uncli, opts)
|
||||
return runDeploy(cmd.Context(), uncli, opts)
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().StringVar(&opts.image, "image", "",
|
||||
"Caddy Docker image to deploy. (default caddy:LATEST_VERSION)")
|
||||
cmd.Flags().StringVarP(&opts.machine, "machine", "m", "",
|
||||
"Machine names to deploy to (comma-separated). (default is all machines)")
|
||||
cmd.Flags().StringSliceVarP(&opts.machines, "machine", "m", nil,
|
||||
"Machine names to deploy to. Can be specified multiple times or as a comma-separated "+
|
||||
"list of machine names. (default is all machines)")
|
||||
cmd.Flags().StringVarP(
|
||||
&opts.cluster, "cluster", "c", "",
|
||||
"Name of the cluster to deploy to. (default is the current cluster)",
|
||||
&opts.context, "context", "c", "",
|
||||
"Name of the cluster context to deploy to. (default is the current context)",
|
||||
)
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func deploy(ctx context.Context, uncli *cli.CLI, opts deployOptions) error {
|
||||
clusterClient, err := uncli.ConnectCluster(ctx, opts.cluster)
|
||||
func runDeploy(ctx context.Context, uncli *cli.CLI, opts deployOptions) error {
|
||||
clusterClient, err := uncli.ConnectCluster(ctx, opts.context)
|
||||
if err != nil {
|
||||
return fmt.Errorf("connect to cluster: %w", err)
|
||||
}
|
||||
@@ -57,7 +59,7 @@ func deploy(ctx context.Context, uncli *cli.CLI, opts deployOptions) error {
|
||||
|
||||
svc, err := clusterClient.InspectService(ctx, client.CaddyServiceName)
|
||||
if err != nil {
|
||||
if !errors.Is(err, client.ErrNotFound) {
|
||||
if !errors.Is(err, api.ErrNotFound) {
|
||||
return fmt.Errorf("inspect caddy service: %w", err)
|
||||
}
|
||||
fmt.Printf("Service: %s (not running)\n", client.CaddyServiceName)
|
||||
@@ -86,74 +88,46 @@ func deploy(ctx context.Context, uncli *cli.CLI, opts deployOptions) error {
|
||||
fmt.Println()
|
||||
fmt.Println("Preparing a deployment plan...")
|
||||
|
||||
var filter client.MachineFilter
|
||||
if opts.machine != "" {
|
||||
machines := strings.Split(opts.machine, ",")
|
||||
for i, m := range machines {
|
||||
machines[i] = strings.TrimSpace(m)
|
||||
}
|
||||
filter = machineFilter(machines)
|
||||
placement := api.Placement{
|
||||
Machines: cli.ExpandCommaSeparatedValues(opts.machines),
|
||||
}
|
||||
|
||||
d, err := clusterClient.NewCaddyDeployment(opts.image, filter)
|
||||
d, err := clusterClient.NewCaddyDeployment(opts.image, placement)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create caddy deployment: %w", err)
|
||||
}
|
||||
|
||||
// Initialize a machine and container name resolver to properly format the plan output.
|
||||
machines, err := clusterClient.ListMachines(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("list machines: %w", err)
|
||||
}
|
||||
machineNames := make(map[string]string, len(machines))
|
||||
for _, m := range machines {
|
||||
machineNames[m.Machine.Id] = m.Machine.Name
|
||||
}
|
||||
containerNames := make(map[string]string, len(svc.Containers))
|
||||
for _, c := range svc.Containers {
|
||||
containerNames[c.Container.ID] = c.Container.NameWithoutSlash()
|
||||
}
|
||||
resolver := client.NewNameResolver(machineNames, containerNames)
|
||||
|
||||
if opts.image == "" {
|
||||
fmt.Printf("Target image: %s (latest stable)\n", d.Spec.Container.Image)
|
||||
}
|
||||
|
||||
plan, err := d.Plan(ctx)
|
||||
if err != nil {
|
||||
if errors.Is(err, client.ErrNoMatchingMachines) {
|
||||
return fmt.Errorf("no machines found matching: %s", opts.machine)
|
||||
}
|
||||
return fmt.Errorf("plan caddy deployment: %w", err)
|
||||
}
|
||||
|
||||
if len(plan.SequenceOperation.Operations) == 0 {
|
||||
if opts.machine != "" {
|
||||
fmt.Printf("%s service is up to date on selected machines.\n", client.CaddyServiceName)
|
||||
} else {
|
||||
fmt.Printf("%s service is up to date.\n", client.CaddyServiceName)
|
||||
}
|
||||
if len(plan.Operations) == 0 {
|
||||
fmt.Printf("%s service is up to date.\n", client.CaddyServiceName)
|
||||
} else {
|
||||
if svc.ID == "" {
|
||||
if opts.machine != "" {
|
||||
fmt.Println("This will run a Caddy container on selected machines.")
|
||||
if len(opts.machines) > 0 {
|
||||
fmt.Println("This will run a Caddy container on each selected machine.")
|
||||
} else {
|
||||
fmt.Println("This will run a Caddy container on each machine.")
|
||||
}
|
||||
} else {
|
||||
if opts.machine != "" {
|
||||
fmt.Println("This will perform a rolling update of Caddy containers on selected machines.")
|
||||
} else {
|
||||
fmt.Println("This will perform a rolling update of Caddy containers on each machine.")
|
||||
}
|
||||
}
|
||||
fmt.Println()
|
||||
|
||||
// Initialise a machine and container name resolver to properly format the plan output.
|
||||
resolver, err := clusterClient.ServiceOperationNameResolver(ctx, svc)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create machine and container name resolver for service operations: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
fmt.Println("Deployment plan:")
|
||||
fmt.Println(plan.Format(resolver))
|
||||
fmt.Println()
|
||||
|
||||
confirmed, err := confirm()
|
||||
confirmed, err := cli.Confirm()
|
||||
if err != nil {
|
||||
return fmt.Errorf("confirm deployment: %w", err)
|
||||
}
|
||||
@@ -178,8 +152,9 @@ func deploy(ctx context.Context, uncli *cli.CLI, opts deployOptions) error {
|
||||
}
|
||||
|
||||
func UpdateDomainRecords(ctx context.Context, clusterClient *client.Client, progressOut *streams.Out) error {
|
||||
if _, err := clusterClient.GetDomain(ctx); err != nil {
|
||||
if errors.Is(err, client.ErrNotFound) {
|
||||
domain, err := clusterClient.GetDomain(ctx)
|
||||
if err != nil {
|
||||
if errors.Is(err, api.ErrNotFound) {
|
||||
fmt.Println("Skipping DNS records update as no cluster domain is reserved (see 'uc dns').")
|
||||
return nil
|
||||
}
|
||||
@@ -190,7 +165,7 @@ func UpdateDomainRecords(ctx context.Context, clusterClient *client.Client, prog
|
||||
// TODO: split the method into two: one to get the records and one to update them to ask for update confirmation.
|
||||
|
||||
var records []*pb.DNSRecord
|
||||
err := progress.RunWithTitle(ctx, func(ctx context.Context) error {
|
||||
err = progress.RunWithTitle(ctx, func(ctx context.Context) error {
|
||||
var err error
|
||||
records, err = clusterClient.CreateIngressRecords(ctx, client.CaddyServiceName)
|
||||
return err
|
||||
@@ -198,18 +173,19 @@ func UpdateDomainRecords(ctx context.Context, clusterClient *client.Client, prog
|
||||
if err != nil {
|
||||
if errors.Is(err, client.ErrNoReachableMachines) {
|
||||
fmt.Println()
|
||||
fmt.Println("DNS records could not be updated as there are no internet-reachable machines running " +
|
||||
"caddy containers.")
|
||||
fmt.Printf("DNS records for domain '%s' could not be updated as there are no internet-reachable "+
|
||||
"machines running caddy containers.\n", domain)
|
||||
fmt.Println()
|
||||
fmt.Println("Possible solutions:")
|
||||
fmt.Println("- Ensure your machines have public IP addresses")
|
||||
fmt.Println("- Use --public-ip flag when adding machines to override the automatically detected IPs")
|
||||
fmt.Println("- Check firewall settings on your machines")
|
||||
fmt.Println("- Configure port forwarding if behind NAT")
|
||||
fmt.Println("- Retry Caddy deployment after resolving connectivity issues with 'uc caddy deploy'")
|
||||
fmt.Println("- Retry Caddy deployment with 'uc caddy deploy' after resolving connectivity issues")
|
||||
fmt.Println()
|
||||
fmt.Println("Your services will not be accessible from the internet until at least one machine " +
|
||||
"becomes reachable.")
|
||||
fmt.Println("Your services won't be accessible from the internet until at least one machine " +
|
||||
"becomes reachable. If you aren't planning to expose any services publicly, you can release " +
|
||||
"the domain by running 'uc dns release'.")
|
||||
}
|
||||
return fmt.Errorf("failed to update DNS records pointing to caddy service: %w", err)
|
||||
}
|
||||
@@ -222,32 +198,3 @@ func UpdateDomainRecords(ctx context.Context, clusterClient *client.Client, prog
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func confirm() (bool, error) {
|
||||
var confirmed bool
|
||||
form := huh.NewForm(
|
||||
huh.NewGroup(
|
||||
huh.NewConfirm().
|
||||
Title(
|
||||
"Do you want to continue?",
|
||||
).
|
||||
Affirmative("Yes!").
|
||||
Negative("No").
|
||||
Value(&confirmed),
|
||||
),
|
||||
)
|
||||
if err := form.Run(); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return confirmed, nil
|
||||
}
|
||||
|
||||
func machineFilter(machines []string) client.MachineFilter {
|
||||
if len(machines) == 0 {
|
||||
return nil
|
||||
}
|
||||
return func(m *pb.MachineInfo) bool {
|
||||
return slices.Contains(machines, m.Name)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
package context
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"maps"
|
||||
"os"
|
||||
"slices"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/psviderski/uncloud/internal/cli"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func NewListCommand() *cobra.Command {
|
||||
var clusterContext string
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "ls",
|
||||
Aliases: []string{"list"},
|
||||
Short: "List available cluster contexts.",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
uncli := cmd.Context().Value("cli").(*cli.CLI)
|
||||
return list(uncli)
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().StringVarP(
|
||||
&clusterContext, "context", "c", "",
|
||||
"Name of the cluster context. (default is the current context)",
|
||||
)
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func list(uncli *cli.CLI) error {
|
||||
if uncli.Config == nil {
|
||||
return fmt.Errorf("context management is not available: Uncloud configuration file is not being used")
|
||||
}
|
||||
|
||||
if len(uncli.Config.Contexts) == 0 {
|
||||
fmt.Println("No contexts found")
|
||||
return nil
|
||||
}
|
||||
|
||||
contextNames := slices.Sorted(maps.Keys(uncli.Config.Contexts))
|
||||
currentContext := uncli.Config.CurrentContext
|
||||
|
||||
tw := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', 0)
|
||||
fmt.Fprintln(tw, "NAME\tCURRENT\tCONNECTIONS")
|
||||
|
||||
for _, name := range contextNames {
|
||||
current := ""
|
||||
if name == currentContext {
|
||||
current = "*"
|
||||
}
|
||||
connCount := len(uncli.Config.Contexts[name].Connections)
|
||||
fmt.Fprintf(tw, "%s\t%s\t%d\n", name, current, connCount)
|
||||
}
|
||||
|
||||
return tw.Flush()
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package context
|
||||
|
||||
import (
|
||||
"github.com/psviderski/uncloud/internal/cli"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func NewRootCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "ctx",
|
||||
Aliases: []string{"context"},
|
||||
Short: "Switch between different cluster contexts. Contains subcommands to manage contexts.",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
uncli := cmd.Context().Value("cli").(*cli.CLI)
|
||||
return selectContext(uncli)
|
||||
},
|
||||
}
|
||||
|
||||
cmd.AddCommand(
|
||||
NewListCommand(),
|
||||
NewUseCommand(),
|
||||
)
|
||||
|
||||
return cmd
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package context
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"maps"
|
||||
"slices"
|
||||
|
||||
"github.com/charmbracelet/huh"
|
||||
"github.com/psviderski/uncloud/internal/cli"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func NewUseCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "use [CONTEXT]",
|
||||
Short: "Switch to a different cluster context.",
|
||||
Long: "Switch to a different cluster context. If no context is provided, " +
|
||||
"a list of available contexts will be displayed for selection.",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
uncli := cmd.Context().Value("cli").(*cli.CLI)
|
||||
|
||||
if len(args) == 1 {
|
||||
if err := uncli.SetCurrentContext(args[0]); err != nil {
|
||||
return fmt.Errorf("failed to set the current cluster context to '%s': %w", args[0], err)
|
||||
}
|
||||
fmt.Printf("Current cluster context is now '%s'.\n", args[0])
|
||||
return nil
|
||||
}
|
||||
|
||||
return selectContext(uncli)
|
||||
},
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func selectContext(uncli *cli.CLI) error {
|
||||
if uncli.Config == nil {
|
||||
return fmt.Errorf("context management is not available: Uncloud configuration file is not being used")
|
||||
}
|
||||
if len(uncli.Config.Contexts) == 0 {
|
||||
return fmt.Errorf("no contexts found in Uncloud config (%s)", uncli.Config.Path())
|
||||
}
|
||||
|
||||
contextNames := slices.Sorted(maps.Keys(uncli.Config.Contexts))
|
||||
|
||||
var selected string
|
||||
form := huh.NewForm(
|
||||
huh.NewGroup(
|
||||
huh.NewSelect[string]().
|
||||
Title("Select a cluster context").
|
||||
Options(buildContextOptions(contextNames, uncli.Config.CurrentContext)...).
|
||||
Value(&selected),
|
||||
),
|
||||
)
|
||||
if err := form.Run(); err != nil {
|
||||
return fmt.Errorf("select cluster context: %w", err)
|
||||
}
|
||||
|
||||
if err := uncli.SetCurrentContext(selected); err != nil {
|
||||
return fmt.Errorf("set current cluster context: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Current cluster context is now '%s'.\n", selected)
|
||||
return nil
|
||||
}
|
||||
|
||||
func buildContextOptions(contexts []string, current string) []huh.Option[string] {
|
||||
options := make([]huh.Option[string], len(contexts))
|
||||
|
||||
for i, ctx := range contexts {
|
||||
opt := huh.NewOption(ctx, ctx)
|
||||
if ctx == current {
|
||||
opt.Key += " (current)"
|
||||
opt = opt.Selected(true)
|
||||
}
|
||||
|
||||
options[i] = opt
|
||||
}
|
||||
|
||||
return options
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
composecli "github.com/compose-spec/compose-go/v2/cli"
|
||||
"github.com/docker/compose/v2/pkg/progress"
|
||||
"github.com/psviderski/uncloud/internal/cli"
|
||||
"github.com/psviderski/uncloud/pkg/api"
|
||||
"github.com/psviderski/uncloud/pkg/client"
|
||||
"github.com/psviderski/uncloud/pkg/client/compose"
|
||||
"github.com/psviderski/uncloud/pkg/client/deploy"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type deployOptions struct {
|
||||
files []string
|
||||
profiles []string
|
||||
services []string
|
||||
noBuild bool
|
||||
|
||||
context string
|
||||
}
|
||||
|
||||
// NewDeployCommand creates a new command to deploy services from a Compose file.
|
||||
func NewDeployCommand() *cobra.Command {
|
||||
opts := deployOptions{}
|
||||
cmd := &cobra.Command{
|
||||
Use: "deploy [FLAGS] [SERVICE...]",
|
||||
Short: "Deploy services from a Compose file.",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
uncli := cmd.Context().Value("cli").(*cli.CLI)
|
||||
|
||||
if len(args) > 0 {
|
||||
opts.services = args
|
||||
}
|
||||
|
||||
return runDeploy(cmd.Context(), uncli, opts)
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().StringSliceVarP(&opts.files, "file", "f", nil,
|
||||
"One or more Compose files to deploy services from. (default compose.yaml)")
|
||||
cmd.Flags().StringSliceVarP(&opts.profiles, "profile", "p", nil,
|
||||
"One or more Compose profiles to enable.")
|
||||
cmd.Flags().StringVarP(&opts.context, "context", "c", "",
|
||||
"Name of the cluster context to deploy to (default is the current context)")
|
||||
cmd.Flags().BoolVarP(&opts.noBuild, "no-build", "n", false,
|
||||
"Do not build images before deploying services. (default false)")
|
||||
|
||||
// TODO: Consider adding a filter flag to specify which machines to deploy to but keep the rest running.
|
||||
// Could be useful to test a new version on a subset of machines before rolling out to all.
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
// projectOpts returns the project options for the Compose file(s).
|
||||
func projectOpts(opts deployOptions) []composecli.ProjectOptionsFn {
|
||||
projectOpts := []composecli.ProjectOptionsFn{}
|
||||
|
||||
if len(opts.profiles) > 0 {
|
||||
projectOpts = append(projectOpts, composecli.WithDefaultProfiles(opts.profiles...))
|
||||
}
|
||||
|
||||
return projectOpts
|
||||
}
|
||||
|
||||
// runDeploy parses the Compose file(s) and deploys the services.
|
||||
func runDeploy(ctx context.Context, uncli *cli.CLI, opts deployOptions) error {
|
||||
projectOpts := projectOpts(opts)
|
||||
|
||||
project, err := compose.LoadProject(ctx, opts.files, projectOpts...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("load compose file(s): %w", err)
|
||||
}
|
||||
|
||||
if len(opts.services) > 0 {
|
||||
// Includes service dependencies by default. This is the default docker compose behavior.
|
||||
project, err = project.WithSelectedServices(opts.services)
|
||||
if err != nil {
|
||||
return fmt.Errorf("select services: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
servicesToBuild := cli.GetServicesThatNeedBuild(project)
|
||||
|
||||
if len(servicesToBuild) > 0 {
|
||||
if opts.noBuild {
|
||||
fmt.Println("Not building services as requested.")
|
||||
} else {
|
||||
buildOpts := cli.BuildOptions{
|
||||
Push: true,
|
||||
NoCache: false,
|
||||
}
|
||||
|
||||
if err := cli.BuildServices(ctx, servicesToBuild, buildOpts); err != nil {
|
||||
return fmt.Errorf("build services: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
clusterClient, err := uncli.ConnectCluster(ctx, opts.context)
|
||||
if err != nil {
|
||||
return fmt.Errorf("connect to cluster: %w", err)
|
||||
}
|
||||
defer clusterClient.Close()
|
||||
|
||||
composeDeploy, err := compose.NewDeployment(ctx, clusterClient, project)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create compose deployment: %w", err)
|
||||
}
|
||||
|
||||
plan, err := composeDeploy.Plan(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("plan deployment: %w", err)
|
||||
}
|
||||
|
||||
if len(plan.Operations) == 0 {
|
||||
fmt.Println("Services are up to date.")
|
||||
return nil
|
||||
}
|
||||
|
||||
fmt.Println("Deployment plan:")
|
||||
if err = printPlan(ctx, clusterClient, plan); err != nil {
|
||||
return fmt.Errorf("print deployment plan: %w", err)
|
||||
}
|
||||
fmt.Println()
|
||||
|
||||
confirmed, err := cli.Confirm()
|
||||
if err != nil {
|
||||
return fmt.Errorf("confirm deployment: %w", err)
|
||||
}
|
||||
if !confirmed {
|
||||
fmt.Println("Cancelled. No changes were made.")
|
||||
return nil
|
||||
}
|
||||
|
||||
return progress.RunWithTitle(ctx, func(ctx context.Context) error {
|
||||
if err := plan.Execute(ctx, clusterClient); err != nil {
|
||||
return fmt.Errorf("deploy services: %w", err)
|
||||
}
|
||||
return nil
|
||||
}, uncli.ProgressOut(), "Deploying services")
|
||||
}
|
||||
|
||||
func printPlan(ctx context.Context, cli *client.Client, plan deploy.SequenceOperation) error {
|
||||
for _, op := range plan.Operations {
|
||||
svcPlan, ok := op.(*deploy.Plan)
|
||||
if !ok {
|
||||
fmt.Println("- " + op.Format(nil))
|
||||
continue
|
||||
}
|
||||
|
||||
svc, err := cli.InspectService(ctx, svcPlan.ServiceID)
|
||||
if err != nil && !errors.Is(err, api.ErrNotFound) {
|
||||
return fmt.Errorf("inspect service: %w", err)
|
||||
}
|
||||
// Initialise a machine and container name resolver to properly format the service plan output.
|
||||
resolver, err := cli.ServiceOperationNameResolver(ctx, svc)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create machine and container name resolver for service operations: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("- Deploy service [name=%s]\n", svcPlan.ServiceName)
|
||||
fmt.Println(indent(svcPlan.Format(resolver), " "))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func indent(text, prefix string) string {
|
||||
lines := strings.Split(text, "\n")
|
||||
for i, line := range lines {
|
||||
lines[i] = prefix + line
|
||||
}
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
@@ -4,15 +4,16 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/psviderski/uncloud/internal/cli"
|
||||
"github.com/spf13/cobra"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
"google.golang.org/protobuf/types/known/emptypb"
|
||||
"uncloud/internal/cli"
|
||||
)
|
||||
|
||||
type releaseOptions struct {
|
||||
cluster string
|
||||
context string
|
||||
}
|
||||
|
||||
func NewReleaseCommand() *cobra.Command {
|
||||
@@ -28,15 +29,15 @@ func NewReleaseCommand() *cobra.Command {
|
||||
}
|
||||
|
||||
cmd.Flags().StringVarP(
|
||||
&opts.cluster, "cluster", "c", "",
|
||||
"Name of the cluster. (default is the current cluster)",
|
||||
&opts.context, "context", "c", "",
|
||||
"Name of the cluster context. (default is the current context)",
|
||||
)
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func release(ctx context.Context, uncli *cli.CLI, opts releaseOptions) error {
|
||||
client, err := uncli.ConnectCluster(ctx, opts.cluster)
|
||||
client, err := uncli.ConnectCluster(ctx, opts.context)
|
||||
if err != nil {
|
||||
return fmt.Errorf("connect to cluster: %w", err)
|
||||
}
|
||||
|
||||
+24
-11
@@ -4,18 +4,22 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/psviderski/uncloud/cmd/uncloud/caddy"
|
||||
"github.com/psviderski/uncloud/internal/cli"
|
||||
"github.com/psviderski/uncloud/internal/machine/api/pb"
|
||||
"github.com/psviderski/uncloud/pkg/api"
|
||||
"github.com/psviderski/uncloud/pkg/client"
|
||||
"github.com/spf13/cobra"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
"uncloud/internal/cli"
|
||||
"uncloud/internal/machine/api/pb"
|
||||
)
|
||||
|
||||
const DefaultUncloudDNSAPIEndpoint = "https://dns.uncloud.run/v1"
|
||||
|
||||
type reserveOptions struct {
|
||||
endpoint string
|
||||
cluster string
|
||||
context string
|
||||
}
|
||||
|
||||
func NewReserveCommand() *cobra.Command {
|
||||
@@ -33,21 +37,21 @@ func NewReserveCommand() *cobra.Command {
|
||||
cmd.Flags().StringVar(&opts.endpoint, "endpoint", DefaultUncloudDNSAPIEndpoint,
|
||||
"API endpoint for the Uncloud DNS service.")
|
||||
cmd.Flags().StringVarP(
|
||||
&opts.cluster, "cluster", "c", "",
|
||||
"Name of the cluster. (default is the current cluster)",
|
||||
&opts.context, "context", "c", "",
|
||||
"Name of the cluster context. (default is the current context)",
|
||||
)
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func reserve(ctx context.Context, uncli *cli.CLI, opts reserveOptions) error {
|
||||
client, err := uncli.ConnectCluster(ctx, opts.cluster)
|
||||
clusterClient, err := uncli.ConnectCluster(ctx, opts.context)
|
||||
if err != nil {
|
||||
return fmt.Errorf("connect to cluster: %w", err)
|
||||
}
|
||||
defer client.Close()
|
||||
defer clusterClient.Close()
|
||||
|
||||
domain, err := client.ReserveDomain(ctx, &pb.ReserveDomainRequest{Endpoint: opts.endpoint})
|
||||
domain, err := clusterClient.ReserveDomain(ctx, &pb.ReserveDomainRequest{Endpoint: opts.endpoint})
|
||||
if err != nil {
|
||||
if status.Convert(err).Code() == codes.AlreadyExists {
|
||||
return errors.New("domain already reserved")
|
||||
@@ -56,7 +60,16 @@ func reserve(ctx context.Context, uncli *cli.CLI, opts reserveOptions) error {
|
||||
}
|
||||
|
||||
fmt.Printf("Reserved cluster domain: %s\n", domain.Name)
|
||||
fmt.Println("Redeploy the Caddy service ('uc caddy deploy') to configure DNS records for the domain " +
|
||||
"to route traffic to the services in the cluster.")
|
||||
return nil
|
||||
|
||||
// Update cluster domain records in Uncloud DNS to point to machines running caddy service if it has been deployed.
|
||||
if _, err = clusterClient.InspectService(ctx, client.CaddyServiceName); err != nil {
|
||||
if errors.Is(err, api.ErrNotFound) {
|
||||
fmt.Println("Deploy the Caddy reverse proxy service ('uc caddy deploy') to enable internet access " +
|
||||
"to your services via the reserved or your custom domain.")
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("inspect caddy service: %w", err)
|
||||
}
|
||||
|
||||
return caddy.UpdateDomainRecords(ctx, clusterClient, uncli.ProgressOut())
|
||||
}
|
||||
|
||||
@@ -4,13 +4,14 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/psviderski/uncloud/internal/cli"
|
||||
"github.com/psviderski/uncloud/pkg/api"
|
||||
"github.com/spf13/cobra"
|
||||
"uncloud/internal/cli"
|
||||
"uncloud/internal/cli/client"
|
||||
)
|
||||
|
||||
type showOptions struct {
|
||||
cluster string
|
||||
context string
|
||||
}
|
||||
|
||||
func NewShowCommand() *cobra.Command {
|
||||
@@ -26,15 +27,15 @@ func NewShowCommand() *cobra.Command {
|
||||
}
|
||||
|
||||
cmd.Flags().StringVarP(
|
||||
&opts.cluster, "cluster", "c", "",
|
||||
"Name of the cluster. (default is the current cluster)",
|
||||
&opts.context, "context", "c", "",
|
||||
"Name of the cluster context. (default is the current context)",
|
||||
)
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func show(ctx context.Context, uncli *cli.CLI, opts showOptions) error {
|
||||
clusterClient, err := uncli.ConnectCluster(ctx, opts.cluster)
|
||||
clusterClient, err := uncli.ConnectCluster(ctx, opts.context)
|
||||
if err != nil {
|
||||
return fmt.Errorf("connect to cluster: %w", err)
|
||||
}
|
||||
@@ -42,7 +43,7 @@ func show(ctx context.Context, uncli *cli.CLI, opts showOptions) error {
|
||||
|
||||
domain, err := clusterClient.GetDomain(ctx)
|
||||
if err != nil {
|
||||
if errors.Is(err, client.ErrNotFound) {
|
||||
if errors.Is(err, api.ErrNotFound) {
|
||||
return errors.New("no domain reserved")
|
||||
}
|
||||
return err
|
||||
|
||||
+44
-26
@@ -4,18 +4,19 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"time"
|
||||
|
||||
"github.com/cenkalti/backoff/v4"
|
||||
"github.com/docker/compose/v2/pkg/progress"
|
||||
"github.com/psviderski/uncloud/cmd/uncloud/caddy"
|
||||
"github.com/psviderski/uncloud/internal/cli"
|
||||
"github.com/psviderski/uncloud/internal/cli/config"
|
||||
"github.com/psviderski/uncloud/pkg/api"
|
||||
"github.com/psviderski/uncloud/pkg/client"
|
||||
"github.com/spf13/cobra"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
"google.golang.org/protobuf/types/known/emptypb"
|
||||
"net/netip"
|
||||
"time"
|
||||
"uncloud/internal/cli"
|
||||
"uncloud/internal/cli/client"
|
||||
"uncloud/internal/cli/config"
|
||||
"uncloud/internal/machine/api/pb"
|
||||
)
|
||||
|
||||
type addOptions struct {
|
||||
@@ -23,7 +24,8 @@ type addOptions struct {
|
||||
noCaddy bool
|
||||
publicIP string
|
||||
sshKey string
|
||||
cluster string
|
||||
context string
|
||||
version string
|
||||
}
|
||||
|
||||
func NewAddCommand() *cobra.Command {
|
||||
@@ -60,13 +62,18 @@ func NewAddCommand() *cobra.Command {
|
||||
"blank '' or 'none' to disable ingress on this machine, or specify an IP address.",
|
||||
)
|
||||
cmd.Flags().StringVarP(
|
||||
&opts.sshKey, "ssh-key", "i", "",
|
||||
"path to SSH private key for SSH remote login. (default ~/.ssh/id_*)",
|
||||
&opts.sshKey, "ssh-key", "i", "~/.ssh/id_ed25519",
|
||||
"Path to SSH private key for remote login (if not already added to SSH agent).",
|
||||
)
|
||||
cmd.Flags().StringVar(
|
||||
&opts.version, "version", "latest",
|
||||
"Version of the Uncloud daemon to install on the machine.",
|
||||
)
|
||||
cmd.Flags().StringVarP(
|
||||
&opts.cluster, "cluster", "c", "",
|
||||
"Name of the cluster to add the machine to. (default is the current cluster)",
|
||||
&opts.context, "context", "c", "",
|
||||
"Name of the cluster context to add the machine to. (default is the current context)",
|
||||
)
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
@@ -85,10 +92,17 @@ func add(ctx context.Context, uncli *cli.CLI, remoteMachine cli.RemoteMachine, o
|
||||
publicIP = &ip
|
||||
}
|
||||
|
||||
machineClient, err := uncli.AddMachine(ctx, remoteMachine, opts.cluster, opts.name, publicIP)
|
||||
clusterClient, machineClient, err := uncli.AddMachine(ctx, cli.AddMachineOptions{
|
||||
Context: opts.context,
|
||||
MachineName: opts.name,
|
||||
PublicIP: publicIP,
|
||||
RemoteMachine: remoteMachine,
|
||||
Version: opts.version,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer clusterClient.Close()
|
||||
defer machineClient.Close()
|
||||
|
||||
if opts.noCaddy {
|
||||
@@ -97,24 +111,20 @@ func add(ctx context.Context, uncli *cli.CLI, remoteMachine cli.RemoteMachine, o
|
||||
|
||||
// Wait for the cluster to be initialised to be able to deploy the Caddy service.
|
||||
fmt.Println("Waiting for the machine to be ready...")
|
||||
fmt.Println()
|
||||
if err = waitClusterInitialised(ctx, machineClient); err != nil {
|
||||
return fmt.Errorf("wait for cluster to be initialised on machine: %w", err)
|
||||
}
|
||||
|
||||
// Inspect the added machine to get its ID to create a filter for the Caddy deployment.
|
||||
minfo, err := machineClient.Inspect(ctx, &emptypb.Empty{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("inspect machine: %w", err)
|
||||
}
|
||||
filter := func(m *pb.MachineInfo) bool {
|
||||
return m.Id == minfo.Id
|
||||
}
|
||||
// Deploy a Caddy service container to the added machine. If caddy service is already deployed on other machines,
|
||||
// use the deployed image version. Otherwise, use the latest version.
|
||||
// NOTE: We use the cluster client to inspect and scale the Caddy service because the newly added machine may have
|
||||
// issues accessing the Machine API of existing machines in the cluster.
|
||||
// See the issue for more details: https://github.com/psviderski/uncloud/issues/65.
|
||||
caddyImage := ""
|
||||
caddySvc, err := machineClient.InspectService(ctx, client.CaddyServiceName)
|
||||
caddySvc, err := clusterClient.InspectService(ctx, client.CaddyServiceName)
|
||||
if err != nil {
|
||||
if !errors.Is(err, client.ErrNotFound) {
|
||||
if !errors.Is(err, api.ErrNotFound) {
|
||||
return fmt.Errorf("inspect caddy service: %w", err)
|
||||
}
|
||||
} else {
|
||||
@@ -133,17 +143,25 @@ func add(ctx context.Context, uncli *cli.CLI, remoteMachine cli.RemoteMachine, o
|
||||
}
|
||||
}
|
||||
|
||||
d, err := machineClient.NewCaddyDeployment(caddyImage, filter)
|
||||
// TODO: scale the existing Caddy service to the new machine instead of running a new deployment
|
||||
// that may cause a small downtime.
|
||||
d, err := clusterClient.NewCaddyDeployment(caddyImage, api.Placement{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("create caddy deployment: %w", err)
|
||||
}
|
||||
|
||||
return progress.RunWithTitle(ctx, func(ctx context.Context) error {
|
||||
err = progress.RunWithTitle(ctx, func(ctx context.Context) error {
|
||||
if _, err = d.Run(ctx); err != nil {
|
||||
return fmt.Errorf("deploy caddy: %w", err)
|
||||
}
|
||||
return nil
|
||||
}, uncli.ProgressOut(), fmt.Sprintf("Deploying service %s", d.Spec.Name))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
return caddy.UpdateDomainRecords(ctx, machineClient, uncli.ProgressOut())
|
||||
}
|
||||
|
||||
func waitClusterInitialised(ctx context.Context, client *client.Client) error {
|
||||
@@ -153,7 +171,7 @@ func waitClusterInitialised(ctx context.Context, client *client.Client) error {
|
||||
), ctx)
|
||||
|
||||
check := func() error {
|
||||
_, err := client.ListMachines(ctx)
|
||||
_, err := client.ListMachines(ctx, nil)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
+33
-18
@@ -3,15 +3,17 @@ package machine
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/docker/compose/v2/pkg/progress"
|
||||
"github.com/spf13/cobra"
|
||||
"net/netip"
|
||||
"uncloud/cmd/uncloud/caddy"
|
||||
"uncloud/cmd/uncloud/dns"
|
||||
"uncloud/internal/cli"
|
||||
"uncloud/internal/cli/config"
|
||||
"uncloud/internal/machine/api/pb"
|
||||
"uncloud/internal/machine/cluster"
|
||||
|
||||
"github.com/docker/compose/v2/pkg/progress"
|
||||
"github.com/psviderski/uncloud/cmd/uncloud/caddy"
|
||||
"github.com/psviderski/uncloud/cmd/uncloud/dns"
|
||||
"github.com/psviderski/uncloud/internal/cli"
|
||||
"github.com/psviderski/uncloud/internal/cli/config"
|
||||
"github.com/psviderski/uncloud/internal/machine/api/pb"
|
||||
"github.com/psviderski/uncloud/internal/machine/cluster"
|
||||
"github.com/psviderski/uncloud/pkg/api"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type initOptions struct {
|
||||
@@ -22,15 +24,17 @@ type initOptions struct {
|
||||
noDNS bool
|
||||
publicIP string
|
||||
sshKey string
|
||||
cluster string
|
||||
version string
|
||||
context string
|
||||
}
|
||||
|
||||
func NewInitCommand() *cobra.Command {
|
||||
opts := initOptions{}
|
||||
cmd := &cobra.Command{
|
||||
Use: "init [USER@HOST:PORT]",
|
||||
Short: "Initialise a new cluster that consists of the local or remote machine.",
|
||||
// TODO: include usage examples of initialising a local and remote machine.
|
||||
Short: "Initialise a new cluster with a remote machine as the first member.",
|
||||
// TODO: include usage examples of initialising a remote machine.
|
||||
// TODO: support initialising a cluster on the local machine.
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
uncli := cmd.Context().Value("cli").(*cli.CLI)
|
||||
@@ -76,12 +80,16 @@ func NewInitCommand() *cobra.Command {
|
||||
"blank '' or 'none' to disable ingress on this machine, or specify an IP address.",
|
||||
)
|
||||
cmd.Flags().StringVarP(
|
||||
&opts.sshKey, "ssh-key", "i", "",
|
||||
"path to SSH private key for SSH remote login. (default ~/.ssh/id_*)",
|
||||
&opts.sshKey, "ssh-key", "i", "~/.ssh/id_ed25519",
|
||||
"Path to SSH private key for remote login (if not already added to SSH agent).",
|
||||
)
|
||||
cmd.Flags().StringVar(
|
||||
&opts.version, "version", "latest",
|
||||
"Version of the Uncloud daemon to install on the machine.",
|
||||
)
|
||||
cmd.Flags().StringVarP(
|
||||
&opts.cluster, "cluster", "c", "",
|
||||
"Name of the cluster in the local config if initialising a remote machine.",
|
||||
&opts.context, "context", "c", "default",
|
||||
"Name of the created context for the initialised cluster in the Uncloud config.",
|
||||
)
|
||||
|
||||
return cmd
|
||||
@@ -106,8 +114,14 @@ func initCluster(ctx context.Context, uncli *cli.CLI, remoteMachine *cli.RemoteM
|
||||
}
|
||||
publicIP = &ip
|
||||
}
|
||||
|
||||
client, err := uncli.InitCluster(ctx, remoteMachine, opts.cluster, opts.name, netPrefix, publicIP)
|
||||
client, err := uncli.InitCluster(ctx, cli.InitClusterOptions{
|
||||
Context: opts.context,
|
||||
MachineName: opts.name,
|
||||
Network: netPrefix,
|
||||
PublicIP: publicIP,
|
||||
RemoteMachine: remoteMachine,
|
||||
Version: opts.version,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -121,6 +135,7 @@ func initCluster(ctx context.Context, uncli *cli.CLI, remoteMachine *cli.RemoteM
|
||||
// The creation of a deployment plan talks to cluster API. Since the API needs a few moments to become available
|
||||
// after cluster initialisation, we keep the user informed during this wait.
|
||||
fmt.Println("Waiting for the machine to be ready...")
|
||||
fmt.Println()
|
||||
|
||||
if !opts.noDNS {
|
||||
domain, err := client.ReserveDomain(ctx, &pb.ReserveDomainRequest{Endpoint: opts.dnsEndpoint})
|
||||
@@ -131,7 +146,7 @@ func initCluster(ctx context.Context, uncli *cli.CLI, remoteMachine *cli.RemoteM
|
||||
}
|
||||
|
||||
if !opts.noCaddy {
|
||||
d, err := client.NewCaddyDeployment("", nil)
|
||||
d, err := client.NewCaddyDeployment("", api.Placement{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("create caddy deployment: %w", err)
|
||||
}
|
||||
|
||||
@@ -3,29 +3,30 @@ package machine
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/spf13/cobra"
|
||||
"net/netip"
|
||||
"os"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
"uncloud/internal/cli"
|
||||
"uncloud/internal/machine/network"
|
||||
|
||||
"github.com/psviderski/uncloud/internal/cli"
|
||||
"github.com/psviderski/uncloud/internal/machine/network"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func NewListCommand() *cobra.Command {
|
||||
var cluster string
|
||||
var contextName string
|
||||
cmd := &cobra.Command{
|
||||
Use: "ls",
|
||||
Aliases: []string{"list"},
|
||||
Short: "List machines in a cluster.",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
uncli := cmd.Context().Value("cli").(*cli.CLI)
|
||||
return list(cmd.Context(), uncli, cluster)
|
||||
return list(cmd.Context(), uncli, contextName)
|
||||
},
|
||||
}
|
||||
cmd.Flags().StringVarP(
|
||||
&cluster, "cluster", "c", "",
|
||||
"Name of the cluster. (default is the current cluster)",
|
||||
&contextName, "context", "c", "",
|
||||
"Name of the cluster context. (default is the current context)",
|
||||
)
|
||||
return cmd
|
||||
}
|
||||
@@ -37,7 +38,7 @@ func list(ctx context.Context, uncli *cli.CLI, clusterName string) error {
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
machines, err := client.ListMachines(ctx)
|
||||
machines, err := client.ListMachines(ctx, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("list machines: %w", err)
|
||||
}
|
||||
@@ -67,7 +68,8 @@ func list(ctx context.Context, uncli *cli.CLI, clusterName string) error {
|
||||
}
|
||||
|
||||
if _, err = fmt.Fprintf(
|
||||
tw, "%s\t%s\t%s\t%s\t%s\n", m.Name, capitalise(member.State.String()), subnet, publicIP, strings.Join(endpoints, ", "),
|
||||
tw, "%s\t%s\t%s\t%s\t%s\n", m.Name, capitalise(member.State.String()), subnet, publicIP,
|
||||
strings.Join(endpoints, ", "),
|
||||
); err != nil {
|
||||
return fmt.Errorf("write row: %w", err)
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
package machine
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"maps"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/charmbracelet/lipgloss/tree"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/psviderski/uncloud/internal/cli"
|
||||
"github.com/psviderski/uncloud/pkg/api"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type removeOptions struct {
|
||||
force bool
|
||||
yes bool
|
||||
context string
|
||||
}
|
||||
|
||||
func NewRmCommand() *cobra.Command {
|
||||
opts := removeOptions{}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "rm MACHINE",
|
||||
Aliases: []string{"remove", "delete"},
|
||||
Short: "Remove a machine from a cluster.",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
uncli := cmd.Context().Value("cli").(*cli.CLI)
|
||||
return remove(cmd.Context(), uncli, args[0], opts)
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().StringVarP(&opts.context, "context", "c", "",
|
||||
"Name of the cluster context. (default is the current context)")
|
||||
cmd.Flags().BoolVarP(&opts.yes, "yes", "y", false,
|
||||
"Do not prompt for confirmation before removing the machine.")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func remove(ctx context.Context, uncli *cli.CLI, machineName string, opts removeOptions) error {
|
||||
client, err := uncli.ConnectCluster(ctx, opts.context)
|
||||
if err != nil {
|
||||
return fmt.Errorf("connect to cluster: %w", err)
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
// Verify the machine exists and list all service containers on it including stopped ones.
|
||||
listCtx, machines, err := api.ProxyMachinesContext(ctx, client, []string{machineName})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(machines) == 0 {
|
||||
return fmt.Errorf("machine '%s' not found in the cluster", machineName)
|
||||
}
|
||||
m := machines[0].Machine
|
||||
|
||||
listOpts := container.ListOptions{All: true}
|
||||
machineContainers, err := client.Docker.ListServiceContainers(listCtx, "", listOpts)
|
||||
if err != nil {
|
||||
return fmt.Errorf("list containers: %w", err)
|
||||
}
|
||||
containers := machineContainers[0].Containers
|
||||
|
||||
if len(containers) > 0 {
|
||||
fmt.Printf("Found %d service containers on machine '%s':\n\n", len(containers), m.Name)
|
||||
fmt.Println(formatContainerTree(containers))
|
||||
fmt.Println()
|
||||
fmt.Println("This will remove all service containers on the machine, reset it to the uninitialised state, " +
|
||||
"and remove it from the cluster.")
|
||||
} else {
|
||||
fmt.Printf("No service containers found on machine '%s'.\n", m.Name)
|
||||
fmt.Println("This will reset the machine to the uninitialised state and remove it from the cluster.")
|
||||
}
|
||||
|
||||
if !opts.yes {
|
||||
confirmed, err := cli.Confirm()
|
||||
if err != nil {
|
||||
return fmt.Errorf("confirm removal: %w", err)
|
||||
}
|
||||
if !confirmed {
|
||||
fmt.Println("Cancelled. Machine was not removed.")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: 3. Remove all service containers on the machine.
|
||||
// TODO: 4. Implement and call ResetMachine via Machine API to reset the machine state to uninitialised.
|
||||
// TODO: 5. Remove the machine from the cluster store.
|
||||
|
||||
fmt.Printf("Machine '%s' removed from the cluster.\n", m.Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
// formatContainerTree formats a list of containers grouped by service as a tree structure.
|
||||
func formatContainerTree(containers []api.ServiceContainer) string {
|
||||
if len(containers) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Group containers by service.
|
||||
serviceContainers := make(map[string][]api.ServiceContainer)
|
||||
for _, ctr := range containers {
|
||||
serviceName := ctr.ServiceName()
|
||||
serviceContainers[serviceName] = append(serviceContainers[serviceName], ctr)
|
||||
}
|
||||
|
||||
// Build tree output.
|
||||
var output []string
|
||||
serviceNames := slices.Sorted(maps.Keys(serviceContainers))
|
||||
for _, serviceName := range serviceNames {
|
||||
ctrs := serviceContainers[serviceName]
|
||||
mode := ctrs[0].ServiceMode()
|
||||
|
||||
// Format a tree for the service with its containers.
|
||||
plural := ""
|
||||
if len(ctrs) > 1 {
|
||||
plural = "s"
|
||||
}
|
||||
t := tree.Root(fmt.Sprintf("• %s (%s, %d container%s)", serviceName, mode, len(ctrs), plural)).
|
||||
EnumeratorStyle(lipgloss.NewStyle().MarginLeft(2).MarginRight(1))
|
||||
|
||||
// Add containers as children.
|
||||
for _, ctr := range ctrs {
|
||||
state, _ := ctr.HumanState()
|
||||
info := fmt.Sprintf("%s • %s • %s", ctr.Name, ctr.Config.Image, state)
|
||||
t.Child(info)
|
||||
}
|
||||
|
||||
output = append(output, t.String())
|
||||
}
|
||||
|
||||
return strings.Join(output, "\n")
|
||||
}
|
||||
@@ -6,13 +6,15 @@ import (
|
||||
|
||||
func NewRootCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "machine",
|
||||
Short: "Manage machines in an Uncloud cluster.",
|
||||
Use: "machine",
|
||||
Aliases: []string{"m"},
|
||||
Short: "Manage machines in an Uncloud cluster.",
|
||||
}
|
||||
cmd.AddCommand(
|
||||
NewAddCommand(),
|
||||
NewInitCommand(),
|
||||
NewListCommand(),
|
||||
NewRmCommand(),
|
||||
NewTokenCommand(),
|
||||
)
|
||||
return cmd
|
||||
|
||||
@@ -3,8 +3,8 @@ package machine
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/spf13/cobra"
|
||||
"uncloud/internal/daemon"
|
||||
"uncloud/internal/machine"
|
||||
"github.com/psviderski/uncloud/internal/daemon"
|
||||
"github.com/psviderski/uncloud/internal/machine"
|
||||
)
|
||||
|
||||
type tokenOptions struct {
|
||||
|
||||
+54
-18
@@ -3,47 +3,81 @@ package main
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/spf13/cobra"
|
||||
"os"
|
||||
"net/netip"
|
||||
"strings"
|
||||
"uncloud/cmd/uncloud/caddy"
|
||||
"uncloud/cmd/uncloud/dns"
|
||||
"uncloud/cmd/uncloud/machine"
|
||||
"uncloud/cmd/uncloud/service"
|
||||
"uncloud/internal/cli"
|
||||
|
||||
"github.com/psviderski/uncloud/cmd/uncloud/caddy"
|
||||
cmdcontext "github.com/psviderski/uncloud/cmd/uncloud/context"
|
||||
"github.com/psviderski/uncloud/cmd/uncloud/dns"
|
||||
"github.com/psviderski/uncloud/cmd/uncloud/machine"
|
||||
"github.com/psviderski/uncloud/cmd/uncloud/service"
|
||||
"github.com/psviderski/uncloud/cmd/uncloud/volume"
|
||||
"github.com/psviderski/uncloud/internal/cli"
|
||||
"github.com/psviderski/uncloud/internal/cli/config"
|
||||
"github.com/psviderski/uncloud/internal/fs"
|
||||
"github.com/psviderski/uncloud/internal/version"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type globalOptions struct {
|
||||
configPath string
|
||||
connect string
|
||||
}
|
||||
|
||||
func main() {
|
||||
var configPath string
|
||||
opts := globalOptions{}
|
||||
cmd := &cobra.Command{
|
||||
Use: "uncloud",
|
||||
Short: "A CLI tool for managing Uncloud resources such as clusters, machines, and services.",
|
||||
Version: version.String(),
|
||||
SilenceUsage: true,
|
||||
SilenceErrors: true,
|
||||
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
if strings.HasPrefix(configPath, "~/") {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return fmt.Errorf("get user home directory to resolve %q: %w", configPath, err)
|
||||
var conn *config.MachineConnection
|
||||
if opts.connect != "" {
|
||||
if strings.HasPrefix(opts.connect, "tcp://") {
|
||||
addrPort, err := netip.ParseAddrPort(opts.connect[len("tcp://"):])
|
||||
if err != nil {
|
||||
return fmt.Errorf("parse TCP address: %w", err)
|
||||
}
|
||||
conn = &config.MachineConnection{
|
||||
TCP: &addrPort,
|
||||
}
|
||||
} else {
|
||||
dest := opts.connect
|
||||
if strings.HasPrefix(dest, "ssh://") {
|
||||
dest = dest[len("ssh://"):]
|
||||
}
|
||||
conn = &config.MachineConnection{
|
||||
SSH: config.SSHDestination(dest),
|
||||
}
|
||||
}
|
||||
configPath = strings.Replace(configPath, "~", home, 1)
|
||||
}
|
||||
|
||||
uncli, err := cli.New(configPath)
|
||||
configPath := fs.ExpandHomeDir(opts.configPath)
|
||||
uncli, err := cli.New(configPath, conn)
|
||||
if err != nil {
|
||||
return fmt.Errorf("initialize CLI: %w", err)
|
||||
return fmt.Errorf("initialise CLI: %w", err)
|
||||
}
|
||||
cmd.SetContext(context.WithValue(cmd.Context(), "cli", uncli))
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().StringVar(&opts.connect, "connect", "",
|
||||
"Connect to a remote cluster machine without using the Uncloud configuration file.\n"+
|
||||
"Format: [ssh://]user@host[:port] or tcp://host:port")
|
||||
// TODO: allow to override using UNCLOUD_CONFIG env var.
|
||||
cmd.PersistentFlags().StringVar(&configPath, "uncloud-config", "~/.config/uncloud/config.toml",
|
||||
"path to the Uncloud configuration file.")
|
||||
_ = cmd.MarkPersistentFlagFilename("uncloud-config", "toml")
|
||||
cmd.PersistentFlags().StringVar(&opts.configPath, "uncloud-config", "~/.config/uncloud/config.yaml",
|
||||
"Path to the Uncloud configuration file.")
|
||||
_ = cmd.MarkPersistentFlagFilename("uncloud-config", "yaml", "yml")
|
||||
// TODO: make --context a global flag and pass it as a value of the command context.
|
||||
|
||||
cmd.AddCommand(
|
||||
NewDeployCommand(),
|
||||
NewBuildCommand(),
|
||||
caddy.NewRootCommand(),
|
||||
cmdcontext.NewRootCommand(),
|
||||
dns.NewRootCommand(),
|
||||
machine.NewRootCommand(),
|
||||
service.NewRootCommand(),
|
||||
@@ -51,6 +85,8 @@ func main() {
|
||||
service.NewListCommand(),
|
||||
service.NewRmCommand(),
|
||||
service.NewRunCommand(),
|
||||
service.NewScaleCommand(),
|
||||
volume.NewRootCommand(),
|
||||
)
|
||||
cobra.CheckErr(cmd.Execute())
|
||||
}
|
||||
|
||||
@@ -3,14 +3,15 @@ package service
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/docker/docker/pkg/stringid"
|
||||
"github.com/docker/go-units"
|
||||
"os"
|
||||
"text/tabwriter"
|
||||
"time"
|
||||
|
||||
"github.com/docker/docker/pkg/stringid"
|
||||
"github.com/docker/go-units"
|
||||
|
||||
"github.com/psviderski/uncloud/internal/cli"
|
||||
"github.com/spf13/cobra"
|
||||
"uncloud/internal/cli"
|
||||
)
|
||||
|
||||
type inspectOptions struct {
|
||||
@@ -21,7 +22,7 @@ type inspectOptions struct {
|
||||
func NewInspectCommand() *cobra.Command {
|
||||
opts := inspectOptions{}
|
||||
cmd := &cobra.Command{
|
||||
Use: "inspect",
|
||||
Use: "inspect SERVICE",
|
||||
Short: "Display detailed information on a service.",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
@@ -31,8 +32,8 @@ func NewInspectCommand() *cobra.Command {
|
||||
},
|
||||
}
|
||||
cmd.Flags().StringVarP(
|
||||
&opts.cluster, "cluster", "c", "",
|
||||
"Name of the cluster. (default is the current cluster)",
|
||||
&opts.cluster, "context", "c", "",
|
||||
"Name of the cluster context. (default is the current context)",
|
||||
)
|
||||
return cmd
|
||||
}
|
||||
@@ -49,7 +50,7 @@ func inspect(ctx context.Context, uncli *cli.CLI, opts inspectOptions) error {
|
||||
return fmt.Errorf("inspect service: %w", err)
|
||||
}
|
||||
|
||||
machines, err := client.ListMachines(ctx)
|
||||
machines, err := client.ListMachines(ctx, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("list machines: %w", err)
|
||||
}
|
||||
|
||||
@@ -3,14 +3,16 @@ package service
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/spf13/cobra"
|
||||
"os"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
"uncloud/internal/cli"
|
||||
|
||||
"github.com/psviderski/uncloud/internal/cli"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func NewListCommand() *cobra.Command {
|
||||
// TODO(lhf): rename to context
|
||||
var cluster string
|
||||
cmd := &cobra.Command{
|
||||
Use: "ls",
|
||||
@@ -22,8 +24,8 @@ func NewListCommand() *cobra.Command {
|
||||
},
|
||||
}
|
||||
cmd.Flags().StringVarP(
|
||||
&cluster, "cluster", "c", "",
|
||||
"Name of the cluster. (default is the current cluster)",
|
||||
&cluster, "context", "c", "",
|
||||
"Name of the cluster context. (default is the current context)",
|
||||
)
|
||||
return cmd
|
||||
}
|
||||
@@ -3,11 +3,10 @@ package service
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/docker/cli/cli/streams"
|
||||
|
||||
"github.com/docker/compose/v2/pkg/progress"
|
||||
"github.com/psviderski/uncloud/internal/cli"
|
||||
"github.com/spf13/cobra"
|
||||
"os"
|
||||
"uncloud/internal/cli"
|
||||
)
|
||||
|
||||
type rmOptions struct {
|
||||
@@ -29,8 +28,8 @@ func NewRmCommand() *cobra.Command {
|
||||
},
|
||||
}
|
||||
cmd.Flags().StringVarP(
|
||||
&opts.cluster, "cluster", "c", "",
|
||||
"Name of the cluster. (default is the current cluster)",
|
||||
&opts.cluster, "context", "c", "",
|
||||
"Name of the cluster context. (default is the current context)",
|
||||
)
|
||||
return cmd
|
||||
}
|
||||
@@ -48,7 +47,7 @@ func rm(ctx context.Context, uncli *cli.CLI, opts rmOptions) error {
|
||||
return fmt.Errorf("remove service '%s': %w", s, err)
|
||||
}
|
||||
return nil
|
||||
}, streams.NewOut(os.Stdout), "Removing service "+s)
|
||||
}, uncli.ProgressOut(), "Removing service "+s)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -6,14 +6,16 @@ import (
|
||||
|
||||
func NewRootCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "service",
|
||||
Short: "Manage services in an Uncloud cluster.",
|
||||
Use: "service",
|
||||
Aliases: []string{"svc"},
|
||||
Short: "Manage services in an Uncloud cluster.",
|
||||
}
|
||||
cmd.AddCommand(
|
||||
NewInspectCommand(),
|
||||
NewListCommand(),
|
||||
NewRmCommand(),
|
||||
NewRunCommand(),
|
||||
NewScaleCommand(),
|
||||
)
|
||||
return cmd
|
||||
}
|
||||
|
||||
+283
-78
@@ -3,24 +3,36 @@ package service
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/spf13/cobra"
|
||||
"slices"
|
||||
"os"
|
||||
"strings"
|
||||
"uncloud/internal/api"
|
||||
"uncloud/internal/cli"
|
||||
"uncloud/internal/cli/client"
|
||||
"uncloud/internal/machine/api/pb"
|
||||
|
||||
dockeropts "github.com/docker/cli/opts"
|
||||
"github.com/docker/compose/v2/pkg/progress"
|
||||
"github.com/docker/docker/daemon/names"
|
||||
"github.com/psviderski/uncloud/internal/cli"
|
||||
"github.com/psviderski/uncloud/internal/secret"
|
||||
"github.com/psviderski/uncloud/pkg/api"
|
||||
"github.com/psviderski/uncloud/pkg/client/deploy"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type runOptions struct {
|
||||
command []string
|
||||
image string
|
||||
machines []string
|
||||
mode string
|
||||
name string
|
||||
publish []string
|
||||
replicas uint
|
||||
volumes []string
|
||||
command []string
|
||||
cpu dockeropts.NanoCPUs
|
||||
entrypoint string
|
||||
entrypointChanged bool
|
||||
env []string
|
||||
image string
|
||||
machines []string
|
||||
memory dockeropts.MemBytes
|
||||
mode string
|
||||
name string
|
||||
privileged bool
|
||||
publish []string
|
||||
pull string
|
||||
replicas uint
|
||||
user string
|
||||
volumes []string
|
||||
|
||||
cluster string
|
||||
}
|
||||
@@ -35,6 +47,7 @@ func NewRunCommand() *cobra.Command {
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
uncli := cmd.Context().Value("cli").(*cli.CLI)
|
||||
|
||||
opts.entrypointChanged = cmd.Flag("entrypoint").Changed
|
||||
opts.image = args[0]
|
||||
if len(args) > 1 {
|
||||
opts.command = args[1:]
|
||||
@@ -44,115 +57,307 @@ func NewRunCommand() *cobra.Command {
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().VarP(&opts.cpu, "cpu", "",
|
||||
"Maximum number of CPU cores a service container can use. Fractional values are allowed: "+
|
||||
"0.5 for half a core or 2.25 for two and a quarter cores.")
|
||||
cmd.Flags().StringVar(&opts.entrypoint, "entrypoint", "",
|
||||
"Overwrite the default ENTRYPOINT of the image. Pass an empty string \"\" to reset it.")
|
||||
cmd.Flags().StringSliceVarP(&opts.env, "env", "e", nil,
|
||||
"Set an environment variable for service containers. Can be specified multiple times.\n"+
|
||||
"Format: VAR=value or just VAR to use the value from the local environment.")
|
||||
cmd.Flags().StringVar(&opts.mode, "mode", api.ServiceModeReplicated,
|
||||
fmt.Sprintf("Replication mode of the service: either %q (a specified number of containers across "+
|
||||
"the machines) or %q (one container on every machine).",
|
||||
fmt.Sprintf("Replication mode of the service: either '%s' (a specified number of containers across "+
|
||||
"the machines) or '%s' (one container on every machine).",
|
||||
api.ServiceModeReplicated, api.ServiceModeGlobal))
|
||||
cmd.Flags().StringSliceVarP(&opts.machines, "machine", "m", nil,
|
||||
"Placement constraint by machine name, 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)")
|
||||
cmd.Flags().VarP(&opts.memory, "memory", "",
|
||||
"Maximum amount of memory a service container can use. Value is a positive integer with optional unit suffix "+
|
||||
"(b, k, m, g). Default unit is bytes if no suffix specified.\n"+
|
||||
"Examples: 1073741824, 1024m, 1g (all equal 1 gibibyte)")
|
||||
cmd.Flags().StringVarP(&opts.name, "name", "n", "",
|
||||
"Assign a name to the service. A random name is generated if not specified.")
|
||||
cmd.Flags().BoolVar(&opts.privileged, "privileged", false,
|
||||
"Give extended privileges to service containers. This is a security risk and should be used with caution.")
|
||||
cmd.Flags().StringSliceVarP(&opts.publish, "publish", "p", nil,
|
||||
"Publish a service port to make it accessible outside the cluster. Can be specified multiple times.\n"+
|
||||
"Format: [hostname:][load_balancer_port:]container_port[/protocol] or [host_ip:]:host_port:container_port[/protocol]@host\n"+
|
||||
"Supported protocols: tcp, udp, http, https (default is tcp). If a hostname for http(s) port is not specified,\n"+
|
||||
"service-name.cluster-domain will be used as the hostname.\n"+
|
||||
"Supported protocols: tcp, udp, http, https (default is tcp). If a hostname for http(s) port is not specified\n"+
|
||||
"and a cluster domain is reserved, service-name.cluster-domain will be used as the hostname.\n"+
|
||||
"Examples:\n"+
|
||||
" -p 8080/https Publish port 8080 as HTTPS via load balancer with default service-name.cluster-domain hostname\n"+
|
||||
" -p app.example.com:8080/https Publish port 8080 as HTTPS via load balancer with custom hostname\n"+
|
||||
" -p 9000:8080 Publish port 8080 as TCP port 9000 via load balancer\n"+
|
||||
" -p 8080/https Publish port 8080 as HTTPS via reverse proxy with default service-name.cluster-domain hostname\n"+
|
||||
" -p app.example.com:8080/https Publish port 8080 as HTTPS via reverse proxy with custom hostname\n"+
|
||||
// TODO: add support for publishing L4 tcp/udp ports.
|
||||
//" -p 9000:8080 Publish port 8080 as TCP port 9000 via reverse proxy\n"+
|
||||
" -p 53:5353/udp@host Bind UDP port 5353 to host port 53")
|
||||
cmd.Flags().StringVar(&opts.pull, "pull", api.PullPolicyMissing,
|
||||
fmt.Sprintf("Pull image from the registry before running service containers ('%s', '%s', '%s').",
|
||||
api.PullPolicyAlways, api.PullPolicyMissing, api.PullPolicyNever))
|
||||
cmd.Flags().UintVar(&opts.replicas, "replicas", 1,
|
||||
"Number of containers to run for the service. Only valid for a replicated service.")
|
||||
cmd.Flags().StringVarP(&opts.user, "user", "u", "",
|
||||
"User name or UID and optionally group name or GID used for running the command inside service containers.\n"+
|
||||
"Format: USER[:GROUP] or UID[:GID]. If not specified, the user is set to the default user of the image.")
|
||||
cmd.Flags().StringSliceVarP(&opts.volumes, "volume", "v", nil,
|
||||
"Bind mount a host file or directory into a service container using the format "+
|
||||
"/host/path:/container/path[:ro]. Can be specified multiple times.")
|
||||
"Mount a data volume or host path into service containers. Service containers will be scheduled on the machine(s) where\n"+
|
||||
"the volume is located. Can be specified multiple times.\n"+
|
||||
"Format: volume_name:/container/path[:ro|volume-nocopy] or /host/path:/container/path[:ro]\n"+
|
||||
"Examples:\n"+
|
||||
" -v postgres-data:/var/lib/postgresql/data Mount volume 'postgres-data' to /var/lib/postgresql/data in container\n"+
|
||||
" -v /data/uploads:/app/uploads Bind mount /data/uploads host directory to /app/uploads in container\n"+
|
||||
" -v /host/path:/container/path:ro Bind mount a host directory or file as read-only")
|
||||
|
||||
cmd.Flags().StringVarP(
|
||||
&opts.cluster, "cluster", "c", "",
|
||||
"Name of the cluster to run the service in. (default is the current cluster)",
|
||||
&opts.cluster, "context", "c", "",
|
||||
"Name of the cluster context to run the service in. (default is the current context)",
|
||||
)
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func run(ctx context.Context, uncli *cli.CLI, opts runOptions) error {
|
||||
spec, err := prepareServiceSpec(opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
clusterClient, err := uncli.ConnectCluster(ctx, opts.cluster)
|
||||
if err != nil {
|
||||
return fmt.Errorf("connect to cluster: %w", err)
|
||||
}
|
||||
defer clusterClient.Close()
|
||||
|
||||
var resp api.RunServiceResponse
|
||||
err = progress.RunWithTitle(ctx, func(ctx context.Context) error {
|
||||
resp, err = clusterClient.RunService(ctx, spec)
|
||||
if err != nil {
|
||||
return fmt.Errorf("run service: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}, uncli.ProgressOut(), fmt.Sprintf("Running service %s (%s mode)", spec.Name, spec.Mode))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
svc, err := clusterClient.InspectService(ctx, resp.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("inspect service: %w", err)
|
||||
}
|
||||
|
||||
endpoints := svc.Endpoints()
|
||||
if len(endpoints) > 0 {
|
||||
fmt.Println()
|
||||
fmt.Printf("%s endpoints:\n", svc.Name)
|
||||
for _, endpoint := range endpoints {
|
||||
fmt.Printf(" • %s\n", endpoint)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func prepareServiceSpec(opts runOptions) (api.ServiceSpec, error) {
|
||||
var spec api.ServiceSpec
|
||||
|
||||
env, err := parseEnv(opts.env)
|
||||
if err != nil {
|
||||
return spec, err
|
||||
}
|
||||
|
||||
switch opts.mode {
|
||||
case api.ServiceModeReplicated, api.ServiceModeGlobal:
|
||||
default:
|
||||
return fmt.Errorf("invalid replication mode: %q", opts.mode)
|
||||
return spec, fmt.Errorf("invalid replication mode: '%s'", opts.mode)
|
||||
}
|
||||
|
||||
var machineFilter client.MachineFilter
|
||||
if len(opts.machines) > 0 {
|
||||
var machines []string
|
||||
for _, value := range opts.machines {
|
||||
if value == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
mlist := strings.Split(value, ",")
|
||||
for _, m := range mlist {
|
||||
if m = strings.TrimSpace(m); m != "" {
|
||||
machines = append(machines, m)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(machines) > 0 {
|
||||
machineFilter = func(m *pb.MachineInfo) bool {
|
||||
return slices.Contains(machines, m.Name)
|
||||
}
|
||||
}
|
||||
switch opts.pull {
|
||||
case api.PullPolicyAlways, api.PullPolicyMissing, api.PullPolicyNever:
|
||||
default:
|
||||
return spec, fmt.Errorf("invalid pull policy: '%s'", opts.pull)
|
||||
}
|
||||
|
||||
ports := make([]api.PortSpec, len(opts.publish))
|
||||
for i, publishPort := range opts.publish {
|
||||
port, err := api.ParsePortSpec(publishPort)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid service port '%s': %w", publishPort, err)
|
||||
return spec, fmt.Errorf("invalid service port '%s': %w", publishPort, err)
|
||||
}
|
||||
ports[i] = port
|
||||
}
|
||||
// TODO: parse and validate opts.volumes to fail fast if invalid.
|
||||
|
||||
spec := api.ServiceSpec{
|
||||
volumes, mounts, err := parseVolumeFlags(opts.volumes)
|
||||
if err != nil {
|
||||
return spec, err
|
||||
}
|
||||
|
||||
placement := api.Placement{
|
||||
Machines: cli.ExpandCommaSeparatedValues(opts.machines),
|
||||
}
|
||||
|
||||
spec = api.ServiceSpec{
|
||||
Container: api.ContainerSpec{
|
||||
Command: opts.command,
|
||||
Image: opts.image,
|
||||
Volumes: opts.volumes,
|
||||
Command: opts.command,
|
||||
Env: env,
|
||||
Image: opts.image,
|
||||
Privileged: opts.privileged,
|
||||
PullPolicy: opts.pull,
|
||||
Resources: api.ContainerResources{
|
||||
CPU: opts.cpu.Value(),
|
||||
Memory: opts.memory.Value(),
|
||||
},
|
||||
User: opts.user,
|
||||
VolumeMounts: mounts,
|
||||
},
|
||||
Mode: opts.mode,
|
||||
Name: opts.name,
|
||||
Ports: ports,
|
||||
Replicas: opts.replicas,
|
||||
}
|
||||
if err := spec.Validate(); err != nil {
|
||||
return fmt.Errorf("invalid service configuration: %w", err)
|
||||
Mode: opts.mode,
|
||||
Name: opts.name,
|
||||
Placement: placement,
|
||||
Ports: ports,
|
||||
Replicas: opts.replicas,
|
||||
Volumes: volumes,
|
||||
}
|
||||
|
||||
client, err := uncli.ConnectCluster(ctx, opts.cluster)
|
||||
if err != nil {
|
||||
return fmt.Errorf("connect to cluster: %w", err)
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
resp, err := client.RunService(ctx, spec, machineFilter)
|
||||
if err != nil {
|
||||
return fmt.Errorf("run service: %w", err)
|
||||
// Overwrite the default ENTRYPOINT of the image or reset it if an empty string is passed.
|
||||
if opts.entrypoint != "" {
|
||||
spec.Container.Entrypoint = []string{opts.entrypoint}
|
||||
} else if opts.entrypointChanged {
|
||||
spec.Container.Entrypoint = []string{""}
|
||||
}
|
||||
|
||||
svc, err := client.InspectService(ctx, resp.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("inspect service: %w", err)
|
||||
if err = spec.Validate(); err != nil {
|
||||
return spec, fmt.Errorf("invalid service configuration: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
fmt.Printf("%s endpoints:\n", svc.Name)
|
||||
for _, endpoint := range svc.Endpoints() {
|
||||
fmt.Printf(" • %s\n", endpoint)
|
||||
// Generate a service name if not specified to be able to include it in the progress title.
|
||||
if spec.Name == "" {
|
||||
spec.Name, err = deploy.GenerateServiceName(spec.Container.Image)
|
||||
if err != nil {
|
||||
return spec, fmt.Errorf("generate service name: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
return spec, err
|
||||
}
|
||||
|
||||
// parseEnv parses the environment variables from the command line arguments.
|
||||
// It supports two formats: "VAR=value" or just "VAR" to use the value from the local environment.
|
||||
func parseEnv(env []string) (api.EnvVars, error) {
|
||||
envVars := make(api.EnvVars)
|
||||
for _, e := range env {
|
||||
key, value, hasValue := strings.Cut(e, "=")
|
||||
if key == "" {
|
||||
return nil, fmt.Errorf("invalid environment variable: '%s'", e)
|
||||
}
|
||||
|
||||
if hasValue {
|
||||
envVars[key] = value
|
||||
} else {
|
||||
if localEnvValue, ok := os.LookupEnv(key); ok {
|
||||
envVars[key] = localEnvValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return envVars, nil
|
||||
}
|
||||
|
||||
// parseVolumeFlags parses volume flag values in Docker CLI format and returns VolumeSpecs and VolumeMounts.
|
||||
// It handles both named volumes (volume_name:/container/path[:ro|volume-nocopy])
|
||||
// and bind mounts (/host/path:/container/path[:ro]).
|
||||
func parseVolumeFlags(volumes []string) ([]api.VolumeSpec, []api.VolumeMount, error) {
|
||||
specs := make([]api.VolumeSpec, 0, len(volumes))
|
||||
mounts := make([]api.VolumeMount, 0, len(volumes))
|
||||
|
||||
// Track volume names to avoid duplicate specs.
|
||||
seenVolumes := make(map[string]struct{})
|
||||
|
||||
for _, vol := range volumes {
|
||||
spec, mount, err := parseVolumeFlagValue(vol)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("invalid volume mount '%s': %w", vol, err)
|
||||
}
|
||||
|
||||
if _, ok := seenVolumes[spec.Name]; !ok {
|
||||
specs = append(specs, spec)
|
||||
seenVolumes[spec.Name] = struct{}{}
|
||||
}
|
||||
|
||||
mounts = append(mounts, mount)
|
||||
}
|
||||
|
||||
return specs, mounts, nil
|
||||
}
|
||||
|
||||
func parseVolumeFlagValue(volume string) (api.VolumeSpec, api.VolumeMount, error) {
|
||||
var spec api.VolumeSpec
|
||||
var mount api.VolumeMount
|
||||
|
||||
parts := strings.Split(volume, ":")
|
||||
switch len(parts) {
|
||||
case 1:
|
||||
return spec, mount, fmt.Errorf("invalid format, must contain at least one separator ':'")
|
||||
case 2, 3:
|
||||
// Format: (volume_name|/host/path):/container/path[:opts]
|
||||
if !strings.HasPrefix(parts[1], "/") {
|
||||
return spec, mount, fmt.Errorf("invalid container mount path: '%s', must be absolute path", parts[1])
|
||||
}
|
||||
|
||||
mount.ContainerPath = parts[1]
|
||||
volumeNoCopy := false
|
||||
|
||||
if len(parts) == 3 {
|
||||
opts := strings.Split(parts[2], ",")
|
||||
for _, opt := range opts {
|
||||
switch opt {
|
||||
case "ro", "readonly":
|
||||
mount.ReadOnly = true
|
||||
case "volume-nocopy":
|
||||
volumeNoCopy = true
|
||||
default:
|
||||
return spec, mount, fmt.Errorf("invalid option: '%s'", opt)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if strings.HasPrefix(parts[0], "/") {
|
||||
// Host path bind mount: /host/path:/container/path
|
||||
suffix, err := secret.RandomAlphaNumeric(4)
|
||||
if err != nil {
|
||||
return spec, mount, fmt.Errorf("generate random suffix: %w", err)
|
||||
}
|
||||
|
||||
spec = api.VolumeSpec{
|
||||
Name: "bind-" + suffix,
|
||||
Type: api.VolumeTypeBind,
|
||||
BindOptions: &api.BindOptions{
|
||||
HostPath: parts[0],
|
||||
CreateHostPath: true,
|
||||
},
|
||||
}
|
||||
} else {
|
||||
// Named volume mount: volume_name:/container/path
|
||||
volumeName := parts[0]
|
||||
if !names.RestrictedNamePattern.MatchString(volumeName) {
|
||||
return spec, mount, fmt.Errorf("volume name '%s' includes invalid characters, only '%s' are allowed. "+
|
||||
"If you intended to pass a host directory or file, use absolute path",
|
||||
volumeName, names.RestrictedNameChars)
|
||||
}
|
||||
|
||||
spec = api.VolumeSpec{
|
||||
Name: volumeName,
|
||||
Type: api.VolumeTypeVolume,
|
||||
VolumeOptions: &api.VolumeOptions{
|
||||
Name: volumeName,
|
||||
NoCopy: volumeNoCopy,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
mount.VolumeName = spec.Name
|
||||
default:
|
||||
return spec, mount, fmt.Errorf("invalid format, must container at most 2 separators ':'")
|
||||
}
|
||||
|
||||
return spec, mount, nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/charmbracelet/huh"
|
||||
"github.com/docker/compose/v2/pkg/progress"
|
||||
"github.com/psviderski/uncloud/internal/cli"
|
||||
"github.com/psviderski/uncloud/pkg/api"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type scaleOptions struct {
|
||||
service string
|
||||
replicas uint
|
||||
cluster string
|
||||
}
|
||||
|
||||
func NewScaleCommand() *cobra.Command {
|
||||
opts := scaleOptions{}
|
||||
cmd := &cobra.Command{
|
||||
Use: "scale SERVICE REPLICAS",
|
||||
Short: "Scale a replicated service by changing the number of replicas.",
|
||||
Long: "Scale a replicated service by changing the number of replicas. Scaling down requires confirmation.",
|
||||
Args: cobra.ExactArgs(2),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
uncli := cmd.Context().Value("cli").(*cli.CLI)
|
||||
|
||||
opts.service = args[0]
|
||||
replicas, err := strconv.ParseUint(args[1], 10, 0)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid number of replicas: %w", err)
|
||||
}
|
||||
opts.replicas = uint(replicas)
|
||||
|
||||
return scale(cmd.Context(), uncli, opts)
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().StringVarP(
|
||||
&opts.cluster, "context", "c", "",
|
||||
"Name of the cluster context. (default is the current context)",
|
||||
)
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func scale(ctx context.Context, uncli *cli.CLI, opts scaleOptions) error {
|
||||
if opts.replicas == 0 {
|
||||
return fmt.Errorf(
|
||||
"scaling to zero replicas is not supported. This would effectively remove the service without preserving "+
|
||||
"its configuration, making it impossible to scale back up. Uncloud derives the service configuration "+
|
||||
"from existing containers. Use 'uc rm %s' instead if you want to remove the service",
|
||||
opts.service,
|
||||
)
|
||||
}
|
||||
|
||||
clusterClient, err := uncli.ConnectCluster(ctx, opts.cluster)
|
||||
if err != nil {
|
||||
return fmt.Errorf("connect to cluster: %w", err)
|
||||
}
|
||||
defer clusterClient.Close()
|
||||
|
||||
svc, err := clusterClient.InspectService(ctx, opts.service)
|
||||
if err != nil {
|
||||
return fmt.Errorf("inspect service '%s': %w", opts.service, err)
|
||||
}
|
||||
|
||||
if svc.Mode != api.ServiceModeReplicated {
|
||||
return fmt.Errorf("scaling is only supported for services in %s mode, service '%s' is in %s mode",
|
||||
api.ServiceModeReplicated, svc.Name, svc.Mode)
|
||||
}
|
||||
|
||||
currentReplicas := uint(len(svc.Containers))
|
||||
|
||||
if currentReplicas == opts.replicas {
|
||||
fmt.Printf("Service '%s' already has %d replicas. No changes required.\n", svc.Name, currentReplicas)
|
||||
return nil
|
||||
}
|
||||
|
||||
// TODO: Check if all containers have the same spec. If not, prompt user to choose which one to scale.
|
||||
// This can happen if a service deployment failed midway and some containers were not updated.
|
||||
spec := svc.Containers[0].Container.ServiceSpec
|
||||
spec.Replicas = opts.replicas
|
||||
deployment := clusterClient.NewDeployment(spec, nil)
|
||||
plan, err := deployment.Plan(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("plan deployment: %w", err)
|
||||
}
|
||||
|
||||
if len(plan.Operations) == 0 {
|
||||
fmt.Printf("Service '%s' is already scaled to %d replicas.\n", svc.Name, opts.replicas)
|
||||
return nil
|
||||
}
|
||||
|
||||
if opts.replicas < currentReplicas {
|
||||
// Initialise a machine and container name resolver to properly format the plan output.
|
||||
resolver, err := clusterClient.ServiceOperationNameResolver(ctx, svc)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create machine and container name resolver for service operations: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Scaling plan for service %s (%d → %d replicas):\n", svc.Name, currentReplicas, opts.replicas)
|
||||
fmt.Println(plan.Format(resolver))
|
||||
fmt.Println()
|
||||
|
||||
// Ask for confirmation before scaling down as it may cause data loss.
|
||||
confirmed, err := confirm()
|
||||
if err != nil {
|
||||
return fmt.Errorf("confirm scaling: %w", err)
|
||||
}
|
||||
if !confirmed {
|
||||
fmt.Println("Cancelled. No changes were made.")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
title := fmt.Sprintf("Scaling service %s (%d → %d replicas)", svc.Name, currentReplicas, opts.replicas)
|
||||
err = progress.RunWithTitle(ctx, func(ctx context.Context) error {
|
||||
if _, err = deployment.Run(ctx); err != nil {
|
||||
return fmt.Errorf("deploy service: %w", err)
|
||||
}
|
||||
return nil
|
||||
}, uncli.ProgressOut(), title)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func confirm() (bool, error) {
|
||||
var confirmed bool
|
||||
form := huh.NewForm(
|
||||
huh.NewGroup(
|
||||
huh.NewConfirm().
|
||||
Title(
|
||||
"Do you want to continue?",
|
||||
).
|
||||
Affirmative("Yes!").
|
||||
Negative("No").
|
||||
Value(&confirmed),
|
||||
),
|
||||
).WithAccessible(true)
|
||||
if err := form.Run(); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return confirmed, nil
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
package volume
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/huh"
|
||||
"github.com/docker/docker/api/types/volume"
|
||||
"github.com/psviderski/uncloud/internal/cli"
|
||||
"github.com/psviderski/uncloud/internal/machine/api/pb"
|
||||
"github.com/psviderski/uncloud/pkg/api"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type createOptions struct {
|
||||
driver string
|
||||
driverOpts []string
|
||||
labels []string
|
||||
machine string
|
||||
context string
|
||||
}
|
||||
|
||||
func NewCreateCommand() *cobra.Command {
|
||||
opts := createOptions{}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "create VOLUME_NAME",
|
||||
Short: "Create a volume on a specific machine.",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
uncli := cmd.Context().Value("cli").(*cli.CLI)
|
||||
opts.driver = strings.TrimSpace(opts.driver)
|
||||
|
||||
volumeName := args[0]
|
||||
if volumeName == "" {
|
||||
return fmt.Errorf("volume name is required")
|
||||
}
|
||||
|
||||
return create(cmd.Context(), uncli, volumeName, opts)
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().StringVarP(&opts.driver, "driver", "d", "local",
|
||||
"Volume driver to use.")
|
||||
cmd.Flags().StringSliceVarP(&opts.driverOpts, "opt", "o", nil,
|
||||
"Driver specific options in the form of 'key=value' pairs. Can be specified multiple times.")
|
||||
cmd.Flags().StringSliceVarP(&opts.labels, "label", "l", nil,
|
||||
"Labels to assign to the volume in the form of 'key=value' pairs. Can be specified multiple times.")
|
||||
cmd.Flags().StringVarP(&opts.machine, "machine", "m", "",
|
||||
"Name or ID of the machine to create the volume on.")
|
||||
cmd.Flags().StringVarP(&opts.context, "context", "c", "",
|
||||
"Name of the cluster context. (default is the current context)")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func create(ctx context.Context, uncli *cli.CLI, name string, opts createOptions) error {
|
||||
client, err := uncli.ConnectCluster(ctx, opts.context)
|
||||
if err != nil {
|
||||
return fmt.Errorf("connect to cluster: %w", err)
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
// Parse driver options.
|
||||
driverOpts := make(map[string]string)
|
||||
for _, opt := range opts.driverOpts {
|
||||
k, v, ok := strings.Cut(opt, "=")
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid driver option format: '%s' (expected key=value)", opt)
|
||||
}
|
||||
driverOpts[k] = v
|
||||
}
|
||||
|
||||
// Parse labels.
|
||||
labels := make(map[string]string)
|
||||
for _, label := range opts.labels {
|
||||
k, v, ok := strings.Cut(label, "=")
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid label format: '%s' (expected key=value)", label)
|
||||
}
|
||||
labels[k] = v
|
||||
}
|
||||
|
||||
// List machines and filter by the specified machine name or ID.
|
||||
// If no machine is specified, prompt the user to select one.
|
||||
machines, err := client.ListMachines(ctx, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("list machines: %w", err)
|
||||
}
|
||||
var selectedMachine *pb.MachineInfo
|
||||
|
||||
if opts.machine == "" {
|
||||
if len(machines) == 1 {
|
||||
selectedMachine = machines[0].Machine
|
||||
} else {
|
||||
if selectedMachine, err = promptSelectMachine(ctx, machines); err != nil {
|
||||
return fmt.Errorf("select machine: %w", err)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
m := machines.FindByNameOrID(opts.machine)
|
||||
if m == nil {
|
||||
return fmt.Errorf("machine '%s' not found", opts.machine)
|
||||
}
|
||||
selectedMachine = m.Machine
|
||||
}
|
||||
|
||||
createOpts := volume.CreateOptions{
|
||||
Name: name,
|
||||
Driver: opts.driver,
|
||||
DriverOpts: driverOpts,
|
||||
Labels: labels,
|
||||
}
|
||||
vol, err := client.CreateVolume(ctx, selectedMachine.Id, createOpts)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create volume '%s' on machine '%s': %w", name, selectedMachine.Name, err)
|
||||
}
|
||||
|
||||
fmt.Printf("Volume '%s' created on machine '%s'.\n", vol.Volume.Name, vol.MachineName)
|
||||
return nil
|
||||
}
|
||||
|
||||
func promptSelectMachine(ctx context.Context, machines api.MachineMembersList) (*pb.MachineInfo, error) {
|
||||
options := make([]huh.Option[*pb.MachineInfo], len(machines))
|
||||
for i, m := range machines {
|
||||
options[i] = huh.NewOption(m.Machine.Name, m.Machine)
|
||||
}
|
||||
slices.SortFunc(options, func(a, b huh.Option[*pb.MachineInfo]) int {
|
||||
return strings.Compare(a.Key, b.Key)
|
||||
})
|
||||
|
||||
var selected *pb.MachineInfo
|
||||
form := huh.NewForm(
|
||||
huh.NewGroup(
|
||||
huh.NewSelect[*pb.MachineInfo]().
|
||||
Title("Select a machine to create the volume on (or specify with --machine flag)").
|
||||
Options(options...).
|
||||
Value(&selected),
|
||||
),
|
||||
)
|
||||
if err := form.RunWithContext(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return selected, nil
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package volume
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/psviderski/uncloud/internal/cli"
|
||||
"github.com/psviderski/uncloud/pkg/api"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type inspectOptions struct {
|
||||
machine string
|
||||
context string
|
||||
}
|
||||
|
||||
func NewInspectCommand() *cobra.Command {
|
||||
opts := inspectOptions{}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "inspect VOLUME_NAME",
|
||||
Short: "Display detailed information on a volume.",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
uncli := cmd.Context().Value("cli").(*cli.CLI)
|
||||
return inspect(cmd.Context(), uncli, args[0], opts)
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().StringVarP(&opts.machine, "machine", "m", "",
|
||||
"Name or ID of the machine where the volume is located. "+
|
||||
"If not specified, the volume will be searched across all machines.")
|
||||
cmd.Flags().StringVarP(&opts.context, "context", "c", "",
|
||||
"Name of the cluster context. (default is the current context)")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func inspect(ctx context.Context, uncli *cli.CLI, name string, opts inspectOptions) error {
|
||||
client, err := uncli.ConnectCluster(ctx, opts.context)
|
||||
if err != nil {
|
||||
return fmt.Errorf("connect to cluster: %w", err)
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
filter := &api.VolumeFilter{
|
||||
Names: []string{name},
|
||||
}
|
||||
if opts.machine != "" {
|
||||
filter.Machines = []string{opts.machine}
|
||||
}
|
||||
|
||||
volumes, err := client.ListVolumes(ctx, filter)
|
||||
if err != nil {
|
||||
return fmt.Errorf("list volumes: %w", err)
|
||||
}
|
||||
|
||||
if len(volumes) == 0 {
|
||||
if opts.machine != "" {
|
||||
return fmt.Errorf("volume '%s' not found on machine '%s'", name, opts.machine)
|
||||
}
|
||||
return fmt.Errorf("volume '%s' not found on any machine", name)
|
||||
}
|
||||
if len(volumes) > 1 {
|
||||
fmt.Printf("Volume '%s' found on multiple machines:\n", name)
|
||||
for _, v := range volumes {
|
||||
fmt.Printf(" • %s\n", v.MachineName)
|
||||
}
|
||||
return errors.New("specify --machine flag to choose which machine to use")
|
||||
}
|
||||
|
||||
data, err := json.MarshalIndent(volumes[0], "", " ")
|
||||
if err != nil {
|
||||
return fmt.Errorf("marshal volume: %w", err)
|
||||
}
|
||||
fmt.Println(string(data))
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
package volume
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"slices"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/psviderski/uncloud/internal/cli"
|
||||
"github.com/psviderski/uncloud/pkg/api"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type listOptions struct {
|
||||
machines []string
|
||||
quiet bool
|
||||
context string
|
||||
}
|
||||
|
||||
func NewListCommand() *cobra.Command {
|
||||
opts := listOptions{}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "ls",
|
||||
Aliases: []string{"list"},
|
||||
Short: "List volumes across all machines in the cluster.",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
uncli := cmd.Context().Value("cli").(*cli.CLI)
|
||||
return list(cmd.Context(), uncli, opts)
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().StringSliceVarP(&opts.machines, "machine", "m", nil,
|
||||
"Filter volumes by machine name or ID. Can be specified multiple times or as a comma-separated list. "+
|
||||
"(default is include all machines)")
|
||||
cmd.Flags().BoolVarP(&opts.quiet, "quiet", "q", false,
|
||||
"Only display volume names.")
|
||||
|
||||
cmd.Flags().StringVarP(&opts.context, "context", "c", "",
|
||||
"Name of the cluster context. (default is the current context)")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func list(ctx context.Context, uncli *cli.CLI, opts listOptions) error {
|
||||
client, err := uncli.ConnectCluster(ctx, opts.context)
|
||||
if err != nil {
|
||||
return fmt.Errorf("connect to cluster: %w", err)
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
// Apply machine filter if specified.
|
||||
var filter *api.VolumeFilter
|
||||
if len(opts.machines) > 0 {
|
||||
machines := cli.ExpandCommaSeparatedValues(opts.machines)
|
||||
filter = &api.VolumeFilter{
|
||||
Machines: machines,
|
||||
}
|
||||
}
|
||||
|
||||
volumes, err := client.ListVolumes(ctx, filter)
|
||||
if err != nil {
|
||||
return fmt.Errorf("list volumes: %w", err)
|
||||
}
|
||||
|
||||
if len(volumes) == 0 {
|
||||
if !opts.quiet {
|
||||
fmt.Println("No volumes found.")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Sort the volumes by name first, then by machine name.
|
||||
slices.SortFunc(volumes, func(a, b api.MachineVolume) int {
|
||||
cmp := strings.Compare(a.Volume.Name, b.Volume.Name)
|
||||
if cmp != 0 {
|
||||
return cmp
|
||||
}
|
||||
return strings.Compare(a.MachineName, b.MachineName)
|
||||
})
|
||||
|
||||
// If quiet mode, just print volume names.
|
||||
if opts.quiet {
|
||||
for _, v := range volumes {
|
||||
fmt.Println(v.Volume.Name)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Print the volumes in a table format.
|
||||
tw := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', 0)
|
||||
fmt.Fprintln(tw, "NAME\tDRIVER\tMACHINE")
|
||||
|
||||
for _, v := range volumes {
|
||||
fmt.Fprintf(tw, "%s\t%s\t%s\n",
|
||||
v.Volume.Name,
|
||||
v.Volume.Driver,
|
||||
v.MachineName,
|
||||
)
|
||||
}
|
||||
|
||||
return tw.Flush()
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
package volume
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/psviderski/uncloud/internal/cli"
|
||||
"github.com/psviderski/uncloud/pkg/api"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type removeOptions struct {
|
||||
force bool
|
||||
machines []string
|
||||
yes bool
|
||||
context string
|
||||
}
|
||||
|
||||
func NewRemoveCommand() *cobra.Command {
|
||||
opts := removeOptions{}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "rm VOLUME_NAME [VOLUME_NAME...]",
|
||||
Aliases: []string{"remove", "delete"},
|
||||
Short: "Remove one or more volumes.",
|
||||
Long: "Remove one or more volumes. You cannot remove a volume that is in use by a container.",
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
uncli := cmd.Context().Value("cli").(*cli.CLI)
|
||||
return remove(cmd.Context(), uncli, args, opts)
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().BoolVarP(&opts.force, "force", "f", false,
|
||||
"Force the removal of one or more volumes.")
|
||||
cmd.Flags().StringSliceVarP(&opts.machines, "machine", "m", nil,
|
||||
"Name or ID of the machine to remove one or more volumes from. "+
|
||||
"Can be specified multiple times or as a comma-separated list.\n"+
|
||||
"If not specified, the found volume(s) will be removed from all machines.")
|
||||
cmd.Flags().BoolVarP(&opts.yes, "yes", "y", false,
|
||||
"Do not prompt for confirmation before removing the volume(s).")
|
||||
|
||||
cmd.Flags().StringVarP(&opts.context, "context", "c", "",
|
||||
"Name of the cluster context. (default is the current context)")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func remove(ctx context.Context, uncli *cli.CLI, names []string, opts removeOptions) error {
|
||||
client, err := uncli.ConnectCluster(ctx, opts.context)
|
||||
if err != nil {
|
||||
return fmt.Errorf("connect to cluster: %w", err)
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
filter := &api.VolumeFilter{
|
||||
Names: names,
|
||||
}
|
||||
|
||||
if len(opts.machines) > 0 {
|
||||
machines := cli.ExpandCommaSeparatedValues(opts.machines)
|
||||
filter.Machines = machines
|
||||
}
|
||||
|
||||
volumes, err := client.ListVolumes(ctx, filter)
|
||||
if err != nil {
|
||||
return fmt.Errorf("list volumes: %w", err)
|
||||
}
|
||||
|
||||
if len(volumes) == 0 {
|
||||
if len(names) == 1 {
|
||||
return fmt.Errorf("volume '%s' not found", names[0])
|
||||
}
|
||||
return fmt.Errorf("no volumes found matching the specified names")
|
||||
}
|
||||
|
||||
// Confirm removal if not using --yes flag.
|
||||
if !opts.yes {
|
||||
fmt.Println("The following volumes will be removed:")
|
||||
for _, v := range volumes {
|
||||
fmt.Printf(" • '%s' on machine '%s'\n", v.Volume.Name, v.MachineName)
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
confirmed, err := cli.Confirm()
|
||||
if err != nil {
|
||||
return fmt.Errorf("confirm removal: %w", err)
|
||||
}
|
||||
if !confirmed {
|
||||
fmt.Println("Cancelled. No volumes were removed.")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Remove the volumes one by one collecting errors.
|
||||
var removeErr error
|
||||
for _, v := range volumes {
|
||||
if err = client.RemoveVolume(ctx, v.MachineID, v.Volume.Name, opts.force); err != nil {
|
||||
if !errors.Is(err, api.ErrNotFound) {
|
||||
removeErr = errors.Join(removeErr, fmt.Errorf("failed to remove volume '%s' on machine '%s': %w",
|
||||
v.Volume.Name, v.MachineName, err))
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
fmt.Printf("Volume '%s' removed from machine '%s'.\n", v.Volume.Name, v.MachineName)
|
||||
}
|
||||
|
||||
return removeErr
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package volume
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func NewRootCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "volume",
|
||||
Short: "Manage volumes in an Uncloud cluster.",
|
||||
}
|
||||
cmd.AddCommand(
|
||||
NewCreateCommand(),
|
||||
NewInspectCommand(),
|
||||
NewListCommand(),
|
||||
NewRemoveCommand(),
|
||||
)
|
||||
return cmd
|
||||
}
|
||||
@@ -2,14 +2,16 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/spf13/cobra"
|
||||
"log/slog"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"uncloud/internal/daemon"
|
||||
"uncloud/internal/log"
|
||||
"uncloud/internal/machine"
|
||||
|
||||
"github.com/psviderski/uncloud/internal/daemon"
|
||||
"github.com/psviderski/uncloud/internal/log"
|
||||
"github.com/psviderski/uncloud/internal/machine"
|
||||
"github.com/psviderski/uncloud/internal/version"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func main() {
|
||||
@@ -22,6 +24,7 @@ func main() {
|
||||
cmd := &cobra.Command{
|
||||
Use: "uncloudd",
|
||||
Short: "Uncloud machine daemon.",
|
||||
Version: version.String(),
|
||||
SilenceUsage: true,
|
||||
SilenceErrors: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
@@ -52,5 +55,4 @@ func main() {
|
||||
}()
|
||||
|
||||
cobra.CheckErr(cmd.ExecuteContext(ctx))
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
# Ignore everything and use an allowlist of what is allowed to not package any secrets by accident.
|
||||
*
|
||||
|
||||
# Allow files and directories.
|
||||
!docs/
|
||||
!src/
|
||||
!static/
|
||||
!pkg/
|
||||
!Caddyfile
|
||||
!*.js
|
||||
!*.json
|
||||
|
||||
# Ignore unnecessary files inside allowed directories.
|
||||
**/.DS_Store
|
||||
@@ -0,0 +1,20 @@
|
||||
# Dependencies
|
||||
/node_modules
|
||||
|
||||
# Production
|
||||
/build
|
||||
|
||||
# Generated files
|
||||
.docusaurus
|
||||
.cache-loader
|
||||
|
||||
# Misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
admin off
|
||||
}
|
||||
|
||||
:8000 {
|
||||
root * /usr/share/caddy
|
||||
file_server
|
||||
try_files {path} /index.html
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
# Stage 1: Base image.
|
||||
## Start with a base image containing NodeJS so we can build Docusaurus.
|
||||
FROM node:lts-alpine AS base
|
||||
## Disable colour output from yarn to make logs easier to read.
|
||||
ENV FORCE_COLOR=0
|
||||
## Enable corepack.
|
||||
RUN corepack enable
|
||||
## Set the working directory to `/opt/docusaurus`.
|
||||
WORKDIR /opt/docusaurus
|
||||
|
||||
# Stage 2: Production build.
|
||||
FROM base AS prod
|
||||
## Set the working directory to `/opt/docusaurus`.
|
||||
WORKDIR /opt/docusaurus
|
||||
## Copy over the source code.
|
||||
COPY . /opt/docusaurus/
|
||||
## Install dependencies with `--immutable` to ensure reproducibility.
|
||||
RUN npm ci
|
||||
## Build the static site.
|
||||
RUN npm run build
|
||||
|
||||
# Stage 3: Serve static site with Caddy.
|
||||
FROM caddy:2.10.0-alpine AS caddy
|
||||
## Copy the Caddyfile.
|
||||
COPY ./Caddyfile /etc/caddy/Caddyfile
|
||||
## Copy the Docusaurus build output.
|
||||
COPY --from=prod /opt/docusaurus/build /usr/share/caddy
|
||||
@@ -0,0 +1,41 @@
|
||||
# Website
|
||||
|
||||
This website is built using [Docusaurus](https://docusaurus.io/), a modern static website generator.
|
||||
|
||||
### Installation
|
||||
|
||||
```
|
||||
$ yarn
|
||||
```
|
||||
|
||||
### Local Development
|
||||
|
||||
```
|
||||
$ yarn start
|
||||
```
|
||||
|
||||
This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server.
|
||||
|
||||
### Build
|
||||
|
||||
```
|
||||
$ yarn build
|
||||
```
|
||||
|
||||
This command generates static content into the `build` directory and can be served using any static contents hosting service.
|
||||
|
||||
### Deployment
|
||||
|
||||
Using SSH:
|
||||
|
||||
```
|
||||
$ USE_SSH=true yarn deploy
|
||||
```
|
||||
|
||||
Not using SSH:
|
||||
|
||||
```
|
||||
$ GIT_USER=<Your GitHub username> yarn deploy
|
||||
```
|
||||
|
||||
If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch.
|
||||
@@ -0,0 +1,8 @@
|
||||
services:
|
||||
uncloud-docs:
|
||||
image: ghcr.io/psviderski/uncloud-docs:latest
|
||||
pull_policy: always
|
||||
user: nobody
|
||||
x-ports:
|
||||
- docs.uncloud.run:8000/https
|
||||
scale: 2
|
||||
@@ -0,0 +1,115 @@
|
||||
---
|
||||
slug: /
|
||||
---
|
||||
|
||||
# Overview
|
||||
|
||||
Uncloud makes **self-hosting web applications** across multiple machines in production dead simple.
|
||||
|
||||
You can connect any machines — from cloud VMs to bare metal servers (no matter where they're located) — into a secure
|
||||
private network. Then run and scale multi-service and multi-container web apps and databases across your machines using
|
||||
simple Docker-like commands and [Docker Compose](https://docs.docker.com/reference/compose-file/) files.
|
||||
|
||||
Uncloud covers all the essentials for operating apps in production without overwhelming you with the complexity of
|
||||
traditional container orchestrators like Kubernetes or Swarm:
|
||||
|
||||
* Initial machine and network setup
|
||||
* Zero-downtime rolling deployments
|
||||
* Health checks and automatic restarts
|
||||
* Automatic HTTPS and reverse proxy configuration
|
||||
* Scaling services across multiple machines
|
||||
* Cross-machine service communication without exposing ports to the internet
|
||||
* DNS-based service discovery
|
||||
* Load balancing
|
||||
* Persistent storage
|
||||
|
||||
## Use cases
|
||||
|
||||
Some of the common use cases Uncloud is a great fit for:
|
||||
|
||||
- **Self-hosting and Homelabs**: Run your self-hosted apps on your own hardware. Start with a single machine and add
|
||||
more as your needs grow.
|
||||
- **Outgrowing Docker Compose**: Level up your Docker Compose setup with zero-downtime deployments, replicas across
|
||||
multiple machines for improved reliability, cross-machine service communication, automated reverse proxy management,
|
||||
and more using the same Compose file.
|
||||
- **Small to medium web applications**: Deploy your SaaS product, websites, or personal projects with redundancy across
|
||||
multiple machines for better reliability and your peace of mind.
|
||||
- **Hybrid setups (cloud + on-prem)**: Combine cloud VMs with on-premise for cost savings and data sovereignty — all
|
||||
managed through the same interface.
|
||||
- **Agencies and freelancers**: Host multiple client projects with proper isolation on shared infrastructure, optimising
|
||||
costs and resources.
|
||||
- **Edge computing**: Deploy applications closer to your users for lower latency and better performance.
|
||||
- **Dev/staging environments**: Spin up additional environments for development and testing that mirror production
|
||||
reusing the same Compose configuration.
|
||||
|
||||
## What makes Uncloud different
|
||||
|
||||
Here are the design decisions that make Uncloud stand out:
|
||||
|
||||
### Decentralised design
|
||||
|
||||
You can think of an Uncloud cluster as a **network of Docker hosts** (machines) that are all aware of each other. All
|
||||
machines in the cluster are equal. You can connect to any of them to manage containers on any other machine in the
|
||||
cluster. If a machine or part of the network goes down, the rest of the cluster keeps running.
|
||||
|
||||
There is no centralised control plane, so no need to worry about maintaining a quorum of machines for it. The time saved
|
||||
can be better spent developing and deploying your apps instead.
|
||||
|
||||
### Zero-config overlay network
|
||||
|
||||
Uncloud automatically configures and maintains a secure **WireGuard mesh network** across your machines. It handles key
|
||||
management, peer discovery, and NAT traversal without any manual configuration. This makes it easy to connect machines
|
||||
from different networks and locations, such as cloud VMs, on-premise servers, or your Raspberry Pi at home.
|
||||
|
||||
Docker containers running on different machines get **unique IP addresses** from the cluster network so they can
|
||||
**communicate directly** as if they were on a single machine without opening up any host ports to the internet.
|
||||
|
||||
The design and implementation were highly inspired by
|
||||
Talos [KubeSpan](https://www.talos.dev/v1.10/talos-guides/network/kubespan/).
|
||||
|
||||
### Managed DNS service
|
||||
|
||||
Uncloud can provide **managed DNS records** like `<service-name>.<cluster-id>.cluster.uncloud.run` for your public
|
||||
services through free [Uncloud DNS](https://github.com/psviderski/uncloud-dns) service. You can deploy a service and
|
||||
instantly access it from anywhere with a proper DNS name and HTTPS without any manual DNS configuration. This makes
|
||||
self-hosting much more accessible and simplifies the process of adding your own domain later.
|
||||
|
||||
### No complex orchestration
|
||||
|
||||
Uncloud operations are done using **imperative CLI commands** that have the taste of Docker and Docker Compose. The
|
||||
deployment and scaling commands can output an execution plan that describes what exactly will be changed on your cluster
|
||||
once you approve it. For example, what containers and volumes will be created or removed, and on which machines.
|
||||
|
||||
This gives you full visibility and control over every change with **immediate feedback** when something goes wrong.
|
||||
|
||||
### Minimal resource footprint
|
||||
|
||||
The Uncloud daemon consists of a couple Go and Rust binaries running alongside the Docker daemon on each machine. It
|
||||
needs no more than **150 MB of RAM** and a few percent of a CPU core in small setups. This minimal overhead maximises
|
||||
the system resources available for your apps.
|
||||
|
||||
You can run Uncloud on machines with as little as 512 MB of RAM, assuming you also need some RAM for the OS and Docker,
|
||||
as well as the apps you want to run.
|
||||
|
||||
### Troubleshooting-friendly
|
||||
|
||||
When something goes wrong, you can dive straight into standard Docker containers without layers of abstraction in your
|
||||
way. You can also SSH into any machine and use the regular Linux troubleshooting tools. For example, `ping` service
|
||||
containers by their service names, `curl` service endpoints, or analyse traffic between containers using `wireshark`.
|
||||
|
||||
## Getting started
|
||||
|
||||
Install Uncloud CLI and deploy your first app in minutes:
|
||||
|
||||
* [Install Uncloud CLI](./2-getting-started/1-install-cli.md)
|
||||
* [Deploy demo app](./2-getting-started/2-deploy-demo-app.md)
|
||||
|
||||
## Getting help
|
||||
|
||||
* **Discord community**: Join our [Discord server](https://discord.gg/eR35KQJhPu) for real-time discussions, support,
|
||||
and updates.
|
||||
* **GitHub issues**: Report bugs or request features on our [GitHub repository](https://github.com/psviderski/uncloud).
|
||||
* **Documentation**: Browse the full documentation (this website you're on) for detailed guides and references. Use
|
||||
search to find what you need quickly.
|
||||
* **Newsletter**: Subscribe to the [newsletter](https://uncloud.run/#subscribe) for development updates and early
|
||||
insights.
|
||||
@@ -0,0 +1,104 @@
|
||||
# Install CLI
|
||||
|
||||
Install the Uncloud command-line utility to manage your machines and deploy apps using `uc` commands. You will run `uc`
|
||||
locally so choose the appropriate installation method for your operating system.
|
||||
|
||||
:::info NOTE
|
||||
|
||||
Windows is not natively supported yet, but you can install and run `uc` in a
|
||||
[WSL](https://learn.microsoft.com/en-us/windows/wsl/) terminal by following the instructions for Linux.
|
||||
:::
|
||||
|
||||
## Homebrew (macOS, Linux)
|
||||
|
||||
If you have [Homebrew](https://brew.sh/) package manager installed, this is the recommended installation method on macOS
|
||||
and Linux:
|
||||
|
||||
```shell
|
||||
brew install psviderski/tap/uncloud
|
||||
```
|
||||
|
||||
To upgrade to the latest version:
|
||||
|
||||
```shell
|
||||
brew upgrade uncloud
|
||||
```
|
||||
|
||||
## Install script (macOS, Linux)
|
||||
|
||||
For a quick automated installation, use the install script:
|
||||
|
||||
```shell
|
||||
curl -fsS https://get.uncloud.run/install.sh | sh
|
||||
```
|
||||
|
||||
The script will:
|
||||
|
||||
- Detect your operating system and architecture
|
||||
- Download the appropriate latest binary from [GitHub releases](https://github.com/psviderski/uncloud/releases)
|
||||
- Install it to `/usr/local/bin/uncloud` using `sudo` (you may need to enter your user password)
|
||||
- Create a shortcut `uc` in `/usr/local/bin` for convenience
|
||||
|
||||
Don't like `curl | sh`? You can download and review the [install script](https://get.uncloud.run/install.sh) first and
|
||||
then run it:
|
||||
|
||||
```shell
|
||||
curl -fsSO https://get.uncloud.run/install.sh
|
||||
cat install.sh
|
||||
sh install.sh
|
||||
```
|
||||
|
||||
## GitHub download (macOS, Linux)
|
||||
|
||||
You can manually download and use a pre-built binary from the
|
||||
[latest release](https://github.com/psviderski/uncloud/releases/latest) on GitHub.
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="macOS (Apple Silicon)">
|
||||
```shell
|
||||
curl -L https://github.com/psviderski/uncloud/releases/latest/download/uncloud_macos_arm64.tar.gz | tar xz
|
||||
mv uncloud uc
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem value="macOS (Intel)">
|
||||
```shell
|
||||
curl -L https://github.com/psviderski/uncloud/releases/latest/download/uncloud_macos_amd64.tar.gz | tar xz
|
||||
mv uncloud uc
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem value="Linux (AMD 64-bit)">
|
||||
```shell
|
||||
curl -L https://github.com/psviderski/uncloud/releases/latest/download/uncloud_linux_amd64.tar.gz | tar xz
|
||||
mv uncloud uc
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem value="Linux (ARM 64-bit)">
|
||||
```shell
|
||||
curl -L https://github.com/psviderski/uncloud/releases/latest/download/uncloud_linux_arm64.tar.gz | tar xz
|
||||
mv uncloud uc
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
You can use the `./uc` binary directly from the current directory, or move it to a directory in your system's `PATH`
|
||||
to run it as `uc` from any location.
|
||||
|
||||
For example, move it to `/usr/local/bin` which is a common location for user-installed binaries:
|
||||
|
||||
```shell
|
||||
sudo mv ./uc /usr/local/bin
|
||||
```
|
||||
|
||||
## Verify installation
|
||||
|
||||
After installation, verify that `uc` command is working:
|
||||
|
||||
```shell
|
||||
uc --version
|
||||
```
|
||||
|
||||
## Next steps
|
||||
|
||||
Now that you have `uc` installed, you're ready to:
|
||||
|
||||
- [Deploy demo app](./2-deploy-demo-app.md)
|
||||
@@ -0,0 +1,315 @@
|
||||
# Deploy demo app
|
||||
|
||||
In this guide, we'll deploy [Excalidraw](https://excalidraw.com) — a popular sketching and diagramming tool — to your
|
||||
Linux server. You'll learn the **basics of Uncloud** and see how simple it is to **run web apps** on your own
|
||||
infrastructure with secure internet access.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before you begin, you'll need:
|
||||
|
||||
- **Uncloud CLI** [installed](1-install-cli.md) on your local machine
|
||||
- A **Ubuntu or Debian server** with **public IP address** and **SSH access** (as `root` or a user with `sudo`
|
||||
privileges) using a **private key**.
|
||||
|
||||
:::tip Need a server?
|
||||
|
||||
A small Virtual Private Server (VPS) or dedicated server from providers like [Hetzner](https://www.hetzner.com) or
|
||||
[DigitalOcean](https://www.digitalocean.com) is a great choice for learning Uncloud and running lightweight services. We
|
||||
recommend using a freshly installed server as existing services on ports 80 and 443 can cause conflicts.
|
||||
|
||||
**Minimum requirements:** 1 vCPU, 512 MB RAM, Ubuntu 22.04 or Debian 11, AMD64 (recommended) or ARM64 architecture.
|
||||
Other Linux distributions may work, but haven't been tested yet.
|
||||
|
||||
:::
|
||||
|
||||
## Set up your server
|
||||
|
||||
First, let's turn your server into an Uncloud **machine**. This simply means setting it up so you can deploy and manage
|
||||
services on it using `uc`.
|
||||
|
||||
```shell
|
||||
uc machine init root@<your-server-ip>
|
||||
```
|
||||
|
||||
If the SSH key to access your server isn't added to your [SSH agent](https://www.ssh.com/academy/ssh/agent), specify it
|
||||
with the `-i` flag:
|
||||
|
||||
```shell
|
||||
uc machine init root@<your-server-ip> -i ~/.ssh/id_xxx
|
||||
```
|
||||
|
||||
This command will:
|
||||
|
||||
- Install the latest stable Docker version on your server if it's not already installed
|
||||
- Install the Uncloud daemon on your server
|
||||
- Create a Docker network for Uncloud-managed containers
|
||||
- Deploy [Caddy](https://caddyserver.com/) as your reverse proxy listening on host ports 80 and 443
|
||||
- Reserve a free `xxxxxx.cluster.uncloud.run` subdomain via the Uncloud managed DNS service and point it to your
|
||||
server's IP
|
||||
|
||||
All in about a minute!
|
||||
|
||||
<details>
|
||||
<summary>💡 Expand to see example output</summary>
|
||||
|
||||
```
|
||||
$ uc machine init root@157.180.72.195
|
||||
Downloading Uncloud install script: https://raw.githubusercontent.com/psviderski/uncloud/refs/heads/main/scripts/install.sh
|
||||
⏳ Running Uncloud install script...
|
||||
⏳ Installing Docker...
|
||||
# Executing docker install script, commit: 53a22f61c0628e58e1d6680b49e82993d304b449
|
||||
+ sh -c apt-get -qq update >/dev/null
|
||||
+ sh -c DEBIAN_FRONTEND=noninteractive apt-get -y -qq install ca-certificates curl >/dev/null
|
||||
+ sh -c install -m 0755 -d /etc/apt/keyrings
|
||||
+ sh -c curl -fsSL "https://download.docker.com/linux/ubuntu/gpg" -o /etc/apt/keyrings/docker.asc
|
||||
+ sh -c chmod a+r /etc/apt/keyrings/docker.asc
|
||||
+ sh -c echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu noble stable" > /etc/apt/sources.list.d/docker.list
|
||||
+ sh -c apt-get -qq update >/dev/null
|
||||
+ sh -c DEBIAN_FRONTEND=noninteractive apt-get -y -qq install docker-ce docker-ce-cli containerd.io docker-compose-plugin docker-ce-rootless-extras docker-buildx-plugin >/dev/null
|
||||
|
||||
Running kernel seems to be up-to-date.
|
||||
|
||||
No services need to be restarted.
|
||||
|
||||
No containers need to be restarted.
|
||||
|
||||
No user sessions are running outdated binaries.
|
||||
|
||||
No VM guests are running outdated hypervisor (qemu) binaries on this host.
|
||||
+ sh -c docker version
|
||||
Client: Docker Engine - Community
|
||||
Version: 28.2.1
|
||||
API version: 1.50
|
||||
Go version: go1.24.3
|
||||
Git commit: 879ac3f
|
||||
Built: Wed May 28 19:25:01 2025
|
||||
OS/Arch: linux/amd64
|
||||
Context: default
|
||||
|
||||
Server: Docker Engine - Community
|
||||
Engine:
|
||||
Version: 28.2.1
|
||||
API version: 1.50 (minimum version 1.24)
|
||||
Go version: go1.24.3
|
||||
Git commit: 0e2cc22
|
||||
Built: Wed May 28 19:25:01 2025
|
||||
OS/Arch: linux/amd64
|
||||
Experimental: false
|
||||
containerd:
|
||||
Version: 1.7.27
|
||||
GitCommit: 05044ec0a9a75232cad458027ca83437aae3f4da
|
||||
runc:
|
||||
Version: 1.2.5
|
||||
GitCommit: v1.2.5-0-g59923ef
|
||||
docker-init:
|
||||
Version: 0.19.0
|
||||
GitCommit: de40ad0
|
||||
|
||||
================================================================================
|
||||
|
||||
To run Docker as a non-privileged user, consider setting up the
|
||||
Docker daemon in rootless mode for your user:
|
||||
|
||||
dockerd-rootless-setuptool.sh install
|
||||
|
||||
Visit https://docs.docker.com/go/rootless/ to learn about rootless mode.
|
||||
|
||||
|
||||
To run the Docker daemon as a fully privileged service, but granting non-root
|
||||
users access, refer to https://docs.docker.com/go/daemon-access/
|
||||
|
||||
WARNING: Access to the remote API on a privileged Docker daemon is equivalent
|
||||
to root access on the host. Refer to the 'Docker daemon attack surface'
|
||||
documentation for details: https://docs.docker.com/go/attack-surface/
|
||||
|
||||
================================================================================
|
||||
|
||||
✓ Docker installed successfully.
|
||||
✓ Linux user and group 'uncloud' created.
|
||||
⏳ Installing Uncloud binaries...
|
||||
⏳ Downloading uncloudd binary: https://github.com/psviderski/uncloud/releases/latest/download/uncloudd_linux_amd64.tar.gz
|
||||
✓ uncloudd binary installed: /usr/local/bin/uncloudd
|
||||
⏳ Downloading uninstall script: https://raw.githubusercontent.com/psviderski/uncloud/refs/heads/main/scripts/uninstall.sh
|
||||
✓ uncloud-uninstall script installed: /usr/local/bin/uncloud-uninstall
|
||||
✓ Systemd unit file created: /etc/systemd/system/uncloud.service
|
||||
Created symlink /etc/systemd/system/multi-user.target.wants/uncloud.service → /etc/systemd/system/uncloud.service.
|
||||
⏳ Downloading uncloud-corrosion binary: https://github.com/psviderski/corrosion/releases/latest/download/corrosion-x86_64-unknown-linux-gnu.tar.gz
|
||||
✓ uncloud-corrosion binary installed: /usr/local/bin/uncloud-corrosion
|
||||
✓ Systemd unit file created: /etc/systemd/system/uncloud-corrosion.service
|
||||
⏳ Starting Uncloud machine daemon (uncloud.service)...
|
||||
✓ Uncloud machine daemon started.
|
||||
✓ Uncloud installed on the machine successfully! 🎉
|
||||
Cluster initialised with machine 'machine-dc3c' and saved as context 'default' in your local config (/Users/spy/.config/uncloud/config.yaml)
|
||||
Current cluster context is now 'default'.
|
||||
Waiting for the machine to be ready...
|
||||
|
||||
Reserved cluster domain: 7za6s7.cluster.uncloud.run
|
||||
[+] Deploying service caddy 7/2
|
||||
✔ Container caddy-d7uk on machine-dc3c Started 6.1s
|
||||
✔ Image caddy:2.10.0 on machine-dc3c Pulled 3.7s
|
||||
|
||||
Updating cluster domain records in Uncloud DNS to point to machines running caddy service...
|
||||
[+] Verifying internet access to caddy service 1/1
|
||||
✔ Machine machine-dc3c (157.180.72.195) Reachable 0.7s
|
||||
|
||||
DNS records updated to use only the internet-reachable machines running caddy service:
|
||||
*.7za6s7.cluster.uncloud.run A → 157.180.72.195
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## Deploy Excalidraw
|
||||
|
||||
Now that your machine is set up, let's deploy `excalidraw` service from the
|
||||
[official Docker image](https://hub.docker.com/r/excalidraw/excalidraw). The service will publish the container port 80
|
||||
as HTTPS endpoint on the previously reserved domain via Caddy.
|
||||
|
||||
```shell
|
||||
uc run --name excalidraw --publish 80/https excalidraw/excalidraw
|
||||
```
|
||||
|
||||
You'll see the progress of the deployment and the public URL where you can access the service:
|
||||
|
||||
```
|
||||
[+] Running service excalidraw (replicated mode) 2/2
|
||||
✔ Container excalidraw-azpc on machine-dc3c Started 8.9s
|
||||
✔ Image excalidraw/excalidraw on machine-dc3c Pulled 4.7s
|
||||
|
||||
excalidraw endpoints:
|
||||
• https://excalidraw.7za6s7.cluster.uncloud.run → :80
|
||||
```
|
||||
|
||||
## Verify your deployment
|
||||
|
||||
After the service is deployed, use the `uc inspect` command to check its status and details:
|
||||
|
||||
```shell
|
||||
uc inspect excalidraw
|
||||
```
|
||||
|
||||
```
|
||||
ID: 4d2de1600b6ada221a03896cd388836c
|
||||
Name: excalidraw
|
||||
Mode: replicated
|
||||
|
||||
CONTAINER ID IMAGE CREATED STATUS MACHINE
|
||||
fde7ac7f11ad excalidraw/excalidraw About a minute ago Up About a minute (healthy) machine-dc3c
|
||||
```
|
||||
|
||||
In this example, the service has one container running on the machine `machine-dc3c` (our server). The container is up
|
||||
and healthy.
|
||||
|
||||
You can also list all deployed services and their public endpoints using the `uc ls` command:
|
||||
|
||||
```shell
|
||||
uc ls
|
||||
```
|
||||
|
||||
```
|
||||
NAME MODE REPLICAS ENDPOINTS
|
||||
caddy global 1
|
||||
excalidraw replicated 1 https://excalidraw.7za6s7.cluster.uncloud.run → :80
|
||||
```
|
||||
|
||||
You can see `caddy` service listed here. That's your reverse proxy, running as a regular Uncloud service.
|
||||
|
||||
## It's live! Start drawing! ✨
|
||||
|
||||
Open your browser and navigate to the URL shown in the endpoints. It may take a moment for Caddy to obtain a TLS
|
||||
certificate from Let's Encrypt. If it doesn't load immediately, wait a few seconds and try again.
|
||||
|
||||

|
||||
|
||||
You now have:
|
||||
|
||||
- Your **own Excalidraw instance** running on your server
|
||||
- A **public URL** with **automatic HTTPS** you can share with your team and friends
|
||||
- **Full control over your data** — no analytics or tracking
|
||||
|
||||
## Convert to Docker Compose format
|
||||
|
||||
Uncloud supports the [Compose file format](https://docs.docker.com/reference/compose-file/) for defining services. This
|
||||
allows you to version control your deployments, share configurations with your team, and deploy complex multi-service
|
||||
applications with a single command.
|
||||
|
||||
Let's create a `compose.yaml` file in your current directory for the `excalidraw` service we just deployed.
|
||||
|
||||
```yaml title="compose.yaml"
|
||||
services:
|
||||
excalidraw:
|
||||
image: excalidraw/excalidraw
|
||||
x-ports:
|
||||
- 80/https
|
||||
```
|
||||
|
||||
:::info note
|
||||
|
||||
The `x-ports` key is an Uncloud-specific extension to the Compose file format. It allows you to specify ports that
|
||||
should be published as HTTP(S) endpoints. Uncloud automatically configures the reverse proxy (Caddy) to route traffic to
|
||||
these ports.
|
||||
|
||||
:::
|
||||
|
||||
Now deploy it:
|
||||
|
||||
```shell
|
||||
uc deploy
|
||||
```
|
||||
|
||||
```
|
||||
Services are up to date.
|
||||
```
|
||||
|
||||
Since `excalidraw` service is already running with the same configuration, Uncloud recognises there's nothing to change.
|
||||
We've successfully converted our deployment created with `uc run` to a Compose file.
|
||||
|
||||
## Use your own domain
|
||||
|
||||
Want to use your own domain, for example, `excalidraw.example.com` instead of `excalidraw.7za6s7.cluster.uncloud.run`?
|
||||
|
||||
Add a CNAME record `excalidraw.example.com` in your DNS provider (Cloudflare, Namecheap, etc.) pointing to
|
||||
`excalidraw.7za6s7.cluster.uncloud.run`. Alternatively, you can add an A record pointing to your server's IP.
|
||||
|
||||
Then update the published port `80/https` in `compose.yaml` to use your domain:
|
||||
|
||||
```yaml title="compose.yaml"
|
||||
...
|
||||
x-ports:
|
||||
- excalidraw.example.com:80/https
|
||||
```
|
||||
|
||||
Finally, deploy the changes:
|
||||
|
||||
```shell
|
||||
uc deploy
|
||||
```
|
||||
|
||||
```
|
||||
Deployment plan:
|
||||
- Deploy service [name=excalidraw]
|
||||
- machine-dc3c: Run container [image=excalidraw/excalidraw]
|
||||
- machine-dc3c: Remove container [name=excalidraw-azpc]
|
||||
|
||||
Do you want to continue?
|
||||
|
||||
Choose [y/N]: y
|
||||
Chose: Yes!
|
||||
|
||||
[+] Deploying services 2/2
|
||||
✔ Container excalidraw-0z12 on machine-dc3c Started 3.5s
|
||||
✔ Container excalidraw-azpc on machine-dc3c Removed 3.4s
|
||||
```
|
||||
|
||||
Notice how Uncloud performed a **zero-downtime deployment** — it started the new container with the updated
|
||||
configuration before removing the old one. Your service stayed available throughout the update.
|
||||
|
||||
Give it a moment for Caddy to obtain a TLS certificate, then visit https://excalidraw.example.com.
|
||||
|
||||
## Clean up
|
||||
|
||||
TBD
|
||||
|
||||
## Next steps
|
||||
|
||||
TBD
|
||||
@@ -0,0 +1,4 @@
|
||||
label: Getting started
|
||||
collapsed: false # keep the category open by default
|
||||
link:
|
||||
type: generated-index
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 389 KiB |
@@ -0,0 +1,153 @@
|
||||
// @ts-check
|
||||
// `@type` JSDoc annotations allow editor autocompletion and type checking
|
||||
// (when paired with `@ts-check`).
|
||||
// There are various equivalent ways to declare your Docusaurus config.
|
||||
// See: https://docusaurus.io/docs/api/docusaurus-config
|
||||
|
||||
import {themes as prismThemes} from 'prism-react-renderer';
|
||||
|
||||
// This runs in Node.js - Don't use client-side code here (browser APIs, JSX...)
|
||||
|
||||
/** @type {import('@docusaurus/types').Config} */
|
||||
const config = {
|
||||
title: 'Uncloud Docs',
|
||||
tagline: 'Dinosaurs are cool',
|
||||
favicon: 'img/favicon.png',
|
||||
|
||||
// Set the production url of your site here
|
||||
url: 'https://docs.uncloud.run',
|
||||
// Set the /<baseUrl>/ pathname under which your site is served
|
||||
// For GitHub pages deployment, it is often '/<projectName>/'
|
||||
baseUrl: '/',
|
||||
|
||||
// GitHub pages deployment config.
|
||||
// If you aren't using GitHub pages, you don't need these.
|
||||
organizationName: '', // Usually your GitHub org/user name.
|
||||
projectName: '', // Usually your repo name.
|
||||
|
||||
onBrokenLinks: 'throw',
|
||||
onBrokenMarkdownLinks: 'warn',
|
||||
|
||||
// Even if you don't use internationalization, you can use this field to set
|
||||
// useful metadata like html lang. For example, if your site is Chinese, you
|
||||
// may want to replace "en" with "zh-Hans".
|
||||
i18n: {
|
||||
defaultLocale: 'en',
|
||||
locales: ['en'],
|
||||
},
|
||||
|
||||
themes: [
|
||||
[
|
||||
'@easyops-cn/docusaurus-search-local',
|
||||
/** @type {import("@easyops-cn/docusaurus-search-local").PluginOptions} */
|
||||
({
|
||||
docsRouteBasePath: '/',
|
||||
hashed: true,
|
||||
highlightSearchTermsOnTargetPage: true,
|
||||
}),
|
||||
],
|
||||
],
|
||||
presets: [
|
||||
[
|
||||
'classic',
|
||||
/** @type {import('@docusaurus/preset-classic').Options} */
|
||||
({
|
||||
docs: {
|
||||
// Remove this to remove the "edit this page" links.
|
||||
editUrl: 'https://github.com/psviderski/uncloud/edit/main/docs/',
|
||||
// Serve the docs at the site's root.
|
||||
routeBasePath: '/',
|
||||
showLastUpdateTime: true,
|
||||
sidebarPath: './sidebars.js',
|
||||
},
|
||||
// blog: {
|
||||
// showReadingTime: true,
|
||||
// feedOptions: {
|
||||
// type: ['rss', 'atom'],
|
||||
// xslt: true,
|
||||
// },
|
||||
// // Please change this to your repo.
|
||||
// // Remove this to remove the "edit this page" links.
|
||||
// editUrl:
|
||||
// 'https://github.com/facebook/docusaurus/tree/main/packages/create-docusaurus/templates/shared/',
|
||||
// // Useful options to enforce blogging best practices
|
||||
// onInlineTags: 'warn',
|
||||
// onInlineAuthors: 'warn',
|
||||
// onUntruncatedBlogPosts: 'warn',
|
||||
// },
|
||||
theme: {
|
||||
customCss: './src/css/custom.css',
|
||||
},
|
||||
}),
|
||||
],
|
||||
],
|
||||
|
||||
themeConfig:
|
||||
/** @type {import('@docusaurus/preset-classic').ThemeConfig} */
|
||||
({
|
||||
colorMode: {
|
||||
// The color mode when user first visits the site.
|
||||
defaultMode: "light",
|
||||
// Whether to use the prefers-color-scheme media-query, using user system preferences,
|
||||
// instead of the hardcoded defaultMode.
|
||||
respectPrefersColorScheme: true,
|
||||
},
|
||||
// The meta image URL for the site (og:image and twitter:image meta tags).
|
||||
// Relative to your site's "static" directory. Cannot be SVGs. Can be external URLs too.
|
||||
image: 'img/social-card.png',
|
||||
navbar: {
|
||||
title: 'Uncloud Docs',
|
||||
logo: {
|
||||
alt: 'Uncloud Logo',
|
||||
src: 'img/logo.svg',
|
||||
},
|
||||
items: [
|
||||
{
|
||||
type: 'search',
|
||||
position: 'right',
|
||||
},
|
||||
{
|
||||
href: 'https://github.com/psviderski/uncloud',
|
||||
label: 'GitHub',
|
||||
position: 'right',
|
||||
},
|
||||
],
|
||||
},
|
||||
footer: {
|
||||
style: 'light',
|
||||
links: [
|
||||
{
|
||||
title: 'Uncloud',
|
||||
items: [
|
||||
{
|
||||
html: 'Made with ❤️ by <a href="https://github.com/psviderski">@psviderski</a> in Australia'
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Community',
|
||||
items: [
|
||||
{
|
||||
label: 'Discord',
|
||||
href: 'https://discord.gg/eR35KQJhPu',
|
||||
},
|
||||
{
|
||||
label: 'X',
|
||||
href: 'https://x.com/psviderski',
|
||||
},
|
||||
{
|
||||
label: 'GitHub',
|
||||
href: 'https://github.com/psviderski/uncloud',
|
||||
},
|
||||
],
|
||||
}
|
||||
]
|
||||
},
|
||||
prism: {
|
||||
theme: prismThemes.palenight,
|
||||
//darkTheme: prismThemes.dracula,
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
export default config;
|
||||
Generated
+18434
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"name": "docs",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"docusaurus": "docusaurus",
|
||||
"start": "docusaurus start",
|
||||
"build": "docusaurus build",
|
||||
"swizzle": "docusaurus swizzle",
|
||||
"deploy": "docusaurus deploy",
|
||||
"clear": "docusaurus clear",
|
||||
"serve": "docusaurus serve",
|
||||
"write-translations": "docusaurus write-translations",
|
||||
"write-heading-ids": "docusaurus write-heading-ids"
|
||||
},
|
||||
"dependencies": {
|
||||
"@docusaurus/core": "3.7.0",
|
||||
"@docusaurus/preset-classic": "3.7.0",
|
||||
"@easyops-cn/docusaurus-search-local": "^0.49.2",
|
||||
"@mdx-js/react": "^3.0.0",
|
||||
"clsx": "^2.0.0",
|
||||
"prism-react-renderer": "^2.3.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@docusaurus/module-type-aliases": "3.7.0",
|
||||
"@docusaurus/types": "3.7.0"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.5%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 3 chrome version",
|
||||
"last 3 firefox version",
|
||||
"last 5 safari version"
|
||||
]
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
// @ts-check
|
||||
|
||||
// This runs in Node.js - Don't use client-side code here (browser APIs, JSX...)
|
||||
|
||||
/**
|
||||
* Creating a sidebar enables you to:
|
||||
- create an ordered group of docs
|
||||
- render a sidebar for each doc of that group
|
||||
- provide next/previous navigation
|
||||
|
||||
The sidebars can be generated from the filesystem, or explicitly defined here.
|
||||
|
||||
Create as many sidebars as you want.
|
||||
|
||||
@type {import('@docusaurus/plugin-content-docs').SidebarsConfig}
|
||||
*/
|
||||
const sidebars = {
|
||||
// By default, Docusaurus generates a sidebar from the docs folder structure
|
||||
sidebar: [{type: 'autogenerated', dirName: '.'}],
|
||||
|
||||
// But you can create a sidebar manually
|
||||
/*
|
||||
tutorialSidebar: [
|
||||
'intro',
|
||||
'hello',
|
||||
{
|
||||
type: 'category',
|
||||
label: 'Tutorial',
|
||||
items: ['tutorial-basics/create-a-document'],
|
||||
},
|
||||
],
|
||||
*/
|
||||
};
|
||||
|
||||
export default sidebars;
|
||||
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Any CSS included here will be global. The classic template
|
||||
* bundles Infima by default. Infima is a CSS framework designed to
|
||||
* work well for content-centric websites.
|
||||
*/
|
||||
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;700&display=swap');
|
||||
|
||||
/* You can override the default Infima variables here. */
|
||||
:root {
|
||||
--ifm-font-family-base: 'Inter', sans-serif;
|
||||
--ifm-heading-font-family: 'Inter', sans-serif;
|
||||
|
||||
--ifm-color-primary: #006be6;
|
||||
--ifm-color-primary-dark: #0060cf;
|
||||
--ifm-color-primary-darker: #005bc4;
|
||||
--ifm-color-primary-darkest: #004ba1;
|
||||
--ifm-color-primary-light: #0076fd;
|
||||
--ifm-color-primary-lighter: #0a7cff;
|
||||
--ifm-color-primary-lightest: #2c8eff;
|
||||
--ifm-code-font-size: 95%;
|
||||
--docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* For readability concerns, you should choose a lighter palette in dark mode. */
|
||||
[data-theme='dark'] {
|
||||
--ifm-color-primary: #3391ee;
|
||||
--ifm-color-primary-dark: #1883ec;
|
||||
--ifm-color-primary-darker: #137be3;
|
||||
--ifm-color-primary-darkest: #1066bb;
|
||||
--ifm-color-primary-light: #4e9ff0;
|
||||
--ifm-color-primary-lighter: #5ba7f1;
|
||||
--ifm-color-primary-lightest: #83bcf5;
|
||||
--docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.menu {
|
||||
font-size: 0.875rem;
|
||||
font-weight: var(--ifm-font-weight-normal);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import React from 'react';
|
||||
// Import the original mapper
|
||||
import MDXComponents from '@theme-original/MDXComponents';
|
||||
import Tabs from '@theme/Tabs';
|
||||
import TabItem from '@theme/TabItem';
|
||||
|
||||
export default {
|
||||
// Re-use the default mapping
|
||||
...MDXComponents,
|
||||
// Global import the <Tabs> and <TabItem> components
|
||||
Tabs,
|
||||
TabItem,
|
||||
};
|
||||
Vendored
BIN
Binary file not shown.
|
After Width: | Height: | Size: 1.2 KiB |
Vendored
+7
@@ -0,0 +1,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 80 80">
|
||||
<g>
|
||||
<rect x="0" y="0" width="80" height="80" rx="10" fill="#18181B"/>
|
||||
<path d="M20 32 L20 48 L36 48 L36 32 M60 32 L45 32 L45 48 L60 48" stroke="white" stroke-width="6" fill="none"
|
||||
stroke-linecap="square" stroke-linejoin="miter"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 341 B |
Vendored
BIN
Binary file not shown.
|
After Width: | Height: | Size: 88 KiB |
@@ -0,0 +1,59 @@
|
||||
# User Guide
|
||||
|
||||
## Initialising a cluster
|
||||
|
||||
To begin setting up a new Uncloud cluster, create the desired nodes. Ensure that their firewall allows the required ports.
|
||||
|
||||
### Ports
|
||||
|
||||
Nodes running Uncloud with a standard configuration must have the following inbound ports allowed in the firewall:
|
||||
|
||||
* 51820/udp (WireGuard meshnet)
|
||||
* 22/tcp (default SSH management port)
|
||||
* 80/tcp (required for challenge if uncloud.run DNS is enabled, pass --no-dns during init to disable)
|
||||
|
||||
In addition, any ports for any workloads you want to expose will need to be open.
|
||||
|
||||
### Configuration
|
||||
|
||||
Uncloud stores its configuration in `~/.config/uncloud/config.yaml`. If you wish to reinitialise a cluster, simply remove it from this config.
|
||||
|
||||
### Initialisation
|
||||
|
||||
Begin by initialising the first node in your cluster with `uc machine init [USER@HOST:PORT]`. If you do not have a need for Caddy reverse proxy, you may disable this feature with `--no-caddy`.
|
||||
|
||||
This command will idempotently install Docker, uncloudd, uncloud-corrosion. If Caddy is enabled, it will set up a reverse proxy. If Uncloud DNS is enabled, it will create a DNS A record for the machine's public IP address under `*.[CLUSTER ID].cluster.uncloud.run`.
|
||||
|
||||
If you wish to uninstall Uncloud and its components, run `uncloud-uninstall`.
|
||||
|
||||
### Adding a node
|
||||
|
||||
Just like the initialisation of the first node, a node can be added to the cluster with `uc machine add`.
|
||||
|
||||
### DNS
|
||||
|
||||
Uncloud (uncloud.run) DNS can be managed with the `uc dns` subcommand.
|
||||
|
||||
* To reserve a domain name, run `uc dns reserve`
|
||||
* To release a domain name, run `uc dns release`.
|
||||
* To see the domain name, run `uc dns show`
|
||||
|
||||
### Running a service
|
||||
|
||||
Services on an Uncloud cluster can be managed with `uc service`.
|
||||
|
||||
You can run a service with two replicas that expose port 80 like the following:
|
||||
|
||||
```
|
||||
uc service run -p 80/http --replicas 2 nginxdemos/hello
|
||||
```
|
||||
|
||||
This requires the Caddy reverse proxy to be deployed. If it wasn't previously, it can be deployed with `uc caddy deploy`.
|
||||
|
||||
Since this doesn't specify a service name, a random one will be generated.
|
||||
|
||||
The service can be deleted by its service name:
|
||||
|
||||
```
|
||||
uc service rm hello-gsdo
|
||||
```
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
"go.uber.org/zap"
|
||||
"net/netip"
|
||||
"time"
|
||||
"uncloud/internal/machine/network"
|
||||
"github.com/psviderski/uncloud/internal/machine/network"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
module uncloud
|
||||
module github.com/psviderski/uncloud
|
||||
|
||||
go 1.23.0
|
||||
|
||||
@@ -11,7 +11,10 @@ require (
|
||||
github.com/caddyserver/caddy/v2 v2.8.4
|
||||
github.com/cenkalti/backoff/v4 v4.3.0
|
||||
github.com/charmbracelet/huh v0.6.0
|
||||
github.com/charmbracelet/lipgloss v0.13.0
|
||||
github.com/compose-spec/compose-go/v2 v2.4.5
|
||||
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf
|
||||
github.com/deckarep/golang-set/v2 v2.8.0
|
||||
github.com/dgraph-io/badger/v3 v3.2103.5
|
||||
github.com/distribution/reference v0.6.0
|
||||
github.com/docker/cli v27.5.0+incompatible
|
||||
@@ -19,6 +22,8 @@ require (
|
||||
github.com/docker/docker v27.4.0-rc.2+incompatible
|
||||
github.com/docker/go-connections v0.5.0
|
||||
github.com/docker/go-units v0.5.0
|
||||
github.com/goccy/go-yaml v1.17.1
|
||||
github.com/google/go-cmp v0.7.0
|
||||
github.com/google/go-containerregistry v0.20.2
|
||||
github.com/hashicorp/memberlist v0.5.1
|
||||
github.com/hashicorp/serf v0.10.1
|
||||
@@ -29,7 +34,11 @@ require (
|
||||
github.com/ipfs/go-ds-crdt v0.5.2
|
||||
github.com/ipfs/go-ipld-format v0.6.0
|
||||
github.com/ipfs/go-log/v2 v2.5.1
|
||||
github.com/jmoiron/sqlx v1.4.0
|
||||
github.com/lmittmann/tint v1.0.5
|
||||
github.com/miekg/dns v1.1.65
|
||||
github.com/moby/term v0.5.0
|
||||
github.com/opencontainers/go-digest v1.0.0
|
||||
github.com/opencontainers/image-spec v1.1.0
|
||||
github.com/siderolabs/discovery-api v0.1.4
|
||||
github.com/siderolabs/discovery-client v0.1.9
|
||||
@@ -39,15 +48,16 @@ require (
|
||||
github.com/vishvananda/netlink v1.3.0
|
||||
go.uber.org/zap v1.27.0
|
||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba
|
||||
golang.org/x/crypto v0.32.0
|
||||
golang.org/x/net v0.34.0
|
||||
golang.org/x/sync v0.10.0
|
||||
golang.org/x/sys v0.29.0
|
||||
golang.org/x/crypto v0.33.0
|
||||
golang.org/x/net v0.35.0
|
||||
golang.org/x/sync v0.11.0
|
||||
golang.org/x/sys v0.31.0
|
||||
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173
|
||||
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576
|
||||
google.golang.org/grpc v1.68.1
|
||||
google.golang.org/protobuf v1.36.3
|
||||
modernc.org/sqlite v1.36.3
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -77,13 +87,11 @@ require (
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/charmbracelet/bubbles v0.20.0 // indirect
|
||||
github.com/charmbracelet/bubbletea v1.1.1 // indirect
|
||||
github.com/charmbracelet/lipgloss v0.13.0 // indirect
|
||||
github.com/charmbracelet/x/ansi v0.3.2 // indirect
|
||||
github.com/charmbracelet/x/exp/strings v0.0.0-20240919170804-a4978c8e603a // indirect
|
||||
github.com/charmbracelet/x/term v0.2.0 // indirect
|
||||
github.com/chzyer/readline v1.5.1 // indirect
|
||||
github.com/cloudflare/cfssl v1.6.4 // indirect
|
||||
github.com/compose-spec/compose-go/v2 v2.4.5 // indirect
|
||||
github.com/containerd/console v1.0.4 // indirect
|
||||
github.com/containerd/containerd v1.7.24 // indirect
|
||||
github.com/containerd/containerd/api v1.7.19 // indirect
|
||||
@@ -118,7 +126,7 @@ require (
|
||||
github.com/go-logfmt/logfmt v0.6.0 // indirect
|
||||
github.com/go-logr/logr v1.4.2 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-sql-driver/mysql v1.7.1 // indirect
|
||||
github.com/go-sql-driver/mysql v1.8.1 // indirect
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.0.0 // indirect
|
||||
github.com/godbus/dbus/v5 v5.1.0 // indirect
|
||||
@@ -131,8 +139,7 @@ require (
|
||||
github.com/google/btree v1.1.2 // indirect
|
||||
github.com/google/cel-go v0.20.1 // indirect
|
||||
github.com/google/flatbuffers v24.3.25+incompatible // indirect
|
||||
github.com/google/go-cmp v0.6.0 // indirect
|
||||
github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad // indirect
|
||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e // indirect
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/gorilla/mux v1.8.1 // indirect
|
||||
@@ -192,7 +199,6 @@ require (
|
||||
github.com/mdlayher/socket v0.5.1 // indirect
|
||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
|
||||
github.com/mholt/acmez/v2 v2.0.3 // indirect
|
||||
github.com/miekg/dns v1.1.62 // indirect
|
||||
github.com/miekg/pkcs11 v1.1.1 // indirect
|
||||
github.com/minio/sha256-simd v1.0.1 // indirect
|
||||
github.com/mitchellh/cli v1.1.5 // indirect
|
||||
@@ -208,7 +214,8 @@ require (
|
||||
github.com/moby/patternmatcher v0.6.0 // indirect
|
||||
github.com/moby/sys/sequential v0.6.0 // indirect
|
||||
github.com/moby/sys/signal v0.7.1 // indirect
|
||||
github.com/moby/term v0.5.0 // indirect
|
||||
github.com/moby/sys/user v0.3.0 // indirect
|
||||
github.com/moby/sys/userns v0.1.0 // indirect
|
||||
github.com/morikuni/aec v1.0.0 // indirect
|
||||
github.com/mr-tron/base58 v1.2.0 // indirect
|
||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
|
||||
@@ -224,8 +231,8 @@ require (
|
||||
github.com/multiformats/go-multistream v0.5.0 // indirect
|
||||
github.com/multiformats/go-varint v0.0.7 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||
github.com/onsi/ginkgo/v2 v2.22.0 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/pelletier/go-toml v1.9.5 // indirect
|
||||
github.com/pires/go-proxyproto v0.7.0 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
@@ -239,6 +246,7 @@ require (
|
||||
github.com/prometheus/procfs v0.15.1 // indirect
|
||||
github.com/quic-go/qpack v0.5.1 // indirect
|
||||
github.com/quic-go/quic-go v0.48.2 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/rs/xid v1.5.0 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
@@ -269,6 +277,9 @@ require (
|
||||
github.com/urfave/cli v1.22.16 // indirect
|
||||
github.com/vbatts/tar-split v0.11.6 // indirect
|
||||
github.com/vishvananda/netns v0.0.4 // indirect
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
|
||||
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
|
||||
github.com/zeebo/blake3 v0.2.4 // indirect
|
||||
go.etcd.io/bbolt v1.3.11 // indirect
|
||||
go.opencensus.io v0.24.0 // indirect
|
||||
@@ -294,11 +305,11 @@ require (
|
||||
go.uber.org/zap/exp v0.2.0 // indirect
|
||||
golang.org/x/crypto/x509roots/fallback v0.0.0-20240507223354-67b13616a595 // indirect
|
||||
golang.org/x/exp v0.0.0-20241215155358-4a5509556b9e // indirect
|
||||
golang.org/x/mod v0.22.0 // indirect
|
||||
golang.org/x/term v0.28.0 // indirect
|
||||
golang.org/x/text v0.21.0 // indirect
|
||||
golang.org/x/mod v0.23.0 // indirect
|
||||
golang.org/x/term v0.29.0 // indirect
|
||||
golang.org/x/text v0.22.0 // indirect
|
||||
golang.org/x/time v0.8.0 // indirect
|
||||
golang.org/x/tools v0.29.0 // indirect
|
||||
golang.org/x/tools v0.30.0 // indirect
|
||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
|
||||
google.golang.org/genproto v0.0.0-20240401170217-c3f982113cda // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 // indirect
|
||||
@@ -306,4 +317,7 @@ require (
|
||||
gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259 // indirect
|
||||
howett.net/plist v1.0.0 // indirect
|
||||
lukechampine.com/blake3 v1.3.0 // indirect
|
||||
modernc.org/libc v1.61.13 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.8.2 // indirect
|
||||
)
|
||||
|
||||
@@ -233,6 +233,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c h1:pFUpOrbxDR6AkioZ1ySsx5yxlDQZ8stG2b88gTPxgJU=
|
||||
github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c/go.mod h1:6UhI8N9EjYm1c2odKpFpAYeR8dsBeM7PtzQhRgxRr9U=
|
||||
github.com/deckarep/golang-set/v2 v2.8.0 h1:swm0rlPCmdWn9mESxKOjWk8hXSqoxOp+ZlfuyaAdFlQ=
|
||||
github.com/deckarep/golang-set/v2 v2.8.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4=
|
||||
github.com/decred/dcrd/crypto/blake256 v1.0.1 h1:7PltbUIQB7u/FfZ39+DGa/ShuMyJ5ilcvdfma9wOH6Y=
|
||||
github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnNEcHYvcCuK6dPZSg=
|
||||
@@ -332,8 +334,8 @@ github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ4
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-sql-driver/mysql v1.3.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
||||
github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI=
|
||||
github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
||||
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
|
||||
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
||||
github.com/go-stack/stack v1.6.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
|
||||
@@ -341,6 +343,8 @@ github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZ
|
||||
github.com/go-viper/mapstructure/v2 v2.0.0 h1:dhn8MZ1gZ0mzeodTG3jt5Vj/o87xZKuNAprG2mQfMfc=
|
||||
github.com/go-viper/mapstructure/v2 v2.0.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0=
|
||||
github.com/goccy/go-yaml v1.17.1 h1:LI34wktB2xEE3ONG/2Ar54+/HJVBriAGJ55PHls4YuY=
|
||||
github.com/goccy/go-yaml v1.17.1/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
|
||||
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E=
|
||||
@@ -395,8 +399,8 @@ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
||||
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/go-containerregistry v0.20.2 h1:B1wPJ1SN/S7pB+ZAimcciVD+r+yV/l/DSArMxlbwseo=
|
||||
github.com/google/go-containerregistry v0.20.2/go.mod h1:z38EKdKh4h7IP2gSfUUqEvalZBqs6AoLeWfUy34nQC8=
|
||||
github.com/google/go-tpm v0.9.0 h1:sQF6YqWMi+SCXpsmS3fd21oPy/vSddwZry4JnmltHVk=
|
||||
@@ -408,8 +412,8 @@ github.com/google/go-tspi v0.3.0/go.mod h1:xfMGI3G0PhxCdNVcYr1C4C+EizojDg/TXuX5b
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8=
|
||||
github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo=
|
||||
github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad h1:a6HEuzUHeKH6hwfN/ZoQgRgVIWFJljSWa/zetS2WTvg=
|
||||
github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
|
||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
|
||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o=
|
||||
github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw=
|
||||
@@ -597,8 +601,8 @@ github.com/jinzhu/gorm v0.0.0-20170222002820-5409931a1bb8/go.mod h1:Vla75njaFJ8c
|
||||
github.com/jinzhu/inflection v0.0.0-20170102125226-1c35d901db3d h1:jRQLvyVGL+iVtDElaEIDdKwpPqUIZJfzkNLV34htpEc=
|
||||
github.com/jinzhu/inflection v0.0.0-20170102125226-1c35d901db3d/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/jmoiron/sqlx v1.3.3 h1:j82X0bf7oQ27XeqxicSZsTU5suPwKElg3oyxNn43iTk=
|
||||
github.com/jmoiron/sqlx v1.3.3/go.mod h1:2BljVx/86SuTyjE+aPYlHCTNvZrnJXghYGpNiXLBMCQ=
|
||||
github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
|
||||
github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
|
||||
github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=
|
||||
github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
|
||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
@@ -639,8 +643,9 @@ github.com/lib/pq v0.0.0-20150723085316-0dad96c0b94f/go.mod h1:5WUZQaWbwv1U+lTRe
|
||||
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8=
|
||||
github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/libdns/libdns v0.2.2 h1:O6ws7bAfRPaBsgAYt8MDe2HcNBGC29hkZ9MX2eUSX3s=
|
||||
github.com/libdns/libdns v0.2.2/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ=
|
||||
github.com/libp2p/go-buffer-pool v0.1.0 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6cdF0Y8=
|
||||
@@ -701,6 +706,8 @@ github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh
|
||||
github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk=
|
||||
github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
|
||||
github.com/mattn/go-sqlite3 v1.6.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
||||
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/mdlayher/genetlink v1.3.2 h1:KdrNKe+CTu+IbZnm/GVUMXSqBBLqcGpRDa0xkQy56gw=
|
||||
github.com/mdlayher/genetlink v1.3.2/go.mod h1:tcC3pkCrPUGIKKsCsp0B3AdaaKuHtaxoJRz3cc+528o=
|
||||
@@ -714,8 +721,8 @@ github.com/mholt/acmez/v2 v2.0.3 h1:CgDBlEwg3QBp6s45tPQmFIBrkRIkBT4rW4orMM6p4sw=
|
||||
github.com/mholt/acmez/v2 v2.0.3/go.mod h1:pQ1ysaDeGrIMvJ9dfJMk5kJNkn7L2sb3UhyrX6Q91cw=
|
||||
github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
|
||||
github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI=
|
||||
github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ=
|
||||
github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ=
|
||||
github.com/miekg/dns v1.1.65 h1:0+tIPHzUW0GCge7IiK3guGP57VAw7hoPDfApjkMD1Fc=
|
||||
github.com/miekg/dns v1.1.65/go.mod h1:Dzw9769uoKVaLuODMDZz9M6ynFU6Em65csPuoi8G0ck=
|
||||
github.com/miekg/pkcs11 v1.0.2/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs=
|
||||
github.com/miekg/pkcs11 v1.1.1 h1:Ugu9pdy6vAYku5DEpVWVFPYnzV+bxB+iRdbuFSu7TvU=
|
||||
github.com/miekg/pkcs11 v1.1.1/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs=
|
||||
@@ -809,6 +816,8 @@ github.com/multiformats/go-varint v0.0.7/go.mod h1:r8PUYw/fD/SjBCiKOoDlGF6QawOEL
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
|
||||
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.12.0/go.mod h1:oUhWkIvk5aDxtKvDDuw8gItl8pKl42LzjC9KZE0HfGg=
|
||||
@@ -925,6 +934,8 @@ github.com/quic-go/webtransport-go v0.8.0 h1:HxSrwun11U+LlmwpgM1kEqIqH90IT4N8auv
|
||||
github.com/quic-go/webtransport-go v0.8.0/go.mod h1:N99tjprW432Ut5ONql/aUhSLT0YVSlwHohQsuac9WaM=
|
||||
github.com/raulk/go-watchdog v1.3.0 h1:oUmdlHxdkXRJlwfG0O9omj8ukerm8MEQavSiDTEtBsk=
|
||||
github.com/raulk/go-watchdog v1.3.0/go.mod h1:fIvOnLbF0b0ZwkB9YU4mOW9Did//4vPZtDqv66NfsMU=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
@@ -1069,6 +1080,13 @@ github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0 h1:GDDkbFiaK8jsSD
|
||||
github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0/go.mod h1:x6AKhvSSexNrVSrViXSHUEbICjmGXhtgABaHIySUSGw=
|
||||
github.com/weppos/publicsuffix-go v0.15.1-0.20210511084619-b1f36a2d6c0b h1:FsyNrX12e5BkplJq7wKOLk0+C6LZ+KGXvuEcKUYm5ss=
|
||||
github.com/weppos/publicsuffix-go v0.15.1-0.20210511084619-b1f36a2d6c0b/go.mod h1:HYux0V0Zi04bHNwOHy4cXJVz/TQjYonnF6aoYhj+3QE=
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo=
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
|
||||
github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
|
||||
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
|
||||
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
@@ -1178,8 +1196,8 @@ golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5y
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
|
||||
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
|
||||
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
|
||||
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
|
||||
golang.org/x/crypto/x509roots/fallback v0.0.0-20240507223354-67b13616a595 h1:TgSqweA595vD0Zt86JzLv3Pb/syKg8gd5KMGGbJPYFw=
|
||||
golang.org/x/crypto/x509roots/fallback v0.0.0-20240507223354-67b13616a595/go.mod h1:kNa9WdvYnzFwC79zRpLRMJbdEFlhyM5RPFBBZp/wWH8=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
@@ -1196,8 +1214,8 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4=
|
||||
golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
|
||||
golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM=
|
||||
golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@@ -1219,8 +1237,8 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug
|
||||
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
|
||||
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
|
||||
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
|
||||
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE=
|
||||
golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||
@@ -1234,8 +1252,8 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
|
||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
|
||||
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@@ -1283,8 +1301,8 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
|
||||
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
|
||||
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
@@ -1292,8 +1310,8 @@ golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg=
|
||||
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
|
||||
golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
|
||||
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
@@ -1304,8 +1322,8 @@ golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||
golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg=
|
||||
golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
@@ -1327,8 +1345,8 @@ golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4f
|
||||
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.29.0 h1:Xx0h3TtM9rzQpQuR4dKLrdglAmCEN5Oi+P74JdhdzXE=
|
||||
golang.org/x/tools v0.29.0/go.mod h1:KMQVMRsVxU6nHCFXrBPhDB8XncLNLM0lIy/F14RP588=
|
||||
golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY=
|
||||
golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY=
|
||||
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
@@ -1420,3 +1438,27 @@ k8s.io/klog/v2 v2.110.1 h1:U/Af64HJf7FcwMcXyKm2RPM22WZzyR7OSpYj5tg3cL0=
|
||||
k8s.io/klog/v2 v2.110.1/go.mod h1:YGtd1984u+GgbuZ7e08/yBuAfKLSO0+uR1Fhi6ExXjo=
|
||||
lukechampine.com/blake3 v1.3.0 h1:sJ3XhFINmHSrYCgl958hscfIa3bw8x4DqMP3u1YvoYE=
|
||||
lukechampine.com/blake3 v1.3.0/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k=
|
||||
modernc.org/cc/v4 v4.24.4 h1:TFkx1s6dCkQpd6dKurBNmpo+G8Zl4Sq/ztJ+2+DEsh0=
|
||||
modernc.org/cc/v4 v4.24.4/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
||||
modernc.org/ccgo/v4 v4.23.16 h1:Z2N+kk38b7SfySC1ZkpGLN2vthNJP1+ZzGZIlH7uBxo=
|
||||
modernc.org/ccgo/v4 v4.23.16/go.mod h1:nNma8goMTY7aQZQNTyN9AIoJfxav4nvTnvKThAeMDdo=
|
||||
modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
|
||||
modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=
|
||||
modernc.org/gc/v2 v2.6.3 h1:aJVhcqAte49LF+mGveZ5KPlsp4tdGdAOT4sipJXADjw=
|
||||
modernc.org/gc/v2 v2.6.3/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
||||
modernc.org/libc v1.61.13 h1:3LRd6ZO1ezsFiX1y+bHd1ipyEHIJKvuprv0sLTBwLW8=
|
||||
modernc.org/libc v1.61.13/go.mod h1:8F/uJWL/3nNil0Lgt1Dpz+GgkApWh04N3el3hxJcA6E=
|
||||
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||
modernc.org/memory v1.8.2 h1:cL9L4bcoAObu4NkxOlKWBWtNHIsnnACGF/TbqQ6sbcI=
|
||||
modernc.org/memory v1.8.2/go.mod h1:ZbjSvMO5NQ1A2i3bWeDiVMxIorXwdClKE/0SZ+BMotU=
|
||||
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
|
||||
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
||||
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
||||
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
||||
modernc.org/sqlite v1.36.3 h1:qYMYlFR+rtLDUzuXoST1SDIdEPbX8xzuhdF90WsX1ss=
|
||||
modernc.org/sqlite v1.36.3/go.mod h1:ADySlx7K4FdY5MaJcEv86hTJ0PjedAloTUuif0YS3ws=
|
||||
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||
|
||||
@@ -1,165 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/distribution/reference"
|
||||
"maps"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"slices"
|
||||
"uncloud/internal/machine/api/pb"
|
||||
)
|
||||
|
||||
const (
|
||||
ServiceModeReplicated = "replicated"
|
||||
ServiceModeGlobal = "global"
|
||||
)
|
||||
|
||||
type ServiceSpec struct {
|
||||
Container ContainerSpec
|
||||
// Mode is the replication mode of the service. Default is ServiceModeReplicated if empty.
|
||||
Mode string
|
||||
Name string
|
||||
// Ports defines what service ports to publish to make the service accessible outside the cluster.
|
||||
Ports []PortSpec
|
||||
// Replicas is the number of containers to run for the service. Only valid for a replicated service.
|
||||
Replicas uint
|
||||
}
|
||||
|
||||
func (s *ServiceSpec) Validate() error {
|
||||
if err := s.Container.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch s.Mode {
|
||||
case "", ServiceModeGlobal, ServiceModeReplicated:
|
||||
default:
|
||||
return fmt.Errorf("invalid mode: %q", s.Mode)
|
||||
}
|
||||
|
||||
// TODO: validate there is no conflict between ports.
|
||||
// TODO: return error if there are non-HTTP/HTTPS ingress ports that we don't support yet.
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Equals returns true if the service spec is equal to the given spec ignoring the number of replicas.
|
||||
func (s *ServiceSpec) Equals(spec ServiceSpec) bool {
|
||||
// TODO: ignore order of ports.
|
||||
sCopy := *s
|
||||
// Ignore the number of replicas when comparing.
|
||||
sCopy.Replicas = 0
|
||||
spec.Replicas = 0
|
||||
return reflect.DeepEqual(*s, spec)
|
||||
}
|
||||
|
||||
var serviceIDRegexp = regexp.MustCompile("^[0-9a-f]{32}$")
|
||||
|
||||
func ValidateServiceID(id string) bool {
|
||||
return serviceIDRegexp.MatchString(id)
|
||||
}
|
||||
|
||||
type ContainerSpec struct {
|
||||
Command []string
|
||||
Image string
|
||||
// Run a custom init inside the container. If nil, use the daemon's configured settings.
|
||||
Init *bool
|
||||
// List of volumes to bind mount into the container.
|
||||
Volumes []string
|
||||
}
|
||||
|
||||
func (s *ContainerSpec) Validate() error {
|
||||
if _, err := reference.ParseDockerRef(s.Image); err != nil {
|
||||
return fmt.Errorf("invalid image: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type Service struct {
|
||||
ID string
|
||||
Name string
|
||||
Mode string
|
||||
Containers []MachineContainer
|
||||
}
|
||||
|
||||
type MachineContainer struct {
|
||||
MachineID string
|
||||
Container Container
|
||||
}
|
||||
|
||||
// Endpoints returns the exposed HTTP and HTTPS endpoints of the service.
|
||||
func (s *Service) Endpoints() []string {
|
||||
endpoints := make(map[string]struct{})
|
||||
|
||||
// Container specs may differ between containers in the same service, e.g. during a rolling update,
|
||||
// so we need to collect all unique endpoints.
|
||||
for _, ctr := range s.Containers {
|
||||
ports, err := ctr.Container.ServicePorts()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, port := range ports {
|
||||
protocol := ""
|
||||
switch port.Protocol {
|
||||
case ProtocolHTTP:
|
||||
protocol = "http"
|
||||
case ProtocolHTTPS:
|
||||
protocol = "https"
|
||||
default:
|
||||
continue
|
||||
}
|
||||
|
||||
if port.Hostname == "" {
|
||||
// There shouldn't be http(s) ports without a hostname but just in case ignore them.
|
||||
continue
|
||||
}
|
||||
|
||||
endpoint := fmt.Sprintf("%s://%s", protocol, port.Hostname)
|
||||
if port.PublishedPort != 0 {
|
||||
// For non-standard ports (80/443), include the port in the URL.
|
||||
if !(port.Protocol == ProtocolHTTP && port.PublishedPort == 80) &&
|
||||
!(port.Protocol == ProtocolHTTPS && port.PublishedPort == 443) {
|
||||
endpoint += fmt.Sprintf(":%d", port.PublishedPort)
|
||||
}
|
||||
}
|
||||
|
||||
endpoint += fmt.Sprintf(" → :%d", port.ContainerPort)
|
||||
endpoints[endpoint] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
return slices.Sorted(maps.Keys(endpoints))
|
||||
}
|
||||
|
||||
func ServiceFromProto(s *pb.Service) (Service, error) {
|
||||
var err error
|
||||
containers := make([]MachineContainer, len(s.Containers))
|
||||
for i, sc := range s.Containers {
|
||||
containers[i], err = machineContainerFromProto(sc)
|
||||
if err != nil {
|
||||
return Service{}, err
|
||||
}
|
||||
}
|
||||
|
||||
return Service{
|
||||
ID: s.Id,
|
||||
Name: s.Name,
|
||||
Mode: s.Mode,
|
||||
Containers: containers,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func machineContainerFromProto(sc *pb.Service_Container) (MachineContainer, error) {
|
||||
var c Container
|
||||
if err := json.Unmarshal(sc.Container, &c); err != nil {
|
||||
return MachineContainer{}, fmt.Errorf("unmarshal container: %w", err)
|
||||
}
|
||||
|
||||
return MachineContainer{
|
||||
MachineID: sc.MachineId,
|
||||
Container: c,
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
composetypes "github.com/compose-spec/compose-go/v2/types"
|
||||
"github.com/distribution/reference"
|
||||
"github.com/docker/cli/cli/config"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/image"
|
||||
dockerclient "github.com/docker/docker/client"
|
||||
"github.com/docker/docker/pkg/archive"
|
||||
"github.com/docker/docker/pkg/jsonmessage"
|
||||
"github.com/docker/docker/registry"
|
||||
"github.com/moby/term"
|
||||
)
|
||||
|
||||
type BuildOptions struct {
|
||||
Files []string
|
||||
Profiles []string
|
||||
Services []string
|
||||
Push bool
|
||||
NoCache bool
|
||||
}
|
||||
|
||||
// GetServicesThatNeedBuild returns a map of services that require building
|
||||
func GetServicesThatNeedBuild(project *composetypes.Project) map[string]composetypes.ServiceConfig {
|
||||
servicesToBuild := make(map[string]composetypes.ServiceConfig, len(project.Services))
|
||||
for serviceName, service := range project.Services {
|
||||
if service.Build == nil {
|
||||
continue
|
||||
}
|
||||
servicesToBuild[serviceName] = service
|
||||
}
|
||||
return servicesToBuild
|
||||
}
|
||||
|
||||
// BuildServices builds the services defined in the provided map.
|
||||
func BuildServices(ctx context.Context, servicesToBuild map[string]composetypes.ServiceConfig, opts BuildOptions) error {
|
||||
fmt.Println("Building services...")
|
||||
|
||||
// Init docker client (can be local or remote, depending on DOCKER_HOST environment variable)
|
||||
dockerCli, err := dockerclient.NewClientWithOpts(dockerclient.FromEnv, dockerclient.WithAPIVersionNegotiation())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer dockerCli.Close()
|
||||
|
||||
serviceImages := make(map[string]string, len(servicesToBuild))
|
||||
|
||||
// Build the services using the local docker client and compose libraries
|
||||
for _, service := range servicesToBuild {
|
||||
fmt.Printf("Building service: %s\n", service.Name)
|
||||
imageName, err := buildSingleService(ctx, dockerCli, service, opts)
|
||||
if err != nil {
|
||||
return fmt.Errorf("build service %s: %w", service.Name, err)
|
||||
}
|
||||
serviceImages[service.Name] = imageName
|
||||
}
|
||||
fmt.Printf("Service images are built.\n")
|
||||
|
||||
if opts.Push {
|
||||
err = pushServiceImages(ctx, dockerCli, serviceImages)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// buildSingleService builds a single service using the Docker client and Compose libraries.
|
||||
func buildSingleService(ctx context.Context, dockerCli *dockerclient.Client, service composetypes.ServiceConfig, opts BuildOptions) (string, error) {
|
||||
if service.Build == nil {
|
||||
return "", fmt.Errorf("service %s has no build configuration", service.Name)
|
||||
}
|
||||
if service.Image == "" {
|
||||
return "", fmt.Errorf("service %s has no image specified; building services without image is not supported yet", service.Name)
|
||||
}
|
||||
|
||||
buildContextPath := service.Build.Context
|
||||
imageName := service.Image
|
||||
|
||||
// Create a tar archive of the build context
|
||||
buildContext, err := archive.TarWithOptions(buildContextPath, &archive.TarOptions{})
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create build context for service %s: %w", service.Name, err)
|
||||
}
|
||||
|
||||
buildOptions := types.ImageBuildOptions{
|
||||
// TODO: Support Dockerfiles outside the build context
|
||||
// See https://github.com/docker/compose/blob/cf89fd1aa1328d5af77658ccc5a1e1b29981ae80/pkg/compose/build_classic.go#L92
|
||||
Dockerfile: service.Build.Dockerfile,
|
||||
Tags: []string{imageName},
|
||||
Remove: true, // Remove intermediate containers
|
||||
NoCache: opts.NoCache,
|
||||
}
|
||||
|
||||
buildResponse, err := dockerCli.ImageBuild(ctx, buildContext, buildOptions)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to build image for service %s: %w", service.Name, err)
|
||||
}
|
||||
defer buildResponse.Body.Close()
|
||||
|
||||
// Display the build response
|
||||
fd, isTerminal := term.GetFdInfo(os.Stdout)
|
||||
if err := jsonmessage.DisplayJSONMessagesStream(buildResponse.Body, os.Stdout, fd, isTerminal, nil); err != nil {
|
||||
return "", fmt.Errorf("failed to display build response for service %s: %w", service.Name, err)
|
||||
}
|
||||
|
||||
return imageName, nil
|
||||
}
|
||||
|
||||
// pushSingleServiceImage pushes a single service image.
|
||||
func pushSingleServiceImage(ctx context.Context, dockerCli *dockerclient.Client, serviceName string, imageName string) error {
|
||||
ref, err := reference.ParseNormalizedNamed(imageName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
repoInfo, err := registry.ParseRepositoryInfo(ref)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
registryKey := repoInfo.Index.Name
|
||||
if repoInfo.Index.Official {
|
||||
registryKey = registry.IndexServer
|
||||
}
|
||||
|
||||
// Load the Docker config file with auth details, if available
|
||||
configFile := config.LoadDefaultConfigFile(os.Stderr)
|
||||
|
||||
authConfig, err := configFile.GetAuthConfig(registryKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
authJSON, err := json.Marshal(authConfig)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal auth config for registry %s: %w", registryKey, err)
|
||||
}
|
||||
|
||||
authStr := base64.URLEncoding.EncodeToString(authJSON)
|
||||
|
||||
pushOptions := image.PushOptions{
|
||||
RegistryAuth: authStr,
|
||||
}
|
||||
pushResponse, err := dockerCli.ImagePush(ctx, imageName, pushOptions)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to push image %s: %w", imageName, err)
|
||||
}
|
||||
defer pushResponse.Close()
|
||||
|
||||
fmt.Printf("Pushing image %s for service %s...\n", imageName, serviceName)
|
||||
|
||||
fd, isTerminal := term.GetFdInfo(os.Stdout)
|
||||
if err := jsonmessage.DisplayJSONMessagesStream(pushResponse, os.Stdout, fd, isTerminal, nil); err != nil {
|
||||
return fmt.Errorf("failed to display push response for image %s: %w", imageName, err)
|
||||
}
|
||||
|
||||
fmt.Printf("Image %s pushed successfully.\n", imageName)
|
||||
return nil
|
||||
}
|
||||
|
||||
// pushServiceImages pushes all built service images to the registry.
|
||||
func pushServiceImages(ctx context.Context, dockerCli *dockerclient.Client, serviceImages map[string]string) error {
|
||||
fmt.Printf("Pushing images...\n")
|
||||
for serviceName, imageName := range serviceImages {
|
||||
if err := pushSingleServiceImage(ctx, dockerCli, serviceName, imageName); err != nil {
|
||||
return fmt.Errorf("push image for service %s: %w", serviceName, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
+199
-126
@@ -4,90 +4,125 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/docker/cli/cli/streams"
|
||||
"net/netip"
|
||||
"os"
|
||||
"uncloud/internal/cli/client"
|
||||
"uncloud/internal/cli/client/connector"
|
||||
"uncloud/internal/cli/config"
|
||||
"uncloud/internal/fs"
|
||||
"uncloud/internal/machine"
|
||||
"uncloud/internal/machine/api/pb"
|
||||
"uncloud/internal/sshexec"
|
||||
|
||||
"github.com/charmbracelet/huh"
|
||||
"github.com/docker/cli/cli/streams"
|
||||
"github.com/psviderski/uncloud/internal/cli/config"
|
||||
"github.com/psviderski/uncloud/internal/fs"
|
||||
"github.com/psviderski/uncloud/internal/machine"
|
||||
"github.com/psviderski/uncloud/internal/machine/api/pb"
|
||||
"github.com/psviderski/uncloud/internal/sshexec"
|
||||
"github.com/psviderski/uncloud/pkg/api"
|
||||
"github.com/psviderski/uncloud/pkg/client"
|
||||
"github.com/psviderski/uncloud/pkg/client/connector"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
"google.golang.org/protobuf/types/known/emptypb"
|
||||
)
|
||||
|
||||
const defaultClusterName = "default"
|
||||
const defaultContextName = "default"
|
||||
|
||||
type CLI struct {
|
||||
config *config.Config
|
||||
Config *config.Config
|
||||
conn *config.MachineConnection
|
||||
}
|
||||
|
||||
func New(configPath string) (*CLI, error) {
|
||||
// New creates a new CLI instance with the given config path or remote machine connection.
|
||||
// If the connection is provided, the config is ignored for all operations which is useful for interacting with
|
||||
// a cluster without creating a config.
|
||||
func New(configPath string, conn *config.MachineConnection) (*CLI, error) {
|
||||
if conn != nil {
|
||||
return &CLI{conn: conn}, nil
|
||||
}
|
||||
|
||||
cfg, err := config.NewFromFile(configPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read Uncloud config: %w", err)
|
||||
}
|
||||
|
||||
return &CLI{
|
||||
config: cfg,
|
||||
Config: cfg,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (cli *CLI) CreateCluster(name string) error {
|
||||
if _, ok := cli.config.Clusters[name]; ok {
|
||||
return fmt.Errorf("cluster %q already exists", name)
|
||||
func (cli *CLI) CreateContext(name string) error {
|
||||
if _, ok := cli.Config.Contexts[name]; ok {
|
||||
return fmt.Errorf("context '%s' already exists", name)
|
||||
}
|
||||
cli.config.Clusters[name] = &config.Cluster{
|
||||
cli.Config.Contexts[name] = &config.Context{
|
||||
Name: name,
|
||||
}
|
||||
return cli.config.Save()
|
||||
return cli.Config.Save()
|
||||
}
|
||||
|
||||
func (cli *CLI) SetCurrentCluster(name string) error {
|
||||
if _, ok := cli.config.Clusters[name]; !ok {
|
||||
return client.ErrNotFound
|
||||
func (cli *CLI) SetCurrentContext(name string) error {
|
||||
if _, ok := cli.Config.Contexts[name]; !ok {
|
||||
return api.ErrNotFound
|
||||
}
|
||||
cli.config.CurrentCluster = name
|
||||
return cli.config.Save()
|
||||
cli.Config.CurrentContext = name
|
||||
return cli.Config.Save()
|
||||
}
|
||||
|
||||
func (cli *CLI) ConnectCluster(ctx context.Context, clusterName string) (*client.Client, error) {
|
||||
if len(cli.config.Clusters) == 0 {
|
||||
return nil, errors.New(
|
||||
"no clusters found in the Uncloud config. " +
|
||||
"Please initialise a cluster with `uncloud machine init` first",
|
||||
// ConnectCluster connects to a cluster using the given context name or the current context if not specified.
|
||||
// If the CLI was initialised with a machine connection, the config is ignored and the connection is used instead.
|
||||
func (cli *CLI) ConnectCluster(ctx context.Context, contextName string) (*client.Client, error) {
|
||||
if cli.conn != nil {
|
||||
return connectCluster(ctx, *cli.conn)
|
||||
}
|
||||
|
||||
if len(cli.Config.Contexts) == 0 {
|
||||
return nil, fmt.Errorf(
|
||||
"no cluster contexts found in the Uncloud config (%s). "+
|
||||
"Please initialise a cluster with 'uncloud machine init' first",
|
||||
cli.Config.Path(),
|
||||
)
|
||||
}
|
||||
if clusterName == "" {
|
||||
if contextName == "" {
|
||||
// If the cluster is not specified, use the current cluster if set.
|
||||
if cli.config.CurrentCluster == "" {
|
||||
return nil, errors.New(
|
||||
"the current cluster is not set in the Uncloud config. " +
|
||||
"Please specify a cluster with the --cluster flag or set current_cluster in the config",
|
||||
)
|
||||
}
|
||||
if _, ok := cli.config.Clusters[cli.config.CurrentCluster]; !ok {
|
||||
if cli.Config.CurrentContext == "" {
|
||||
return nil, fmt.Errorf(
|
||||
"current cluster %q not found in the config. "+
|
||||
"Please specify a cluster with the --cluster flag or update current_cluster in the config",
|
||||
cli.config.CurrentCluster,
|
||||
"the current cluster context is not set in the Uncloud config (%s). "+
|
||||
"Please specify the context with the '--context' flag or set 'current_context' in the config",
|
||||
cli.Config.Path(),
|
||||
)
|
||||
}
|
||||
clusterName = cli.config.CurrentCluster
|
||||
if _, ok := cli.Config.Contexts[cli.Config.CurrentContext]; !ok {
|
||||
return nil, fmt.Errorf(
|
||||
"current cluster context '%s' not found in the Uncloud config (%s). "+
|
||||
"Please specify the context with the '--context' flag or update 'current_context' in the config",
|
||||
cli.Config.CurrentContext,
|
||||
cli.Config.Path(),
|
||||
)
|
||||
}
|
||||
contextName = cli.Config.CurrentContext
|
||||
}
|
||||
|
||||
cfg, ok := cli.config.Clusters[clusterName]
|
||||
cfg, ok := cli.Config.Contexts[contextName]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("cluster %q not found in the config", clusterName)
|
||||
return nil, fmt.Errorf("cluster context '%s' not found in the Uncloud config (%s)",
|
||||
contextName, cli.Config.Path())
|
||||
}
|
||||
if len(cfg.Connections) == 0 {
|
||||
return nil, fmt.Errorf("no connection configurations found for cluster %q in the config", clusterName)
|
||||
return nil, fmt.Errorf(
|
||||
"no connection configurations found for cluster context '%s' in the Uncloud config (%s)",
|
||||
contextName, cli.Config.Path(),
|
||||
)
|
||||
}
|
||||
|
||||
// TODO: iterate over all connections and try to connect to the cluster using the first successful connection.
|
||||
conn := cfg.Connections[0]
|
||||
|
||||
c, err := connectCluster(ctx, conn)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("connect to cluster (context '%s'): %w", contextName, err)
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func connectCluster(ctx context.Context, conn config.MachineConnection) (*client.Client, error) {
|
||||
if conn.SSH != "" {
|
||||
user, host, port, err := conn.SSH.Parse()
|
||||
if err != nil {
|
||||
@@ -103,45 +138,42 @@ func (cli *CLI) ConnectCluster(ctx context.Context, clusterName string) (*client
|
||||
KeyPath: keyPath,
|
||||
}
|
||||
return client.New(ctx, connector.NewSSHConnector(sshConfig))
|
||||
} else if conn.TCP.IsValid() {
|
||||
return client.New(ctx, connector.NewTCPConnector(conn.TCP))
|
||||
} else if conn.TCP != nil && conn.TCP.IsValid() {
|
||||
return client.New(ctx, connector.NewTCPConnector(*conn.TCP))
|
||||
}
|
||||
return nil, errors.New("no valid connection configuration found for the cluster")
|
||||
|
||||
return nil, errors.New("connection configuration is invalid")
|
||||
}
|
||||
|
||||
type InitClusterOptions struct {
|
||||
Context string
|
||||
MachineName string
|
||||
Network netip.Prefix
|
||||
PublicIP *netip.Addr
|
||||
RemoteMachine *RemoteMachine
|
||||
Version string
|
||||
}
|
||||
|
||||
// InitCluster initialises a new cluster on a remote machine and returns a client to interact with the cluster.
|
||||
// The client should be closed after use by the caller.
|
||||
func (cli *CLI) InitCluster(
|
||||
ctx context.Context,
|
||||
remoteMachine *RemoteMachine,
|
||||
clusterName,
|
||||
machineName string,
|
||||
netPrefix netip.Prefix,
|
||||
publicIP *netip.Addr,
|
||||
) (*client.Client, error) {
|
||||
if remoteMachine != nil {
|
||||
return cli.initRemoteMachine(ctx, *remoteMachine, clusterName, machineName, netPrefix, publicIP)
|
||||
func (cli *CLI) InitCluster(ctx context.Context, opts InitClusterOptions) (*client.Client, error) {
|
||||
if opts.RemoteMachine != nil {
|
||||
return cli.initRemoteMachine(ctx, opts)
|
||||
}
|
||||
// TODO: implement local machine initialisation
|
||||
return nil, fmt.Errorf("local machine initialisation is not implemented yet")
|
||||
}
|
||||
|
||||
func (cli *CLI) initRemoteMachine(
|
||||
ctx context.Context,
|
||||
remoteMachine RemoteMachine,
|
||||
clusterName,
|
||||
machineName string,
|
||||
netPrefix netip.Prefix,
|
||||
publicIP *netip.Addr,
|
||||
) (*client.Client, error) {
|
||||
if clusterName == "" {
|
||||
clusterName = defaultClusterName
|
||||
func (cli *CLI) initRemoteMachine(ctx context.Context, opts InitClusterOptions) (*client.Client, error) {
|
||||
contextName := opts.Context
|
||||
if contextName == "" {
|
||||
contextName = defaultContextName
|
||||
}
|
||||
if _, ok := cli.config.Clusters[clusterName]; ok {
|
||||
return nil, fmt.Errorf("cluster %q already exists", clusterName)
|
||||
if _, ok := cli.Config.Contexts[contextName]; ok {
|
||||
return nil, fmt.Errorf("cluster context '%s' already exists", contextName)
|
||||
}
|
||||
|
||||
machineClient, err := cli.provisionRemoteMachine(ctx, remoteMachine)
|
||||
machineClient, err := cli.provisionRemoteMachine(ctx, *opts.RemoteMachine, opts.Version)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -163,13 +195,24 @@ func (cli *CLI) initRemoteMachine(
|
||||
}
|
||||
}
|
||||
|
||||
req := &pb.InitClusterRequest{
|
||||
MachineName: machineName,
|
||||
Network: pb.NewIPPrefix(netPrefix),
|
||||
// Check machine meets all necessary system requirements before proceeding.
|
||||
checkResp, err := machineClient.CheckPrerequisites(ctx, &emptypb.Empty{})
|
||||
// TODO(lhf): remove Unimplemented check when v0.9.0 is released.
|
||||
if err != nil {
|
||||
if status.Convert(err).Code() != codes.Unimplemented {
|
||||
return nil, fmt.Errorf("check machine prerequisites: %w", err)
|
||||
}
|
||||
} else if !checkResp.Satisfied {
|
||||
return nil, fmt.Errorf("machine prerequisites not satisfied: %s", checkResp.Error)
|
||||
}
|
||||
if publicIP != nil {
|
||||
if publicIP.IsValid() {
|
||||
req.PublicIpConfig = &pb.InitClusterRequest_PublicIp{PublicIp: pb.NewIP(*publicIP)}
|
||||
|
||||
req := &pb.InitClusterRequest{
|
||||
MachineName: opts.MachineName,
|
||||
Network: pb.NewIPPrefix(opts.Network),
|
||||
}
|
||||
if opts.PublicIP != nil {
|
||||
if opts.PublicIP.IsValid() {
|
||||
req.PublicIpConfig = &pb.InitClusterRequest_PublicIp{PublicIp: pb.NewIP(*opts.PublicIP)}
|
||||
} else {
|
||||
// Invalid or in other words zero IP means to automatically detect the public IP.
|
||||
req.PublicIpConfig = &pb.InitClusterRequest_PublicIpAuto{PublicIpAuto: true}
|
||||
@@ -180,44 +223,58 @@ func (cli *CLI) initRemoteMachine(
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("init cluster: %w", err)
|
||||
}
|
||||
fmt.Printf("Cluster %q initialised with machine %q\n", clusterName, resp.Machine.Name)
|
||||
|
||||
if err = cli.CreateCluster(clusterName); err != nil {
|
||||
return nil, fmt.Errorf("save cluster to config: %w", err)
|
||||
fmt.Printf("Cluster initialised with machine '%s' and saved as context '%s' in your local config (%s)\n",
|
||||
resp.Machine.Name, contextName, cli.Config.Path())
|
||||
if err = cli.CreateContext(contextName); err != nil {
|
||||
return nil, fmt.Errorf("save cluster context to config: %w", err)
|
||||
}
|
||||
// Set the current cluster to the just created one if it is the only cluster in the config.
|
||||
if len(cli.config.Clusters) == 1 {
|
||||
if err = cli.SetCurrentCluster(clusterName); err != nil {
|
||||
return nil, fmt.Errorf("set current cluster: %w", err)
|
||||
}
|
||||
if err = cli.SetCurrentContext(contextName); err != nil {
|
||||
return nil, fmt.Errorf("set current cluster context: %w", err)
|
||||
}
|
||||
fmt.Printf("Current cluster context is now '%s'.\n", contextName)
|
||||
|
||||
// Save the machine's SSH connection details in the cluster config.
|
||||
// Save the machine's SSH connection details in the context config.
|
||||
connCfg := config.MachineConnection{
|
||||
SSH: config.NewSSHDestination(remoteMachine.User, remoteMachine.Host, remoteMachine.Port),
|
||||
SSHKeyFile: remoteMachine.KeyPath,
|
||||
SSH: config.NewSSHDestination(opts.RemoteMachine.User, opts.RemoteMachine.Host, opts.RemoteMachine.Port),
|
||||
SSHKeyFile: opts.RemoteMachine.KeyPath,
|
||||
}
|
||||
cli.config.Clusters[clusterName].Connections = append(cli.config.Clusters[clusterName].Connections, connCfg)
|
||||
if err = cli.config.Save(); err != nil {
|
||||
cli.Config.Contexts[contextName].Connections = append(cli.Config.Contexts[contextName].Connections, connCfg)
|
||||
if err = cli.Config.Save(); err != nil {
|
||||
return nil, fmt.Errorf("save config: %w", err)
|
||||
}
|
||||
return machineClient, nil
|
||||
}
|
||||
|
||||
// AddMachine provisions a remote machine and adds it to the cluster. It returns a client to interact with the machine
|
||||
// which should be closed after use by the caller.
|
||||
func (cli *CLI) AddMachine(
|
||||
ctx context.Context, remoteMachine RemoteMachine, clusterName, machineName string, publicIP *netip.Addr,
|
||||
) (*client.Client, error) {
|
||||
c, err := cli.ConnectCluster(ctx, clusterName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("connect to cluster: %w", err)
|
||||
}
|
||||
defer c.Close()
|
||||
type AddMachineOptions struct {
|
||||
Context string
|
||||
MachineName string
|
||||
PublicIP *netip.Addr
|
||||
RemoteMachine RemoteMachine
|
||||
Version string
|
||||
}
|
||||
|
||||
machineClient, err := cli.provisionRemoteMachine(ctx, remoteMachine)
|
||||
// AddMachine provisions a remote machine and adds it to the cluster. It returns a cluster client and a machine client.
|
||||
// The cluster client is connected to the existing machine in the cluster. It was used to add the new machine to the
|
||||
// cluster. The machine client is connected to the new machine and can be used to interact with it.
|
||||
// Both client should be closed after use by the caller.
|
||||
func (cli *CLI) AddMachine(ctx context.Context, opts AddMachineOptions) (*client.Client, *client.Client, error) {
|
||||
contextName := opts.Context
|
||||
if contextName == "" {
|
||||
contextName = cli.Config.CurrentContext
|
||||
}
|
||||
c, err := cli.ConnectCluster(ctx, contextName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, fmt.Errorf("connect to cluster (context '%s'): %w", contextName, err)
|
||||
}
|
||||
defer func() {
|
||||
if err != nil {
|
||||
c.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
machineClient, err := cli.provisionRemoteMachine(ctx, opts.RemoteMachine, opts.Version)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
defer func() {
|
||||
if err != nil {
|
||||
@@ -228,21 +285,32 @@ func (cli *CLI) AddMachine(
|
||||
// Check if the machine is already initialised as a cluster member and prompt the user to reset it first.
|
||||
minfo, err := machineClient.Inspect(ctx, &emptypb.Empty{})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("inspect machine: %w", err)
|
||||
return nil, nil, fmt.Errorf("inspect machine: %w", err)
|
||||
}
|
||||
if minfo.Id != "" {
|
||||
if err = cli.promptResetMachine(); err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Check machine meets all necessary system requirements before proceeding.
|
||||
checkResp, err := machineClient.CheckPrerequisites(ctx, &emptypb.Empty{})
|
||||
// TODO(lhf): remove Unimplemented check when v0.9.0 is released.
|
||||
if err != nil {
|
||||
if status.Convert(err).Code() != codes.Unimplemented {
|
||||
return nil, nil, fmt.Errorf("check machine prerequisites: %w", err)
|
||||
}
|
||||
} else if !checkResp.Satisfied {
|
||||
return nil, nil, fmt.Errorf("machine prerequisites not satisfied: %s", checkResp.Error)
|
||||
}
|
||||
|
||||
tokenResp, err := machineClient.Token(ctx, &emptypb.Empty{})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get remote machine token: %w", err)
|
||||
return nil, nil, fmt.Errorf("get remote machine token: %w", err)
|
||||
}
|
||||
token, err := machine.ParseToken(tokenResp.Token)
|
||||
if err != nil {
|
||||
return 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.
|
||||
@@ -251,15 +319,15 @@ func (cli *CLI) AddMachine(
|
||||
endpoints[i] = pb.NewIPPort(addrPort)
|
||||
}
|
||||
addReq := &pb.AddMachineRequest{
|
||||
Name: machineName,
|
||||
Name: opts.MachineName,
|
||||
Network: &pb.NetworkConfig{
|
||||
Endpoints: endpoints,
|
||||
PublicKey: token.PublicKey,
|
||||
},
|
||||
}
|
||||
if publicIP != nil {
|
||||
if publicIP.IsValid() {
|
||||
addReq.PublicIp = pb.NewIP(*publicIP)
|
||||
if opts.PublicIP != nil {
|
||||
if opts.PublicIP.IsValid() {
|
||||
addReq.PublicIp = pb.NewIP(*opts.PublicIP)
|
||||
} else if token.PublicIP.IsValid() {
|
||||
// Invalid or in other words zero IP means to use an automatically detected public IP from the token.
|
||||
addReq.PublicIp = pb.NewIP(token.PublicIP)
|
||||
@@ -268,13 +336,13 @@ func (cli *CLI) AddMachine(
|
||||
|
||||
addResp, err := c.AddMachine(ctx, addReq)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("add machine to cluster: %w", err)
|
||||
return nil, nil, fmt.Errorf("add machine to cluster (context '%s'): %w", contextName, err)
|
||||
}
|
||||
|
||||
// List other machines in the cluster to include them in the join request.
|
||||
machines, err := c.ListMachines(ctx)
|
||||
machines, err := c.ListMachines(ctx, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list cluster machines: %w", err)
|
||||
return nil, nil, fmt.Errorf("list cluster machines: %w", err)
|
||||
}
|
||||
otherMachines := make([]*pb.MachineInfo, 0, len(machines)-1)
|
||||
for _, m := range machines {
|
||||
@@ -289,30 +357,34 @@ func (cli *CLI) AddMachine(
|
||||
OtherMachines: otherMachines,
|
||||
}
|
||||
if _, err = machineClient.JoinCluster(ctx, joinReq); err != nil {
|
||||
return nil, fmt.Errorf("join cluster: %w", err)
|
||||
return nil, nil, fmt.Errorf("join cluster: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Machine %q added to cluster\n", addResp.Machine.Name)
|
||||
// TODO: fix empty context name when using the current context (contextName == "").
|
||||
fmt.Printf("Machine '%s' added to the cluster (context '%s').\n", addResp.Machine.Name, contextName)
|
||||
|
||||
// Save the machine's SSH connection details in the cluster config.
|
||||
// Save the machine's SSH connection details in the context config.
|
||||
connCfg := config.MachineConnection{
|
||||
SSH: config.NewSSHDestination(remoteMachine.User, remoteMachine.Host, remoteMachine.Port),
|
||||
SSHKeyFile: remoteMachine.KeyPath,
|
||||
SSH: config.NewSSHDestination(opts.RemoteMachine.User, opts.RemoteMachine.Host, opts.RemoteMachine.Port),
|
||||
SSHKeyFile: opts.RemoteMachine.KeyPath,
|
||||
}
|
||||
if clusterName == "" {
|
||||
clusterName = cli.config.CurrentCluster
|
||||
if contextName == "" {
|
||||
contextName = cli.Config.CurrentContext
|
||||
}
|
||||
cli.config.Clusters[clusterName].Connections = append(cli.config.Clusters[clusterName].Connections, connCfg)
|
||||
if err = cli.config.Save(); err != nil {
|
||||
return nil, fmt.Errorf("save config: %w", err)
|
||||
cli.Config.Contexts[contextName].Connections = append(cli.Config.Contexts[contextName].Connections, connCfg)
|
||||
if err = cli.Config.Save(); err != nil {
|
||||
return nil, nil, fmt.Errorf("save config: %w", err)
|
||||
}
|
||||
|
||||
return machineClient, nil
|
||||
return c, machineClient, nil
|
||||
}
|
||||
|
||||
// provisionRemoteMachine installs the Uncloud daemon and dependencies on the remote machine over SSH and returns
|
||||
// a machine API client to interact with the machine. The client should be closed after use by the caller.
|
||||
func (cli *CLI) provisionRemoteMachine(ctx context.Context, remoteMachine RemoteMachine) (*client.Client, error) {
|
||||
// The version parameter specifies the version of the Uncloud daemon to install. If empty, the latest version is used.
|
||||
func (cli *CLI) provisionRemoteMachine(
|
||||
ctx context.Context, remoteMachine RemoteMachine, version string,
|
||||
) (*client.Client, error) {
|
||||
// Provision the remote machine by installing the Uncloud daemon and dependencies over SSH.
|
||||
sshClient, err := sshexec.Connect(remoteMachine.User, remoteMachine.Host, remoteMachine.Port, remoteMachine.KeyPath)
|
||||
if err != nil {
|
||||
@@ -323,7 +395,7 @@ func (cli *CLI) provisionRemoteMachine(ctx context.Context, remoteMachine Remote
|
||||
}
|
||||
exec := sshexec.NewRemote(sshClient)
|
||||
// Install and run the Uncloud daemon and dependencies on the remote machine.
|
||||
if err = provisionMachine(ctx, exec); err != nil {
|
||||
if err = provisionMachine(ctx, exec, version); err != nil {
|
||||
return nil, fmt.Errorf("provision machine: %w", err)
|
||||
}
|
||||
|
||||
@@ -360,7 +432,7 @@ func (cli *CLI) promptResetMachine() error {
|
||||
Negative("No").
|
||||
Value(&confirm),
|
||||
),
|
||||
)
|
||||
).WithAccessible(true)
|
||||
if err := form.Run(); err != nil {
|
||||
return fmt.Errorf("prompt user to confirm: %w", err)
|
||||
}
|
||||
@@ -369,7 +441,8 @@ func (cli *CLI) promptResetMachine() error {
|
||||
return fmt.Errorf("remote machine is already initialised as a cluster member")
|
||||
}
|
||||
// TODO: implement resetting the remote machine.
|
||||
return fmt.Errorf("resetting the remote machine is not implemented yet")
|
||||
return fmt.Errorf("resetting the remote machine is not implemented yet. " +
|
||||
"Please manually run 'uncloud-uninstall' on the remote machine to fully uninstall Uncloud from it")
|
||||
}
|
||||
|
||||
// ProgressOut returns an output stream for progress writer.
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"testing"
|
||||
"uncloud/internal/api"
|
||||
)
|
||||
|
||||
func TestClient_NewCaddyDeployment(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cli := &Client{}
|
||||
|
||||
t.Run("latest image from Docker Hub", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
deploy, err := cli.NewCaddyDeployment("", nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "caddy", deploy.Spec.Name)
|
||||
assert.Equal(t, api.ServiceModeGlobal, deploy.Spec.Mode)
|
||||
assert.Regexp(t, `^caddy:2\.\d+\.\d+$`, deploy.Spec.Container.Image)
|
||||
expectedPorts := []api.PortSpec{
|
||||
{
|
||||
PublishedPort: 80,
|
||||
ContainerPort: 80,
|
||||
Protocol: api.ProtocolTCP,
|
||||
Mode: api.PortModeHost,
|
||||
},
|
||||
{
|
||||
PublishedPort: 443,
|
||||
ContainerPort: 443,
|
||||
Protocol: api.ProtocolTCP,
|
||||
Mode: api.PortModeHost,
|
||||
},
|
||||
}
|
||||
assert.Equal(t, expectedPorts, deploy.Spec.Ports)
|
||||
// TODO:
|
||||
//assert.Equal(t, alwaysPullImage, deploy.Spec.Container.PullPolicy)
|
||||
})
|
||||
|
||||
t.Run("custom image", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
image := "my-caddy:1.2.3"
|
||||
deploy, err := cli.NewCaddyDeployment(image, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "caddy", deploy.Spec.Name)
|
||||
assert.Equal(t, api.ServiceModeGlobal, deploy.Spec.Mode)
|
||||
assert.Equal(t, image, deploy.Spec.Container.Image)
|
||||
expectedPorts := []api.PortSpec{
|
||||
{
|
||||
PublishedPort: 80,
|
||||
ContainerPort: 80,
|
||||
Protocol: api.ProtocolTCP,
|
||||
Mode: api.PortModeHost,
|
||||
},
|
||||
{
|
||||
PublishedPort: 443,
|
||||
ContainerPort: 443,
|
||||
Protocol: api.ProtocolTCP,
|
||||
Mode: api.PortModeHost,
|
||||
},
|
||||
}
|
||||
assert.Equal(t, expectedPorts, deploy.Spec.Ports)
|
||||
})
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"google.golang.org/protobuf/types/known/emptypb"
|
||||
"uncloud/internal/machine/api/pb"
|
||||
)
|
||||
|
||||
func (cli *Client) InspectMachine(ctx context.Context, id string) (*pb.MachineMember, error) {
|
||||
machines, err := cli.ListMachines(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, m := range machines {
|
||||
if m.Machine.Id == id || m.Machine.Name == id {
|
||||
return m, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
|
||||
func (cli *Client) ListMachines(ctx context.Context) ([]*pb.MachineMember, error) {
|
||||
resp, err := cli.ClusterClient.ListMachines(ctx, &emptypb.Empty{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp.Machines, nil
|
||||
}
|
||||
@@ -1,116 +0,0 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/distribution/reference"
|
||||
"strings"
|
||||
"uncloud/internal/api"
|
||||
"uncloud/internal/secret"
|
||||
)
|
||||
|
||||
// ServiceSpecResolver transforms user-provided service specs into deployment-ready form.
|
||||
type ServiceSpecResolver struct {
|
||||
ClusterDomain string
|
||||
}
|
||||
|
||||
func NewServiceSpecResolver(clusterDomain string) *ServiceSpecResolver {
|
||||
return &ServiceSpecResolver{ClusterDomain: clusterDomain}
|
||||
}
|
||||
|
||||
// Resolve transforms a service spec into its fully resolved form ready for deployment.
|
||||
func (r *ServiceSpecResolver) Resolve(spec *api.ServiceSpec) error {
|
||||
if err := spec.Validate(); err != nil {
|
||||
return fmt.Errorf("invalid service spec: %w", err)
|
||||
}
|
||||
|
||||
steps := []func(*api.ServiceSpec) error{
|
||||
r.applyDefaults,
|
||||
r.resolveServiceName,
|
||||
r.expandIngressPorts,
|
||||
}
|
||||
|
||||
for _, step := range steps {
|
||||
if err := step(spec); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *ServiceSpecResolver) applyDefaults(spec *api.ServiceSpec) error {
|
||||
if spec.Mode == "" {
|
||||
spec.Mode = api.ServiceModeReplicated
|
||||
}
|
||||
// Ensure the replicated service has at least one replica.
|
||||
if spec.Mode == api.ServiceModeReplicated && spec.Replicas == 0 {
|
||||
spec.Replicas = 1
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *ServiceSpecResolver) resolveServiceName(spec *api.ServiceSpec) error {
|
||||
if spec.Name != "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Generate a random service name from the image when not provided.
|
||||
img, err := reference.ParseDockerRef(spec.Container.Image)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid image: %w", err)
|
||||
}
|
||||
// Get the image name without the repository and tag/digest parts.
|
||||
imageName := reference.FamiliarName(img)
|
||||
// Get the last part of the image name (path), e.g. "nginx" from "bitnami/nginx".
|
||||
if i := strings.LastIndex(imageName, "/"); i != -1 {
|
||||
imageName = imageName[i+1:]
|
||||
}
|
||||
// Append a random suffix to the image name to generate an optimistically unique service name.
|
||||
suffix, err := secret.RandomAlphaNumeric(4)
|
||||
if err != nil {
|
||||
return fmt.Errorf("generate random suffix: %w", err)
|
||||
}
|
||||
spec.Name = fmt.Sprintf("%s-%s", imageName, suffix)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// expandIngressPorts processes HTTP(S) ingress ports in a service spec by:
|
||||
// 1. Setting a default hostname (service-name.cluster-domain) for ports without a hostname.
|
||||
// 2. Duplicating a port with a cluster domain hostname for ports with external domains.
|
||||
// This ensures every ingress port is accessible via the cluster domain, while preserving any custom domains specified
|
||||
// by the user.
|
||||
func (r *ServiceSpecResolver) expandIngressPorts(spec *api.ServiceSpec) error {
|
||||
for i, port := range spec.Ports {
|
||||
if port.Protocol != api.ProtocolHTTP && port.Protocol != api.ProtocolHTTPS {
|
||||
continue
|
||||
}
|
||||
|
||||
if port.Hostname == "" {
|
||||
if r.ClusterDomain == "" {
|
||||
return fmt.Errorf("cluster domain must be reserved to generate hostname for ingress port: %d/%s",
|
||||
port.ContainerPort, port.Protocol)
|
||||
}
|
||||
// Assign the default hostname (service-name.cluster-domain).
|
||||
spec.Ports[i].Hostname = fmt.Sprintf("%s.%s", spec.Name, r.ClusterDomain)
|
||||
} else {
|
||||
if r.ClusterDomain == "" {
|
||||
// When no cluster domain is reserved, use only the provided hostname.
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.HasSuffix(port.Hostname, "."+r.ClusterDomain) {
|
||||
// If the hostname is already a cluster subdomain, use as is.
|
||||
continue
|
||||
}
|
||||
// For external domains, duplicate the port with a service-name.cluster-domain hostname so the service
|
||||
// can be accessed via both hostnames.
|
||||
newPort := port
|
||||
newPort.Hostname = fmt.Sprintf("%s.%s", spec.Name, r.ClusterDomain)
|
||||
spec.Ports = append(spec.Ports, newPort)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
package config
|
||||
|
||||
type Cluster struct {
|
||||
Name string `toml:"-"`
|
||||
Connections []MachineConnection `toml:"connections"`
|
||||
}
|
||||
@@ -2,14 +2,15 @@ package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/BurntSushi/toml"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/goccy/go-yaml"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Clusters map[string]*Cluster `toml:"clusters"`
|
||||
CurrentCluster string `toml:"current_cluster"`
|
||||
CurrentContext string `yaml:"current_context"`
|
||||
Contexts map[string]*Context `yaml:"contexts"`
|
||||
|
||||
// path is the file path config is read from.
|
||||
path string
|
||||
@@ -18,10 +19,10 @@ type Config struct {
|
||||
func NewFromFile(path string) (*Config, error) {
|
||||
_, err := os.Stat(path)
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return nil, fmt.Errorf("check file permissions %q: %w", path, err)
|
||||
return nil, fmt.Errorf("check file permissions '%s': %w", path, err)
|
||||
}
|
||||
c := &Config{
|
||||
Clusters: map[string]*Cluster{},
|
||||
Contexts: map[string]*Context{},
|
||||
path: path,
|
||||
}
|
||||
if os.IsNotExist(err) {
|
||||
@@ -34,30 +35,37 @@ func NewFromFile(path string) (*Config, error) {
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (c *Config) Path() string {
|
||||
return c.path
|
||||
}
|
||||
|
||||
func (c *Config) Read() error {
|
||||
_, err := toml.DecodeFile(c.path, c)
|
||||
data, err := os.ReadFile(c.path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read config file %q: %w", c.path, err)
|
||||
return fmt.Errorf("read config file '%s': %w", c.path, err)
|
||||
}
|
||||
if err = yaml.Unmarshal(data, c); err != nil {
|
||||
return fmt.Errorf("parse config file '%s': %s", c.path, yaml.FormatError(err, true, true))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Config) Save() error {
|
||||
dir, _ := filepath.Split(c.path)
|
||||
if err := os.MkdirAll(dir, 0700); err != nil {
|
||||
return fmt.Errorf("create config directory %q: %w", dir, err)
|
||||
return fmt.Errorf("create config directory '%s': %w", dir, err)
|
||||
}
|
||||
|
||||
f, err := os.OpenFile(c.path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
|
||||
if err != nil {
|
||||
return fmt.Errorf("write config file %q: %w", c.path, err)
|
||||
return fmt.Errorf("write config file '%s': %w", c.path, err)
|
||||
}
|
||||
|
||||
encoder := toml.NewEncoder(f)
|
||||
encoder.Indent = ""
|
||||
encoder := yaml.NewEncoder(f, yaml.Indent(2), yaml.IndentSequence(true))
|
||||
if err = encoder.Encode(c); err != nil {
|
||||
_ = f.Close()
|
||||
return fmt.Errorf("encode config file %q: %w", c.path, err)
|
||||
return fmt.Errorf("encode config file '%s': %w", c.path, err)
|
||||
}
|
||||
return f.Close()
|
||||
}
|
||||
|
||||
@@ -5,7 +5,8 @@ import (
|
||||
"net/netip"
|
||||
"strconv"
|
||||
"strings"
|
||||
"uncloud/internal/secret"
|
||||
|
||||
"github.com/psviderski/uncloud/internal/secret"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -14,11 +15,13 @@ const (
|
||||
)
|
||||
|
||||
type MachineConnection struct {
|
||||
SSH SSHDestination `toml:"ssh,omitempty"`
|
||||
TCP netip.AddrPort `toml:"tcp,omitempty"`
|
||||
Host string `toml:"host,omitempty"`
|
||||
PublicKey secret.Secret `toml:"public_key,omitempty"`
|
||||
SSHKeyFile string `toml:"ssh_key_file,omitempty"`
|
||||
SSH SSHDestination `yaml:"ssh,omitempty"`
|
||||
SSHKeyFile string `yaml:"ssh_key_file,omitempty"`
|
||||
// TCP is the address and port of the machine's API server.
|
||||
// The pointer is used to omit the field when not set. Otherwise, yaml marshalling includes an empty object.
|
||||
TCP *netip.AddrPort `yaml:"tcp,omitempty"`
|
||||
Host string `yaml:"host,omitempty"`
|
||||
PublicKey secret.Secret `yaml:"public_key,omitempty"`
|
||||
}
|
||||
|
||||
// SSHDestination represents an SSH destination string in the canonical form of "user@host:port".
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
package config
|
||||
|
||||
type Context struct {
|
||||
Name string `yaml:"-"`
|
||||
Connections []MachineConnection `yaml:"connections"`
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ExpandCommaSeparatedValues takes a slice of strings and expands any comma-separated values into individual elements.
|
||||
func ExpandCommaSeparatedValues(values []string) []string {
|
||||
if len(values) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var expanded []string
|
||||
for _, value := range values {
|
||||
for _, v := range strings.Split(value, ",") {
|
||||
if v = strings.TrimSpace(v); v != "" {
|
||||
expanded = append(expanded, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return expanded
|
||||
}
|
||||
+20
-16
@@ -4,7 +4,9 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"uncloud/internal/sshexec"
|
||||
"strings"
|
||||
|
||||
"github.com/psviderski/uncloud/internal/sshexec"
|
||||
)
|
||||
|
||||
// TODO: support pinning the script version to the CLI version.
|
||||
@@ -17,36 +19,38 @@ type RemoteMachine struct {
|
||||
KeyPath string
|
||||
}
|
||||
|
||||
func installCmd(user string) string {
|
||||
sudoPrefix := "sudo"
|
||||
func installCmd(user string, version string) string {
|
||||
sudoPrefix := ""
|
||||
var env []string
|
||||
|
||||
// Add the SSH user (non-root) to the uncloud group to allow access to the Uncloud daemon unix socket.
|
||||
env := "UNCLOUD_GROUP_ADD_USER=" + user
|
||||
|
||||
curlBashCmd := fmt.Sprintf(
|
||||
"curl -fsSL %s | %s %s bash", sshexec.Quote(installScriptURL), sudoPrefix, sshexec.Quote(env),
|
||||
)
|
||||
|
||||
if user == "root" {
|
||||
curlBashCmd = fmt.Sprintf(
|
||||
"curl -fsSL %s | bash", sshexec.Quote(installScriptURL),
|
||||
)
|
||||
if user != "root" {
|
||||
sudoPrefix = "sudo"
|
||||
env = append(env, "UNCLOUD_GROUP_ADD_USER="+sshexec.Quote(user))
|
||||
}
|
||||
if version != "" {
|
||||
env = append(env, "UNCLOUD_VERSION="+sshexec.Quote(version))
|
||||
}
|
||||
|
||||
envCmd := strings.Join(env, " ")
|
||||
curlBashCmd := fmt.Sprintf("curl -fsSL %s | %s %s bash", sshexec.Quote(installScriptURL), sudoPrefix, envCmd)
|
||||
|
||||
return curlBashCmd
|
||||
}
|
||||
|
||||
// provisionMachine provisions the remote machine by downloading the Uncloud install script from GitHub and running it.
|
||||
func provisionMachine(ctx context.Context, exec sshexec.Executor) error {
|
||||
// If version is specified, it will be passed to the install script as UNCLOUD_VERSION environment variable.
|
||||
func provisionMachine(ctx context.Context, exec sshexec.Executor, version string) error {
|
||||
user, err := exec.Run(ctx, "whoami")
|
||||
if err != nil {
|
||||
return fmt.Errorf("run whoami: %w", err)
|
||||
}
|
||||
|
||||
installCmd := installCmd(user)
|
||||
cmd := installCmd(user, version)
|
||||
|
||||
fmt.Println("Downloading Uncloud install script:", installScriptURL)
|
||||
|
||||
cmd := sshexec.QuoteCommand("bash", "-c", "set -o pipefail; "+installCmd)
|
||||
cmd = sshexec.QuoteCommand("bash", "-c", "set -o pipefail; "+cmd)
|
||||
if err = exec.Stream(ctx, cmd, os.Stdout, os.Stderr); err != nil {
|
||||
return fmt.Errorf("download and run install script: %w", err)
|
||||
}
|
||||
|
||||
@@ -8,12 +8,29 @@ import (
|
||||
|
||||
func TestInstallCmd(t *testing.T) {
|
||||
t.Run("root", func(t *testing.T) {
|
||||
cmd := installCmd("root")
|
||||
cmd := installCmd("root", "")
|
||||
assert.NotContains(t, cmd, "sudo")
|
||||
assert.NotContains(t, cmd, "UNCLOUD_GROUP_ADD_USER")
|
||||
})
|
||||
|
||||
// Test with version
|
||||
t.Run("root with version", func(t *testing.T) {
|
||||
cmd := installCmd("root", "v1.2.3")
|
||||
assert.NotContains(t, cmd, "sudo")
|
||||
assert.NotContains(t, cmd, "UNCLOUD_GROUP_ADD_USER")
|
||||
assert.Contains(t, cmd, "UNCLOUD_VERSION=v1.2.3")
|
||||
})
|
||||
|
||||
t.Run("nonroot", func(t *testing.T) {
|
||||
cmd := installCmd("nonroot")
|
||||
cmd := installCmd("nonroot", "")
|
||||
assert.Contains(t, cmd, "sudo")
|
||||
assert.Contains(t, cmd, "UNCLOUD_GROUP_ADD_USER=nonroot")
|
||||
})
|
||||
|
||||
t.Run("nonroot with version", func(t *testing.T) {
|
||||
cmd := installCmd("nonroot", "v1.2.3")
|
||||
assert.Contains(t, cmd, "sudo")
|
||||
assert.Contains(t, cmd, "UNCLOUD_GROUP_ADD_USER=nonroot")
|
||||
assert.Contains(t, cmd, "UNCLOUD_VERSION=v1.2.3")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package cli
|
||||
|
||||
import "github.com/charmbracelet/huh"
|
||||
|
||||
func Confirm() (bool, error) {
|
||||
var confirmed bool
|
||||
form := huh.NewForm(
|
||||
huh.NewGroup(
|
||||
huh.NewConfirm().
|
||||
Title(
|
||||
"Do you want to continue?",
|
||||
).
|
||||
Affirmative("Yes!").
|
||||
Negative("No").
|
||||
Value(&confirmed),
|
||||
),
|
||||
).WithAccessible(true)
|
||||
if err := form.Run(); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return confirmed, nil
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"fmt"
|
||||
systemd "github.com/coreos/go-systemd/daemon"
|
||||
"log/slog"
|
||||
"uncloud/internal/machine"
|
||||
"github.com/psviderski/uncloud/internal/machine"
|
||||
)
|
||||
|
||||
type Daemon struct {
|
||||
|
||||
@@ -3,10 +3,10 @@ package daemon
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/psviderski/uncloud/internal/machine"
|
||||
"github.com/psviderski/uncloud/internal/machine/network"
|
||||
"net/netip"
|
||||
"os"
|
||||
"uncloud/internal/machine"
|
||||
"uncloud/internal/machine/network"
|
||||
)
|
||||
|
||||
// MachineToken returns the local machine's token that can be used for adding the machine to a cluster.
|
||||
|
||||
+10
-2
@@ -45,11 +45,19 @@ func (h *SlogTextHandler) Enabled(ctx context.Context, level slog.Level) bool {
|
||||
}
|
||||
|
||||
func (h *SlogTextHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
|
||||
return h.TextHandler.WithAttrs(attrs)
|
||||
return &SlogTextHandler{
|
||||
TextHandler: h.TextHandler.WithAttrs(attrs).(*slog.TextHandler),
|
||||
opts: h.opts,
|
||||
w: h.w,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *SlogTextHandler) WithGroup(name string) slog.Handler {
|
||||
return h.TextHandler.WithGroup(name)
|
||||
return &SlogTextHandler{
|
||||
TextHandler: h.TextHandler.WithGroup(name).(*slog.TextHandler),
|
||||
opts: h.opts,
|
||||
w: h.w,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *SlogTextHandler) Handle(ctx context.Context, r slog.Record) error {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -14,23 +14,37 @@ service Docker {
|
||||
rpc StopContainer(StopContainerRequest) returns (google.protobuf.Empty);
|
||||
rpc ListContainers(ListContainersRequest) returns (ListContainersResponse);
|
||||
rpc RemoveContainer(RemoveContainerRequest) returns (google.protobuf.Empty);
|
||||
|
||||
rpc PullImage(PullImageRequest) returns (stream JSONMessage);
|
||||
rpc InspectImage(InspectImageRequest) returns (InspectImageResponse);
|
||||
// InspectRemoteImage returns the image metadata for an image in a remote registry using the machine's
|
||||
// Docker auth credentials if necessary.
|
||||
rpc InspectRemoteImage(InspectRemoteImageRequest) returns (InspectRemoteImageResponse);
|
||||
|
||||
rpc CreateVolume(CreateVolumeRequest) returns (CreateVolumeResponse);
|
||||
rpc ListVolumes(ListVolumesRequest) returns (ListVolumesResponse);
|
||||
rpc RemoveVolume(RemoveVolumeRequest) returns (google.protobuf.Empty);
|
||||
|
||||
rpc CreateServiceContainer(CreateServiceContainerRequest) returns (CreateContainerResponse);
|
||||
rpc InspectServiceContainer(InspectContainerRequest) returns (ServiceContainer);
|
||||
rpc ListServiceContainers(ListServiceContainersRequest) returns (ListServiceContainersResponse);
|
||||
rpc RemoveServiceContainer(RemoveContainerRequest) returns (google.protobuf.Empty);
|
||||
}
|
||||
|
||||
message CreateContainerRequest {
|
||||
// JSON serialized container.Config.
|
||||
// JSON serialised container.Config.
|
||||
bytes config = 1;
|
||||
// JSON serialized container.HostConfig.
|
||||
// JSON serialised container.HostConfig.
|
||||
bytes host_config = 2;
|
||||
// JSON serialized network.NetworkingConfig.
|
||||
// JSON serialised network.NetworkingConfig.
|
||||
bytes network_config = 3;
|
||||
// JSON serialized ocispec.Platform.
|
||||
// JSON serialised ocispec.Platform.
|
||||
bytes platform = 4;
|
||||
string name = 5;
|
||||
}
|
||||
|
||||
message CreateContainerResponse {
|
||||
// JSON serialized container.CreateResponse.
|
||||
// JSON serialised container.CreateResponse.
|
||||
bytes response = 1;
|
||||
}
|
||||
|
||||
@@ -39,51 +53,145 @@ message InspectContainerRequest {
|
||||
}
|
||||
|
||||
message InspectContainerResponse {
|
||||
// JSON serialized container.InspectResponse.
|
||||
// JSON serialised container.InspectResponse.
|
||||
bytes response = 1;
|
||||
}
|
||||
|
||||
message StartContainerRequest {
|
||||
string id = 1;
|
||||
// JSON serialized container.StartOptions.
|
||||
// JSON serialised container.StartOptions.
|
||||
bytes options = 2;
|
||||
}
|
||||
|
||||
message StopContainerRequest {
|
||||
string id = 1;
|
||||
// JSON serialized container.StopOptions.
|
||||
// JSON serialised container.StopOptions.
|
||||
bytes options = 2;
|
||||
}
|
||||
|
||||
message ListContainersRequest {
|
||||
// JSON serialized container.ListOptions.
|
||||
// JSON serialised container.ListOptions.
|
||||
bytes options = 1;
|
||||
}
|
||||
|
||||
// ListContainersResponse structure allows broadcasting ListContainers requests to multiple machines.
|
||||
message ListContainersResponse {
|
||||
// Must contain only one repeated messages field to allow broadcasting ListContainers requests to multiple machines.
|
||||
repeated MachineContainers messages = 1;
|
||||
}
|
||||
|
||||
message MachineContainers {
|
||||
Metadata metadata = 1;
|
||||
// JSON serialized []container.ContainerJSON.
|
||||
// JSON serialised []container.InspectResponse.
|
||||
bytes containers = 2;
|
||||
}
|
||||
|
||||
message RemoveContainerRequest {
|
||||
string id = 1;
|
||||
// JSON serialized container.RemoveOptions.
|
||||
// JSON serialised container.RemoveOptions.
|
||||
bytes options = 2;
|
||||
}
|
||||
|
||||
message PullImageRequest {
|
||||
string image = 1;
|
||||
// JSON serialized image.PullOptions.
|
||||
// JSON serialised image.PullOptions.
|
||||
bytes options = 2;
|
||||
}
|
||||
|
||||
message JSONMessage {
|
||||
// JSON serialized jsonmessage.JSONMessage.
|
||||
// JSON serialised jsonmessage.JSONMessage.
|
||||
bytes message = 1;
|
||||
}
|
||||
|
||||
message InspectImageRequest {
|
||||
string id = 1;
|
||||
}
|
||||
|
||||
message InspectImageResponse {
|
||||
// Must contain only one repeated messages field to allow broadcasting InspectImage requests to multiple machines.
|
||||
repeated Image messages = 1;
|
||||
}
|
||||
|
||||
message Image {
|
||||
Metadata metadata = 1;
|
||||
// JSON serialised types.ImageInspect.
|
||||
bytes image = 2;
|
||||
}
|
||||
|
||||
message InspectRemoteImageRequest {
|
||||
string id = 1;
|
||||
}
|
||||
|
||||
message InspectRemoteImageResponse {
|
||||
// Must contain only one repeated messages field to allow broadcasting InspectRemoteImage requests to multiple machines.
|
||||
repeated RemoteImage messages = 1;
|
||||
}
|
||||
|
||||
message RemoteImage {
|
||||
Metadata metadata = 1;
|
||||
// Image reference in the canonical form with the digest.
|
||||
string reference = 2;
|
||||
// Raw JSON manifest from the registry.
|
||||
bytes manifest = 3;
|
||||
}
|
||||
|
||||
message CreateVolumeRequest {
|
||||
// JSON serialised volume.CreateOptions.
|
||||
bytes options = 1;
|
||||
}
|
||||
|
||||
message CreateVolumeResponse {
|
||||
// JSON serialised volume.Volume.
|
||||
bytes volume = 1;
|
||||
}
|
||||
|
||||
message ListVolumesRequest {
|
||||
// JSON serialised volume.ListOptions.
|
||||
bytes options = 1;
|
||||
}
|
||||
|
||||
message ListVolumesResponse {
|
||||
// Must contain only one repeated messages field to allow broadcasting ListVolumes requests to multiple machines.
|
||||
repeated MachineVolumes messages = 1;
|
||||
}
|
||||
|
||||
message MachineVolumes {
|
||||
Metadata metadata = 1;
|
||||
// JSON serialised volume.ListResponse.
|
||||
bytes response = 2;
|
||||
}
|
||||
|
||||
message RemoveVolumeRequest {
|
||||
string id = 1;
|
||||
bool force = 2;
|
||||
}
|
||||
|
||||
message CreateServiceContainerRequest {
|
||||
string service_id = 1;
|
||||
// JSON serialised api.ServiceSpec.
|
||||
bytes service_spec = 2;
|
||||
string container_name = 3;
|
||||
}
|
||||
|
||||
message ServiceContainer {
|
||||
// JSON serialised container.InspectResponse.
|
||||
bytes container = 1;
|
||||
// JSON serialised api.ServiceSpec.
|
||||
bytes service_spec = 2;
|
||||
}
|
||||
|
||||
message ListServiceContainersRequest {
|
||||
string service_id = 1;
|
||||
// JSON serialised container.ListOptions.
|
||||
bytes options = 2;
|
||||
}
|
||||
|
||||
message ListServiceContainersResponse {
|
||||
// Must contain only one repeated messages field to allow broadcasting ListServiceContainers requests
|
||||
// to multiple machines.
|
||||
repeated MachineServiceContainers messages = 1;
|
||||
}
|
||||
|
||||
message MachineServiceContainers {
|
||||
Metadata metadata = 1;
|
||||
repeated ServiceContainer containers = 2;
|
||||
}
|
||||
|
||||
@@ -20,13 +20,22 @@ import (
|
||||
const _ = grpc.SupportPackageIsVersion9
|
||||
|
||||
const (
|
||||
Docker_CreateContainer_FullMethodName = "/api.Docker/CreateContainer"
|
||||
Docker_InspectContainer_FullMethodName = "/api.Docker/InspectContainer"
|
||||
Docker_StartContainer_FullMethodName = "/api.Docker/StartContainer"
|
||||
Docker_StopContainer_FullMethodName = "/api.Docker/StopContainer"
|
||||
Docker_ListContainers_FullMethodName = "/api.Docker/ListContainers"
|
||||
Docker_RemoveContainer_FullMethodName = "/api.Docker/RemoveContainer"
|
||||
Docker_PullImage_FullMethodName = "/api.Docker/PullImage"
|
||||
Docker_CreateContainer_FullMethodName = "/api.Docker/CreateContainer"
|
||||
Docker_InspectContainer_FullMethodName = "/api.Docker/InspectContainer"
|
||||
Docker_StartContainer_FullMethodName = "/api.Docker/StartContainer"
|
||||
Docker_StopContainer_FullMethodName = "/api.Docker/StopContainer"
|
||||
Docker_ListContainers_FullMethodName = "/api.Docker/ListContainers"
|
||||
Docker_RemoveContainer_FullMethodName = "/api.Docker/RemoveContainer"
|
||||
Docker_PullImage_FullMethodName = "/api.Docker/PullImage"
|
||||
Docker_InspectImage_FullMethodName = "/api.Docker/InspectImage"
|
||||
Docker_InspectRemoteImage_FullMethodName = "/api.Docker/InspectRemoteImage"
|
||||
Docker_CreateVolume_FullMethodName = "/api.Docker/CreateVolume"
|
||||
Docker_ListVolumes_FullMethodName = "/api.Docker/ListVolumes"
|
||||
Docker_RemoveVolume_FullMethodName = "/api.Docker/RemoveVolume"
|
||||
Docker_CreateServiceContainer_FullMethodName = "/api.Docker/CreateServiceContainer"
|
||||
Docker_InspectServiceContainer_FullMethodName = "/api.Docker/InspectServiceContainer"
|
||||
Docker_ListServiceContainers_FullMethodName = "/api.Docker/ListServiceContainers"
|
||||
Docker_RemoveServiceContainer_FullMethodName = "/api.Docker/RemoveServiceContainer"
|
||||
)
|
||||
|
||||
// DockerClient is the client API for Docker service.
|
||||
@@ -40,6 +49,17 @@ type DockerClient interface {
|
||||
ListContainers(ctx context.Context, in *ListContainersRequest, opts ...grpc.CallOption) (*ListContainersResponse, error)
|
||||
RemoveContainer(ctx context.Context, in *RemoveContainerRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
|
||||
PullImage(ctx context.Context, in *PullImageRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[JSONMessage], error)
|
||||
InspectImage(ctx context.Context, in *InspectImageRequest, opts ...grpc.CallOption) (*InspectImageResponse, error)
|
||||
// InspectRemoteImage returns the image metadata for an image in a remote registry using the machine's
|
||||
// Docker auth credentials if necessary.
|
||||
InspectRemoteImage(ctx context.Context, in *InspectRemoteImageRequest, opts ...grpc.CallOption) (*InspectRemoteImageResponse, error)
|
||||
CreateVolume(ctx context.Context, in *CreateVolumeRequest, opts ...grpc.CallOption) (*CreateVolumeResponse, error)
|
||||
ListVolumes(ctx context.Context, in *ListVolumesRequest, opts ...grpc.CallOption) (*ListVolumesResponse, error)
|
||||
RemoveVolume(ctx context.Context, in *RemoveVolumeRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
|
||||
CreateServiceContainer(ctx context.Context, in *CreateServiceContainerRequest, opts ...grpc.CallOption) (*CreateContainerResponse, error)
|
||||
InspectServiceContainer(ctx context.Context, in *InspectContainerRequest, opts ...grpc.CallOption) (*ServiceContainer, error)
|
||||
ListServiceContainers(ctx context.Context, in *ListServiceContainersRequest, opts ...grpc.CallOption) (*ListServiceContainersResponse, error)
|
||||
RemoveServiceContainer(ctx context.Context, in *RemoveContainerRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
|
||||
}
|
||||
|
||||
type dockerClient struct {
|
||||
@@ -129,6 +149,96 @@ func (c *dockerClient) PullImage(ctx context.Context, in *PullImageRequest, opts
|
||||
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
|
||||
type Docker_PullImageClient = grpc.ServerStreamingClient[JSONMessage]
|
||||
|
||||
func (c *dockerClient) InspectImage(ctx context.Context, in *InspectImageRequest, opts ...grpc.CallOption) (*InspectImageResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(InspectImageResponse)
|
||||
err := c.cc.Invoke(ctx, Docker_InspectImage_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *dockerClient) InspectRemoteImage(ctx context.Context, in *InspectRemoteImageRequest, opts ...grpc.CallOption) (*InspectRemoteImageResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(InspectRemoteImageResponse)
|
||||
err := c.cc.Invoke(ctx, Docker_InspectRemoteImage_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *dockerClient) CreateVolume(ctx context.Context, in *CreateVolumeRequest, opts ...grpc.CallOption) (*CreateVolumeResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(CreateVolumeResponse)
|
||||
err := c.cc.Invoke(ctx, Docker_CreateVolume_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *dockerClient) ListVolumes(ctx context.Context, in *ListVolumesRequest, opts ...grpc.CallOption) (*ListVolumesResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(ListVolumesResponse)
|
||||
err := c.cc.Invoke(ctx, Docker_ListVolumes_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *dockerClient) RemoveVolume(ctx context.Context, in *RemoveVolumeRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(emptypb.Empty)
|
||||
err := c.cc.Invoke(ctx, Docker_RemoveVolume_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *dockerClient) CreateServiceContainer(ctx context.Context, in *CreateServiceContainerRequest, opts ...grpc.CallOption) (*CreateContainerResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(CreateContainerResponse)
|
||||
err := c.cc.Invoke(ctx, Docker_CreateServiceContainer_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *dockerClient) InspectServiceContainer(ctx context.Context, in *InspectContainerRequest, opts ...grpc.CallOption) (*ServiceContainer, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(ServiceContainer)
|
||||
err := c.cc.Invoke(ctx, Docker_InspectServiceContainer_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *dockerClient) ListServiceContainers(ctx context.Context, in *ListServiceContainersRequest, opts ...grpc.CallOption) (*ListServiceContainersResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(ListServiceContainersResponse)
|
||||
err := c.cc.Invoke(ctx, Docker_ListServiceContainers_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *dockerClient) RemoveServiceContainer(ctx context.Context, in *RemoveContainerRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(emptypb.Empty)
|
||||
err := c.cc.Invoke(ctx, Docker_RemoveServiceContainer_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// DockerServer is the server API for Docker service.
|
||||
// All implementations must embed UnimplementedDockerServer
|
||||
// for forward compatibility.
|
||||
@@ -140,6 +250,17 @@ type DockerServer interface {
|
||||
ListContainers(context.Context, *ListContainersRequest) (*ListContainersResponse, error)
|
||||
RemoveContainer(context.Context, *RemoveContainerRequest) (*emptypb.Empty, error)
|
||||
PullImage(*PullImageRequest, grpc.ServerStreamingServer[JSONMessage]) error
|
||||
InspectImage(context.Context, *InspectImageRequest) (*InspectImageResponse, error)
|
||||
// InspectRemoteImage returns the image metadata for an image in a remote registry using the machine's
|
||||
// Docker auth credentials if necessary.
|
||||
InspectRemoteImage(context.Context, *InspectRemoteImageRequest) (*InspectRemoteImageResponse, error)
|
||||
CreateVolume(context.Context, *CreateVolumeRequest) (*CreateVolumeResponse, error)
|
||||
ListVolumes(context.Context, *ListVolumesRequest) (*ListVolumesResponse, error)
|
||||
RemoveVolume(context.Context, *RemoveVolumeRequest) (*emptypb.Empty, error)
|
||||
CreateServiceContainer(context.Context, *CreateServiceContainerRequest) (*CreateContainerResponse, error)
|
||||
InspectServiceContainer(context.Context, *InspectContainerRequest) (*ServiceContainer, error)
|
||||
ListServiceContainers(context.Context, *ListServiceContainersRequest) (*ListServiceContainersResponse, error)
|
||||
RemoveServiceContainer(context.Context, *RemoveContainerRequest) (*emptypb.Empty, error)
|
||||
mustEmbedUnimplementedDockerServer()
|
||||
}
|
||||
|
||||
@@ -171,6 +292,33 @@ func (UnimplementedDockerServer) RemoveContainer(context.Context, *RemoveContain
|
||||
func (UnimplementedDockerServer) PullImage(*PullImageRequest, grpc.ServerStreamingServer[JSONMessage]) error {
|
||||
return status.Errorf(codes.Unimplemented, "method PullImage not implemented")
|
||||
}
|
||||
func (UnimplementedDockerServer) InspectImage(context.Context, *InspectImageRequest) (*InspectImageResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method InspectImage not implemented")
|
||||
}
|
||||
func (UnimplementedDockerServer) InspectRemoteImage(context.Context, *InspectRemoteImageRequest) (*InspectRemoteImageResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method InspectRemoteImage not implemented")
|
||||
}
|
||||
func (UnimplementedDockerServer) CreateVolume(context.Context, *CreateVolumeRequest) (*CreateVolumeResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method CreateVolume not implemented")
|
||||
}
|
||||
func (UnimplementedDockerServer) ListVolumes(context.Context, *ListVolumesRequest) (*ListVolumesResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListVolumes not implemented")
|
||||
}
|
||||
func (UnimplementedDockerServer) RemoveVolume(context.Context, *RemoveVolumeRequest) (*emptypb.Empty, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method RemoveVolume not implemented")
|
||||
}
|
||||
func (UnimplementedDockerServer) CreateServiceContainer(context.Context, *CreateServiceContainerRequest) (*CreateContainerResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method CreateServiceContainer not implemented")
|
||||
}
|
||||
func (UnimplementedDockerServer) InspectServiceContainer(context.Context, *InspectContainerRequest) (*ServiceContainer, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method InspectServiceContainer not implemented")
|
||||
}
|
||||
func (UnimplementedDockerServer) ListServiceContainers(context.Context, *ListServiceContainersRequest) (*ListServiceContainersResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListServiceContainers not implemented")
|
||||
}
|
||||
func (UnimplementedDockerServer) RemoveServiceContainer(context.Context, *RemoveContainerRequest) (*emptypb.Empty, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method RemoveServiceContainer not implemented")
|
||||
}
|
||||
func (UnimplementedDockerServer) mustEmbedUnimplementedDockerServer() {}
|
||||
func (UnimplementedDockerServer) testEmbeddedByValue() {}
|
||||
|
||||
@@ -311,6 +459,168 @@ func _Docker_PullImage_Handler(srv interface{}, stream grpc.ServerStream) error
|
||||
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
|
||||
type Docker_PullImageServer = grpc.ServerStreamingServer[JSONMessage]
|
||||
|
||||
func _Docker_InspectImage_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(InspectImageRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(DockerServer).InspectImage(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: Docker_InspectImage_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(DockerServer).InspectImage(ctx, req.(*InspectImageRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _Docker_InspectRemoteImage_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(InspectRemoteImageRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(DockerServer).InspectRemoteImage(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: Docker_InspectRemoteImage_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(DockerServer).InspectRemoteImage(ctx, req.(*InspectRemoteImageRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _Docker_CreateVolume_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(CreateVolumeRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(DockerServer).CreateVolume(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: Docker_CreateVolume_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(DockerServer).CreateVolume(ctx, req.(*CreateVolumeRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _Docker_ListVolumes_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(ListVolumesRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(DockerServer).ListVolumes(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: Docker_ListVolumes_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(DockerServer).ListVolumes(ctx, req.(*ListVolumesRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _Docker_RemoveVolume_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(RemoveVolumeRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(DockerServer).RemoveVolume(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: Docker_RemoveVolume_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(DockerServer).RemoveVolume(ctx, req.(*RemoveVolumeRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _Docker_CreateServiceContainer_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(CreateServiceContainerRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(DockerServer).CreateServiceContainer(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: Docker_CreateServiceContainer_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(DockerServer).CreateServiceContainer(ctx, req.(*CreateServiceContainerRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _Docker_InspectServiceContainer_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(InspectContainerRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(DockerServer).InspectServiceContainer(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: Docker_InspectServiceContainer_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(DockerServer).InspectServiceContainer(ctx, req.(*InspectContainerRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _Docker_ListServiceContainers_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(ListServiceContainersRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(DockerServer).ListServiceContainers(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: Docker_ListServiceContainers_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(DockerServer).ListServiceContainers(ctx, req.(*ListServiceContainersRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _Docker_RemoveServiceContainer_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(RemoveContainerRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(DockerServer).RemoveServiceContainer(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: Docker_RemoveServiceContainer_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(DockerServer).RemoveServiceContainer(ctx, req.(*RemoveContainerRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
// Docker_ServiceDesc is the grpc.ServiceDesc for Docker service.
|
||||
// It's only intended for direct use with grpc.RegisterService,
|
||||
// and not to be introspected or modified (even as a copy)
|
||||
@@ -342,6 +652,42 @@ var Docker_ServiceDesc = grpc.ServiceDesc{
|
||||
MethodName: "RemoveContainer",
|
||||
Handler: _Docker_RemoveContainer_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "InspectImage",
|
||||
Handler: _Docker_InspectImage_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "InspectRemoteImage",
|
||||
Handler: _Docker_InspectRemoteImage_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "CreateVolume",
|
||||
Handler: _Docker_CreateVolume_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "ListVolumes",
|
||||
Handler: _Docker_ListVolumes_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "RemoveVolume",
|
||||
Handler: _Docker_RemoveVolume_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "CreateServiceContainer",
|
||||
Handler: _Docker_CreateServiceContainer_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "InspectServiceContainer",
|
||||
Handler: _Docker_InspectServiceContainer_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "ListServiceContainers",
|
||||
Handler: _Docker_ListServiceContainers_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "RemoveServiceContainer",
|
||||
Handler: _Docker_RemoveServiceContainer_Handler,
|
||||
},
|
||||
},
|
||||
Streams: []grpc.StreamDesc{
|
||||
{
|
||||
|
||||
@@ -163,6 +163,63 @@ func (x *NetworkConfig) GetPublicKey() []byte {
|
||||
return nil
|
||||
}
|
||||
|
||||
type CheckPrerequisitesResponse struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
// Overall status of the checks.
|
||||
Satisfied bool `protobuf:"varint,1,opt,name=satisfied,proto3" json:"satisfied,omitempty"`
|
||||
// Error message if not satisfied (empty if all checks pass).
|
||||
Error string `protobuf:"bytes,2,opt,name=error,proto3" json:"error,omitempty"`
|
||||
}
|
||||
|
||||
func (x *CheckPrerequisitesResponse) Reset() {
|
||||
*x = CheckPrerequisitesResponse{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[2]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *CheckPrerequisitesResponse) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*CheckPrerequisitesResponse) ProtoMessage() {}
|
||||
|
||||
func (x *CheckPrerequisitesResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[2]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use CheckPrerequisitesResponse.ProtoReflect.Descriptor instead.
|
||||
func (*CheckPrerequisitesResponse) Descriptor() ([]byte, []int) {
|
||||
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{2}
|
||||
}
|
||||
|
||||
func (x *CheckPrerequisitesResponse) GetSatisfied() bool {
|
||||
if x != nil {
|
||||
return x.Satisfied
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (x *CheckPrerequisitesResponse) GetError() string {
|
||||
if x != nil {
|
||||
return x.Error
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type InitClusterRequest struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
@@ -180,7 +237,7 @@ type InitClusterRequest struct {
|
||||
func (x *InitClusterRequest) Reset() {
|
||||
*x = InitClusterRequest{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[2]
|
||||
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[3]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@@ -193,7 +250,7 @@ func (x *InitClusterRequest) String() string {
|
||||
func (*InitClusterRequest) ProtoMessage() {}
|
||||
|
||||
func (x *InitClusterRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[2]
|
||||
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[3]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@@ -206,7 +263,7 @@ func (x *InitClusterRequest) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use InitClusterRequest.ProtoReflect.Descriptor instead.
|
||||
func (*InitClusterRequest) Descriptor() ([]byte, []int) {
|
||||
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{2}
|
||||
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{3}
|
||||
}
|
||||
|
||||
func (x *InitClusterRequest) GetMachineName() string {
|
||||
@@ -271,7 +328,7 @@ type InitClusterResponse struct {
|
||||
func (x *InitClusterResponse) Reset() {
|
||||
*x = InitClusterResponse{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[3]
|
||||
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[4]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@@ -284,7 +341,7 @@ func (x *InitClusterResponse) String() string {
|
||||
func (*InitClusterResponse) ProtoMessage() {}
|
||||
|
||||
func (x *InitClusterResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[3]
|
||||
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[4]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@@ -297,7 +354,7 @@ func (x *InitClusterResponse) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use InitClusterResponse.ProtoReflect.Descriptor instead.
|
||||
func (*InitClusterResponse) Descriptor() ([]byte, []int) {
|
||||
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{3}
|
||||
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{4}
|
||||
}
|
||||
|
||||
func (x *InitClusterResponse) GetMachine() *MachineInfo {
|
||||
@@ -319,7 +376,7 @@ type JoinClusterRequest struct {
|
||||
func (x *JoinClusterRequest) Reset() {
|
||||
*x = JoinClusterRequest{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[4]
|
||||
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[5]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@@ -332,7 +389,7 @@ func (x *JoinClusterRequest) String() string {
|
||||
func (*JoinClusterRequest) ProtoMessage() {}
|
||||
|
||||
func (x *JoinClusterRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[4]
|
||||
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[5]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@@ -345,7 +402,7 @@ func (x *JoinClusterRequest) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use JoinClusterRequest.ProtoReflect.Descriptor instead.
|
||||
func (*JoinClusterRequest) Descriptor() ([]byte, []int) {
|
||||
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{4}
|
||||
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{5}
|
||||
}
|
||||
|
||||
func (x *JoinClusterRequest) GetMachine() *MachineInfo {
|
||||
@@ -373,7 +430,7 @@ type TokenResponse struct {
|
||||
func (x *TokenResponse) Reset() {
|
||||
*x = TokenResponse{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[5]
|
||||
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[6]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@@ -386,7 +443,7 @@ func (x *TokenResponse) String() string {
|
||||
func (*TokenResponse) ProtoMessage() {}
|
||||
|
||||
func (x *TokenResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[5]
|
||||
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 {
|
||||
@@ -399,7 +456,7 @@ func (x *TokenResponse) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use TokenResponse.ProtoReflect.Descriptor instead.
|
||||
func (*TokenResponse) Descriptor() ([]byte, []int) {
|
||||
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{5}
|
||||
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{6}
|
||||
}
|
||||
|
||||
func (x *TokenResponse) GetToken() string {
|
||||
@@ -423,7 +480,7 @@ type Service struct {
|
||||
func (x *Service) Reset() {
|
||||
*x = Service{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[6]
|
||||
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[7]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@@ -436,7 +493,7 @@ func (x *Service) String() string {
|
||||
func (*Service) ProtoMessage() {}
|
||||
|
||||
func (x *Service) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[6]
|
||||
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 {
|
||||
@@ -449,7 +506,7 @@ func (x *Service) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use Service.ProtoReflect.Descriptor instead.
|
||||
func (*Service) Descriptor() ([]byte, []int) {
|
||||
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{6}
|
||||
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{7}
|
||||
}
|
||||
|
||||
func (x *Service) GetId() string {
|
||||
@@ -491,7 +548,7 @@ type InspectServiceRequest struct {
|
||||
func (x *InspectServiceRequest) Reset() {
|
||||
*x = InspectServiceRequest{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[7]
|
||||
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[8]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@@ -504,7 +561,7 @@ func (x *InspectServiceRequest) String() string {
|
||||
func (*InspectServiceRequest) ProtoMessage() {}
|
||||
|
||||
func (x *InspectServiceRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[7]
|
||||
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[8]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@@ -517,7 +574,7 @@ func (x *InspectServiceRequest) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use InspectServiceRequest.ProtoReflect.Descriptor instead.
|
||||
func (*InspectServiceRequest) Descriptor() ([]byte, []int) {
|
||||
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{7}
|
||||
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{8}
|
||||
}
|
||||
|
||||
func (x *InspectServiceRequest) GetId() string {
|
||||
@@ -538,7 +595,7 @@ type InspectServiceResponse struct {
|
||||
func (x *InspectServiceResponse) Reset() {
|
||||
*x = InspectServiceResponse{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[8]
|
||||
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[9]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@@ -551,7 +608,7 @@ func (x *InspectServiceResponse) String() string {
|
||||
func (*InspectServiceResponse) ProtoMessage() {}
|
||||
|
||||
func (x *InspectServiceResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[8]
|
||||
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[9]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@@ -564,7 +621,7 @@ func (x *InspectServiceResponse) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use InspectServiceResponse.ProtoReflect.Descriptor instead.
|
||||
func (*InspectServiceResponse) Descriptor() ([]byte, []int) {
|
||||
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{8}
|
||||
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{9}
|
||||
}
|
||||
|
||||
func (x *InspectServiceResponse) GetService() *Service {
|
||||
@@ -587,7 +644,7 @@ type Service_Container struct {
|
||||
func (x *Service_Container) Reset() {
|
||||
*x = Service_Container{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[9]
|
||||
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[10]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@@ -600,7 +657,7 @@ func (x *Service_Container) String() string {
|
||||
func (*Service_Container) ProtoMessage() {}
|
||||
|
||||
func (x *Service_Container) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[9]
|
||||
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[10]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@@ -613,7 +670,7 @@ func (x *Service_Container) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use Service_Container.ProtoReflect.Descriptor instead.
|
||||
func (*Service_Container) Descriptor() ([]byte, []int) {
|
||||
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{6, 0}
|
||||
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{7, 0}
|
||||
}
|
||||
|
||||
func (x *Service_Container) GetMachineId() string {
|
||||
@@ -659,77 +716,87 @@ var file_internal_machine_api_pb_machine_proto_rawDesc = []byte{
|
||||
0x0b, 0x32, 0x0b, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x50, 0x50, 0x6f, 0x72, 0x74, 0x52, 0x09,
|
||||
0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x70, 0x75, 0x62,
|
||||
0x6c, 0x69, 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x70,
|
||||
0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x22, 0xc3, 0x01, 0x0a, 0x12, 0x49, 0x6e, 0x69,
|
||||
0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12,
|
||||
0x20, 0x0a, 0x0b, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x01,
|
||||
0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x4e, 0x61, 0x6d,
|
||||
0x65, 0x12, 0x27, 0x0a, 0x07, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x18, 0x02, 0x20, 0x01,
|
||||
0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x50, 0x50, 0x72, 0x65, 0x66, 0x69,
|
||||
0x78, 0x52, 0x07, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x12, 0x26, 0x0a, 0x09, 0x70, 0x75,
|
||||
0x62, 0x6c, 0x69, 0x63, 0x5f, 0x69, 0x70, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x07, 0x2e,
|
||||
0x61, 0x70, 0x69, 0x2e, 0x49, 0x50, 0x48, 0x00, 0x52, 0x08, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63,
|
||||
0x49, 0x70, 0x12, 0x26, 0x0a, 0x0e, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x69, 0x70, 0x5f,
|
||||
0x61, 0x75, 0x74, 0x6f, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x48, 0x00, 0x52, 0x0c, 0x70, 0x75,
|
||||
0x62, 0x6c, 0x69, 0x63, 0x49, 0x70, 0x41, 0x75, 0x74, 0x6f, 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, 0x79, 0x0a, 0x12, 0x4a, 0x6f, 0x69, 0x6e, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72,
|
||||
0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2a, 0x0a, 0x07, 0x6d, 0x61, 0x63, 0x68, 0x69,
|
||||
0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x22, 0x50, 0x0a, 0x1a, 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, 0x1c, 0x0a, 0x09, 0x73, 0x61, 0x74, 0x69, 0x73, 0x66,
|
||||
0x69, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x73, 0x61, 0x74, 0x69, 0x73,
|
||||
0x66, 0x69, 0x65, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x02, 0x20,
|
||||
0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x22, 0xc3, 0x01, 0x0a, 0x12, 0x49,
|
||||
0x6e, 0x69, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
|
||||
0x74, 0x12, 0x20, 0x0a, 0x0b, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x4e, 0x61, 0x6d, 0x65,
|
||||
0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x4e,
|
||||
0x61, 0x6d, 0x65, 0x12, 0x27, 0x0a, 0x07, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x18, 0x02,
|
||||
0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x50, 0x50, 0x72, 0x65,
|
||||
0x66, 0x69, 0x78, 0x52, 0x07, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x12, 0x26, 0x0a, 0x09,
|
||||
0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x69, 0x70, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32,
|
||||
0x07, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x50, 0x48, 0x00, 0x52, 0x08, 0x70, 0x75, 0x62, 0x6c,
|
||||
0x69, 0x63, 0x49, 0x70, 0x12, 0x26, 0x0a, 0x0e, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x69,
|
||||
0x70, 0x5f, 0x61, 0x75, 0x74, 0x6f, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x48, 0x00, 0x52, 0x0c,
|
||||
0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x49, 0x70, 0x41, 0x75, 0x74, 0x6f, 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, 0x12, 0x37, 0x0a, 0x0e, 0x6f, 0x74, 0x68, 0x65, 0x72, 0x5f, 0x6d, 0x61, 0x63,
|
||||
0x68, 0x69, 0x6e, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x61, 0x70,
|
||||
0x69, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x0d, 0x6f,
|
||||
0x74, 0x68, 0x65, 0x72, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x22, 0x25, 0x0a, 0x0d,
|
||||
0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x14, 0x0a,
|
||||
0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x6f,
|
||||
0x6b, 0x65, 0x6e, 0x22, 0xc3, 0x01, 0x0a, 0x07, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12,
|
||||
0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12,
|
||||
0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e,
|
||||
0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28,
|
||||
0x09, 0x52, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x12, 0x36, 0x0a, 0x0a, 0x63, 0x6f, 0x6e, 0x74, 0x61,
|
||||
0x69, 0x6e, 0x65, 0x72, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x61, 0x70,
|
||||
0x69, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69,
|
||||
0x6e, 0x65, 0x72, 0x52, 0x0a, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x1a,
|
||||
0x48, 0x0a, 0x09, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x1d, 0x0a, 0x0a,
|
||||
0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
|
||||
0x52, 0x09, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x64, 0x12, 0x1c, 0x0a, 0x09, 0x63,
|
||||
0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09,
|
||||
0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x22, 0x27, 0x0a, 0x15, 0x49, 0x6e, 0x73,
|
||||
0x70, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65,
|
||||
0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02,
|
||||
0x69, 0x64, 0x22, 0x40, 0x0a, 0x16, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72,
|
||||
0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x26, 0x0a, 0x07,
|
||||
0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0c, 0x2e,
|
||||
0x61, 0x70, 0x69, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x07, 0x73, 0x65, 0x72,
|
||||
0x76, 0x69, 0x63, 0x65, 0x32, 0xc0, 0x02, 0x0a, 0x07, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65,
|
||||
0x12, 0x40, 0x0a, 0x0b, 0x49, 0x6e, 0x69, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x12,
|
||||
0x17, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x69, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65,
|
||||
0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49,
|
||||
0x6e, 0x69, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
|
||||
0x73, 0x65, 0x12, 0x3e, 0x0a, 0x0b, 0x4a, 0x6f, 0x69, 0x6e, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65,
|
||||
0x72, 0x12, 0x17, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4a, 0x6f, 0x69, 0x6e, 0x43, 0x6c, 0x75, 0x73,
|
||||
0x74, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f,
|
||||
0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70,
|
||||
0x74, 0x79, 0x12, 0x33, 0x0a, 0x05, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x16, 0x2e, 0x67, 0x6f,
|
||||
0x69, 0x6e, 0x65, 0x22, 0x79, 0x0a, 0x12, 0x4a, 0x6f, 0x69, 0x6e, 0x43, 0x6c, 0x75, 0x73, 0x74,
|
||||
0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2a, 0x0a, 0x07, 0x6d, 0x61, 0x63,
|
||||
0x68, 0x69, 0x6e, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x61, 0x70, 0x69,
|
||||
0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x07, 0x6d, 0x61,
|
||||
0x63, 0x68, 0x69, 0x6e, 0x65, 0x12, 0x37, 0x0a, 0x0e, 0x6f, 0x74, 0x68, 0x65, 0x72, 0x5f, 0x6d,
|
||||
0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x10, 0x2e,
|
||||
0x61, 0x70, 0x69, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52,
|
||||
0x0d, 0x6f, 0x74, 0x68, 0x65, 0x72, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x22, 0x25,
|
||||
0x0a, 0x0d, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12,
|
||||
0x14, 0x0a, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05,
|
||||
0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0xc3, 0x01, 0x0a, 0x07, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63,
|
||||
0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69,
|
||||
0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52,
|
||||
0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x18, 0x03, 0x20,
|
||||
0x01, 0x28, 0x09, 0x52, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x12, 0x36, 0x0a, 0x0a, 0x63, 0x6f, 0x6e,
|
||||
0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e,
|
||||
0x61, 0x70, 0x69, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x43, 0x6f, 0x6e, 0x74,
|
||||
0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x0a, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72,
|
||||
0x73, 0x1a, 0x48, 0x0a, 0x09, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x1d,
|
||||
0x0a, 0x0a, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01,
|
||||
0x28, 0x09, 0x52, 0x09, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x64, 0x12, 0x1c, 0x0a,
|
||||
0x09, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c,
|
||||
0x52, 0x09, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x22, 0x27, 0x0a, 0x15, 0x49,
|
||||
0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x71,
|
||||
0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
|
||||
0x52, 0x02, 0x69, 0x64, 0x22, 0x40, 0x0a, 0x16, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53,
|
||||
0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x26,
|
||||
0x0a, 0x07, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32,
|
||||
0x0c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x07, 0x73,
|
||||
0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x32, 0x8f, 0x03, 0x0a, 0x07, 0x4d, 0x61, 0x63, 0x68, 0x69,
|
||||
0x6e, 0x65, 0x12, 0x4d, 0x0a, 0x12, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x50, 0x72, 0x65, 0x72, 0x65,
|
||||
0x71, 0x75, 0x69, 0x73, 0x69, 0x74, 0x65, 0x73, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c,
|
||||
0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79,
|
||||
0x1a, 0x1f, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x50, 0x72, 0x65, 0x72,
|
||||
0x65, 0x71, 0x75, 0x69, 0x73, 0x69, 0x74, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
|
||||
0x65, 0x12, 0x40, 0x0a, 0x0b, 0x49, 0x6e, 0x69, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72,
|
||||
0x12, 0x17, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x69, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74,
|
||||
0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x61, 0x70, 0x69, 0x2e,
|
||||
0x49, 0x6e, 0x69, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f,
|
||||
0x6e, 0x73, 0x65, 0x12, 0x3e, 0x0a, 0x0b, 0x4a, 0x6f, 0x69, 0x6e, 0x43, 0x6c, 0x75, 0x73, 0x74,
|
||||
0x65, 0x72, 0x12, 0x17, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4a, 0x6f, 0x69, 0x6e, 0x43, 0x6c, 0x75,
|
||||
0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f,
|
||||
0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d,
|
||||
0x70, 0x74, 0x79, 0x1a, 0x12, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52,
|
||||
0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x33, 0x0a, 0x07, 0x49, 0x6e, 0x73, 0x70, 0x65,
|
||||
0x63, 0x74, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74,
|
||||
0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x10, 0x2e, 0x61, 0x70, 0x69,
|
||||
0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x49, 0x0a, 0x0e,
|
||||
0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x1a,
|
||||
0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72, 0x76,
|
||||
0x69, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x61, 0x70, 0x69,
|
||||
0x2e, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52,
|
||||
0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x37, 0x5a, 0x35, 0x67, 0x69, 0x74, 0x68, 0x75,
|
||||
0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x70, 0x73, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x6b, 0x69,
|
||||
0x2f, 0x75, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61,
|
||||
0x6c, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x62,
|
||||
0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||
0x70, 0x74, 0x79, 0x12, 0x33, 0x0a, 0x05, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x16, 0x2e, 0x67,
|
||||
0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45,
|
||||
0x6d, 0x70, 0x74, 0x79, 0x1a, 0x12, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x54, 0x6f, 0x6b, 0x65, 0x6e,
|
||||
0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x33, 0x0a, 0x07, 0x49, 0x6e, 0x73, 0x70,
|
||||
0x65, 0x63, 0x74, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f,
|
||||
0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x10, 0x2e, 0x61, 0x70,
|
||||
0x69, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x49, 0x0a,
|
||||
0x0e, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12,
|
||||
0x1a, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72,
|
||||
0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x61, 0x70,
|
||||
0x69, 0x2e, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65,
|
||||
0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x37, 0x5a, 0x35, 0x67, 0x69, 0x74, 0x68,
|
||||
0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x70, 0x73, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x6b,
|
||||
0x69, 0x2f, 0x75, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e,
|
||||
0x61, 0x6c, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70,
|
||||
0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -744,48 +811,51 @@ func file_internal_machine_api_pb_machine_proto_rawDescGZIP() []byte {
|
||||
return file_internal_machine_api_pb_machine_proto_rawDescData
|
||||
}
|
||||
|
||||
var file_internal_machine_api_pb_machine_proto_msgTypes = make([]protoimpl.MessageInfo, 10)
|
||||
var file_internal_machine_api_pb_machine_proto_msgTypes = make([]protoimpl.MessageInfo, 11)
|
||||
var file_internal_machine_api_pb_machine_proto_goTypes = []any{
|
||||
(*MachineInfo)(nil), // 0: api.MachineInfo
|
||||
(*NetworkConfig)(nil), // 1: api.NetworkConfig
|
||||
(*InitClusterRequest)(nil), // 2: api.InitClusterRequest
|
||||
(*InitClusterResponse)(nil), // 3: api.InitClusterResponse
|
||||
(*JoinClusterRequest)(nil), // 4: api.JoinClusterRequest
|
||||
(*TokenResponse)(nil), // 5: api.TokenResponse
|
||||
(*Service)(nil), // 6: api.Service
|
||||
(*InspectServiceRequest)(nil), // 7: api.InspectServiceRequest
|
||||
(*InspectServiceResponse)(nil), // 8: api.InspectServiceResponse
|
||||
(*Service_Container)(nil), // 9: api.Service.Container
|
||||
(*IP)(nil), // 10: api.IP
|
||||
(*IPPrefix)(nil), // 11: api.IPPrefix
|
||||
(*IPPort)(nil), // 12: api.IPPort
|
||||
(*emptypb.Empty)(nil), // 13: google.protobuf.Empty
|
||||
(*MachineInfo)(nil), // 0: api.MachineInfo
|
||||
(*NetworkConfig)(nil), // 1: api.NetworkConfig
|
||||
(*CheckPrerequisitesResponse)(nil), // 2: api.CheckPrerequisitesResponse
|
||||
(*InitClusterRequest)(nil), // 3: api.InitClusterRequest
|
||||
(*InitClusterResponse)(nil), // 4: api.InitClusterResponse
|
||||
(*JoinClusterRequest)(nil), // 5: api.JoinClusterRequest
|
||||
(*TokenResponse)(nil), // 6: api.TokenResponse
|
||||
(*Service)(nil), // 7: api.Service
|
||||
(*InspectServiceRequest)(nil), // 8: api.InspectServiceRequest
|
||||
(*InspectServiceResponse)(nil), // 9: api.InspectServiceResponse
|
||||
(*Service_Container)(nil), // 10: api.Service.Container
|
||||
(*IP)(nil), // 11: api.IP
|
||||
(*IPPrefix)(nil), // 12: api.IPPrefix
|
||||
(*IPPort)(nil), // 13: api.IPPort
|
||||
(*emptypb.Empty)(nil), // 14: google.protobuf.Empty
|
||||
}
|
||||
var file_internal_machine_api_pb_machine_proto_depIdxs = []int32{
|
||||
1, // 0: api.MachineInfo.network:type_name -> api.NetworkConfig
|
||||
10, // 1: api.MachineInfo.public_ip:type_name -> api.IP
|
||||
11, // 2: api.NetworkConfig.subnet:type_name -> api.IPPrefix
|
||||
10, // 3: api.NetworkConfig.management_ip:type_name -> api.IP
|
||||
12, // 4: api.NetworkConfig.endpoints:type_name -> api.IPPort
|
||||
11, // 5: api.InitClusterRequest.network:type_name -> api.IPPrefix
|
||||
10, // 6: api.InitClusterRequest.public_ip:type_name -> api.IP
|
||||
11, // 1: api.MachineInfo.public_ip:type_name -> api.IP
|
||||
12, // 2: api.NetworkConfig.subnet:type_name -> api.IPPrefix
|
||||
11, // 3: api.NetworkConfig.management_ip:type_name -> api.IP
|
||||
13, // 4: api.NetworkConfig.endpoints:type_name -> api.IPPort
|
||||
12, // 5: api.InitClusterRequest.network:type_name -> api.IPPrefix
|
||||
11, // 6: api.InitClusterRequest.public_ip:type_name -> api.IP
|
||||
0, // 7: api.InitClusterResponse.machine:type_name -> api.MachineInfo
|
||||
0, // 8: api.JoinClusterRequest.machine:type_name -> api.MachineInfo
|
||||
0, // 9: api.JoinClusterRequest.other_machines:type_name -> api.MachineInfo
|
||||
9, // 10: api.Service.containers:type_name -> api.Service.Container
|
||||
6, // 11: api.InspectServiceResponse.service:type_name -> api.Service
|
||||
2, // 12: api.Machine.InitCluster:input_type -> api.InitClusterRequest
|
||||
4, // 13: api.Machine.JoinCluster:input_type -> api.JoinClusterRequest
|
||||
13, // 14: api.Machine.Token:input_type -> google.protobuf.Empty
|
||||
13, // 15: api.Machine.Inspect:input_type -> google.protobuf.Empty
|
||||
7, // 16: api.Machine.InspectService:input_type -> api.InspectServiceRequest
|
||||
3, // 17: api.Machine.InitCluster:output_type -> api.InitClusterResponse
|
||||
13, // 18: api.Machine.JoinCluster:output_type -> google.protobuf.Empty
|
||||
5, // 19: api.Machine.Token:output_type -> api.TokenResponse
|
||||
0, // 20: api.Machine.Inspect:output_type -> api.MachineInfo
|
||||
8, // 21: api.Machine.InspectService:output_type -> api.InspectServiceResponse
|
||||
17, // [17:22] is the sub-list for method output_type
|
||||
12, // [12:17] is the sub-list for method input_type
|
||||
10, // 10: api.Service.containers:type_name -> api.Service.Container
|
||||
7, // 11: api.InspectServiceResponse.service:type_name -> api.Service
|
||||
14, // 12: api.Machine.CheckPrerequisites:input_type -> google.protobuf.Empty
|
||||
3, // 13: api.Machine.InitCluster:input_type -> api.InitClusterRequest
|
||||
5, // 14: api.Machine.JoinCluster:input_type -> api.JoinClusterRequest
|
||||
14, // 15: api.Machine.Token:input_type -> google.protobuf.Empty
|
||||
14, // 16: api.Machine.Inspect:input_type -> google.protobuf.Empty
|
||||
8, // 17: api.Machine.InspectService:input_type -> api.InspectServiceRequest
|
||||
2, // 18: api.Machine.CheckPrerequisites:output_type -> api.CheckPrerequisitesResponse
|
||||
4, // 19: api.Machine.InitCluster:output_type -> api.InitClusterResponse
|
||||
14, // 20: api.Machine.JoinCluster:output_type -> google.protobuf.Empty
|
||||
6, // 21: api.Machine.Token:output_type -> api.TokenResponse
|
||||
0, // 22: api.Machine.Inspect:output_type -> api.MachineInfo
|
||||
9, // 23: api.Machine.InspectService:output_type -> api.InspectServiceResponse
|
||||
18, // [18:24] is the sub-list for method output_type
|
||||
12, // [12:18] is the sub-list for method input_type
|
||||
12, // [12:12] is the sub-list for extension type_name
|
||||
12, // [12:12] is the sub-list for extension extendee
|
||||
0, // [0:12] is the sub-list for field type_name
|
||||
@@ -823,7 +893,7 @@ func file_internal_machine_api_pb_machine_proto_init() {
|
||||
}
|
||||
}
|
||||
file_internal_machine_api_pb_machine_proto_msgTypes[2].Exporter = func(v any, i int) any {
|
||||
switch v := v.(*InitClusterRequest); i {
|
||||
switch v := v.(*CheckPrerequisitesResponse); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
@@ -835,7 +905,7 @@ func file_internal_machine_api_pb_machine_proto_init() {
|
||||
}
|
||||
}
|
||||
file_internal_machine_api_pb_machine_proto_msgTypes[3].Exporter = func(v any, i int) any {
|
||||
switch v := v.(*InitClusterResponse); i {
|
||||
switch v := v.(*InitClusterRequest); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
@@ -847,7 +917,7 @@ func file_internal_machine_api_pb_machine_proto_init() {
|
||||
}
|
||||
}
|
||||
file_internal_machine_api_pb_machine_proto_msgTypes[4].Exporter = func(v any, i int) any {
|
||||
switch v := v.(*JoinClusterRequest); i {
|
||||
switch v := v.(*InitClusterResponse); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
@@ -859,7 +929,7 @@ func file_internal_machine_api_pb_machine_proto_init() {
|
||||
}
|
||||
}
|
||||
file_internal_machine_api_pb_machine_proto_msgTypes[5].Exporter = func(v any, i int) any {
|
||||
switch v := v.(*TokenResponse); i {
|
||||
switch v := v.(*JoinClusterRequest); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
@@ -871,7 +941,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 {
|
||||
switch v := v.(*Service); i {
|
||||
switch v := v.(*TokenResponse); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
@@ -883,7 +953,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 {
|
||||
switch v := v.(*InspectServiceRequest); i {
|
||||
switch v := v.(*Service); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
@@ -895,7 +965,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 {
|
||||
switch v := v.(*InspectServiceResponse); i {
|
||||
switch v := v.(*InspectServiceRequest); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
@@ -907,6 +977,18 @@ 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 {
|
||||
switch v := v.(*InspectServiceResponse); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
return &v.sizeCache
|
||||
case 2:
|
||||
return &v.unknownFields
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
file_internal_machine_api_pb_machine_proto_msgTypes[10].Exporter = func(v any, i int) any {
|
||||
switch v := v.(*Service_Container); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
@@ -919,7 +1001,7 @@ func file_internal_machine_api_pb_machine_proto_init() {
|
||||
}
|
||||
}
|
||||
}
|
||||
file_internal_machine_api_pb_machine_proto_msgTypes[2].OneofWrappers = []any{
|
||||
file_internal_machine_api_pb_machine_proto_msgTypes[3].OneofWrappers = []any{
|
||||
(*InitClusterRequest_PublicIp)(nil),
|
||||
(*InitClusterRequest_PublicIpAuto)(nil),
|
||||
}
|
||||
@@ -929,7 +1011,7 @@ func file_internal_machine_api_pb_machine_proto_init() {
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: file_internal_machine_api_pb_machine_proto_rawDesc,
|
||||
NumEnums: 0,
|
||||
NumMessages: 10,
|
||||
NumMessages: 11,
|
||||
NumExtensions: 0,
|
||||
NumServices: 1,
|
||||
},
|
||||
|
||||
@@ -8,6 +8,8 @@ import "google/protobuf/empty.proto";
|
||||
import "internal/machine/api/pb/common.proto";
|
||||
|
||||
service Machine {
|
||||
// CheckPrerequisites verifies if the machine meets all necessary system requirements to participate in the cluster.
|
||||
rpc CheckPrerequisites(google.protobuf.Empty) returns (CheckPrerequisitesResponse);
|
||||
rpc InitCluster(InitClusterRequest) returns (InitClusterResponse);
|
||||
rpc JoinCluster(JoinClusterRequest) returns (google.protobuf.Empty);
|
||||
rpc Token(google.protobuf.Empty) returns (TokenResponse);
|
||||
@@ -29,6 +31,13 @@ message NetworkConfig {
|
||||
bytes public_key = 4;
|
||||
}
|
||||
|
||||
message CheckPrerequisitesResponse {
|
||||
// Overall status of the checks.
|
||||
bool satisfied = 1;
|
||||
// Error message if not satisfied (empty if all checks pass).
|
||||
string error = 2;
|
||||
}
|
||||
|
||||
message InitClusterRequest {
|
||||
string machineName = 1;
|
||||
IPPrefix network = 2;
|
||||
|
||||
@@ -20,17 +20,20 @@ import (
|
||||
const _ = grpc.SupportPackageIsVersion9
|
||||
|
||||
const (
|
||||
Machine_InitCluster_FullMethodName = "/api.Machine/InitCluster"
|
||||
Machine_JoinCluster_FullMethodName = "/api.Machine/JoinCluster"
|
||||
Machine_Token_FullMethodName = "/api.Machine/Token"
|
||||
Machine_Inspect_FullMethodName = "/api.Machine/Inspect"
|
||||
Machine_InspectService_FullMethodName = "/api.Machine/InspectService"
|
||||
Machine_CheckPrerequisites_FullMethodName = "/api.Machine/CheckPrerequisites"
|
||||
Machine_InitCluster_FullMethodName = "/api.Machine/InitCluster"
|
||||
Machine_JoinCluster_FullMethodName = "/api.Machine/JoinCluster"
|
||||
Machine_Token_FullMethodName = "/api.Machine/Token"
|
||||
Machine_Inspect_FullMethodName = "/api.Machine/Inspect"
|
||||
Machine_InspectService_FullMethodName = "/api.Machine/InspectService"
|
||||
)
|
||||
|
||||
// MachineClient is the client API for Machine service.
|
||||
//
|
||||
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
|
||||
type MachineClient interface {
|
||||
// CheckPrerequisites verifies if the machine meets all necessary system requirements to participate in the cluster.
|
||||
CheckPrerequisites(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*CheckPrerequisitesResponse, error)
|
||||
InitCluster(ctx context.Context, in *InitClusterRequest, opts ...grpc.CallOption) (*InitClusterResponse, 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)
|
||||
@@ -46,6 +49,16 @@ func NewMachineClient(cc grpc.ClientConnInterface) MachineClient {
|
||||
return &machineClient{cc}
|
||||
}
|
||||
|
||||
func (c *machineClient) CheckPrerequisites(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*CheckPrerequisitesResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(CheckPrerequisitesResponse)
|
||||
err := c.cc.Invoke(ctx, Machine_CheckPrerequisites_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *machineClient) InitCluster(ctx context.Context, in *InitClusterRequest, opts ...grpc.CallOption) (*InitClusterResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(InitClusterResponse)
|
||||
@@ -100,6 +113,8 @@ func (c *machineClient) InspectService(ctx context.Context, in *InspectServiceRe
|
||||
// All implementations must embed UnimplementedMachineServer
|
||||
// for forward compatibility.
|
||||
type MachineServer interface {
|
||||
// CheckPrerequisites verifies if the machine meets all necessary system requirements to participate in the cluster.
|
||||
CheckPrerequisites(context.Context, *emptypb.Empty) (*CheckPrerequisitesResponse, error)
|
||||
InitCluster(context.Context, *InitClusterRequest) (*InitClusterResponse, error)
|
||||
JoinCluster(context.Context, *JoinClusterRequest) (*emptypb.Empty, error)
|
||||
Token(context.Context, *emptypb.Empty) (*TokenResponse, error)
|
||||
@@ -115,6 +130,9 @@ type MachineServer interface {
|
||||
// pointer dereference when methods are called.
|
||||
type UnimplementedMachineServer struct{}
|
||||
|
||||
func (UnimplementedMachineServer) CheckPrerequisites(context.Context, *emptypb.Empty) (*CheckPrerequisitesResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method CheckPrerequisites not implemented")
|
||||
}
|
||||
func (UnimplementedMachineServer) InitCluster(context.Context, *InitClusterRequest) (*InitClusterResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method InitCluster not implemented")
|
||||
}
|
||||
@@ -151,6 +169,24 @@ func RegisterMachineServer(s grpc.ServiceRegistrar, srv MachineServer) {
|
||||
s.RegisterService(&Machine_ServiceDesc, srv)
|
||||
}
|
||||
|
||||
func _Machine_CheckPrerequisites_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(emptypb.Empty)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(MachineServer).CheckPrerequisites(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: Machine_CheckPrerequisites_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(MachineServer).CheckPrerequisites(ctx, req.(*emptypb.Empty))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _Machine_InitCluster_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(InitClusterRequest)
|
||||
if err := dec(in); err != nil {
|
||||
@@ -248,6 +284,10 @@ var Machine_ServiceDesc = grpc.ServiceDesc{
|
||||
ServiceName: "api.Machine",
|
||||
HandlerType: (*MachineServer)(nil),
|
||||
Methods: []grpc.MethodDesc{
|
||||
{
|
||||
MethodName: "CheckPrerequisites",
|
||||
Handler: _Machine_CheckPrerequisites_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "InitCluster",
|
||||
Handler: _Machine_InitCluster_Handler,
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"google.golang.org/grpc/status"
|
||||
"google.golang.org/protobuf/encoding/protowire"
|
||||
"google.golang.org/protobuf/proto"
|
||||
"uncloud/internal/machine/api/pb"
|
||||
"github.com/psviderski/uncloud/internal/machine/api/pb"
|
||||
)
|
||||
|
||||
// One2ManyResponder converts upstream responses into messages from upstreams, so that multiple
|
||||
|
||||
@@ -1,56 +1,61 @@
|
||||
package caddyfile
|
||||
package caddyconfig
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp/reverseproxy"
|
||||
"log/slog"
|
||||
"maps"
|
||||
"net"
|
||||
"net/http"
|
||||
"slices"
|
||||
"strconv"
|
||||
"uncloud/internal/api"
|
||||
"uncloud/internal/machine/docker"
|
||||
"time"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp/reverseproxy"
|
||||
"github.com/psviderski/uncloud/pkg/api"
|
||||
)
|
||||
|
||||
func GenerateConfig(containers []api.Container, verifyResponse string) (*caddy.Config, error) {
|
||||
func GenerateConfig(containers []api.ServiceContainer, verifyResponse string) (*caddy.Config, error) {
|
||||
// Maps hostnames to lists of upstreams (container IP:port pairs).
|
||||
httpHostUpstreams := make(map[string][]string)
|
||||
httpsHostUpstreams := make(map[string][]string)
|
||||
for _, ctr := range containers {
|
||||
logger := slog.With("container", ctr.ID)
|
||||
network, ok := ctr.NetworkSettings.Networks[docker.NetworkName]
|
||||
if !ok {
|
||||
// Container is not connected to the uncloud Docker network (could be host network).
|
||||
continue
|
||||
}
|
||||
if network.IPAddress == "" {
|
||||
logger.Error("Container has no IPv4 address.")
|
||||
if !ctr.Healthy() {
|
||||
continue
|
||||
}
|
||||
|
||||
ip := ctr.UncloudNetworkIP()
|
||||
if !ip.IsValid() {
|
||||
// Container is not connected to the uncloud Docker network (could be host network).
|
||||
continue
|
||||
}
|
||||
log := slog.With("container", ctr.ID)
|
||||
|
||||
ports, err := ctr.ServicePorts()
|
||||
if err != nil {
|
||||
logger.Error("Failed to parse service ports for container.", "err", err)
|
||||
log.Error("Failed to parse service ports for container.", "err", err)
|
||||
continue
|
||||
}
|
||||
|
||||
for _, port := range ports {
|
||||
if port.Mode != api.PortModeIngress {
|
||||
continue
|
||||
}
|
||||
|
||||
switch port.Protocol {
|
||||
case api.ProtocolHTTP:
|
||||
upstream := net.JoinHostPort(network.IPAddress, strconv.Itoa(int(port.ContainerPort)))
|
||||
upstream := net.JoinHostPort(ip.String(), strconv.Itoa(int(port.ContainerPort)))
|
||||
httpHostUpstreams[port.Hostname] = append(httpHostUpstreams[port.Hostname], upstream)
|
||||
case api.ProtocolHTTPS:
|
||||
upstream := net.JoinHostPort(network.IPAddress, strconv.Itoa(int(port.ContainerPort)))
|
||||
upstream := net.JoinHostPort(ip.String(), strconv.Itoa(int(port.ContainerPort)))
|
||||
httpsHostUpstreams[port.Hostname] = append(httpsHostUpstreams[port.Hostname], upstream)
|
||||
default:
|
||||
// TODO: implement L4 ingress routing for TCP and UDP.
|
||||
logger.Error("Unsupported protocol for ingress port.", "port", port)
|
||||
log.Error("Unsupported protocol for ingress port.", "port", port)
|
||||
continue
|
||||
}
|
||||
}
|
||||
@@ -60,6 +65,8 @@ func GenerateConfig(containers []api.Container, verifyResponse string) (*caddy.C
|
||||
servers := make(map[string]*caddyhttp.Server)
|
||||
servers["http"] = &caddyhttp.Server{
|
||||
Listen: []string{fmt.Sprintf(":%d", caddyhttp.DefaultHTTPPort)},
|
||||
// All http requests to this server are logged to the default logger.
|
||||
Logs: &caddyhttp.ServerLogConfig{},
|
||||
Routes: append(
|
||||
hostUpstreamsToRoutes(httpHostUpstreams, &warnings),
|
||||
// Add a route to respond with a static verification response at the /.uncloud-verify path.
|
||||
@@ -68,6 +75,8 @@ func GenerateConfig(containers []api.Container, verifyResponse string) (*caddy.C
|
||||
}
|
||||
servers["https"] = &caddyhttp.Server{
|
||||
Listen: []string{fmt.Sprintf(":%d", caddyhttp.DefaultHTTPSPort)},
|
||||
// All https requests to this server are logged to the default logger.
|
||||
Logs: &caddyhttp.ServerLogConfig{},
|
||||
Routes: hostUpstreamsToRoutes(httpsHostUpstreams, &warnings),
|
||||
}
|
||||
|
||||
@@ -108,6 +117,16 @@ func hostUpstreamsToRoutes(hostUpstreams map[string][]string, warnings *[]caddyc
|
||||
}
|
||||
}
|
||||
handler := &reverseproxy.Handler{
|
||||
HealthChecks: &reverseproxy.HealthChecks{
|
||||
// Enable passive health checks to automatically detect unhealthy upstreams.
|
||||
Passive: &reverseproxy.PassiveHealthChecks{
|
||||
FailDuration: caddy.Duration(30 * time.Second),
|
||||
},
|
||||
},
|
||||
LoadBalancing: &reverseproxy.LoadBalancing{
|
||||
// Retry failed requests to skip over temporarily unavailable upstreams.
|
||||
Retries: 3,
|
||||
},
|
||||
Upstreams: upstreamPool,
|
||||
}
|
||||
|
||||
+203
-115
@@ -1,51 +1,56 @@
|
||||
package caddyfile
|
||||
package caddyconfig
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/network"
|
||||
"github.com/psviderski/uncloud/internal/machine/docker"
|
||||
"github.com/psviderski/uncloud/pkg/api"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"strings"
|
||||
"testing"
|
||||
"uncloud/internal/api"
|
||||
"uncloud/internal/machine/docker"
|
||||
)
|
||||
|
||||
func TestGenerateConfig(t *testing.T) {
|
||||
configWithoutServices := `{
|
||||
"servers": {
|
||||
"http": {
|
||||
"listen": [":80"],
|
||||
"routes": [{
|
||||
"match": [{"path": ["/.uncloud-verify"]}],
|
||||
"handle": [{
|
||||
"body": "verification-response-body",
|
||||
"handler": "static_response",
|
||||
"status_code": 200
|
||||
}]
|
||||
}],
|
||||
"logs": {}
|
||||
},
|
||||
"https": {
|
||||
"listen": [":443"],
|
||||
"logs": {}
|
||||
}
|
||||
}
|
||||
}`
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
containers []api.Container
|
||||
containers []api.ServiceContainer
|
||||
want string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "empty containers",
|
||||
containers: []api.Container{},
|
||||
want: `{
|
||||
"servers": {
|
||||
"http": {
|
||||
"listen": [":80"],
|
||||
"routes": [{
|
||||
"match": [{"path": ["/.uncloud-verify"]}],
|
||||
"handle": [{
|
||||
"body": "verification-response-body",
|
||||
"handler": "static_response",
|
||||
"status_code": 200
|
||||
}]
|
||||
}]
|
||||
},
|
||||
"https": {
|
||||
"listen": [":443"]
|
||||
}
|
||||
}
|
||||
}`,
|
||||
wantErr: false,
|
||||
containers: []api.ServiceContainer{},
|
||||
want: configWithoutServices,
|
||||
wantErr: false,
|
||||
},
|
||||
|
||||
{
|
||||
name: "HTTP container",
|
||||
containers: []api.Container{
|
||||
containers: []api.ServiceContainer{
|
||||
newContainer("10.210.0.2", "app.example.com:8080/http"),
|
||||
},
|
||||
want: `{
|
||||
@@ -57,6 +62,14 @@ func TestGenerateConfig(t *testing.T) {
|
||||
"match": [{"host": ["app.example.com"]}],
|
||||
"handle": [{
|
||||
"handler": "reverse_proxy",
|
||||
"health_checks": {
|
||||
"passive": {
|
||||
"fail_duration": 30000000000
|
||||
}
|
||||
},
|
||||
"load_balancing": {
|
||||
"retries": 3
|
||||
},
|
||||
"upstreams": [{"dial": "10.210.0.2:8080"}]
|
||||
}]
|
||||
},
|
||||
@@ -68,10 +81,12 @@ func TestGenerateConfig(t *testing.T) {
|
||||
"status_code": 200
|
||||
}]
|
||||
}
|
||||
]
|
||||
],
|
||||
"logs": {}
|
||||
},
|
||||
"https": {
|
||||
"listen": [":443"]
|
||||
"listen": [":443"],
|
||||
"logs": {}
|
||||
}
|
||||
}
|
||||
}`,
|
||||
@@ -79,7 +94,7 @@ func TestGenerateConfig(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "load balancing multiple containers",
|
||||
containers: []api.Container{
|
||||
containers: []api.ServiceContainer{
|
||||
newContainer("10.210.0.2", "app.example.com:8080/http"),
|
||||
newContainer("10.210.0.3", "app.example.com:8080/http"),
|
||||
},
|
||||
@@ -92,6 +107,14 @@ func TestGenerateConfig(t *testing.T) {
|
||||
"match": [{"host": ["app.example.com"]}],
|
||||
"handle": [{
|
||||
"handler": "reverse_proxy",
|
||||
"health_checks": {
|
||||
"passive": {
|
||||
"fail_duration": 30000000000
|
||||
}
|
||||
},
|
||||
"load_balancing": {
|
||||
"retries": 3
|
||||
},
|
||||
"upstreams": [
|
||||
{"dial": "10.210.0.2:8080"},
|
||||
{"dial": "10.210.0.3:8080"}
|
||||
@@ -106,10 +129,12 @@ func TestGenerateConfig(t *testing.T) {
|
||||
"status_code": 200
|
||||
}]
|
||||
}
|
||||
]
|
||||
],
|
||||
"logs": {}
|
||||
},
|
||||
"https": {
|
||||
"listen": [":443"]
|
||||
"listen": [":443"],
|
||||
"logs": {}
|
||||
}
|
||||
}
|
||||
}`,
|
||||
@@ -117,7 +142,7 @@ func TestGenerateConfig(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "HTTPS container",
|
||||
containers: []api.Container{
|
||||
containers: []api.ServiceContainer{
|
||||
newContainer("10.210.0.2", "secure.example.com:8000/https"),
|
||||
},
|
||||
want: `{
|
||||
@@ -133,7 +158,8 @@ func TestGenerateConfig(t *testing.T) {
|
||||
"status_code": 200
|
||||
}]
|
||||
}
|
||||
]
|
||||
],
|
||||
"logs": {}
|
||||
},
|
||||
"https": {
|
||||
"listen": [":443"],
|
||||
@@ -142,10 +168,19 @@ func TestGenerateConfig(t *testing.T) {
|
||||
"match": [{"host": ["secure.example.com"]}],
|
||||
"handle": [{
|
||||
"handler": "reverse_proxy",
|
||||
"health_checks": {
|
||||
"passive": {
|
||||
"fail_duration": 30000000000
|
||||
}
|
||||
},
|
||||
"load_balancing": {
|
||||
"retries": 3
|
||||
},
|
||||
"upstreams": [{"dial": "10.210.0.2:8000"}]
|
||||
}]
|
||||
}
|
||||
]
|
||||
],
|
||||
"logs": {}
|
||||
}
|
||||
}
|
||||
}`,
|
||||
@@ -153,7 +188,7 @@ func TestGenerateConfig(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "mixed HTTP and HTTPS",
|
||||
containers: []api.Container{
|
||||
containers: []api.ServiceContainer{
|
||||
newContainer("10.210.0.2",
|
||||
"app.example.com:8080/http",
|
||||
"web.example.com:8000/http"),
|
||||
@@ -177,6 +212,14 @@ func TestGenerateConfig(t *testing.T) {
|
||||
"match": [{"host": ["app.example.com"]}],
|
||||
"handle": [{
|
||||
"handler": "reverse_proxy",
|
||||
"health_checks": {
|
||||
"passive": {
|
||||
"fail_duration": 30000000000
|
||||
}
|
||||
},
|
||||
"load_balancing": {
|
||||
"retries": 3
|
||||
},
|
||||
"upstreams": [
|
||||
{"dial": "10.210.0.2:8080"},
|
||||
{"dial": "10.210.0.3:8080"},
|
||||
@@ -188,6 +231,14 @@ func TestGenerateConfig(t *testing.T) {
|
||||
"match": [{"host": ["web.example.com"]}],
|
||||
"handle": [{
|
||||
"handler": "reverse_proxy",
|
||||
"health_checks": {
|
||||
"passive": {
|
||||
"fail_duration": 30000000000
|
||||
}
|
||||
},
|
||||
"load_balancing": {
|
||||
"retries": 3
|
||||
},
|
||||
"upstreams": [
|
||||
{"dial": "10.210.0.2:8000"},
|
||||
{"dial": "10.210.0.4:8000"},
|
||||
@@ -203,7 +254,8 @@ func TestGenerateConfig(t *testing.T) {
|
||||
"status_code": 200
|
||||
}]
|
||||
}
|
||||
]
|
||||
],
|
||||
"logs": {}
|
||||
},
|
||||
"https": {
|
||||
"listen": [":443"],
|
||||
@@ -212,6 +264,14 @@ func TestGenerateConfig(t *testing.T) {
|
||||
"match": [{"host": ["secure.example.com"]}],
|
||||
"handle": [{
|
||||
"handler": "reverse_proxy",
|
||||
"health_checks": {
|
||||
"passive": {
|
||||
"fail_duration": 30000000000
|
||||
}
|
||||
},
|
||||
"load_balancing": {
|
||||
"retries": 3
|
||||
},
|
||||
"upstreams": [
|
||||
{"dial": "10.210.0.3:8888"},
|
||||
{"dial": "10.210.0.4:8888"},
|
||||
@@ -219,7 +279,8 @@ func TestGenerateConfig(t *testing.T) {
|
||||
]
|
||||
}]
|
||||
}
|
||||
]
|
||||
],
|
||||
"logs": {}
|
||||
}
|
||||
}
|
||||
}`,
|
||||
@@ -227,76 +288,87 @@ func TestGenerateConfig(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "container without uncloud network ignored",
|
||||
containers: []api.Container{
|
||||
containers: []api.ServiceContainer{
|
||||
newContainerWithoutNetwork("ignored.example.com:8080/http"),
|
||||
},
|
||||
want: `{
|
||||
"servers": {
|
||||
"http": {
|
||||
"listen": [":80"],
|
||||
"routes": [{
|
||||
"match": [{"path": ["/.uncloud-verify"]}],
|
||||
"handle": [{
|
||||
"body": "verification-response-body",
|
||||
"handler": "static_response",
|
||||
"status_code": 200
|
||||
}]
|
||||
}]
|
||||
},
|
||||
"https": {
|
||||
"listen": [":443"]
|
||||
}
|
||||
}
|
||||
}`,
|
||||
want: configWithoutServices,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "container with invalid port ignored",
|
||||
containers: []api.Container{
|
||||
containers: []api.ServiceContainer{
|
||||
newContainer("10.210.0.2", "invalid-port"),
|
||||
},
|
||||
want: `{
|
||||
"servers": {
|
||||
"http": {
|
||||
"listen": [":80"],
|
||||
"routes": [{
|
||||
"match": [{"path": ["/.uncloud-verify"]}],
|
||||
"handle": [{
|
||||
"body": "verification-response-body",
|
||||
"handler": "static_response",
|
||||
"status_code": 200
|
||||
}]
|
||||
}]
|
||||
},
|
||||
"https": {
|
||||
"listen": [":443"]
|
||||
}
|
||||
}
|
||||
}`,
|
||||
want: configWithoutServices,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "containers with unsupported protocols ignored",
|
||||
containers: []api.Container{
|
||||
name: "containers with unsupported protocols and host mode ignored",
|
||||
containers: []api.ServiceContainer{
|
||||
newContainer("10.210.0.2", "5000/tcp"),
|
||||
newContainer("10.210.0.3", "5000/udp"),
|
||||
newContainer("10.210.0.4", "80:8080/tcp@host"),
|
||||
},
|
||||
want: configWithoutServices,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "restarting container ignored",
|
||||
containers: []api.ServiceContainer{
|
||||
newRestartingContainer("10.210.0.2", "app.example.com:8080/http"),
|
||||
},
|
||||
want: configWithoutServices,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "stopped container ignored",
|
||||
containers: []api.ServiceContainer{
|
||||
newStoppedContainer("10.210.0.2", "app.example.com:8080/http"),
|
||||
},
|
||||
want: configWithoutServices,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "mix of running, restarting, and stopped containers",
|
||||
containers: []api.ServiceContainer{
|
||||
newContainer("10.210.0.2", "app.example.com:8080/http"),
|
||||
newRestartingContainer("10.210.0.3", "app.example.com:8080/http"),
|
||||
newStoppedContainer("10.210.0.4", "app.example.com:8080/http"),
|
||||
},
|
||||
want: `{
|
||||
"servers": {
|
||||
"http": {
|
||||
"listen": [":80"],
|
||||
"routes": [{
|
||||
"match": [{"path": ["/.uncloud-verify"]}],
|
||||
"handle": [{
|
||||
"body": "verification-response-body",
|
||||
"handler": "static_response",
|
||||
"status_code": 200
|
||||
}]
|
||||
}]
|
||||
"routes": [
|
||||
{
|
||||
"match": [{"host": ["app.example.com"]}],
|
||||
"handle": [{
|
||||
"handler": "reverse_proxy",
|
||||
"health_checks": {
|
||||
"passive": {
|
||||
"fail_duration": 30000000000
|
||||
}
|
||||
},
|
||||
"load_balancing": {
|
||||
"retries": 3
|
||||
},
|
||||
"upstreams": [{"dial": "10.210.0.2:8080"}]
|
||||
}]
|
||||
},
|
||||
{
|
||||
"match": [{"path": ["/.uncloud-verify"]}],
|
||||
"handle": [{
|
||||
"body": "verification-response-body",
|
||||
"handler": "static_response",
|
||||
"status_code": 200
|
||||
}]
|
||||
}
|
||||
],
|
||||
"logs": {}
|
||||
},
|
||||
"https": {
|
||||
"listen": [":443"]
|
||||
"listen": [":443"],
|
||||
"logs": {}
|
||||
}
|
||||
}
|
||||
}`,
|
||||
@@ -322,44 +394,60 @@ func TestGenerateConfig(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func newContainer(ip string, ports ...string) api.Container {
|
||||
func newContainer(ip string, ports ...string) api.ServiceContainer {
|
||||
portsLabel := strings.Join(ports, ",")
|
||||
return api.Container{
|
||||
ContainerJSON: types.ContainerJSON{
|
||||
ContainerJSONBase: &types.ContainerJSONBase{},
|
||||
NetworkSettings: &types.NetworkSettings{
|
||||
Networks: map[string]*network.EndpointSettings{
|
||||
docker.NetworkName: {
|
||||
IPAddress: ip,
|
||||
},
|
||||
},
|
||||
return api.ServiceContainer{Container: api.Container{ContainerJSON: types.ContainerJSON{
|
||||
ContainerJSONBase: &types.ContainerJSONBase{
|
||||
State: &types.ContainerState{
|
||||
Running: true,
|
||||
},
|
||||
Config: &container.Config{
|
||||
Labels: map[string]string{
|
||||
api.LabelServicePorts: portsLabel,
|
||||
},
|
||||
NetworkSettings: &types.NetworkSettings{
|
||||
Networks: map[string]*network.EndpointSettings{
|
||||
docker.NetworkName: {
|
||||
IPAddress: ip,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
Config: &container.Config{
|
||||
Labels: map[string]string{
|
||||
api.LabelServicePorts: portsLabel,
|
||||
},
|
||||
},
|
||||
}}}
|
||||
}
|
||||
|
||||
func newContainerWithoutNetwork(ports ...string) api.Container {
|
||||
func newContainerWithoutNetwork(ports ...string) api.ServiceContainer {
|
||||
portsLabel := strings.Join(ports, ",")
|
||||
return api.Container{
|
||||
ContainerJSON: types.ContainerJSON{
|
||||
ContainerJSONBase: &types.ContainerJSONBase{},
|
||||
NetworkSettings: &types.NetworkSettings{
|
||||
Networks: map[string]*network.EndpointSettings{
|
||||
"other-network": {
|
||||
IPAddress: "172.17.0.2",
|
||||
},
|
||||
},
|
||||
return api.ServiceContainer{Container: api.Container{ContainerJSON: types.ContainerJSON{
|
||||
ContainerJSONBase: &types.ContainerJSONBase{
|
||||
State: &types.ContainerState{
|
||||
Running: true,
|
||||
},
|
||||
Config: &container.Config{
|
||||
Labels: map[string]string{
|
||||
api.LabelServicePorts: portsLabel,
|
||||
},
|
||||
NetworkSettings: &types.NetworkSettings{
|
||||
Networks: map[string]*network.EndpointSettings{
|
||||
"other-network": {
|
||||
IPAddress: "172.17.0.2",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
Config: &container.Config{
|
||||
Labels: map[string]string{
|
||||
api.LabelServicePorts: portsLabel,
|
||||
},
|
||||
},
|
||||
}}}
|
||||
}
|
||||
|
||||
func newRestartingContainer(ip string, ports ...string) api.ServiceContainer {
|
||||
ctr := newContainer(ip, ports...)
|
||||
ctr.Container.State.Restarting = true
|
||||
return ctr
|
||||
}
|
||||
|
||||
func newStoppedContainer(ip string, ports ...string) api.ServiceContainer {
|
||||
ctr := newContainer(ip, ports...)
|
||||
ctr.Container.State.Running = false
|
||||
return ctr
|
||||
}
|
||||
+15
-9
@@ -1,4 +1,4 @@
|
||||
package caddyfile
|
||||
package caddyconfig
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -7,9 +7,10 @@ import (
|
||||
"log/slog"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"uncloud/internal/api"
|
||||
"uncloud/internal/fs"
|
||||
"uncloud/internal/machine/store"
|
||||
|
||||
"github.com/psviderski/uncloud/internal/fs"
|
||||
"github.com/psviderski/uncloud/internal/machine/store"
|
||||
"github.com/psviderski/uncloud/pkg/api"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -18,7 +19,7 @@ const (
|
||||
)
|
||||
|
||||
// Controller monitors container changes in the cluster store and generates a configuration file for Caddy reverse
|
||||
// proxy. The generated Caddyfile allows Caddy to route external traffic to service containers across the internal
|
||||
// proxy. The generated configuration allows Caddy to route external traffic to service containers across the internal
|
||||
// network.
|
||||
type Controller struct {
|
||||
store *store.Store
|
||||
@@ -89,15 +90,20 @@ func (c *Controller) Run(ctx context.Context) error {
|
||||
// filterAvailableContainers filters out containers from this machine that are likely unavailable. The availability
|
||||
// is determined by the cluster membership state of the machine that the container is running on.
|
||||
// TODO: implement machine membership check using Corrossion Admin client.
|
||||
func (c *Controller) filterAvailableContainers(containerRecords []store.ContainerRecord) ([]api.Container, error) {
|
||||
containers := make([]api.Container, len(containerRecords))
|
||||
func (c *Controller) filterAvailableContainers(
|
||||
containerRecords []store.ContainerRecord,
|
||||
) ([]api.ServiceContainer, error) {
|
||||
containers := make([]api.ServiceContainer, len(containerRecords))
|
||||
for i, cr := range containerRecords {
|
||||
containers[i] = cr.Container
|
||||
containers[i] = api.ServiceContainer{
|
||||
Container: cr.Container,
|
||||
// TODO: restore ServiceSpec from the container record once it's saved in the store.
|
||||
}
|
||||
}
|
||||
return containers, nil
|
||||
}
|
||||
|
||||
func (c *Controller) generateConfig(containers []api.Container) error {
|
||||
func (c *Controller) generateConfig(containers []api.ServiceContainer) error {
|
||||
config, err := GenerateConfig(containers, c.verifyResponse)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -11,11 +11,11 @@ import (
|
||||
"log/slog"
|
||||
"net/netip"
|
||||
"time"
|
||||
"uncloud/internal/corrosion"
|
||||
"uncloud/internal/machine/api/pb"
|
||||
"uncloud/internal/machine/network"
|
||||
"uncloud/internal/machine/store"
|
||||
"uncloud/internal/secret"
|
||||
"github.com/psviderski/uncloud/internal/corrosion"
|
||||
"github.com/psviderski/uncloud/internal/machine/api/pb"
|
||||
"github.com/psviderski/uncloud/internal/machine/network"
|
||||
"github.com/psviderski/uncloud/internal/machine/store"
|
||||
"github.com/psviderski/uncloud/internal/secret"
|
||||
)
|
||||
|
||||
type Cluster struct {
|
||||
|
||||
@@ -4,12 +4,12 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"github.com/psviderski/uncloud/internal/dns"
|
||||
"github.com/psviderski/uncloud/internal/machine/api/pb"
|
||||
"github.com/psviderski/uncloud/internal/machine/store"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
"google.golang.org/protobuf/types/known/emptypb"
|
||||
"uncloud/internal/dns"
|
||||
"uncloud/internal/machine/api/pb"
|
||||
"uncloud/internal/machine/store"
|
||||
)
|
||||
|
||||
// uncloudDNSKey is the key used to store the details of the reserved domain in the store.
|
||||
|
||||
@@ -2,7 +2,7 @@ package cluster
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"uncloud/internal/secret"
|
||||
"github.com/psviderski/uncloud/internal/secret"
|
||||
)
|
||||
|
||||
// NewMachineID generates a new unique machine ID.
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"net/netip"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"uncloud/internal/fs"
|
||||
"github.com/psviderski/uncloud/internal/fs"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
package machine
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
_ "modernc.org/sqlite"
|
||||
)
|
||||
|
||||
const DBFileName = "machine.db"
|
||||
|
||||
// NewDB creates a new connection to machine SQLite database and runs schema migrations if necessary.
|
||||
func NewDB(path string) (*sqlx.DB, error) {
|
||||
// Create the database file with 0600 permissions if it doesn't exist, or update permissions if exists.
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||
file, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR, 0600)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create SQLite database '%s': %w", path, err)
|
||||
}
|
||||
file.Close()
|
||||
} else {
|
||||
if err = os.Chmod(path, 0600); err != nil {
|
||||
return nil, fmt.Errorf("update SQLite database permissions '%s': %w", path, err)
|
||||
}
|
||||
}
|
||||
|
||||
// - Write-Ahead Logging (WAL) mode for better read/write performance.
|
||||
// - Busy timeout (5s) to make concurrent writes wait on each other instead of failing immediately.
|
||||
conn := path + "?_pragma=journal_mode=WAL&_pragma=synchronous=NORMAL&_pragma=busy_timeout=5000&_time_format=sqlite"
|
||||
db, err := sqlx.Connect("sqlite", conn)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("connect to SQLite database '%s': %w", conn, err)
|
||||
}
|
||||
|
||||
schema := `
|
||||
CREATE TABLE IF NOT EXISTS containers (
|
||||
id TEXT NOT NULL PRIMARY KEY,
|
||||
service_spec TEXT NOT NULL CHECK (json_valid(service_spec)),
|
||||
-- 'subsecond' modifier is used to store timestamps with millisecond precision.
|
||||
created_at TIMESTAMP NOT NULL DEFAULT (datetime('subsecond')),
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT (datetime('subsecond'))
|
||||
);
|
||||
`
|
||||
|
||||
if _, err = db.Exec(schema); err != nil {
|
||||
return nil, fmt.Errorf("create schema: %w", err)
|
||||
}
|
||||
|
||||
return db, nil
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
package dns
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/netip"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/psviderski/uncloud/internal/machine/store"
|
||||
"github.com/psviderski/uncloud/pkg/api"
|
||||
)
|
||||
|
||||
// ClusterResolver implements Resolver by tracking containers in the cluster and resolving service names
|
||||
// to their IP addresses.
|
||||
type ClusterResolver struct {
|
||||
store *store.Store
|
||||
// serviceIPs maps service names to container IPs.
|
||||
serviceIPs map[string][]netip.Addr
|
||||
// mu protects the serviceIPs map.
|
||||
mu sync.RWMutex
|
||||
// lastUpdate tracks when records were last updated.
|
||||
lastUpdate time.Time
|
||||
log *slog.Logger
|
||||
}
|
||||
|
||||
// NewClusterResolver creates a new cluster resolver using the cluster store.
|
||||
func NewClusterResolver(store *store.Store) *ClusterResolver {
|
||||
return &ClusterResolver{
|
||||
store: store,
|
||||
serviceIPs: make(map[string][]netip.Addr),
|
||||
log: slog.With("component", "dns-resolver"),
|
||||
}
|
||||
}
|
||||
|
||||
// Run starts watching for container changes and updates DNS records accordingly.
|
||||
func (r *ClusterResolver) Run(ctx context.Context) error {
|
||||
containers, changes, err := r.store.SubscribeContainers(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("subscribe to container changes: %w", err)
|
||||
}
|
||||
r.log.Info("Subscribed to container changes in the cluster to keep DNS records updated.")
|
||||
|
||||
// TODO: implement machine membership check using Corrossion Admin client to filter available containers.
|
||||
r.updateServiceIPs(containers)
|
||||
|
||||
for {
|
||||
select {
|
||||
case _, ok := <-changes:
|
||||
if !ok {
|
||||
return fmt.Errorf("containers subscription failed")
|
||||
}
|
||||
r.log.Debug("Cluster containers changed, updating DNS records.")
|
||||
|
||||
containers, err = r.store.ListContainers(ctx, store.ListOptions{})
|
||||
if err != nil {
|
||||
r.log.Error("Failed to list containers.", "err", err)
|
||||
continue
|
||||
}
|
||||
|
||||
// TODO: implement machine membership check using Corrossion Admin client to filter available containers.
|
||||
r.updateServiceIPs(containers)
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// updateServiceIPs processes container records and updates the serviceIPs map.
|
||||
func (r *ClusterResolver) updateServiceIPs(containers []store.ContainerRecord) {
|
||||
newServiceIPs := make(map[string][]netip.Addr, len(r.serviceIPs))
|
||||
|
||||
containersCount := 0
|
||||
for _, record := range containers {
|
||||
if !record.Container.Healthy() {
|
||||
continue
|
||||
}
|
||||
|
||||
ip := record.Container.UncloudNetworkIP()
|
||||
if !ip.IsValid() {
|
||||
// Container is not connected to the uncloud Docker network (could be host network).
|
||||
continue
|
||||
}
|
||||
|
||||
ctr := api.ServiceContainer{Container: record.Container}
|
||||
if ctr.ServiceID() == "" || ctr.ServiceName() == "" {
|
||||
// Container is not part of a service, skip it.
|
||||
continue
|
||||
}
|
||||
|
||||
// TODO: remove normalisation after implementing service name validation:
|
||||
//.https://github.com/psviderski/uncloud/issues/53
|
||||
serviceName := strings.ToLower(ctr.ServiceName())
|
||||
|
||||
newServiceIPs[serviceName] = append(newServiceIPs[serviceName], ip)
|
||||
// Also add the service ID as a valid lookup.
|
||||
newServiceIPs[ctr.ServiceID()] = append(newServiceIPs[ctr.ServiceID()], ip)
|
||||
containersCount++
|
||||
}
|
||||
|
||||
// Update the serviceIPs map atomically.
|
||||
r.mu.Lock()
|
||||
r.serviceIPs = newServiceIPs
|
||||
r.mu.Unlock()
|
||||
|
||||
r.log.Debug("DNS records updated.", "services", len(newServiceIPs)/2, "containers", containersCount)
|
||||
}
|
||||
|
||||
// Resolve returns IP addresses of the service containers.
|
||||
func (r *ClusterResolver) Resolve(serviceName string) []netip.Addr {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
|
||||
ips, ok := r.serviceIPs[serviceName]
|
||||
if !ok || len(ips) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Return a copy of the IPs slice to prevent modification of the original.
|
||||
ipsCopy := make([]netip.Addr, len(ips))
|
||||
copy(ipsCopy, ips)
|
||||
|
||||
return ipsCopy
|
||||
}
|
||||
@@ -0,0 +1,347 @@
|
||||
package dns
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"math/rand/v2"
|
||||
"net"
|
||||
"net/netip"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
const (
|
||||
// InternalDomain is the cluster internal domain for service discovery. All DNS queries ending with this suffix
|
||||
// will be resolved using the internal DNS server.
|
||||
InternalDomain = "internal."
|
||||
// Port is the standard DNS port.
|
||||
Port = 53
|
||||
// maxConcurrentForwards is the maximum number of concurrent forwarded queries to upstream DNS servers.
|
||||
// 1024 is the default used by the Docker internal DNS server.
|
||||
maxConcurrentForwards = 1024
|
||||
// forwardingTimeout is the timeout for forwarding a DNS query to an upstream server.
|
||||
forwardingTimeout = 3 * time.Second
|
||||
)
|
||||
|
||||
// Resolver is an interface for resolving service names to IP addresses.
|
||||
type Resolver interface {
|
||||
// Resolve returns a list of IP addresses of the service containers.
|
||||
// An empty list is returned if no service is found.
|
||||
Resolve(serviceName string) []netip.Addr
|
||||
}
|
||||
|
||||
// Server is an embedded internal DNS server for service discovery and forwarding external queries
|
||||
// to upstream DNS servers.
|
||||
type Server struct {
|
||||
listenAddr netip.Addr
|
||||
resolver Resolver
|
||||
upstreamServers []netip.AddrPort
|
||||
|
||||
udpServer *dns.Server
|
||||
tcpServer *dns.Server
|
||||
inProgressReqs sync.WaitGroup
|
||||
forwardSemaphore chan struct{}
|
||||
log *slog.Logger
|
||||
}
|
||||
|
||||
// NewServer creates a new DNS server with the given configuration.
|
||||
// If upstreams is nil, nameservers from /etc/resolv.conf will be used. An empty upstreams list means to only resolve
|
||||
// internal DNS queries and not forward any external queries.
|
||||
func NewServer(listenAddr netip.Addr, resolver Resolver, upstreams []netip.AddrPort) (*Server, error) {
|
||||
if !listenAddr.IsValid() {
|
||||
return nil, fmt.Errorf("invalid listen address: %s", listenAddr)
|
||||
}
|
||||
if resolver == nil {
|
||||
return nil, fmt.Errorf("resolver must be provided")
|
||||
}
|
||||
|
||||
// Load upstreams from /etc/resolv.conf and set fallback servers only if upstreams is not provided.
|
||||
if upstreams == nil {
|
||||
if nameservers, err := parseNameserversFromResolvConf(); err != nil {
|
||||
slog.Warn("Failed to parse nameservers from /etc/resolv.conf.", "err", err)
|
||||
} else {
|
||||
for _, ns := range nameservers {
|
||||
if ns.Compare(listenAddr) == 0 {
|
||||
// Skip this internal DNS server address if it has been configured in /etc/resolv.conf
|
||||
// to not create a forwarding loop.
|
||||
continue
|
||||
}
|
||||
upstreams = append(upstreams, netip.AddrPortFrom(ns, Port))
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to common public DNS servers if no nameservers were found in /etc/resolv.conf.
|
||||
if upstreams == nil {
|
||||
upstreams = []netip.AddrPort{
|
||||
netip.AddrPortFrom(netip.MustParseAddr("1.1.1.1"), Port), // Cloudflare DNS
|
||||
netip.AddrPortFrom(netip.MustParseAddr("8.8.8.8"), Port), // Google DNS
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &Server{
|
||||
listenAddr: listenAddr,
|
||||
resolver: resolver,
|
||||
upstreamServers: upstreams,
|
||||
forwardSemaphore: make(chan struct{}, maxConcurrentForwards),
|
||||
log: slog.With("component", "dns-server"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ListenAddr returns the address the DNS server is listening on.
|
||||
func (s *Server) ListenAddr() netip.Addr {
|
||||
return s.listenAddr
|
||||
}
|
||||
|
||||
// Run starts the DNS server listening on both UDP and TCP ports. The server on TCP is not critical so it won't return
|
||||
// an error if it fails to start. The server will run until the context is canceled or an error occurs.
|
||||
func (s *Server) Run(ctx context.Context) error {
|
||||
addr := net.JoinHostPort(s.listenAddr.String(), strconv.Itoa(Port))
|
||||
s.udpServer = &dns.Server{
|
||||
Addr: addr,
|
||||
Net: "udp",
|
||||
Handler: dns.HandlerFunc(s.handleRequest),
|
||||
}
|
||||
s.tcpServer = &dns.Server{
|
||||
Addr: addr,
|
||||
Net: "tcp",
|
||||
Handler: dns.HandlerFunc(s.handleRequest),
|
||||
}
|
||||
|
||||
errCh := make(chan error, 1) // Buffer size 1 is for UDP server error only.
|
||||
|
||||
go func() {
|
||||
s.log.Info("Starting DNS server on UDP port.", "addr", addr, "upstreams", s.upstreamServers)
|
||||
if err := s.udpServer.ListenAndServe(); err != nil {
|
||||
errCh <- fmt.Errorf("listen and serve on %s/udp: %w", addr, err)
|
||||
}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
s.log.Info("Starting DNS server on TCP port.", "addr", addr, "upstreams", s.upstreamServers)
|
||||
if err := s.tcpServer.ListenAndServe(); err != nil {
|
||||
// TCP server is not critical, so log the error and continue.
|
||||
slog.Warn("Failed to listen and serve DNS server on TCP port. "+
|
||||
"TCP server is not critical and will be ignored.", "addr", addr, "err", err)
|
||||
}
|
||||
}()
|
||||
|
||||
select {
|
||||
case err := <-errCh:
|
||||
// Stop the servers if the UDP one fails.
|
||||
s.stop()
|
||||
return err
|
||||
case <-ctx.Done():
|
||||
s.log.Info("Stopping DNS server.")
|
||||
return s.stop()
|
||||
}
|
||||
}
|
||||
|
||||
// stop gracefully shuts down the DNS server.
|
||||
func (s *Server) stop() error {
|
||||
var udpErr, tcpErr error
|
||||
|
||||
if s.udpServer != nil {
|
||||
udpErr = s.udpServer.Shutdown()
|
||||
}
|
||||
if s.tcpServer != nil {
|
||||
tcpErr = s.tcpServer.Shutdown()
|
||||
}
|
||||
|
||||
// Wait for all in-progress requests to finish.
|
||||
s.inProgressReqs.Wait()
|
||||
|
||||
if udpErr != nil {
|
||||
return fmt.Errorf("shutdown DNS server (UDP): %w", udpErr)
|
||||
}
|
||||
|
||||
if tcpErr != nil {
|
||||
return fmt.Errorf("shutdown DNS server (TCP): %w", tcpErr)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleRequest processes a DNS query and returns an appropriate response.
|
||||
func (s *Server) handleRequest(w dns.ResponseWriter, req *dns.Msg) {
|
||||
s.inProgressReqs.Add(1)
|
||||
defer s.inProgressReqs.Done()
|
||||
|
||||
if len(req.Question) == 0 {
|
||||
resp := new(dns.Msg).SetRcode(req, dns.RcodeFormatError)
|
||||
s.reply(w, req, resp)
|
||||
return
|
||||
}
|
||||
|
||||
// While the original DNS RFCs allow multiple questions, in practice it never works. So handle only the first one.
|
||||
q := req.Question[0]
|
||||
log := s.log.With("name", q.Name, "type", dns.TypeToString[q.Qtype])
|
||||
log.Debug("Received DNS query.")
|
||||
|
||||
if !dns.IsSubDomain(InternalDomain, dns.CanonicalName(q.Name)) {
|
||||
log.Debug("Forwarding non-internal DNS query to upstream DNS servers.")
|
||||
|
||||
// Use the same transport for the forwarded request as the original request.
|
||||
resp, err := s.forwardRequest(req, w.LocalAddr().Network())
|
||||
if err != nil {
|
||||
log.Error("Failed to forward DNS query.", "err", err)
|
||||
resp = new(dns.Msg).SetRcode(req, dns.RcodeServerFailure)
|
||||
}
|
||||
|
||||
s.reply(w, req, resp)
|
||||
return
|
||||
}
|
||||
|
||||
// Handle the query for the internal domain.
|
||||
resp := new(dns.Msg).SetReply(req)
|
||||
resp.Authoritative = true
|
||||
resp.RecursionAvailable = true
|
||||
|
||||
switch q.Qtype {
|
||||
case dns.TypeA:
|
||||
records := s.handleAQuery(q.Name)
|
||||
if len(records) > 0 {
|
||||
log.Debug("Found A records for internal DNS query.", "count", len(records))
|
||||
resp.Answer = append(resp.Answer, records...)
|
||||
} else {
|
||||
log.Debug("No records found for internal DNS query.")
|
||||
resp.SetRcode(req, dns.RcodeNameError)
|
||||
}
|
||||
// TODO: Handle other query types (SRV, TXT, etc.) as needed.
|
||||
}
|
||||
|
||||
// Truncate the response if it exceeds the maximum size for the transport protocol.
|
||||
maxSize := dns.MinMsgSize
|
||||
if w.LocalAddr().Network() == "tcp" {
|
||||
maxSize = dns.MaxMsgSize
|
||||
} else {
|
||||
// Retrieve the UDP buffer size from the EDNS0 record if present.
|
||||
if opt := req.IsEdns0(); opt != nil {
|
||||
if udpSize := int(opt.UDPSize()); udpSize > maxSize {
|
||||
maxSize = udpSize
|
||||
}
|
||||
}
|
||||
}
|
||||
resp.Truncate(maxSize)
|
||||
|
||||
s.reply(w, req, resp)
|
||||
}
|
||||
|
||||
func (s *Server) reply(w dns.ResponseWriter, req *dns.Msg, resp *dns.Msg) error {
|
||||
if err := w.WriteMsg(resp); err != nil {
|
||||
s.log.Error("Failed to write DNS response.", "err", err, "msg", resp)
|
||||
// It may fail due to a malformed response message, e.g. exceeding the maximum size. In that case,
|
||||
// attempt to send a server failure response instead so the client doesn't have to wait for a timeout.
|
||||
if resp.Rcode != dns.RcodeServerFailure {
|
||||
resp = new(dns.Msg).SetRcode(req, dns.RcodeServerFailure)
|
||||
if err2 := w.WriteMsg(resp); err2 != nil {
|
||||
s.log.Error("Failed to write DNS error response.", "err", err2, "msg", resp)
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// forwardRequest forwards a DNS query to system DNS servers
|
||||
func (s *Server) forwardRequest(req *dns.Msg, proto string) (*dns.Msg, error) {
|
||||
if len(s.upstreamServers) == 0 {
|
||||
return nil, errors.New("no upstream DNS servers configured")
|
||||
}
|
||||
|
||||
// Apply concurrency control for forwarded queries.
|
||||
select {
|
||||
case s.forwardSemaphore <- struct{}{}:
|
||||
defer func() {
|
||||
<-s.forwardSemaphore
|
||||
}()
|
||||
default:
|
||||
return nil, fmt.Errorf("too many concurrent forwarded queries (max: %d)", maxConcurrentForwards)
|
||||
}
|
||||
|
||||
// Create DNS client with timeout and same transport as original request
|
||||
client := &dns.Client{
|
||||
Net: proto,
|
||||
Timeout: forwardingTimeout,
|
||||
}
|
||||
|
||||
var lastErr error
|
||||
for _, server := range s.upstreamServers {
|
||||
resp, _, err := client.Exchange(req, server.String())
|
||||
if err == nil {
|
||||
return resp, nil
|
||||
}
|
||||
lastErr = err
|
||||
s.log.Debug("Failed to forward DNS query to upstream server.", "server", server, "err", err)
|
||||
}
|
||||
|
||||
return nil, lastErr
|
||||
}
|
||||
|
||||
// handleAQuery processes an A query for the internal domain and returns A records for the requested name.
|
||||
// The internal domain suffix is already stripped from the name. An empty list is returned if no records are found.
|
||||
func (s *Server) handleAQuery(name string) []dns.RR {
|
||||
serviceName := trimInternalDomain(name)
|
||||
ips := s.resolver.Resolve(serviceName)
|
||||
if len(ips) == 0 {
|
||||
s.log.Debug("Failed to resolve service name.", "service", serviceName)
|
||||
return nil
|
||||
}
|
||||
s.log.Debug("Resolved service name.", "service", serviceName, "ips", ips)
|
||||
|
||||
if len(ips) > 1 {
|
||||
// TODO: sort by proximity to the requesting container/machine. For now, just shuffle the IPs.
|
||||
rand.Shuffle(len(ips), func(i, j int) {
|
||||
ips[i], ips[j] = ips[j], ips[i]
|
||||
})
|
||||
}
|
||||
|
||||
// Create A records for each IP.
|
||||
records := make([]dns.RR, 0, len(ips))
|
||||
for _, ip := range ips {
|
||||
records = append(records, &dns.A{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: name,
|
||||
Rrtype: dns.TypeA,
|
||||
Class: dns.ClassINET,
|
||||
// TODO: should we increate the TTL to some reasonably small value like 5-30 seconds to allow
|
||||
// at least some caching?
|
||||
Ttl: 0,
|
||||
},
|
||||
A: net.ParseIP(ip.String()),
|
||||
})
|
||||
}
|
||||
return records
|
||||
}
|
||||
|
||||
// parseNameserversFromResolvConf parses the nameservers from /etc/resolv.conf.
|
||||
func parseNameserversFromResolvConf() ([]netip.Addr, error) {
|
||||
config, err := dns.ClientConfigFromFile("/etc/resolv.conf")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var servers []netip.Addr
|
||||
for _, server := range config.Servers {
|
||||
if addr, err := netip.ParseAddr(server); err == nil {
|
||||
servers = append(servers, addr)
|
||||
}
|
||||
}
|
||||
|
||||
return servers, nil
|
||||
}
|
||||
|
||||
func trimInternalDomain(name string) string {
|
||||
name = dns.CanonicalName(name)
|
||||
if !dns.IsSubDomain(InternalDomain, name) {
|
||||
return name
|
||||
}
|
||||
|
||||
return strings.TrimSuffix(name, "."+InternalDomain)
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user