mirror of
https://github.com/psviderski/uncloud.git
synced 2026-08-26 19:13:34 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1ce3e62dbb | ||
|
|
4be8339c51 | ||
|
|
8dd69b46da | ||
|
|
4cc1e556dd | ||
|
|
9186d31d12 | ||
|
|
dd7bc6c982 | ||
|
|
12c07812a2 | ||
|
|
ec73f9ecd8 | ||
|
|
879c7c1876 | ||
|
|
c67127f83f | ||
|
|
5d3f1fe225 | ||
|
|
2e585d0183 | ||
|
|
ae9f943404 | ||
|
|
8805178a58 | ||
|
|
ec3de3a099 | ||
|
|
2c02139369 | ||
|
|
6c244bb8f9 | ||
|
|
fc0bf4a91b | ||
|
|
bc577fe405 | ||
|
|
6cc0611d75 | ||
|
|
8beb9e2679 | ||
|
|
0c2ff499c6 | ||
|
|
b39585df4c | ||
|
|
35d0a90125 | ||
|
|
a7273c8c96 | ||
|
|
9b48003a3f | ||
|
|
1e750cc7e1 | ||
|
|
2d73b541c5 | ||
|
|
cd9c0a1d2c | ||
|
|
37a76809ec | ||
|
|
f6062cf343 | ||
|
|
4fe360c573 | ||
|
|
cac1550b7d | ||
|
|
0bd42b2fca | ||
|
|
ad83d942e1 | ||
|
|
65f5a714dd | ||
|
|
2714587ec5 | ||
|
|
4166474ee8 | ||
|
|
3faaac4da7 | ||
|
|
1b6130447f | ||
|
|
614212a24c | ||
|
|
3ab708a437 | ||
|
|
7a6c5bf6d7 | ||
|
|
2acfafe218 | ||
|
|
b24b55c4c7 | ||
|
|
9a88e914f4 | ||
|
|
f613d3ce6d | ||
|
|
2c3bea64e5 | ||
|
|
da3634b690 | ||
|
|
6fb07db4b2 | ||
|
|
fea7edcbc5 | ||
|
|
a54555cd13 | ||
|
|
87c7889c2e | ||
|
|
05d0078451 | ||
|
|
053d73048c | ||
|
|
4ec3e97a54 | ||
|
|
10bbe9fbc5 | ||
|
|
31cd4c77e9 | ||
|
|
492a0af2b2 | ||
|
|
ff7bb25f2c | ||
|
|
a73cbfd691 | ||
|
|
55773e92ce | ||
|
|
37f543b905 | ||
|
|
e5f96509bf | ||
|
|
2f35ac6498 | ||
|
|
f75afa0cec | ||
|
|
0f38b128fa | ||
|
|
535b91fe52 | ||
|
|
5c8beb8bd3 | ||
|
|
2b4da1e6fe | ||
|
|
be8b4f079f | ||
|
|
59b96cc01f | ||
|
|
4c77fe2cfd | ||
|
|
3b89af4a11 | ||
|
|
1abadaef50 | ||
|
|
3b1ce42dd7 | ||
|
|
1640b4c433 | ||
|
|
44549c00fd | ||
|
|
26f34df3e6 | ||
|
|
89fb9efcfa | ||
|
|
638f320959 | ||
|
|
3f2e60ea74 | ||
|
|
8e0a24e781 | ||
|
|
e3e39dde32 | ||
|
|
fc61e2fc07 | ||
|
|
2a0d31cec0 | ||
|
|
2f55af0cab | ||
|
|
971b21d6c1 | ||
|
|
b651d95183 | ||
|
|
d45c1b6869 | ||
|
|
a5fe9fbfb2 | ||
|
|
4590a516ad | ||
|
|
5f9ed6dacf | ||
|
|
e97497d737 | ||
|
|
b4bf1d17d0 | ||
|
|
cfb4693286 | ||
|
|
fd3facb6b0 | ||
|
|
bd03bb9bfa | ||
|
|
eec728620b | ||
|
|
d2dd5e3c89 | ||
|
|
2bfcce7803 | ||
|
|
1ec5184f21 | ||
|
|
23dfe45b2f | ||
|
|
d9e906a77e | ||
|
|
51f7ebf3bf | ||
|
|
ae04dea808 | ||
|
|
009c0e1b4c | ||
|
|
18d38e96b4 | ||
|
|
12c86f1dd4 | ||
|
|
4df1820f96 | ||
|
|
6f9ac1fcc7 | ||
|
|
6a98acbe79 | ||
|
|
b0059af9b1 | ||
|
|
7edd8f2976 | ||
|
|
735e36012a | ||
|
|
69ebce57e9 | ||
|
|
365a5fc0b5 | ||
|
|
624caf037b | ||
|
|
ff1a8fbcf9 | ||
|
|
3aa8fead76 | ||
|
|
4075f85b72 | ||
|
|
59f6fa75ca | ||
|
|
8b66a11394 | ||
|
|
fa3d992dbc | ||
|
|
a7d8afb052 | ||
|
|
0d820bc75a | ||
|
|
6a4e63dbd4 | ||
|
|
7699b530c2 | ||
|
|
f1abbbea6d | ||
|
|
683ad22358 | ||
|
|
8f00e0082e | ||
|
|
c34542a9d8 | ||
|
|
88db1cb6a9 | ||
|
|
dfa6e001c5 | ||
|
|
f49ec465d0 | ||
|
|
1ffe9acdd7 | ||
|
|
e35e69b4be | ||
|
|
11c2e641f1 | ||
|
|
bb97537df7 | ||
|
|
9f4b8179b5 | ||
|
|
f7b079c4f2 | ||
|
|
3648187219 | ||
|
|
93346301fb | ||
|
|
cc86c8660c | ||
|
|
2963436538 | ||
|
|
6e326277bb | ||
|
|
469d6fadcd | ||
|
|
5f214247ca | ||
|
|
0b8495e964 | ||
|
|
6068e24f4f | ||
|
|
7861d56dff | ||
|
|
7bc69b9a9e | ||
|
|
38800924ed | ||
|
|
cf7e333579 | ||
|
|
769f5e47c3 | ||
|
|
6e1165ec57 | ||
|
|
8d16da596c | ||
|
|
d6c06828b3 | ||
|
|
fe93d91894 | ||
|
|
3f70018e7c | ||
|
|
70e9385032 | ||
|
|
db3016fe89 | ||
|
|
c56385602e | ||
|
|
fbbf839258 | ||
|
|
eee28e2ead | ||
|
|
ef57b62625 | ||
|
|
e854bbb3e1 |
@@ -0,0 +1,20 @@
|
|||||||
|
# EditorConfig is awesome: https://editorconfig.org
|
||||||
|
|
||||||
|
root = true
|
||||||
|
|
||||||
|
# Default settings for all files
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
end_of_line = lf
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
# Settings for Go files
|
||||||
|
[*.go]
|
||||||
|
indent_style = tab
|
||||||
|
indent_size = 4
|
||||||
|
|
||||||
|
# Settings for Bash scripts
|
||||||
|
[*.sh]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 4
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
github: [psviderski]
|
||||||
Symlink
+1
@@ -0,0 +1 @@
|
|||||||
|
../AI.md
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 525 KiB |
@@ -0,0 +1,69 @@
|
|||||||
|
name: Go Tests
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- "main"
|
||||||
|
- "test/**"
|
||||||
|
- "release/**"
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
paths:
|
||||||
|
- ".github/**"
|
||||||
|
- "**.go"
|
||||||
|
- "**.proto"
|
||||||
|
- "go.*"
|
||||||
|
- "Makefile"
|
||||||
|
- "scripts/**"
|
||||||
|
- "test/**"
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
|
|
||||||
|
- name: Set up Go
|
||||||
|
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
|
||||||
|
with:
|
||||||
|
go-version: "1.23.2"
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
go mod tidy
|
||||||
|
git diff --exit-code ||
|
||||||
|
(echo "go.mod or go.sum has changed. Please run 'go mod tidy' and commit the changes." && exit 1)
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
run: |
|
||||||
|
make ucind-image
|
||||||
|
make test
|
||||||
|
timeout-minutes: 10
|
||||||
|
|
||||||
|
check-protobuf:
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
timeout-minutes: 10
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
# Run on more platforms so that mise lockfile checksums are checked/generated for all platforms.
|
||||||
|
# See https://github.com/jdx/mise/issues/4276 for details.
|
||||||
|
os: [ubuntu-latest, macos-latest]
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
|
|
||||||
|
- name: Install Mise
|
||||||
|
uses: jdx/mise-action@13abe502c30c1559a5c37dff303831bab82c9402 # v2.2.3
|
||||||
|
with:
|
||||||
|
version: "2025.6.5"
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ github.token }}
|
||||||
|
|
||||||
|
- name: Generate Protobuf files
|
||||||
|
run: |
|
||||||
|
make proto
|
||||||
|
# check if the generated files are up to date
|
||||||
|
git diff --exit-code ||
|
||||||
|
(echo "Generated protobuf files are not up to date. Please run 'make proto' and commit the changes." && exit 1)
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
name: Lint
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- "main"
|
||||||
|
- "test/**"
|
||||||
|
- "release/**"
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
paths:
|
||||||
|
- "**.go"
|
||||||
|
- "go.*"
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
jobs:
|
||||||
|
lint:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
|
|
||||||
|
- name: Set up Go
|
||||||
|
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
|
||||||
|
with:
|
||||||
|
go-version: "1.23.2"
|
||||||
|
|
||||||
|
- name: golangci-lint
|
||||||
|
uses: golangci/golangci-lint-action@4afd733a84b1f43292c63897423277bb7f4313a9 # v8.0.0
|
||||||
|
with:
|
||||||
|
version: v2.2.2
|
||||||
|
|
||||||
|
- name: Format code
|
||||||
|
run: |
|
||||||
|
make format
|
||||||
|
git diff --exit-code ||
|
||||||
|
(echo "Code is not formatted. Please run 'make format' and commit the changes." && exit 1)
|
||||||
|
|
||||||
|
timeout-minutes: 10
|
||||||
@@ -30,6 +30,7 @@ go.work.sum
|
|||||||
/data/
|
/data/
|
||||||
/dist/
|
/dist/
|
||||||
/uc
|
/uc
|
||||||
|
/tmp
|
||||||
|
|
||||||
# Web
|
# Web
|
||||||
node_modules/
|
node_modules/
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
version: "2"
|
||||||
|
|
||||||
|
run:
|
||||||
|
concurrency: 4
|
||||||
|
tests: true
|
||||||
|
timeout: 5m
|
||||||
|
|
||||||
|
linters:
|
||||||
|
default: none
|
||||||
|
enable:
|
||||||
|
- bodyclose
|
||||||
|
# - dogsled
|
||||||
|
- dupl
|
||||||
|
# - errcheck
|
||||||
|
- gochecknoinits
|
||||||
|
- goconst
|
||||||
|
# - gocritic
|
||||||
|
# - gocyclo
|
||||||
|
# - godot
|
||||||
|
# - gosec
|
||||||
|
# - govet
|
||||||
|
- ineffassign
|
||||||
|
- misspell
|
||||||
|
- nakedret
|
||||||
|
# - prealloc
|
||||||
|
# - revive
|
||||||
|
# - staticcheck
|
||||||
|
- unconvert
|
||||||
|
# - unparam
|
||||||
|
# - unused
|
||||||
|
- whitespace
|
||||||
|
exclusions:
|
||||||
|
rules:
|
||||||
|
- path: ^test/e2e
|
||||||
|
linters:
|
||||||
|
- goconst # constants here add no value, so we skip goconst only for test/e2e.
|
||||||
|
|
||||||
|
formatters:
|
||||||
|
enable:
|
||||||
|
- gofumpt
|
||||||
|
- goimports
|
||||||
|
|
||||||
@@ -24,6 +24,8 @@ builds:
|
|||||||
goarch:
|
goarch:
|
||||||
- amd64
|
- amd64
|
||||||
- arm64
|
- arm64
|
||||||
|
ldflags:
|
||||||
|
- -s -w -X github.com/psviderski/uncloud/internal/version.version={{ .Version }}
|
||||||
|
|
||||||
- id: uncloudd
|
- id: uncloudd
|
||||||
main: ./cmd/uncloudd
|
main: ./cmd/uncloudd
|
||||||
@@ -35,6 +37,8 @@ builds:
|
|||||||
goarch:
|
goarch:
|
||||||
- amd64
|
- amd64
|
||||||
- arm64
|
- arm64
|
||||||
|
ldflags:
|
||||||
|
- -s -w -X github.com/psviderski/uncloud/internal/version.version={{ .Version }}
|
||||||
|
|
||||||
archives:
|
archives:
|
||||||
- id: uncloud
|
- id: uncloud
|
||||||
|
|||||||
+39
@@ -0,0 +1,39 @@
|
|||||||
|
[tools.go]
|
||||||
|
version = "1.23.10"
|
||||||
|
backend = "core:go"
|
||||||
|
|
||||||
|
[tools.go.checksums]
|
||||||
|
"go1.23.10.darwin-arm64.tar.gz" = "sha256:25c64bfa8a8fd8e7f62fb54afa4354af8409a4bb2358c2699a1003b733e6fce5"
|
||||||
|
"go1.23.10.linux-amd64.tar.gz" = "sha256:535f9f81802499f2a7dbfa70abb8fda3793725fcc29460f719815f6e10b5fd60"
|
||||||
|
|
||||||
|
[tools.golangci-lint]
|
||||||
|
version = "2.2.2"
|
||||||
|
backend = "aqua:golangci/golangci-lint"
|
||||||
|
|
||||||
|
[tools.golangci-lint.checksums]
|
||||||
|
"golangci-lint-2.2.2-darwin-arm64.tar.gz" = "sha256:d84d94d042c0d495fd1746f3d18948a75de163b17a14e8de3ef840928dd2df74"
|
||||||
|
"golangci-lint-2.2.2-linux-amd64.tar.gz" = "sha256:c27fbde948a87d326feacd21df2f61a9c54dbd2e3bfa185c0a1cd6917a6f964f"
|
||||||
|
|
||||||
|
[tools.protoc]
|
||||||
|
version = "27.3"
|
||||||
|
backend = "aqua:protocolbuffers/protobuf/protoc"
|
||||||
|
|
||||||
|
[tools.protoc.checksums]
|
||||||
|
"protoc-27.3-linux-x86_64.zip" = "sha256:6dab2adab83f915126cab53540d48957c40e9e9023969c3e84d44bfb936c7741"
|
||||||
|
"protoc-27.3-osx-aarch_64.zip" = "sha256:b22116bd97cdbd7ea25346abe635a9df268515fe5ef5afa93cd9a68fc2513f84"
|
||||||
|
|
||||||
|
[tools.protoc-gen-go]
|
||||||
|
version = "1.34.2"
|
||||||
|
backend = "aqua:protocolbuffers/protobuf-go/protoc-gen-go"
|
||||||
|
|
||||||
|
[tools.protoc-gen-go.checksums]
|
||||||
|
"protoc-gen-go.v1.34.2.darwin.arm64.tar.gz" = "sha256:17aca7f948dbb624049030cf841e35895cf34183ba006e721247fdeb95ff2780"
|
||||||
|
"protoc-gen-go.v1.34.2.linux.amd64.tar.gz" = "sha256:b87bc134dee55576a842141bf0ed27761c635d746780fce5dee038c6dd16554f"
|
||||||
|
|
||||||
|
[tools.protoc-gen-go-grpc]
|
||||||
|
version = "1.5.1"
|
||||||
|
backend = "aqua:grpc/grpc-go/protoc-gen-go-grpc"
|
||||||
|
|
||||||
|
[tools.protoc-gen-go-grpc.checksums]
|
||||||
|
"protoc-gen-go-grpc.v1.5.1.darwin.arm64.tar.gz" = "sha256:d6083feb51dcfe59f26793e99ef01ef5eac68b64294ec2546711f614ac5878f3"
|
||||||
|
"protoc-gen-go-grpc.v1.5.1.linux.amd64.tar.gz" = "sha256:a6cac4ea731e54aea304ad44d704a69d1cdc82997084b33637e21a89dc9229d6"
|
||||||
@@ -1,5 +1,12 @@
|
|||||||
|
[settings]
|
||||||
|
experimental = true
|
||||||
|
|
||||||
[tools]
|
[tools]
|
||||||
go = "1.23"
|
go = "1.23"
|
||||||
|
golangci-lint = "2.2.2"
|
||||||
|
protoc = "27.3"
|
||||||
|
protoc-gen-go = "1.34.2"
|
||||||
|
protoc-gen-go-grpc = "1.5.1"
|
||||||
|
|
||||||
[env]
|
[env]
|
||||||
_.file = ".env"
|
_.file = ".env"
|
||||||
|
|||||||
@@ -0,0 +1,244 @@
|
|||||||
|
# AI.md - Uncloud Project Guide
|
||||||
|
|
||||||
|
This document provides comprehensive information about the Uncloud project for AI assistants to understand the codebase, architecture, and development practices.
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
**Uncloud** is a lightweight clustering and container orchestration tool that enables deployment and management of web applications across cloud VMs and bare metal servers. It creates a secure WireGuard mesh network between Docker hosts and provides automatic service discovery, load balancing, HTTPS ingress, and simple CLI commands for application management.
|
||||||
|
|
||||||
|
### Key Characteristics
|
||||||
|
|
||||||
|
- **Language**: Go
|
||||||
|
- **Architecture**: Decentralized, no control plane
|
||||||
|
- **Target**: Self-hosted infrastructure without Kubernetes complexity
|
||||||
|
- **License**: View LICENSE file for details
|
||||||
|
- **Status**: Active development, not yet ready for production
|
||||||
|
|
||||||
|
## Core Features
|
||||||
|
|
||||||
|
### 🏗️ Infrastructure
|
||||||
|
|
||||||
|
- **Multi-machine deployment**: Combine cloud VMs, dedicated servers, and bare metal
|
||||||
|
- **Zero-config networking**: Automatic WireGuard mesh with NAT traversal
|
||||||
|
- **Decentralized design**: No central control plane, all machines are equal
|
||||||
|
- **Service discovery**: Built-in DNS server resolves service names to container IPs
|
||||||
|
|
||||||
|
### 🚀 Application Management
|
||||||
|
|
||||||
|
- **Docker Compose compatibility**: Uses familiar Docker Compose format
|
||||||
|
- **Zero-downtime deployments**: Rolling updates without service interruption
|
||||||
|
- **Automatic HTTPS**: Caddy reverse proxy with Let's Encrypt integration
|
||||||
|
- **Managed DNS**: Free `*.cluster.uncloud.run` subdomains via Uncloud DNS service
|
||||||
|
- **Cross-machine scaling**: Run containers across multiple machines
|
||||||
|
|
||||||
|
### 🔧 Developer Experience
|
||||||
|
|
||||||
|
- **Docker-like CLI**: Familiar commands (`uc` binary)
|
||||||
|
- **Imperative operations**: Direct commands vs. declarative state reconciliation
|
||||||
|
- **Remote management**: Control entire infrastructure via SSH to any machine
|
||||||
|
- **Minimal overhead**: ~150MB RAM footprint per machine
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Core Components
|
||||||
|
|
||||||
|
1. **CLI (`uc`)** - Main user interface for cluster management
|
||||||
|
2. **Daemon (`uncloudd`)** - Machine daemon running on each node
|
||||||
|
3. **Corrosion** - Distributed SQLite database for cluster state (Fly.io project)
|
||||||
|
4. **Caddy** - Reverse proxy for HTTPS termination and routing
|
||||||
|
5. **WireGuard** - Secure mesh networking between machines
|
||||||
|
|
||||||
|
### Network Architecture
|
||||||
|
|
||||||
|
- Each machine gets unique subnet (e.g., `10.210.0.0/24`, `10.210.1.0/24`)
|
||||||
|
- Containers get cluster-unique IPs for direct communication
|
||||||
|
- Automatic peer discovery and key management
|
||||||
|
- NAT traversal for machines behind firewalls
|
||||||
|
|
||||||
|
### State Management
|
||||||
|
|
||||||
|
- **CRDT-based distributed storage** using Corrosion
|
||||||
|
- **Eventually consistent** state across all machines
|
||||||
|
- **Gossip protocol** (Serf) for state propagation
|
||||||
|
- **No quorum requirements** - partial network splits remain functional
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
### Key Directories
|
||||||
|
|
||||||
|
- **`cmd/`**: Contains main applications
|
||||||
|
|
||||||
|
- `uncloud/`: CLI tool with subcommands for machine, service, volume management
|
||||||
|
- `uncloudd/`: Daemon that runs on each machine
|
||||||
|
- `ucind/`: Development cluster management for testing
|
||||||
|
|
||||||
|
- **`internal/`**: Internal implementation packages
|
||||||
|
|
||||||
|
- `cli/`: Command-line interface logic
|
||||||
|
- `machine/`: Machine lifecycle and state management
|
||||||
|
- `daemon/`: Daemon implementation and gRPC services
|
||||||
|
- `dns/`: Internal DNS server for service discovery
|
||||||
|
|
||||||
|
- **`pkg/`**: Public API packages for external use
|
||||||
|
|
||||||
|
- `api/`: Core API types and definitions
|
||||||
|
- `client/`: Client libraries for interacting with Uncloud
|
||||||
|
|
||||||
|
- **`experiment/`**: Experimental features and prototypes
|
||||||
|
- **`scripts/`**: Installation and utility scripts
|
||||||
|
- **`test/`**: Test suites and test infrastructure
|
||||||
|
- **`website/`**: Documentation website (Docusaurus)
|
||||||
|
- **`misc/`**: Design documents and guides
|
||||||
|
|
||||||
|
## Key Technologies
|
||||||
|
|
||||||
|
### Core Dependencies
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Networking and orchestration
|
||||||
|
github.com/docker/docker // Docker API client
|
||||||
|
github.com/docker/compose/v2 // Docker Compose integration
|
||||||
|
golang.zx2c4.com/wireguard // WireGuard implementation
|
||||||
|
github.com/hashicorp/serf // Gossip protocol
|
||||||
|
|
||||||
|
// State management
|
||||||
|
github.com/ipfs/go-ds-crdt // CRDT distributed storage
|
||||||
|
github.com/dgraph-io/badger/v3 // Embedded database
|
||||||
|
|
||||||
|
// Web proxy
|
||||||
|
github.com/caddyserver/caddy/v2 // HTTP server and reverse proxy
|
||||||
|
|
||||||
|
// CLI and UX
|
||||||
|
github.com/spf13/cobra // CLI framework
|
||||||
|
github.com/charmbracelet/huh // Interactive forms
|
||||||
|
|
||||||
|
// gRPC and networking
|
||||||
|
google.golang.org/grpc // gRPC framework
|
||||||
|
github.com/siderolabs/grpc-proxy // gRPC proxy for forwarding
|
||||||
|
```
|
||||||
|
|
||||||
|
## Development Workflow
|
||||||
|
|
||||||
|
### Build and Development
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build binaries
|
||||||
|
go build -o uncloud ./cmd/uncloud
|
||||||
|
go build -o uncloudd ./cmd/uncloudd
|
||||||
|
```
|
||||||
|
|
||||||
|
### Key Make Targets
|
||||||
|
|
||||||
|
- `proto`: Generate protobuf code
|
||||||
|
- `ucind-cluster`: Create development cluster
|
||||||
|
- `update-dev`: Deploy to development machines
|
||||||
|
- `demo-reset`: Reset demo environment
|
||||||
|
- `fmt`: Format code
|
||||||
|
- `test`: Run all tests
|
||||||
|
- `lint`: Lint the code using golangci-lint
|
||||||
|
- `lint-and-fix`: Lint the code and fix issues whenever possible
|
||||||
|
|
||||||
|
## CLI Commands Structure
|
||||||
|
|
||||||
|
The `uc` CLI provides these main command groups:
|
||||||
|
|
||||||
|
### Machine Management
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uc machine init <user@host> # Initialize new cluster
|
||||||
|
uc machine add <user@host> # Add machine to cluster
|
||||||
|
uc machine ls # List machines
|
||||||
|
uc machine rm <name> # Remove machine
|
||||||
|
```
|
||||||
|
|
||||||
|
### Service Management
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uc run <image> # Run container from image
|
||||||
|
uc deploy # Deploy from compose.yaml
|
||||||
|
uc scale <service> <count> # Scale service replicas
|
||||||
|
uc ls # List services
|
||||||
|
uc rm <service> # Remove service
|
||||||
|
```
|
||||||
|
|
||||||
|
### Context and Connectivity
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uc context ls # List available contexts
|
||||||
|
uc context use <name> # Switch context
|
||||||
|
```
|
||||||
|
|
||||||
|
### Global Flags
|
||||||
|
|
||||||
|
- `--connect`: Connect to remote machine directly, without a config file
|
||||||
|
- `--uncloud-config`: Override config file path
|
||||||
|
|
||||||
|
## Development Guidelines
|
||||||
|
|
||||||
|
### Code Organization
|
||||||
|
|
||||||
|
- **Package naming**: Use clear, descriptive names
|
||||||
|
- **Error handling**: Wrap errors with context using `fmt.Errorf`
|
||||||
|
- **Logging**: Use structured logging with levels
|
||||||
|
- **gRPC**: Services defined in `internal/machine/api/pb/`
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
|
||||||
|
- Unit tests alongside source files (`*_test.go`)
|
||||||
|
- Integration tests in `test/e2e/`
|
||||||
|
- Test fixtures in `test/fixtures/`
|
||||||
|
|
||||||
|
### Dependencies
|
||||||
|
|
||||||
|
- Prefer standard library when possible
|
||||||
|
- Pin versions in `go.mod`
|
||||||
|
- Document rationale for external dependencies
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
|
||||||
|
- Support environment variables for key settings
|
||||||
|
- Validate configuration early
|
||||||
|
- Provide sensible defaults
|
||||||
|
|
||||||
|
## Troubleshooting and Debugging
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
- **Networking**: Check WireGuard status, iptables rules
|
||||||
|
- **DNS**: Verify service discovery resolution
|
||||||
|
- **Containers**: Use standard Docker debugging tools
|
||||||
|
- **State sync**: Check Corrosion logs for replication issues
|
||||||
|
|
||||||
|
### Debugging Tools
|
||||||
|
|
||||||
|
- Standard Linux networking tools (`ping`, `traceroute`, `wireshark`)
|
||||||
|
- Docker commands (`docker ps`, `docker logs`)
|
||||||
|
- SSH access to machines for direct inspection
|
||||||
|
- gRPC debugging tools
|
||||||
|
|
||||||
|
### Logs and Monitoring
|
||||||
|
|
||||||
|
- Systemd services (getting logs via `journalctl -u SERVICE_NAME`)
|
||||||
|
- `uncloud` -- Uncloud daemon
|
||||||
|
- `uncloud-corrosion` -- Corrosion process
|
||||||
|
- Machine daemon logs
|
||||||
|
- Container logs via Docker
|
||||||
|
|
||||||
|
## File Patterns and Conventions
|
||||||
|
|
||||||
|
### Important Files to Understand
|
||||||
|
|
||||||
|
- `cmd/uncloud/main.go`: CLI entry point and command structure
|
||||||
|
- `internal/cli/cli.go`: CLI implementation and configuration
|
||||||
|
- `internal/machine/machine.go`: Core machine management
|
||||||
|
- `pkg/api/`: Public API definitions
|
||||||
|
- `misc/design.md`: Architecture and design philosophy
|
||||||
|
- `README.md`: User-facing documentation
|
||||||
|
|
||||||
|
### Configuration Files
|
||||||
|
|
||||||
|
- `go.mod/go.sum`: Go dependency management
|
||||||
|
- `Makefile`: Build and development tasks
|
||||||
|
- `Dockerfile`: Container build instructions forUncloud-in-Docker (used for testing)
|
||||||
|
|
||||||
|
This document should help AI assistants understand the project structure, make informed suggestions, and contribute effectively to the Uncloud codebase.
|
||||||
+12
-3
@@ -17,13 +17,22 @@ RUN GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -o uncloudd cmd/uncloudd/main
|
|||||||
|
|
||||||
|
|
||||||
FROM alpine:${ALPINE_VERSION} AS corrosion-download
|
FROM alpine:${ALPINE_VERSION} AS corrosion-download
|
||||||
RUN wget -q -O /tmp/corrosion.tar.gz \
|
ARG TARGETARCH
|
||||||
https://github.com/psviderski/corrosion/releases/latest/download/corrosion-aarch64-unknown-linux-gnu.tar.gz \
|
|
||||||
|
RUN CORROSION_ARCH=$(case "${TARGETARCH}" in \
|
||||||
|
"amd64") echo "x86_64" ;; \
|
||||||
|
"arm64") echo "aarch64" ;; \
|
||||||
|
*) echo "Architecture '${TARGETARCH}' not supported" >&2 && exit 1 ;; \
|
||||||
|
esac) \
|
||||||
|
&& wget -q -O /tmp/corrosion.tar.gz \
|
||||||
|
"https://github.com/psviderski/corrosion/releases/latest/download/corrosion-${CORROSION_ARCH}-unknown-linux-gnu.tar.gz" \
|
||||||
&& tar -xzf /tmp/corrosion.tar.gz -C /tmp \
|
&& tar -xzf /tmp/corrosion.tar.gz -C /tmp \
|
||||||
&& install /tmp/corrosion /usr/local/bin/corrosion \
|
&& install /tmp/corrosion /usr/local/bin/corrosion \
|
||||||
&& rm /tmp/corrosion.tar.gz /tmp/corrosion
|
&& rm /tmp/corrosion.tar.gz /tmp/corrosion
|
||||||
|
|
||||||
FROM chainguard/wolfi-base:latest AS corrosion
|
# Beware that more modern images like chainguard/wolfi-base build glibc with flags that require newer CPU features,
|
||||||
|
# e.g. x86-64-v2. So such image may fail with "Fatal glibc error: CPU does not support x86-64-v2" on older CPUs.
|
||||||
|
FROM gcr.io/distroless/cc-debian12:latest AS corrosion
|
||||||
COPY --from=corrosion-download /usr/local/bin/corrosion /usr/local/bin/corrosion
|
COPY --from=corrosion-download /usr/local/bin/corrosion /usr/local/bin/corrosion
|
||||||
CMD ["corrosion", "agent"]
|
CMD ["corrosion", "agent"]
|
||||||
|
|
||||||
|
|||||||
@@ -36,23 +36,78 @@ demo-reset:
|
|||||||
ssh root@5.223.45.199 "AUTO_CONFIRM=true sudo -E uncloud-uninstall"
|
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"
|
ssh spy@192.168.40.243 "AUTO_CONFIRM=true sudo -E uncloud-uninstall"
|
||||||
|
|
||||||
|
.PHONY: ucind-cluster
|
||||||
|
ucind-cluster:
|
||||||
|
go run ./cmd/ucind cluster rm && go run ./cmd/ucind cluster create -m 3
|
||||||
|
|
||||||
.PHONY: proto
|
.PHONY: proto
|
||||||
proto:
|
proto:
|
||||||
protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative \
|
protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative \
|
||||||
--proto_path=. --proto_path=internal/machine/api/vendor internal/machine/api/pb/*.proto
|
--proto_path=. --proto_path=internal/machine/api/vendor internal/machine/api/pb/*.proto
|
||||||
|
|
||||||
|
.PHONY: proto-mise
|
||||||
|
proto-mise:
|
||||||
|
mise exec -- protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative \
|
||||||
|
--proto_path=. --proto_path=internal/machine/api/vendor internal/machine/api/pb/*.proto
|
||||||
|
|
||||||
.PHONY: corrosion-image
|
.PHONY: corrosion-image
|
||||||
corrosion-image:
|
corrosion-image:
|
||||||
docker build -t "$(CORROSION_IMAGE)" --target corrosion .
|
docker build -t "$(CORROSION_IMAGE)" --target corrosion .
|
||||||
|
|
||||||
.PHONY: corrosion-image-push
|
.PHONY: corrosion-multiarch-image-push
|
||||||
corrosion-image-push: corrosion-image
|
corrosion-multiarch-image-push:
|
||||||
docker push "$(CORROSION_IMAGE)"
|
docker buildx build --push --platform linux/amd64,linux/arm64 -t "$(CORROSION_IMAGE)" --target corrosion .
|
||||||
|
|
||||||
.PHONY: ucind-image
|
.PHONY: ucind-image
|
||||||
ucind-image:
|
ucind-image:
|
||||||
docker build -t "$(UCIND_IMAGE)" --target ucind .
|
docker build -t "$(UCIND_IMAGE)" --target ucind .
|
||||||
|
|
||||||
.PHONY: ucind-image-push
|
.PHONY: ucind-multiarch-image-push
|
||||||
ucind-image-push: ucind-image
|
ucind-multiarch-image-push:
|
||||||
docker push "$(UCIND_IMAGE)"
|
docker buildx build --push --platform linux/amd64,linux/arm64 -t "$(UCIND_IMAGE)" --target ucind .
|
||||||
|
|
||||||
|
.PHONY: test
|
||||||
|
test:
|
||||||
|
ifeq ($(TEST_NAME),)
|
||||||
|
go test -count=1 -v ./...
|
||||||
|
else
|
||||||
|
go test -race -count=1 -v -run ^$(TEST_NAME)$$ ./...
|
||||||
|
endif
|
||||||
|
|
||||||
|
.PHONY: test-e2e
|
||||||
|
test-e2e:
|
||||||
|
go test -race -count=1 -v ./test/e2e
|
||||||
|
|
||||||
|
.PHONY: test-clean
|
||||||
|
test-clean:
|
||||||
|
@CONTAINERS=$$(docker ps --filter "name=ucind-test" -q); \
|
||||||
|
if [ -n "$$CONTAINERS" ]; then \
|
||||||
|
echo "Killing containers..."; \
|
||||||
|
docker kill $$CONTAINERS; \
|
||||||
|
fi; \
|
||||||
|
CONTAINERS_STOPPED=$$(docker ps -a --filter "name=ucind-test" -q); \
|
||||||
|
if [ -n "$$CONTAINERS_STOPPED" ]; then \
|
||||||
|
echo "Removing stopped containers..."; \
|
||||||
|
docker rm $$CONTAINERS_STOPPED; \
|
||||||
|
else \
|
||||||
|
echo "Nothing to clean."; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
.PHONY: vet
|
||||||
|
vet:
|
||||||
|
go vet ./...
|
||||||
|
|
||||||
|
.PHONY: format fmt
|
||||||
|
format fmt:
|
||||||
|
GOOS=linux golangci-lint fmt
|
||||||
|
|
||||||
|
LINT_TARGETS := lint lint-and-fix
|
||||||
|
.PHONY: $(LINT_TARGETS) _lint
|
||||||
|
$(LINT_TARGETS): _lint
|
||||||
|
lint: ARGS=
|
||||||
|
lint-and-fix: ARGS=--fix
|
||||||
|
_lint:
|
||||||
|
# Explicitly set OS to Linux to not skip *_linux.go files when running on macOS.
|
||||||
|
# Uncloud daemon won't likely support OS other than Linux anytime soon, so for now we can rely on that.
|
||||||
|
GOOS=linux golangci-lint run $(ARGS)
|
||||||
|
|
||||||
|
|||||||
@@ -1,40 +1,43 @@
|
|||||||
<div align="center">
|
<div align="center">
|
||||||
<img src="./website/images/logo.svg" height="100" alt="Uncloud logo"/>
|
<img src="./website/landing/images/logo.svg" height="100" width="100" alt="Uncloud logo"/>
|
||||||
<h1>Uncloud</h1>
|
<h1>Uncloud</h1>
|
||||||
<p><strong>Docker simplicity. Multi-machine power.</strong></p>
|
<p><strong>Docker simplicity. Multi-machine power.</strong></p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<a href="https://uncloud.run/docs"><img src="https://img.shields.io/badge/Docs-blue.svg?style=for-the-badge&logo=gitbook&logoColor=white" alt="Documentation"></a>
|
||||||
|
<a href="https://discord.gg/eR35KQJhPu"><img src="https://img.shields.io/badge/discord-5865F2.svg?style=for-the-badge&logo=discord&logoColor=white" alt="Join Discord"></a>
|
||||||
|
<a href="https://x.com/psviderski"><img src="https://img.shields.io/badge/follow-black?style=for-the-badge&logo=X&logoColor=while" alt="Follow on X"></a>
|
||||||
|
<a href="https://github.com/sponsors/psviderski"><img src="https://img.shields.io/badge/Donate-EA4AAA.svg?style=for-the-badge&logo=githubsponsors&logoColor=white" alt="Donate"></a>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
Uncloud is a lightweight clustering and container orchestration tool that lets you deploy and manage web apps across
|
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
|
your Docker hosts and provides automatic service discovery, load balancing, ingress with HTTPS, and simple CLI commands
|
||||||
to manage your apps.
|
to manage your apps.
|
||||||
|
|
||||||
Unlike traditional orchestrators, there's no central control plane and quorum to maintain. Each machine maintains a
|
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.
|
some machines go offline.
|
||||||
|
|
||||||
Uncloud aims to be the solution for developers who want the flexibility of self-hosted infrastructure without the
|
Uncloud is the solution for developers who want the flexibility of self-hosted infrastructure without the operational
|
||||||
operational complexity of Kubernetes.
|
complexity of Kubernetes.
|
||||||
|
|
||||||
## 🎬 Quick demo
|
|
||||||
|
|
||||||
Deploy a highly available web app with automatic HTTPS across multiple regions and on-premises in just a couple minutes.
|
|
||||||
|
|
||||||
<a href="https://uncloud.wistia.com/medias/k47uwt9uau?wvideo=k47uwt9uau">
|
|
||||||
<img src="https://embed-ssl.wistia.com/deliveries/3cf7014a48b93afc556444bed3e39a8c.jpg?image_crop_resized=900x526&image_play_button_rounded=true&image_play_button_size=2x&image_play_button_color=18181Be0" alt="Uncloud demo" width="450" height="263" />
|
|
||||||
</a>
|
|
||||||
|
|
||||||
## ✨ Features
|
## ✨ Features
|
||||||
|
|
||||||
* **Deploy anywhere**: Combine cloud VMs, dedicated servers, and bare metal into a unified computing environment —
|
* **Deploy anywhere**: Combine cloud VMs, dedicated servers, and bare metal into a unified computing environment —
|
||||||
regardless of location or provider.
|
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
|
* **Zero-config private network**: Automatic WireGuard mesh with peer discovery and NAT traversal. Containers get unique
|
||||||
IPs for direct cross-machine communication.
|
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
|
* **Imperative over declarative**: Favoring imperative operations over state reconciliation simplifies both the mental
|
||||||
model and troubleshooting.
|
model and troubleshooting.
|
||||||
* **Zero-downtime deployments**: Rolling updates with health checks and automatic rollback on failure.
|
|
||||||
* **Service discovery**: Built-in DNS server resolves service names to container IPs.
|
|
||||||
* **Managed DNS**: Automatic DNS records `*.<id>.cluster.uncloud.run` for services with public access via managed
|
* **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.
|
[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
|
* **Automatic HTTPS**: Built-in Caddy reverse proxy handles TLS certificate provisioning and renewal using Let's
|
||||||
@@ -42,17 +45,27 @@ Deploy a highly available web app with automatic HTTPS across multiple regions a
|
|||||||
* **Docker-like CLI**: Familiar commands for managing both infrastructure and applications.
|
* **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.
|
* **Remote management**: Control your entire infrastructure through SSH access to any single machine in the cluster.
|
||||||
|
|
||||||
Coming soon:
|
### 🚀 Coming soon
|
||||||
|
|
||||||
* Infrastructure as Code using Docker Compose format
|
* **[Unregistry](https://github.com/psviderski/unregistry) integration**: Push your Docker images directly to your
|
||||||
* Project isolation through environments/namespaces
|
machines without an external registry. It will transfer only the missing layers, making it fast and efficient.
|
||||||
* Persistent volumes and secrets management
|
|
||||||
* Monitoring and log aggregation
|
|
||||||
* Database deployment and management
|
|
||||||
|
|
||||||
Here is a diagram of an Uncloud multi-provider cluster of 3 machines:
|
## 🎬 Quick demo
|
||||||
|
|
||||||

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

|
||||||
|
|
||||||
|
Here is a more advanced use case. Deploy a highly available web app with automatic HTTPS across multiple regions and
|
||||||
|
on-premises in just a couple minutes.
|
||||||
|
|
||||||
|
<a href="https://uncloud.wistia.com/medias/k47uwt9uau?wvideo=k47uwt9uau">
|
||||||
|
<img src="https://embed-ssl.wistia.com/deliveries/3cf7014a48b93afc556444bed3e39a8c.jpg?image_crop_resized=900x526&image_play_button_rounded=true&image_play_button_size=2x&image_play_button_color=18181Be0" alt="Uncloud demo" width="450" height="263" />
|
||||||
|
</a>
|
||||||
|
|
||||||
## 💫 Why Uncloud?
|
## 💫 Why Uncloud?
|
||||||
|
|
||||||
@@ -62,14 +75,15 @@ believe there's a sweet spot in between — a pragmatic solution for the majorit
|
|||||||
scale. You should be able to:
|
scale. You should be able to:
|
||||||
|
|
||||||
* **Own your infrastructure and data**: Whether driven by costs, compliance, or flexibility, run applications on any
|
* **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.
|
combination of cloud VMs and personal hardware while controlling your data and maintaining the cloud-like experience
|
||||||
* **Stay simple**: Don't worry about control planes, highly-available clusters, or complex YAML configurations for
|
you love.
|
||||||
common use cases.
|
* **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
|
* **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.
|
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.
|
platform, whether you're running on a $5 VPS, a spare Mac mini, or a rack of bare metal servers.
|
||||||
|
|
||||||
## 🚀 Quick start
|
## 🚀 Quick start
|
||||||
@@ -83,7 +97,9 @@ platform, whether you're running on a $5 VPS, a spare Mac mini, or a rack of bar
|
|||||||
curl -fsS https://get.uncloud.run/install.sh | sh
|
curl -fsS https://get.uncloud.run/install.sh | sh
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Initialize your first machine:
|
See [Installation](https://uncloud.run/docs/getting-started/install-cli) for more options.
|
||||||
|
|
||||||
|
2. Initialise your first machine:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
uc machine init root@your-server-ip
|
uc machine init root@your-server-ip
|
||||||
@@ -114,14 +130,20 @@ platform, whether you're running on a $5 VPS, a spare Mac mini, or a rack of bar
|
|||||||
uncloud-uninstall
|
uncloud-uninstall
|
||||||
```
|
```
|
||||||
|
|
||||||
View the [user guide](docs/user_guide.md) for more information.
|
View the [Documentation](https://uncloud.run/docs) for more information.
|
||||||
|
|
||||||
## ⚙️ How it works
|
## ⚙️ How it works
|
||||||
|
|
||||||
Check out the [design document](docs/design.md) to understand Uncloud's design philosophy and goals. Here, let's peek
|
Check out the [design document](misc/design.md) to understand Uncloud's design philosophy and goals.
|
||||||
under the hood to see what happens when you run certain commands.
|
|
||||||
|
|
||||||
**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
|
```bash
|
||||||
$ uc machine init --name oracle-vm ubuntu@152.67.101.197
|
$ uc machine init --name oracle-vm ubuntu@152.67.101.197
|
||||||
@@ -254,23 +276,61 @@ 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
|
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
|
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.
|
machine is a full backup of your control plane.
|
||||||
|
</details>
|
||||||
|
|
||||||
## 🏗 Project status
|
## 🏗 Project status
|
||||||
|
|
||||||
Uncloud is currently in active development and is **not ready for production use**. Features may change significantly
|
Uncloud is currently in active development and is **not ready for production use**. Features may change significantly
|
||||||
and there may be breaking changes between releases.
|
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)
|
* 🐛 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
|
## 📫 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.
|
features, and be the first to know when it's ready for production use.
|
||||||
* Watch this repository for releases.
|
* Watch this repository for releases.
|
||||||
* Follow [@psviderski](https://github.com/psviderski) on GitHub.
|
|
||||||
|
## 💖 Sponsors
|
||||||
|
|
||||||
|
These companies and projects are helping Uncloud with their generous sponsorship and/or services:
|
||||||
|
|
||||||
|
<!-- Sentry -->
|
||||||
|
<a href="https://sentry.io/welcome/">
|
||||||
|
<img height="100" alt="Sentry" src="https://github.com/user-attachments/assets/6c1439c0-d20d-40dc-a669-c9aa94651dfa" />
|
||||||
|
</a>
|
||||||
|
|
||||||
## ❤️ Contributors
|
## ❤️ Contributors
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,9 @@ package cluster
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"github.com/psviderski/uncloud/internal/ucind"
|
"github.com/psviderski/uncloud/internal/ucind"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewCreateCommand() *cobra.Command {
|
func NewCreateCommand() *cobra.Command {
|
||||||
|
|||||||
@@ -2,8 +2,9 @@ package cluster
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"github.com/psviderski/uncloud/internal/ucind"
|
"github.com/psviderski/uncloud/internal/ucind"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewRemoveCommand() *cobra.Command {
|
func NewRemoveCommand() *cobra.Command {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ func NewRootCommand() *cobra.Command {
|
|||||||
}
|
}
|
||||||
cmd.AddCommand(
|
cmd.AddCommand(
|
||||||
NewCreateCommand(),
|
NewCreateCommand(),
|
||||||
//NewListCommand(),
|
// NewListCommand(),
|
||||||
NewRemoveCommand(),
|
NewRemoveCommand(),
|
||||||
)
|
)
|
||||||
return cmd
|
return cmd
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -152,7 +152,8 @@ func runDeploy(ctx context.Context, uncli *cli.CLI, opts deployOptions) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func UpdateDomainRecords(ctx context.Context, clusterClient *client.Client, progressOut *streams.Out) error {
|
func UpdateDomainRecords(ctx context.Context, clusterClient *client.Client, progressOut *streams.Out) error {
|
||||||
if _, err := clusterClient.GetDomain(ctx); err != nil {
|
domain, err := clusterClient.GetDomain(ctx)
|
||||||
|
if err != nil {
|
||||||
if errors.Is(err, api.ErrNotFound) {
|
if errors.Is(err, api.ErrNotFound) {
|
||||||
fmt.Println("Skipping DNS records update as no cluster domain is reserved (see 'uc dns').")
|
fmt.Println("Skipping DNS records update as no cluster domain is reserved (see 'uc dns').")
|
||||||
return nil
|
return nil
|
||||||
@@ -164,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.
|
// 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
|
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
|
var err error
|
||||||
records, err = clusterClient.CreateIngressRecords(ctx, client.CaddyServiceName)
|
records, err = clusterClient.CreateIngressRecords(ctx, client.CaddyServiceName)
|
||||||
return err
|
return err
|
||||||
@@ -172,18 +173,19 @@ func UpdateDomainRecords(ctx context.Context, clusterClient *client.Client, prog
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, client.ErrNoReachableMachines) {
|
if errors.Is(err, client.ErrNoReachableMachines) {
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
fmt.Println("DNS records could not be updated as there are no internet-reachable machines running " +
|
fmt.Printf("DNS records for domain '%s' could not be updated as there are no internet-reachable "+
|
||||||
"caddy containers.")
|
"machines running caddy containers.\n", domain)
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
fmt.Println("Possible solutions:")
|
fmt.Println("Possible solutions:")
|
||||||
fmt.Println("- Ensure your machines have public IP addresses")
|
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("- Use --public-ip flag when adding machines to override the automatically detected IPs")
|
||||||
fmt.Println("- Check firewall settings on your machines")
|
fmt.Println("- Check firewall settings on your machines")
|
||||||
fmt.Println("- Configure port forwarding if behind NAT")
|
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()
|
||||||
fmt.Println("Your services will not be accessible from the internet until at least one machine " +
|
fmt.Println("Your services won't be accessible from the internet until at least one machine " +
|
||||||
"becomes reachable.")
|
"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)
|
return fmt.Errorf("failed to update DNS records pointing to caddy service: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,6 +78,5 @@ func buildContextOptions(contexts []string, current string) []huh.Option[string]
|
|||||||
options[i] = opt
|
options[i] = opt
|
||||||
}
|
}
|
||||||
|
|
||||||
options[1].Selected(true)
|
|
||||||
return options
|
return options
|
||||||
}
|
}
|
||||||
|
|||||||
+48
-3
@@ -6,6 +6,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
composecli "github.com/compose-spec/compose-go/v2/cli"
|
||||||
"github.com/docker/compose/v2/pkg/progress"
|
"github.com/docker/compose/v2/pkg/progress"
|
||||||
"github.com/psviderski/uncloud/internal/cli"
|
"github.com/psviderski/uncloud/internal/cli"
|
||||||
"github.com/psviderski/uncloud/pkg/api"
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
@@ -17,7 +18,10 @@ import (
|
|||||||
|
|
||||||
type deployOptions struct {
|
type deployOptions struct {
|
||||||
files []string
|
files []string
|
||||||
|
profiles []string
|
||||||
services []string
|
services []string
|
||||||
|
noBuild bool
|
||||||
|
recreate bool
|
||||||
|
|
||||||
context string
|
context string
|
||||||
}
|
}
|
||||||
@@ -40,18 +44,38 @@ func NewDeployCommand() *cobra.Command {
|
|||||||
}
|
}
|
||||||
|
|
||||||
cmd.Flags().StringSliceVarP(&opts.files, "file", "f", nil,
|
cmd.Flags().StringSliceVarP(&opts.files, "file", "f", nil,
|
||||||
"One or more Compose files to deploy services from. (default compose-ports-long.yaml)")
|
"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", "",
|
cmd.Flags().StringVarP(&opts.context, "context", "c", "",
|
||||||
"Name of the cluster context to deploy to (default is the current context)")
|
"Name of the cluster context to deploy to (default is the current context)")
|
||||||
|
cmd.Flags().BoolVarP(&opts.noBuild, "no-build", "n", false,
|
||||||
|
"Do not build images before deploying services. (default false)")
|
||||||
|
cmd.Flags().BoolVar(&opts.recreate, "recreate", false,
|
||||||
|
"Recreate containers even if their configuration and image haven't changed.")
|
||||||
|
|
||||||
// TODO: Consider adding a filter flag to specify which machines to deploy to but keep the rest running.
|
// 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.
|
// Could be useful to test a new version on a subset of machines before rolling out to all.
|
||||||
|
|
||||||
return cmd
|
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.
|
// runDeploy parses the Compose file(s) and deploys the services.
|
||||||
func runDeploy(ctx context.Context, uncli *cli.CLI, opts deployOptions) error {
|
func runDeploy(ctx context.Context, uncli *cli.CLI, opts deployOptions) error {
|
||||||
project, err := compose.LoadProject(ctx, opts.files)
|
projectOpts := projectOpts(opts)
|
||||||
|
|
||||||
|
project, err := compose.LoadProject(ctx, opts.files, projectOpts...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("load compose file(s): %w", err)
|
return fmt.Errorf("load compose file(s): %w", err)
|
||||||
}
|
}
|
||||||
@@ -64,13 +88,34 @@ func runDeploy(ctx context.Context, uncli *cli.CLI, opts deployOptions) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)
|
clusterClient, err := uncli.ConnectCluster(ctx, opts.context)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("connect to cluster: %w", err)
|
return fmt.Errorf("connect to cluster: %w", err)
|
||||||
}
|
}
|
||||||
defer clusterClient.Close()
|
defer clusterClient.Close()
|
||||||
|
|
||||||
composeDeploy, err := compose.NewDeployment(ctx, clusterClient, project)
|
var strategy deploy.Strategy
|
||||||
|
if opts.recreate {
|
||||||
|
strategy = &deploy.RollingStrategy{ForceRecreate: true}
|
||||||
|
}
|
||||||
|
composeDeploy, err := compose.NewDeploymentWithStrategy(ctx, clusterClient, project, strategy)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("create compose deployment: %w", err)
|
return fmt.Errorf("create compose deployment: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ func NewAddCommand() *cobra.Command {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("parse remote machine: %w", err)
|
return fmt.Errorf("parse remote machine: %w", err)
|
||||||
}
|
}
|
||||||
remoteMachine := cli.RemoteMachine{
|
remoteMachine := &cli.RemoteMachine{
|
||||||
User: user,
|
User: user,
|
||||||
Host: host,
|
Host: host,
|
||||||
Port: port,
|
Port: port,
|
||||||
@@ -59,11 +59,12 @@ func NewAddCommand() *cobra.Command {
|
|||||||
cmd.Flags().StringVar(
|
cmd.Flags().StringVar(
|
||||||
&opts.publicIP, "public-ip", "auto",
|
&opts.publicIP, "public-ip", "auto",
|
||||||
"Public IP address of the machine for ingress configuration. Use 'auto' for automatic detection, "+
|
"Public IP address of the machine for ingress configuration. Use 'auto' for automatic detection, "+
|
||||||
"blank '' or 'none' to disable ingress on this machine, or specify an IP address.",
|
fmt.Sprintf("blank '' or '%s' to disable ingress on this machine, or specify an IP address.", PublicIPNone),
|
||||||
)
|
)
|
||||||
cmd.Flags().StringVarP(
|
cmd.Flags().StringVarP(
|
||||||
&opts.sshKey, "ssh-key", "i", "",
|
&opts.sshKey, "ssh-key", "i", "",
|
||||||
"path to SSH private key for SSH remote login. (default ~/.ssh/id_*)",
|
fmt.Sprintf("Path to SSH private key for remote login (if not already added to SSH agent). (default %q)",
|
||||||
|
cli.DefaultSSHKeyPath),
|
||||||
)
|
)
|
||||||
cmd.Flags().StringVar(
|
cmd.Flags().StringVar(
|
||||||
&opts.version, "version", "latest",
|
&opts.version, "version", "latest",
|
||||||
@@ -77,12 +78,12 @@ func NewAddCommand() *cobra.Command {
|
|||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
func add(ctx context.Context, uncli *cli.CLI, remoteMachine cli.RemoteMachine, opts addOptions) error {
|
func add(ctx context.Context, uncli *cli.CLI, remoteMachine *cli.RemoteMachine, opts addOptions) error {
|
||||||
var publicIP *netip.Addr
|
var publicIP *netip.Addr
|
||||||
switch opts.publicIP {
|
switch opts.publicIP {
|
||||||
case "auto":
|
case "auto":
|
||||||
publicIP = &netip.Addr{}
|
publicIP = &netip.Addr{}
|
||||||
case "", "none":
|
case "", PublicIPNone:
|
||||||
publicIP = nil
|
publicIP = nil
|
||||||
default:
|
default:
|
||||||
ip, err := netip.ParseAddr(opts.publicIP)
|
ip, err := netip.ParseAddr(opts.publicIP)
|
||||||
@@ -92,7 +93,7 @@ func add(ctx context.Context, uncli *cli.CLI, remoteMachine cli.RemoteMachine, o
|
|||||||
publicIP = &ip
|
publicIP = &ip
|
||||||
}
|
}
|
||||||
|
|
||||||
machineClient, err := uncli.AddMachine(ctx, cli.AddMachineOptions{
|
clusterClient, machineClient, err := uncli.AddMachine(ctx, cli.AddMachineOptions{
|
||||||
Context: opts.context,
|
Context: opts.context,
|
||||||
MachineName: opts.name,
|
MachineName: opts.name,
|
||||||
PublicIP: publicIP,
|
PublicIP: publicIP,
|
||||||
@@ -102,6 +103,7 @@ func add(ctx context.Context, uncli *cli.CLI, remoteMachine cli.RemoteMachine, o
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
defer clusterClient.Close()
|
||||||
defer machineClient.Close()
|
defer machineClient.Close()
|
||||||
|
|
||||||
if opts.noCaddy {
|
if opts.noCaddy {
|
||||||
@@ -117,8 +119,11 @@ func add(ctx context.Context, uncli *cli.CLI, remoteMachine cli.RemoteMachine, o
|
|||||||
|
|
||||||
// Deploy a Caddy service container to the added machine. If caddy service is already deployed on other machines,
|
// Deploy a Caddy service container to the added machine. If caddy service is already deployed on other machines,
|
||||||
// use the deployed image version. Otherwise, use the latest version.
|
// use the deployed image version. 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 := ""
|
caddyImage := ""
|
||||||
caddySvc, err := machineClient.InspectService(ctx, client.CaddyServiceName)
|
caddySvc, err := clusterClient.InspectService(ctx, client.CaddyServiceName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !errors.Is(err, api.ErrNotFound) {
|
if !errors.Is(err, api.ErrNotFound) {
|
||||||
return fmt.Errorf("inspect caddy service: %w", err)
|
return fmt.Errorf("inspect caddy service: %w", err)
|
||||||
@@ -141,7 +146,7 @@ func add(ctx context.Context, uncli *cli.CLI, remoteMachine cli.RemoteMachine, o
|
|||||||
|
|
||||||
// TODO: scale the existing Caddy service to the new machine instead of running a new deployment
|
// TODO: scale the existing Caddy service to the new machine instead of running a new deployment
|
||||||
// that may cause a small downtime.
|
// that may cause a small downtime.
|
||||||
d, err := machineClient.NewCaddyDeployment(caddyImage, api.Placement{})
|
d, err := clusterClient.NewCaddyDeployment(caddyImage, api.Placement{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("create caddy deployment: %w", err)
|
return fmt.Errorf("create caddy deployment: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package machine
|
||||||
|
|
||||||
|
const (
|
||||||
|
// PublicIPNone is the value used to indicate removal of public IP
|
||||||
|
PublicIPNone = "none"
|
||||||
|
)
|
||||||
@@ -77,11 +77,12 @@ func NewInitCommand() *cobra.Command {
|
|||||||
cmd.Flags().StringVar(
|
cmd.Flags().StringVar(
|
||||||
&opts.publicIP, "public-ip", "auto",
|
&opts.publicIP, "public-ip", "auto",
|
||||||
"Public IP address of the machine for ingress configuration. Use 'auto' for automatic detection, "+
|
"Public IP address of the machine for ingress configuration. Use 'auto' for automatic detection, "+
|
||||||
"blank '' or 'none' to disable ingress on this machine, or specify an IP address.",
|
fmt.Sprintf("blank '' or '%s' to disable ingress on this machine, or specify an IP address.", PublicIPNone),
|
||||||
)
|
)
|
||||||
cmd.Flags().StringVarP(
|
cmd.Flags().StringVarP(
|
||||||
&opts.sshKey, "ssh-key", "i", "",
|
&opts.sshKey, "ssh-key", "i", "",
|
||||||
"Path to SSH private key for SSH remote login. (default ~/.ssh/id_*)",
|
fmt.Sprintf("Path to SSH private key for remote login (if not already added to SSH agent). (default %q)",
|
||||||
|
cli.DefaultSSHKeyPath),
|
||||||
)
|
)
|
||||||
cmd.Flags().StringVar(
|
cmd.Flags().StringVar(
|
||||||
&opts.version, "version", "latest",
|
&opts.version, "version", "latest",
|
||||||
@@ -105,7 +106,7 @@ func initCluster(ctx context.Context, uncli *cli.CLI, remoteMachine *cli.RemoteM
|
|||||||
switch opts.publicIP {
|
switch opts.publicIP {
|
||||||
case "auto":
|
case "auto":
|
||||||
publicIP = &netip.Addr{}
|
publicIP = &netip.Addr{}
|
||||||
case "", "none":
|
case "", PublicIPNone:
|
||||||
publicIP = nil
|
publicIP = nil
|
||||||
default:
|
default:
|
||||||
ip, err := netip.ParseAddr(opts.publicIP)
|
ip, err := netip.ParseAddr(opts.publicIP)
|
||||||
@@ -114,7 +115,6 @@ func initCluster(ctx context.Context, uncli *cli.CLI, remoteMachine *cli.RemoteM
|
|||||||
}
|
}
|
||||||
publicIP = &ip
|
publicIP = &ip
|
||||||
}
|
}
|
||||||
|
|
||||||
client, err := uncli.InitCluster(ctx, cli.InitClusterOptions{
|
client, err := uncli.InitCluster(ctx, cli.InitClusterOptions{
|
||||||
Context: opts.context,
|
Context: opts.context,
|
||||||
MachineName: opts.name,
|
MachineName: opts.name,
|
||||||
|
|||||||
@@ -14,18 +14,18 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func NewListCommand() *cobra.Command {
|
func NewListCommand() *cobra.Command {
|
||||||
var clusterContext string
|
var contextName string
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "ls",
|
Use: "ls",
|
||||||
Aliases: []string{"list"},
|
Aliases: []string{"list"},
|
||||||
Short: "List machines in a cluster.",
|
Short: "List machines in a cluster.",
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
uncli := cmd.Context().Value("cli").(*cli.CLI)
|
uncli := cmd.Context().Value("cli").(*cli.CLI)
|
||||||
return list(cmd.Context(), uncli, clusterContext)
|
return list(cmd.Context(), uncli, contextName)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
cmd.Flags().StringVarP(
|
cmd.Flags().StringVarP(
|
||||||
&clusterContext, "context", "c", "",
|
&contextName, "context", "c", "",
|
||||||
"Name of the cluster context. (default is the current context)",
|
"Name of the cluster context. (default is the current context)",
|
||||||
)
|
)
|
||||||
return cmd
|
return cmd
|
||||||
@@ -68,7 +68,8 @@ func list(ctx context.Context, uncli *cli.CLI, clusterName string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if _, err = fmt.Fprintf(
|
if _, err = fmt.Fprintf(
|
||||||
tw, "%s\t%s\t%s\t%s\t%s\n", m.Name, capitalise(member.State.String()), subnet, 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 {
|
); err != nil {
|
||||||
return fmt.Errorf("write row: %w", err)
|
return fmt.Errorf("write row: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
package machine
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/psviderski/uncloud/internal/cli"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewRenameCommand() *cobra.Command {
|
||||||
|
var contextName string
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "rename OLD_NAME NEW_NAME",
|
||||||
|
Short: "Rename a machine in the cluster.",
|
||||||
|
Long: `Rename a machine in the cluster.
|
||||||
|
|
||||||
|
This command changes the name of an existing machine while preserving all other
|
||||||
|
configuration including network settings, public IP, and cluster membership.`,
|
||||||
|
Args: cobra.ExactArgs(2),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
uncli := cmd.Context().Value("cli").(*cli.CLI)
|
||||||
|
return rename(cmd.Context(), uncli, contextName, args[0], args[1])
|
||||||
|
},
|
||||||
|
}
|
||||||
|
cmd.Flags().StringVarP(
|
||||||
|
&contextName, "context", "c", "",
|
||||||
|
"Name of the cluster context. (default is the current context)",
|
||||||
|
)
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func rename(ctx context.Context, uncli *cli.CLI, contextName, oldName, newName string) error {
|
||||||
|
client, err := uncli.ConnectCluster(ctx, contextName)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
machine, err := client.RenameMachine(ctx, oldName, newName)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("rename machine: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Machine %q renamed to %q (ID: %s)\n", oldName, machine.Name, machine.Id)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,249 @@
|
|||||||
|
package machine
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"maps"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/lipgloss"
|
||||||
|
"github.com/charmbracelet/lipgloss/tree"
|
||||||
|
"github.com/docker/compose/v2/pkg/progress"
|
||||||
|
"github.com/docker/docker/api/types/container"
|
||||||
|
"github.com/psviderski/uncloud/internal/cli"
|
||||||
|
"github.com/psviderski/uncloud/internal/machine/api/pb"
|
||||||
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
type removeOptions struct {
|
||||||
|
noReset bool
|
||||||
|
yes bool
|
||||||
|
context string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRmCommand() *cobra.Command {
|
||||||
|
opts := removeOptions{}
|
||||||
|
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "rm MACHINE",
|
||||||
|
Aliases: []string{"remove", "delete"},
|
||||||
|
Short: "Remove a machine from a cluster and reset it.",
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
uncli := cmd.Context().Value("cli").(*cli.CLI)
|
||||||
|
return remove(cmd.Context(), uncli, args[0], opts)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Flags().StringVarP(&opts.context, "context", "c", "",
|
||||||
|
"Name of the cluster context. (default is the current context)")
|
||||||
|
cmd.Flags().BoolVarP(&opts.yes, "yes", "y", false,
|
||||||
|
"Do not prompt for confirmation before removing the machine.")
|
||||||
|
cmd.Flags().BoolVar(&opts.noReset, "no-reset", false,
|
||||||
|
"Do not reset the machine after removing it from the cluster. This will leave all containers and data intact.")
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func remove(ctx context.Context, uncli *cli.CLI, nameOrID string, opts removeOptions) error {
|
||||||
|
// TODO: automatically choose a connection to the machine that is not being removed.
|
||||||
|
client, err := uncli.ConnectCluster(ctx, opts.context)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("connect to cluster: %w", err)
|
||||||
|
}
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
// Verify the machine exists and list all service containers on it including stopped ones.
|
||||||
|
mctx, machines, err := api.ProxyMachinesContext(ctx, client, []string{nameOrID})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(machines) == 0 {
|
||||||
|
return fmt.Errorf("machine '%s' not found in the cluster", nameOrID)
|
||||||
|
}
|
||||||
|
m := machines[0].Machine
|
||||||
|
|
||||||
|
// Verify if the machine being removed is the proxy machine we're connected to.
|
||||||
|
proxyMachine, err := client.MachineClient.Inspect(ctx, nil)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("inspect proxy machine: %w", err)
|
||||||
|
}
|
||||||
|
if proxyMachine.Id == m.Id {
|
||||||
|
allMachines, err := client.ListMachines(ctx, nil)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("list machines: %w", err)
|
||||||
|
}
|
||||||
|
if len(allMachines) > 1 {
|
||||||
|
return errors.New("cannot remove the machine you are currently connected to. " +
|
||||||
|
"Please connect to another machine in the cluster and try again. " +
|
||||||
|
"Use --connect flag or update 'connections' for the cluster context in your Uncloud config")
|
||||||
|
// It's ok to remove the proxy machine if it's the last one in the cluster.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: mark the machine as being removed and unschedulable when this is possible to prevent new containers
|
||||||
|
// from being scheduled on it while the removal is in progress.
|
||||||
|
|
||||||
|
reset := !opts.noReset
|
||||||
|
var containers []api.ServiceContainer
|
||||||
|
reachable := false
|
||||||
|
if reset {
|
||||||
|
// Check if the machine is up and has service containers.
|
||||||
|
listOpts := container.ListOptions{All: true}
|
||||||
|
machineContainers, err := client.Docker.ListServiceContainers(mctx, "", listOpts)
|
||||||
|
if err == nil {
|
||||||
|
reachable = true
|
||||||
|
containers = machineContainers[0].Containers
|
||||||
|
if len(containers) > 0 {
|
||||||
|
plural := ""
|
||||||
|
if len(containers) > 1 {
|
||||||
|
plural = "s"
|
||||||
|
}
|
||||||
|
fmt.Printf("Found %d service container%s on machine '%s':\n", len(containers), plural, m.Name)
|
||||||
|
fmt.Println(formatContainerTree(containers))
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Println("This will remove all service containers from the machine, remove it from the cluster, " +
|
||||||
|
"and reset it to the uninitialised state.")
|
||||||
|
} else {
|
||||||
|
fmt.Printf("No service containers found on machine '%s'.\n", m.Name)
|
||||||
|
fmt.Println("This will remove the machine from the cluster and reset it to the uninitialised state.")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fmt.Printf("This will remove machine '%s' from the cluster without resetting it as it's unreachable.\n",
|
||||||
|
m.Name)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fmt.Printf("This will remove machine '%s' from the cluster without resetting it.\n", m.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !opts.yes {
|
||||||
|
confirmed, err := cli.Confirm()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("confirm removal: %w", err)
|
||||||
|
}
|
||||||
|
if !confirmed {
|
||||||
|
fmt.Println("Cancelled. Machine was not removed.")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if reset && len(containers) > 0 {
|
||||||
|
err = progress.RunWithTitle(ctx, func(ctx context.Context) error {
|
||||||
|
return removeContainers(ctx, client, containers)
|
||||||
|
}, uncli.ProgressOut(), "Removing containers")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("remove containers: %w", err)
|
||||||
|
}
|
||||||
|
fmt.Println()
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err = client.RemoveMachine(ctx, &pb.RemoveMachineRequest{Id: m.Id}); err != nil {
|
||||||
|
return fmt.Errorf("remove machine from cluster: %w", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("Machine '%s' removed from the cluster.\n", m.Name)
|
||||||
|
|
||||||
|
if reset && reachable {
|
||||||
|
_, err = client.MachineClient.Reset(mctx, &pb.ResetRequest{})
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("WARNING: Failed to reset machine: %v\n", err)
|
||||||
|
} else {
|
||||||
|
fmt.Println("Machine reset initiated and will complete in the background.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: remove the connection to the machine from the uncloud config if it exists. We need a way to associate
|
||||||
|
// the machine with its connection in the config, e.g. by storing the machine name in the connection metadata.
|
||||||
|
|
||||||
|
// TODO: If Caddy was running on this machine and a cluster domain is reserved,
|
||||||
|
// let the user know that the DNS records should be updated.
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// formatContainerTree formats a list of containers grouped by service as a tree structure.
|
||||||
|
func formatContainerTree(containers []api.ServiceContainer) string {
|
||||||
|
if len(containers) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// Group containers by service.
|
||||||
|
serviceContainers := make(map[string][]api.ServiceContainer)
|
||||||
|
for _, ctr := range containers {
|
||||||
|
serviceName := ctr.ServiceName()
|
||||||
|
serviceContainers[serviceName] = append(serviceContainers[serviceName], ctr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build tree output.
|
||||||
|
var output []string
|
||||||
|
serviceNames := slices.Sorted(maps.Keys(serviceContainers))
|
||||||
|
for _, serviceName := range serviceNames {
|
||||||
|
ctrs := serviceContainers[serviceName]
|
||||||
|
mode := ctrs[0].ServiceMode()
|
||||||
|
|
||||||
|
// Format a tree for the service with its containers.
|
||||||
|
plural := ""
|
||||||
|
if len(ctrs) > 1 {
|
||||||
|
plural = "s"
|
||||||
|
}
|
||||||
|
t := tree.Root(fmt.Sprintf("• %s (%s, %d container%s)", serviceName, mode, len(ctrs), plural)).
|
||||||
|
EnumeratorStyle(lipgloss.NewStyle().MarginLeft(2).MarginRight(1))
|
||||||
|
|
||||||
|
// Add containers as children.
|
||||||
|
for _, ctr := range ctrs {
|
||||||
|
state, _ := ctr.HumanState()
|
||||||
|
info := fmt.Sprintf("%s • %s • %s", ctr.Name, ctr.Config.Image, state)
|
||||||
|
t.Child(info)
|
||||||
|
}
|
||||||
|
|
||||||
|
output = append(output, t.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Join(output, "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// removeContainers removes the given service containers from the machine.
|
||||||
|
func removeContainers(ctx context.Context, client api.Client, containers []api.ServiceContainer) error {
|
||||||
|
if len(containers) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
wg := sync.WaitGroup{}
|
||||||
|
errCh := make(chan error)
|
||||||
|
|
||||||
|
for _, ctr := range containers {
|
||||||
|
wg.Add(1)
|
||||||
|
go func(c api.ServiceContainer) {
|
||||||
|
defer wg.Done()
|
||||||
|
|
||||||
|
// Gracefully stop the container before removing it.
|
||||||
|
err := client.StopContainer(ctx, c.ServiceID(), c.ID, container.StopOptions{})
|
||||||
|
if err != nil && !errors.Is(err, api.ErrNotFound) {
|
||||||
|
errCh <- fmt.Errorf("stop container '%s': %w", c.ID, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = client.RemoveContainer(ctx, c.ServiceID(), c.ID, container.RemoveOptions{
|
||||||
|
// Remove anonymous volumes created by the container.
|
||||||
|
RemoveVolumes: true,
|
||||||
|
})
|
||||||
|
if err != nil && !errors.Is(err, api.ErrNotFound) {
|
||||||
|
errCh <- fmt.Errorf("remove container '%s': %w", c.ID, err)
|
||||||
|
}
|
||||||
|
}(ctr)
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
wg.Wait()
|
||||||
|
close(errCh)
|
||||||
|
}()
|
||||||
|
|
||||||
|
var err error
|
||||||
|
for e := range errCh {
|
||||||
|
err = errors.Join(err, e)
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
@@ -6,13 +6,17 @@ import (
|
|||||||
|
|
||||||
func NewRootCommand() *cobra.Command {
|
func NewRootCommand() *cobra.Command {
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "machine",
|
Use: "machine",
|
||||||
Short: "Manage machines in an Uncloud cluster.",
|
Aliases: []string{"m"},
|
||||||
|
Short: "Manage machines in an Uncloud cluster.",
|
||||||
}
|
}
|
||||||
cmd.AddCommand(
|
cmd.AddCommand(
|
||||||
NewAddCommand(),
|
NewAddCommand(),
|
||||||
NewInitCommand(),
|
NewInitCommand(),
|
||||||
NewListCommand(),
|
NewListCommand(),
|
||||||
|
NewRenameCommand(),
|
||||||
|
NewRmCommand(),
|
||||||
|
NewUpdateCommand(),
|
||||||
NewTokenCommand(),
|
NewTokenCommand(),
|
||||||
)
|
)
|
||||||
return cmd
|
return cmd
|
||||||
|
|||||||
@@ -2,9 +2,10 @@ package machine
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"github.com/psviderski/uncloud/internal/daemon"
|
"github.com/psviderski/uncloud/internal/daemon"
|
||||||
"github.com/psviderski/uncloud/internal/machine"
|
"github.com/psviderski/uncloud/internal/machine"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
type tokenOptions struct {
|
type tokenOptions struct {
|
||||||
|
|||||||
@@ -0,0 +1,128 @@
|
|||||||
|
package machine
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/netip"
|
||||||
|
|
||||||
|
"github.com/psviderski/uncloud/internal/cli"
|
||||||
|
"github.com/psviderski/uncloud/internal/machine/api/pb"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
type updateOptions struct {
|
||||||
|
name string
|
||||||
|
publicIP string
|
||||||
|
context string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewUpdateCommand() *cobra.Command {
|
||||||
|
opts := updateOptions{}
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "update",
|
||||||
|
Short: "Update machine configuration in the cluster.",
|
||||||
|
Long: `Update machine configuration in the cluster.
|
||||||
|
|
||||||
|
This command allows setting various machine properties including:
|
||||||
|
- Machine name (--name)
|
||||||
|
- Public IP address (--public-ip)
|
||||||
|
|
||||||
|
At least one flag must be specified to perform an update operation.`,
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
uncli := cmd.Context().Value("cli").(*cli.CLI)
|
||||||
|
return update(cmd.Context(), uncli, cmd, opts, args[0])
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Flags().StringVar(
|
||||||
|
&opts.name, "name", "",
|
||||||
|
"New name for the machine",
|
||||||
|
)
|
||||||
|
cmd.Flags().StringVar(
|
||||||
|
&opts.publicIP, "public-ip", "",
|
||||||
|
fmt.Sprintf("Public IP address of the machine for ingress configuration. Use '%s' or '' to remove the public IP.", PublicIPNone),
|
||||||
|
)
|
||||||
|
cmd.Flags().StringVarP(
|
||||||
|
&opts.context, "context", "c", "",
|
||||||
|
"Name of the cluster context. (default is the current context)",
|
||||||
|
)
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func update(ctx context.Context, uncli *cli.CLI, cmd *cobra.Command, opts updateOptions, machineNameOrID string) error {
|
||||||
|
// Check if at least one flag was explicitly set
|
||||||
|
if !cmd.Flags().Changed("name") && !cmd.Flags().Changed("public-ip") {
|
||||||
|
return fmt.Errorf("at least one update flag must be specified (--name, --public-ip)")
|
||||||
|
}
|
||||||
|
|
||||||
|
client, err := uncli.ConnectCluster(ctx, opts.context)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
// First, resolve the machine to get its ID
|
||||||
|
machine, err := client.InspectMachine(ctx, machineNameOrID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("find machine: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the update request
|
||||||
|
req := &pb.UpdateMachineRequest{
|
||||||
|
MachineId: machine.Machine.Id,
|
||||||
|
}
|
||||||
|
|
||||||
|
if opts.name != "" {
|
||||||
|
req.Name = &opts.name
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if --public-ip flag was explicitly provided
|
||||||
|
if cmd.Flags().Changed("public-ip") {
|
||||||
|
if opts.publicIP == "" || opts.publicIP == PublicIPNone {
|
||||||
|
req.PublicIp = &pb.IP{} // Empty IP to signal removal
|
||||||
|
} else {
|
||||||
|
// Parse and validate the public IP
|
||||||
|
ip, err := netip.ParseAddr(opts.publicIP)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid public IP address %q: %w", opts.publicIP, err)
|
||||||
|
}
|
||||||
|
req.PublicIp = pb.NewIP(ip)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perform the update operation
|
||||||
|
updatedMachine, err := client.UpdateMachine(ctx, req)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("update machine: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Report what was changed
|
||||||
|
changes := make([]string, 0)
|
||||||
|
if opts.name != "" {
|
||||||
|
changes = append(changes, fmt.Sprintf("name: %q -> %q", machine.Machine.Name, updatedMachine.Name))
|
||||||
|
}
|
||||||
|
if cmd.Flags().Changed("public-ip") {
|
||||||
|
oldIP := PublicIPNone
|
||||||
|
if machine.Machine.PublicIp != nil {
|
||||||
|
if addr, err := machine.Machine.PublicIp.ToAddr(); err == nil {
|
||||||
|
oldIP = addr.String()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
newIP := PublicIPNone
|
||||||
|
if updatedMachine.PublicIp != nil {
|
||||||
|
if addr, err := updatedMachine.PublicIp.ToAddr(); err == nil {
|
||||||
|
newIP = addr.String()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
changes = append(changes, fmt.Sprintf("public IP: %s -> %s", oldIP, newIP))
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Machine %q (ID: %s) configuration updated:\n", updatedMachine.Name, updatedMachine.Id)
|
||||||
|
for _, change := range changes {
|
||||||
|
fmt.Printf(" %s\n", change)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -15,6 +15,7 @@ import (
|
|||||||
"github.com/psviderski/uncloud/internal/cli"
|
"github.com/psviderski/uncloud/internal/cli"
|
||||||
"github.com/psviderski/uncloud/internal/cli/config"
|
"github.com/psviderski/uncloud/internal/cli/config"
|
||||||
"github.com/psviderski/uncloud/internal/fs"
|
"github.com/psviderski/uncloud/internal/fs"
|
||||||
|
"github.com/psviderski/uncloud/internal/version"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -28,6 +29,7 @@ func main() {
|
|||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "uncloud",
|
Use: "uncloud",
|
||||||
Short: "A CLI tool for managing Uncloud resources such as clusters, machines, and services.",
|
Short: "A CLI tool for managing Uncloud resources such as clusters, machines, and services.",
|
||||||
|
Version: version.String(),
|
||||||
SilenceUsage: true,
|
SilenceUsage: true,
|
||||||
SilenceErrors: true,
|
SilenceErrors: true,
|
||||||
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
|
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
|
||||||
@@ -73,6 +75,7 @@ func main() {
|
|||||||
|
|
||||||
cmd.AddCommand(
|
cmd.AddCommand(
|
||||||
NewDeployCommand(),
|
NewDeployCommand(),
|
||||||
|
NewBuildCommand(),
|
||||||
caddy.NewRootCommand(),
|
caddy.NewRootCommand(),
|
||||||
cmdcontext.NewRootCommand(),
|
cmdcontext.NewRootCommand(),
|
||||||
dns.NewRootCommand(),
|
dns.NewRootCommand(),
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import (
|
|||||||
|
|
||||||
"github.com/docker/docker/pkg/stringid"
|
"github.com/docker/docker/pkg/stringid"
|
||||||
"github.com/docker/go-units"
|
"github.com/docker/go-units"
|
||||||
|
|
||||||
"github.com/psviderski/uncloud/internal/cli"
|
"github.com/psviderski/uncloud/internal/cli"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
@@ -22,7 +21,7 @@ type inspectOptions struct {
|
|||||||
func NewInspectCommand() *cobra.Command {
|
func NewInspectCommand() *cobra.Command {
|
||||||
opts := inspectOptions{}
|
opts := inspectOptions{}
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "inspect",
|
Use: "inspect SERVICE",
|
||||||
Short: "Display detailed information on a service.",
|
Short: "Display detailed information on a service.",
|
||||||
Args: cobra.ExactArgs(1),
|
Args: cobra.ExactArgs(1),
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
|||||||
@@ -86,9 +86,10 @@ func NewRunCommand() *cobra.Command {
|
|||||||
"Supported protocols: tcp, udp, http, https (default is tcp). If a hostname for http(s) port is not specified\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"+
|
"and a cluster domain is reserved, service-name.cluster-domain will be used as the hostname.\n"+
|
||||||
"Examples:\n"+
|
"Examples:\n"+
|
||||||
" -p 8080/https Publish port 8080 as HTTPS via load balancer with default service-name.cluster-domain hostname\n"+
|
" -p 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 load balancer with custom hostname\n"+
|
" -p app.example.com:8080/https Publish port 8080 as HTTPS via reverse proxy with custom hostname\n"+
|
||||||
" -p 9000:8080 Publish port 8080 as TCP port 9000 via load balancer\n"+
|
// TODO: add support for publishing L4 tcp/udp ports.
|
||||||
|
//" -p 9000:8080 Publish port 8080 as TCP port 9000 via reverse proxy\n"+
|
||||||
" -p 53:5353/udp@host Bind UDP port 5353 to host port 53")
|
" -p 53:5353/udp@host Bind UDP port 5353 to host port 53")
|
||||||
cmd.Flags().StringVar(&opts.pull, "pull", api.PullPolicyMissing,
|
cmd.Flags().StringVar(&opts.pull, "pull", api.PullPolicyMissing,
|
||||||
fmt.Sprintf("Pull image from the registry before running service containers ('%s', '%s', '%s').",
|
fmt.Sprintf("Pull image from the registry before running service containers ('%s', '%s', '%s').",
|
||||||
|
|||||||
@@ -143,7 +143,7 @@ func confirm() (bool, error) {
|
|||||||
Negative("No").
|
Negative("No").
|
||||||
Value(&confirmed),
|
Value(&confirmed),
|
||||||
),
|
),
|
||||||
)
|
).WithAccessible(true)
|
||||||
if err := form.Run(); err != nil {
|
if err := form.Run(); err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ func NewCreateCommand() *cobra.Command {
|
|||||||
opts := createOptions{}
|
opts := createOptions{}
|
||||||
|
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "create [FLAGS] VOLUME_NAME",
|
Use: "create VOLUME_NAME",
|
||||||
Short: "Create a volume on a specific machine.",
|
Short: "Create a volume on a specific machine.",
|
||||||
Args: cobra.ExactArgs(1),
|
Args: cobra.ExactArgs(1),
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ func NewInspectCommand() *cobra.Command {
|
|||||||
opts := inspectOptions{}
|
opts := inspectOptions{}
|
||||||
|
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "inspect [FLAGS] VOLUME_NAME",
|
Use: "inspect VOLUME_NAME",
|
||||||
Short: "Display detailed information on a volume.",
|
Short: "Display detailed information on a volume.",
|
||||||
Args: cobra.ExactArgs(1),
|
Args: cobra.ExactArgs(1),
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ func NewListCommand() *cobra.Command {
|
|||||||
opts := listOptions{}
|
opts := listOptions{}
|
||||||
|
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "ls [FLAGS]",
|
Use: "ls",
|
||||||
Aliases: []string{"list"},
|
Aliases: []string{"list"},
|
||||||
Short: "List volumes across all machines in the cluster.",
|
Short: "List volumes across all machines in the cluster.",
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ func NewRemoveCommand() *cobra.Command {
|
|||||||
opts := removeOptions{}
|
opts := removeOptions{}
|
||||||
|
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "rm [FLAGS] VOLUME_NAME [VOLUME_NAME...]",
|
Use: "rm VOLUME_NAME [VOLUME_NAME...]",
|
||||||
Aliases: []string{"remove", "delete"},
|
Aliases: []string{"remove", "delete"},
|
||||||
Short: "Remove one or more volumes.",
|
Short: "Remove one or more volumes.",
|
||||||
Long: "Remove one or more volumes. You cannot remove a volume that is in use by a container.",
|
Long: "Remove one or more volumes. You cannot remove a volume that is in use by a container.",
|
||||||
|
|||||||
@@ -2,14 +2,16 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
"github.com/psviderski/uncloud/internal/daemon"
|
"github.com/psviderski/uncloud/internal/daemon"
|
||||||
"github.com/psviderski/uncloud/internal/log"
|
"github.com/psviderski/uncloud/internal/log"
|
||||||
"github.com/psviderski/uncloud/internal/machine"
|
"github.com/psviderski/uncloud/internal/machine"
|
||||||
|
"github.com/psviderski/uncloud/internal/version"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
@@ -22,6 +24,7 @@ func main() {
|
|||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "uncloudd",
|
Use: "uncloudd",
|
||||||
Short: "Uncloud machine daemon.",
|
Short: "Uncloud machine daemon.",
|
||||||
|
Version: version.String(),
|
||||||
SilenceUsage: true,
|
SilenceUsage: true,
|
||||||
SilenceErrors: true,
|
SilenceErrors: true,
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
@@ -52,5 +55,4 @@ func main() {
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
cobra.CheckErr(cmd.ExecuteContext(ctx))
|
cobra.CheckErr(cmd.ExecuteContext(ctx))
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,10 +3,11 @@ package main
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/hashicorp/serf/serf"
|
|
||||||
crdt "github.com/ipfs/go-ds-crdt"
|
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/hashicorp/serf/serf"
|
||||||
|
crdt "github.com/ipfs/go-ds-crdt"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Implements the Broadcaster interface.
|
// Implements the Broadcaster interface.
|
||||||
|
|||||||
@@ -2,9 +2,10 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/dgraph-io/badger/v3"
|
|
||||||
"log"
|
"log"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/dgraph-io/badger/v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
func customTimeEncoder(t time.Time) string {
|
func customTimeEncoder(t time.Time) string {
|
||||||
|
|||||||
@@ -2,9 +2,10 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/ipfs/go-log/v2"
|
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
"github.com/ipfs/go-log/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ipfsLogger is an slog logger that implements the IPFS go-log StandardLogger interface.
|
// ipfsLogger is an slog logger that implements the IPFS go-log StandardLogger interface.
|
||||||
|
|||||||
@@ -2,12 +2,13 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/docker/docker/libnetwork/networkdb"
|
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/docker/docker/libnetwork/networkdb"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|||||||
+13
-12
@@ -4,6 +4,13 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/hashicorp/memberlist"
|
"github.com/hashicorp/memberlist"
|
||||||
"github.com/hashicorp/serf/cmd/serf/command/agent"
|
"github.com/hashicorp/serf/cmd/serf/command/agent"
|
||||||
"github.com/hashicorp/serf/serf"
|
"github.com/hashicorp/serf/serf"
|
||||||
@@ -11,12 +18,6 @@ import (
|
|||||||
badger "github.com/ipfs/go-ds-badger3"
|
badger "github.com/ipfs/go-ds-badger3"
|
||||||
crdt "github.com/ipfs/go-ds-crdt"
|
crdt "github.com/ipfs/go-ds-crdt"
|
||||||
"github.com/lmittmann/tint"
|
"github.com/lmittmann/tint"
|
||||||
"log/slog"
|
|
||||||
"net"
|
|
||||||
"os"
|
|
||||||
"os/signal"
|
|
||||||
"syscall"
|
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func createSerfAgentConfig(name, bindAddr, rpcAddr, profile string) *agent.Config {
|
func createSerfAgentConfig(name, bindAddr, rpcAddr, profile string) *agent.Config {
|
||||||
@@ -49,9 +50,9 @@ func createSerfAgent(config *agent.Config) (*agent.Agent, error) {
|
|||||||
|
|
||||||
serfConfig.MemberlistConfig.BindAddr = bindIP
|
serfConfig.MemberlistConfig.BindAddr = bindIP
|
||||||
serfConfig.MemberlistConfig.BindPort = bindPort
|
serfConfig.MemberlistConfig.BindPort = bindPort
|
||||||
//serfConfig.MemberlistConfig.AdvertiseAddr = advertiseIP
|
// serfConfig.MemberlistConfig.AdvertiseAddr = advertiseIP
|
||||||
//serfConfig.MemberlistConfig.AdvertisePort = advertisePort
|
// serfConfig.MemberlistConfig.AdvertisePort = advertisePort
|
||||||
//serfConfig.MemberlistConfig.SecretKey = encryptKey
|
// serfConfig.MemberlistConfig.SecretKey = encryptKey
|
||||||
serfConfig.NodeName = config.NodeName
|
serfConfig.NodeName = config.NodeName
|
||||||
serfConfig.Tags = config.Tags
|
serfConfig.Tags = config.Tags
|
||||||
serfConfig.SnapshotPath = config.SnapshotPath
|
serfConfig.SnapshotPath = config.SnapshotPath
|
||||||
@@ -129,7 +130,7 @@ func main() {
|
|||||||
logger := slog.New(tint.NewHandler(os.Stdout, &tint.Options{
|
logger := slog.New(tint.NewHandler(os.Stdout, &tint.Options{
|
||||||
AddSource: true,
|
AddSource: true,
|
||||||
Level: slog.LevelDebug,
|
Level: slog.LevelDebug,
|
||||||
//Level: slog.LevelInfo,
|
// Level: slog.LevelInfo,
|
||||||
TimeFormat: time.RFC3339Nano,
|
TimeFormat: time.RFC3339Nano,
|
||||||
}))
|
}))
|
||||||
slog.SetDefault(logger)
|
slog.SetDefault(logger)
|
||||||
@@ -164,7 +165,7 @@ func main() {
|
|||||||
|
|
||||||
opts := crdt.DefaultOptions()
|
opts := crdt.DefaultOptions()
|
||||||
opts.Logger = newIPFSLogger(logger)
|
opts.Logger = newIPFSLogger(logger)
|
||||||
//opts.MultiHeadProcessing = true
|
// opts.MultiHeadProcessing = true
|
||||||
// TODO: debug why the heads count may grow on the receiving side if the event backlog is huge and the processing
|
// TODO: debug why the heads count may grow on the receiving side if the event backlog is huge and the processing
|
||||||
// is slow.
|
// is slow.
|
||||||
store, err := crdt.New(localStore, ds.NewKey("/"), syncer, broadcaster, opts)
|
store, err := crdt.New(localStore, ds.NewKey("/"), syncer, broadcaster, opts)
|
||||||
@@ -172,7 +173,7 @@ func main() {
|
|||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
//ticker := time.NewTicker(10 * time.Millisecond)
|
// ticker := time.NewTicker(10 * time.Millisecond)
|
||||||
ticker := time.NewTicker(3 * time.Second)
|
ticker := time.NewTicker(3 * time.Second)
|
||||||
go func() {
|
go func() {
|
||||||
for {
|
for {
|
||||||
|
|||||||
@@ -4,18 +4,19 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
|
||||||
"github.com/hashicorp/serf/serf"
|
"github.com/hashicorp/serf/serf"
|
||||||
"github.com/ipfs/boxo/datastore/dshelp"
|
"github.com/ipfs/boxo/datastore/dshelp"
|
||||||
dag "github.com/ipfs/boxo/ipld/merkledag"
|
dag "github.com/ipfs/boxo/ipld/merkledag"
|
||||||
"github.com/ipfs/go-cid"
|
"github.com/ipfs/go-cid"
|
||||||
ds "github.com/ipfs/go-datastore"
|
ds "github.com/ipfs/go-datastore"
|
||||||
ipld "github.com/ipfs/go-ipld-format"
|
ipld "github.com/ipfs/go-ipld-format"
|
||||||
"log/slog"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Implements the DAGService interface.
|
// Implements the DAGService interface.
|
||||||
// TODO: implement SessionDAGService to optimize node fetching.
|
// TODO: implement SessionDAGService to optimize node fetching.
|
||||||
// TOOD: persistentSerfDAG?
|
// TODO: persistentSerfDAG?
|
||||||
type dagSyncer struct {
|
type dagSyncer struct {
|
||||||
// Persistent storage for the nodes.
|
// Persistent storage for the nodes.
|
||||||
store ds.Datastore
|
store ds.Datastore
|
||||||
@@ -50,8 +51,7 @@ func (d *dagSyncer) Get(ctx context.Context, cid cid.Cid) (ipld.Node, error) {
|
|||||||
}
|
}
|
||||||
slog.Debug("Queried node from peers", "cid", cid, "deadline", query.Deadline())
|
slog.Debug("Queried node from peers", "cid", cid, "deadline", query.Deadline())
|
||||||
|
|
||||||
responded := false
|
for {
|
||||||
for !responded {
|
|
||||||
select {
|
select {
|
||||||
case resp, ok := <-query.ResponseCh():
|
case resp, ok := <-query.ResponseCh():
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -63,7 +63,6 @@ func (d *dagSyncer) Get(ctx context.Context, cid cid.Cid) (ipld.Node, error) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
slog.Debug("Received node from peer", "cid", cid, "peer", resp.From)
|
slog.Debug("Received node from peer", "cid", cid, "peer", resp.From)
|
||||||
responded = true
|
|
||||||
query.Close()
|
query.Close()
|
||||||
|
|
||||||
node, err = nodeFromBytes(resp.Payload)
|
node, err = nodeFromBytes(resp.Payload)
|
||||||
|
|||||||
@@ -6,12 +6,13 @@ import (
|
|||||||
"crypto/cipher"
|
"crypto/cipher"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/netip"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/psviderski/uncloud/internal/machine/network"
|
||||||
"github.com/siderolabs/discovery-api/api/v1alpha1/client/pb"
|
"github.com/siderolabs/discovery-api/api/v1alpha1/client/pb"
|
||||||
discovery "github.com/siderolabs/discovery-client/pkg/client"
|
discovery "github.com/siderolabs/discovery-client/pkg/client"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
"net/netip"
|
|
||||||
"time"
|
|
||||||
"github.com/psviderski/uncloud/internal/machine/network"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|||||||
@@ -2,8 +2,6 @@ module github.com/psviderski/uncloud
|
|||||||
|
|
||||||
go 1.23.0
|
go 1.23.0
|
||||||
|
|
||||||
toolchain go1.23.2
|
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/BurntSushi/toml v1.4.0
|
github.com/BurntSushi/toml v1.4.0
|
||||||
github.com/Masterminds/semver v1.5.0
|
github.com/Masterminds/semver v1.5.0
|
||||||
@@ -36,6 +34,9 @@ require (
|
|||||||
github.com/ipfs/go-log/v2 v2.5.1
|
github.com/ipfs/go-log/v2 v2.5.1
|
||||||
github.com/jmoiron/sqlx v1.4.0
|
github.com/jmoiron/sqlx v1.4.0
|
||||||
github.com/lmittmann/tint v1.0.5
|
github.com/lmittmann/tint v1.0.5
|
||||||
|
github.com/miekg/dns v1.1.65
|
||||||
|
github.com/mitchellh/mapstructure v1.5.0
|
||||||
|
github.com/moby/term v0.5.0
|
||||||
github.com/opencontainers/go-digest v1.0.0
|
github.com/opencontainers/go-digest v1.0.0
|
||||||
github.com/opencontainers/image-spec v1.1.0
|
github.com/opencontainers/image-spec v1.1.0
|
||||||
github.com/siderolabs/discovery-api v0.1.4
|
github.com/siderolabs/discovery-api v0.1.4
|
||||||
@@ -46,9 +47,9 @@ require (
|
|||||||
github.com/vishvananda/netlink v1.3.0
|
github.com/vishvananda/netlink v1.3.0
|
||||||
go.uber.org/zap v1.27.0
|
go.uber.org/zap v1.27.0
|
||||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba
|
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba
|
||||||
golang.org/x/crypto v0.32.0
|
golang.org/x/crypto v0.33.0
|
||||||
golang.org/x/net v0.34.0
|
golang.org/x/net v0.35.0
|
||||||
golang.org/x/sync v0.10.0
|
golang.org/x/sync v0.11.0
|
||||||
golang.org/x/sys v0.31.0
|
golang.org/x/sys v0.31.0
|
||||||
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173
|
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173
|
||||||
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6
|
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6
|
||||||
@@ -197,7 +198,6 @@ require (
|
|||||||
github.com/mdlayher/socket v0.5.1 // indirect
|
github.com/mdlayher/socket v0.5.1 // indirect
|
||||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
|
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
|
||||||
github.com/mholt/acmez/v2 v2.0.3 // indirect
|
github.com/mholt/acmez/v2 v2.0.3 // indirect
|
||||||
github.com/miekg/dns v1.1.62 // indirect
|
|
||||||
github.com/miekg/pkcs11 v1.1.1 // indirect
|
github.com/miekg/pkcs11 v1.1.1 // indirect
|
||||||
github.com/minio/sha256-simd v1.0.1 // indirect
|
github.com/minio/sha256-simd v1.0.1 // indirect
|
||||||
github.com/mitchellh/cli v1.1.5 // indirect
|
github.com/mitchellh/cli v1.1.5 // indirect
|
||||||
@@ -205,7 +205,6 @@ require (
|
|||||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||||
github.com/mitchellh/go-ps v1.0.0 // indirect
|
github.com/mitchellh/go-ps v1.0.0 // indirect
|
||||||
github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect
|
github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect
|
||||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
|
||||||
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||||
github.com/moby/buildkit v0.17.2 // indirect
|
github.com/moby/buildkit v0.17.2 // indirect
|
||||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||||
@@ -213,7 +212,8 @@ require (
|
|||||||
github.com/moby/patternmatcher v0.6.0 // indirect
|
github.com/moby/patternmatcher v0.6.0 // indirect
|
||||||
github.com/moby/sys/sequential v0.6.0 // indirect
|
github.com/moby/sys/sequential v0.6.0 // indirect
|
||||||
github.com/moby/sys/signal v0.7.1 // indirect
|
github.com/moby/sys/signal v0.7.1 // indirect
|
||||||
github.com/moby/term v0.5.0 // indirect
|
github.com/moby/sys/user v0.3.0 // indirect
|
||||||
|
github.com/moby/sys/userns v0.1.0 // indirect
|
||||||
github.com/morikuni/aec v1.0.0 // indirect
|
github.com/morikuni/aec v1.0.0 // indirect
|
||||||
github.com/mr-tron/base58 v1.2.0 // indirect
|
github.com/mr-tron/base58 v1.2.0 // indirect
|
||||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
|
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
|
||||||
@@ -303,11 +303,11 @@ require (
|
|||||||
go.uber.org/zap/exp v0.2.0 // indirect
|
go.uber.org/zap/exp v0.2.0 // indirect
|
||||||
golang.org/x/crypto/x509roots/fallback v0.0.0-20240507223354-67b13616a595 // indirect
|
golang.org/x/crypto/x509roots/fallback v0.0.0-20240507223354-67b13616a595 // indirect
|
||||||
golang.org/x/exp v0.0.0-20241215155358-4a5509556b9e // indirect
|
golang.org/x/exp v0.0.0-20241215155358-4a5509556b9e // indirect
|
||||||
golang.org/x/mod v0.22.0 // indirect
|
golang.org/x/mod v0.23.0 // indirect
|
||||||
golang.org/x/term v0.28.0 // indirect
|
golang.org/x/term v0.29.0 // indirect
|
||||||
golang.org/x/text v0.21.0 // indirect
|
golang.org/x/text v0.22.0 // indirect
|
||||||
golang.org/x/time v0.8.0 // indirect
|
golang.org/x/time v0.8.0 // indirect
|
||||||
golang.org/x/tools v0.29.0 // indirect
|
golang.org/x/tools v0.30.0 // indirect
|
||||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
|
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
|
||||||
google.golang.org/genproto v0.0.0-20240401170217-c3f982113cda // indirect
|
google.golang.org/genproto v0.0.0-20240401170217-c3f982113cda // indirect
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 // indirect
|
google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 // indirect
|
||||||
|
|||||||
@@ -105,6 +105,8 @@ github.com/aws/smithy-go v1.20.2 h1:tbp628ireGtzcHDDmLT/6ADHidqnwgF57XOXZe6tp4Q=
|
|||||||
github.com/aws/smithy-go v1.20.2/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E=
|
github.com/aws/smithy-go v1.20.2/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E=
|
||||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
||||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
||||||
|
github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8=
|
||||||
|
github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA=
|
||||||
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
||||||
github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o=
|
github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o=
|
||||||
github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
||||||
@@ -153,6 +155,8 @@ github.com/charmbracelet/lipgloss v0.13.0 h1:4X3PPeoWEDCMvzDvGmTajSyYPcZM4+y8sCA
|
|||||||
github.com/charmbracelet/lipgloss v0.13.0/go.mod h1:nw4zy0SBX/F/eAO1cWdcvy6qnkDUxr8Lw7dvFrAIbbY=
|
github.com/charmbracelet/lipgloss v0.13.0/go.mod h1:nw4zy0SBX/F/eAO1cWdcvy6qnkDUxr8Lw7dvFrAIbbY=
|
||||||
github.com/charmbracelet/x/ansi v0.3.2 h1:wsEwgAN+C9U06l9dCVMX0/L3x7ptvY1qmjMwyfE6USY=
|
github.com/charmbracelet/x/ansi v0.3.2 h1:wsEwgAN+C9U06l9dCVMX0/L3x7ptvY1qmjMwyfE6USY=
|
||||||
github.com/charmbracelet/x/ansi v0.3.2/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw=
|
github.com/charmbracelet/x/ansi v0.3.2/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw=
|
||||||
|
github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b h1:MnAMdlwSltxJyULnrYbkZpp4k58Co7Tah3ciKhSNo0Q=
|
||||||
|
github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
|
||||||
github.com/charmbracelet/x/exp/strings v0.0.0-20240919170804-a4978c8e603a h1:JMdM89Udp/cOl5tC3MuUJXTPE/nAdU1oyt9jRU44qq8=
|
github.com/charmbracelet/x/exp/strings v0.0.0-20240919170804-a4978c8e603a h1:JMdM89Udp/cOl5tC3MuUJXTPE/nAdU1oyt9jRU44qq8=
|
||||||
github.com/charmbracelet/x/exp/strings v0.0.0-20240919170804-a4978c8e603a/go.mod h1:pBhA0ybfXv6hDjQUZ7hk1lVxBiUbupdw5R31yPUViVQ=
|
github.com/charmbracelet/x/exp/strings v0.0.0-20240919170804-a4978c8e603a/go.mod h1:pBhA0ybfXv6hDjQUZ7hk1lVxBiUbupdw5R31yPUViVQ=
|
||||||
github.com/charmbracelet/x/term v0.2.0 h1:cNB9Ot9q8I711MyZ7myUR5HFWL/lc3OpU8jZ4hwm0x0=
|
github.com/charmbracelet/x/term v0.2.0 h1:cNB9Ot9q8I711MyZ7myUR5HFWL/lc3OpU8jZ4hwm0x0=
|
||||||
@@ -721,8 +725,8 @@ github.com/mholt/acmez/v2 v2.0.3 h1:CgDBlEwg3QBp6s45tPQmFIBrkRIkBT4rW4orMM6p4sw=
|
|||||||
github.com/mholt/acmez/v2 v2.0.3/go.mod h1:pQ1ysaDeGrIMvJ9dfJMk5kJNkn7L2sb3UhyrX6Q91cw=
|
github.com/mholt/acmez/v2 v2.0.3/go.mod h1:pQ1ysaDeGrIMvJ9dfJMk5kJNkn7L2sb3UhyrX6Q91cw=
|
||||||
github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
|
github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
|
||||||
github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI=
|
github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI=
|
||||||
github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ=
|
github.com/miekg/dns v1.1.65 h1:0+tIPHzUW0GCge7IiK3guGP57VAw7hoPDfApjkMD1Fc=
|
||||||
github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ=
|
github.com/miekg/dns v1.1.65/go.mod h1:Dzw9769uoKVaLuODMDZz9M6ynFU6Em65csPuoi8G0ck=
|
||||||
github.com/miekg/pkcs11 v1.0.2/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs=
|
github.com/miekg/pkcs11 v1.0.2/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs=
|
||||||
github.com/miekg/pkcs11 v1.1.1 h1:Ugu9pdy6vAYku5DEpVWVFPYnzV+bxB+iRdbuFSu7TvU=
|
github.com/miekg/pkcs11 v1.1.1 h1:Ugu9pdy6vAYku5DEpVWVFPYnzV+bxB+iRdbuFSu7TvU=
|
||||||
github.com/miekg/pkcs11 v1.1.1/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs=
|
github.com/miekg/pkcs11 v1.1.1/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs=
|
||||||
@@ -1196,8 +1200,8 @@ golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5y
|
|||||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
|
golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
|
||||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||||
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
|
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
|
||||||
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
|
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
|
||||||
golang.org/x/crypto/x509roots/fallback v0.0.0-20240507223354-67b13616a595 h1:TgSqweA595vD0Zt86JzLv3Pb/syKg8gd5KMGGbJPYFw=
|
golang.org/x/crypto/x509roots/fallback v0.0.0-20240507223354-67b13616a595 h1:TgSqweA595vD0Zt86JzLv3Pb/syKg8gd5KMGGbJPYFw=
|
||||||
golang.org/x/crypto/x509roots/fallback v0.0.0-20240507223354-67b13616a595/go.mod h1:kNa9WdvYnzFwC79zRpLRMJbdEFlhyM5RPFBBZp/wWH8=
|
golang.org/x/crypto/x509roots/fallback v0.0.0-20240507223354-67b13616a595/go.mod h1:kNa9WdvYnzFwC79zRpLRMJbdEFlhyM5RPFBBZp/wWH8=
|
||||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
@@ -1214,8 +1218,8 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
|||||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4=
|
golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM=
|
||||||
golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
|
golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
|
||||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
@@ -1237,8 +1241,8 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug
|
|||||||
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
|
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
|
||||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||||
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
|
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
|
||||||
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
|
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
|
||||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||||
golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE=
|
golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE=
|
||||||
golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||||
@@ -1252,8 +1256,8 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ
|
|||||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
|
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
|
||||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
@@ -1310,8 +1314,8 @@ golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
|
|||||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||||
golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg=
|
golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
|
||||||
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
|
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
@@ -1322,8 +1326,8 @@ golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
|||||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||||
golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg=
|
golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg=
|
||||||
golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
@@ -1345,8 +1349,8 @@ golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4f
|
|||||||
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||||
golang.org/x/tools v0.29.0 h1:Xx0h3TtM9rzQpQuR4dKLrdglAmCEN5Oi+P74JdhdzXE=
|
golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY=
|
||||||
golang.org/x/tools v0.29.0/go.mod h1:KMQVMRsVxU6nHCFXrBPhDB8XncLNLM0lIy/F14RP588=
|
golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY=
|
||||||
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
+80
-51
@@ -6,6 +6,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"os"
|
"os"
|
||||||
|
"slices"
|
||||||
|
|
||||||
"github.com/docker/cli/cli/streams"
|
"github.com/docker/cli/cli/streams"
|
||||||
"github.com/psviderski/uncloud/internal/cli/config"
|
"github.com/psviderski/uncloud/internal/cli/config"
|
||||||
@@ -16,12 +17,17 @@ import (
|
|||||||
"github.com/psviderski/uncloud/pkg/api"
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
"github.com/psviderski/uncloud/pkg/client"
|
"github.com/psviderski/uncloud/pkg/client"
|
||||||
"github.com/psviderski/uncloud/pkg/client/connector"
|
"github.com/psviderski/uncloud/pkg/client/connector"
|
||||||
|
"google.golang.org/grpc/codes"
|
||||||
"github.com/charmbracelet/huh"
|
"google.golang.org/grpc/status"
|
||||||
"google.golang.org/protobuf/types/known/emptypb"
|
"google.golang.org/protobuf/types/known/emptypb"
|
||||||
)
|
)
|
||||||
|
|
||||||
const defaultContextName = "default"
|
const (
|
||||||
|
// DefaultSSHKeyPath is the fallback location for the SSH private key when provisioning remote machines.
|
||||||
|
// Used when no key is explicitly provided and SSH agent authentication fails.
|
||||||
|
DefaultSSHKeyPath = "~/.ssh/id_ed25519"
|
||||||
|
defaultContextName = "default"
|
||||||
|
)
|
||||||
|
|
||||||
type CLI struct {
|
type CLI struct {
|
||||||
Config *config.Config
|
Config *config.Config
|
||||||
@@ -172,7 +178,7 @@ func (cli *CLI) initRemoteMachine(ctx context.Context, opts InitClusterOptions)
|
|||||||
return nil, fmt.Errorf("cluster context '%s' already exists", contextName)
|
return nil, fmt.Errorf("cluster context '%s' already exists", contextName)
|
||||||
}
|
}
|
||||||
|
|
||||||
machineClient, err := cli.provisionRemoteMachine(ctx, *opts.RemoteMachine, opts.Version)
|
machineClient, err := provisionRemoteMachine(ctx, opts.RemoteMachine, opts.Version)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -189,11 +195,22 @@ func (cli *CLI) initRemoteMachine(ctx context.Context, opts InitClusterOptions)
|
|||||||
return nil, fmt.Errorf("inspect machine: %w", err)
|
return nil, fmt.Errorf("inspect machine: %w", err)
|
||||||
}
|
}
|
||||||
if minfo.Id != "" {
|
if minfo.Id != "" {
|
||||||
if err = cli.promptResetMachine(); err != nil {
|
if err = promptResetMachine(ctx, machineClient.MachineClient); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check machine meets all necessary system requirements before proceeding.
|
||||||
|
checkResp, err := machineClient.CheckPrerequisites(ctx, &emptypb.Empty{})
|
||||||
|
// TODO(lhf): remove Unimplemented check when v0.9.0 is released.
|
||||||
|
if err != nil {
|
||||||
|
if status.Convert(err).Code() != codes.Unimplemented {
|
||||||
|
return nil, fmt.Errorf("check machine prerequisites: %w", err)
|
||||||
|
}
|
||||||
|
} else if !checkResp.Satisfied {
|
||||||
|
return nil, fmt.Errorf("machine prerequisites not satisfied: %s", checkResp.Error)
|
||||||
|
}
|
||||||
|
|
||||||
req := &pb.InitClusterRequest{
|
req := &pb.InitClusterRequest{
|
||||||
MachineName: opts.MachineName,
|
MachineName: opts.MachineName,
|
||||||
Network: pb.NewIPPrefix(opts.Network),
|
Network: pb.NewIPPrefix(opts.Network),
|
||||||
@@ -237,26 +254,32 @@ type AddMachineOptions struct {
|
|||||||
Context string
|
Context string
|
||||||
MachineName string
|
MachineName string
|
||||||
PublicIP *netip.Addr
|
PublicIP *netip.Addr
|
||||||
RemoteMachine RemoteMachine
|
RemoteMachine *RemoteMachine
|
||||||
Version string
|
Version string
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddMachine provisions a remote machine and adds it to the cluster. It returns a client to interact with the machine
|
// AddMachine provisions a remote machine and adds it to the cluster. It returns a cluster client and a machine client.
|
||||||
// which should be closed after use by the caller.
|
// The cluster client is connected to the existing machine in the cluster. It was used to add the new machine to the
|
||||||
func (cli *CLI) AddMachine(ctx context.Context, opts AddMachineOptions) (*client.Client, error) {
|
// 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
|
contextName := opts.Context
|
||||||
if contextName == "" {
|
if contextName == "" {
|
||||||
contextName = cli.Config.CurrentContext
|
contextName = cli.Config.CurrentContext
|
||||||
}
|
}
|
||||||
c, err := cli.ConnectCluster(ctx, contextName)
|
c, err := cli.ConnectCluster(ctx, contextName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("connect to cluster (context '%s'): %w", contextName, err)
|
return nil, nil, fmt.Errorf("connect to cluster (context '%s'): %w", contextName, err)
|
||||||
}
|
}
|
||||||
defer c.Close()
|
defer func() {
|
||||||
|
if err != nil {
|
||||||
|
c.Close()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
machineClient, err := cli.provisionRemoteMachine(ctx, opts.RemoteMachine, opts.Version)
|
machineClient, err := provisionRemoteMachine(ctx, opts.RemoteMachine, opts.Version)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
defer func() {
|
defer func() {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -267,21 +290,43 @@ func (cli *CLI) AddMachine(ctx context.Context, opts AddMachineOptions) (*client
|
|||||||
// Check if the machine is already initialised as a cluster member and prompt the user to reset it first.
|
// Check if the machine is already initialised as a cluster member and prompt the user to reset it first.
|
||||||
minfo, err := machineClient.Inspect(ctx, &emptypb.Empty{})
|
minfo, err := machineClient.Inspect(ctx, &emptypb.Empty{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("inspect machine: %w", err)
|
return nil, nil, fmt.Errorf("inspect machine: %w", err)
|
||||||
}
|
}
|
||||||
if minfo.Id != "" {
|
if minfo.Id != "" {
|
||||||
if err = cli.promptResetMachine(); err != nil {
|
// Check if the machine is already a member of this cluster.
|
||||||
return nil, err
|
machines, err := c.ListMachines(ctx, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("list cluster machines: %w", err)
|
||||||
}
|
}
|
||||||
|
if slices.ContainsFunc(machines, func(m *pb.MachineMember) bool {
|
||||||
|
return m.Machine.Id == minfo.Id
|
||||||
|
}) {
|
||||||
|
return nil, nil, fmt.Errorf("machine is already a member of this cluster (%s)", minfo.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = promptResetMachine(ctx, machineClient.MachineClient); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check machine meets all necessary system requirements before proceeding.
|
||||||
|
checkResp, err := machineClient.CheckPrerequisites(ctx, &emptypb.Empty{})
|
||||||
|
// TODO(lhf): remove Unimplemented check when v0.9.0 is released.
|
||||||
|
if err != nil {
|
||||||
|
if status.Convert(err).Code() != codes.Unimplemented {
|
||||||
|
return nil, nil, fmt.Errorf("check machine prerequisites: %w", err)
|
||||||
|
}
|
||||||
|
} else if !checkResp.Satisfied {
|
||||||
|
return nil, nil, fmt.Errorf("machine prerequisites not satisfied: %s", checkResp.Error)
|
||||||
}
|
}
|
||||||
|
|
||||||
tokenResp, err := machineClient.Token(ctx, &emptypb.Empty{})
|
tokenResp, err := machineClient.Token(ctx, &emptypb.Empty{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 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)
|
token, err := machine.ParseToken(tokenResp.Token)
|
||||||
if err != nil {
|
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.
|
// Register the machine in the cluster using its public key and endpoints from the token.
|
||||||
@@ -307,13 +352,13 @@ func (cli *CLI) AddMachine(ctx context.Context, opts AddMachineOptions) (*client
|
|||||||
|
|
||||||
addResp, err := c.AddMachine(ctx, addReq)
|
addResp, err := c.AddMachine(ctx, addReq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("add machine to cluster (context '%s'): %w", contextName, err)
|
return nil, nil, fmt.Errorf("add machine to cluster (context '%s'): %w", contextName, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// List other machines in the cluster to include them in the join request.
|
// Get the most up-to-date list of other machines in the cluster to include them in the join request.
|
||||||
machines, err := c.ListMachines(ctx, nil)
|
machines, err := c.ListMachines(ctx, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
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)
|
otherMachines := make([]*pb.MachineInfo, 0, len(machines)-1)
|
||||||
for _, m := range machines {
|
for _, m := range machines {
|
||||||
@@ -328,7 +373,7 @@ func (cli *CLI) AddMachine(ctx context.Context, opts AddMachineOptions) (*client
|
|||||||
OtherMachines: otherMachines,
|
OtherMachines: otherMachines,
|
||||||
}
|
}
|
||||||
if _, err = machineClient.JoinCluster(ctx, joinReq); err != nil {
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: fix empty context name when using the current context (contextName == "").
|
// TODO: fix empty context name when using the current context (contextName == "").
|
||||||
@@ -344,20 +389,29 @@ func (cli *CLI) AddMachine(ctx context.Context, opts AddMachineOptions) (*client
|
|||||||
}
|
}
|
||||||
cli.Config.Contexts[contextName].Connections = append(cli.Config.Contexts[contextName].Connections, connCfg)
|
cli.Config.Contexts[contextName].Connections = append(cli.Config.Contexts[contextName].Connections, connCfg)
|
||||||
if err = cli.Config.Save(); err != nil {
|
if err = cli.Config.Save(); err != nil {
|
||||||
return nil, fmt.Errorf("save config: %w", err)
|
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
|
// provisionRemoteMachine installs the Uncloud daemon and dependencies on the remote machine over SSH and returns
|
||||||
// a machine API client to interact with the machine. The client should be closed after use by the caller.
|
// a machine API client to interact with the machine. The client should be closed after use by the caller.
|
||||||
// The version parameter specifies the version of the Uncloud daemon to install. If empty, the latest version is used.
|
// The version parameter specifies the version of the Uncloud daemon to install. If empty, the latest version is used.
|
||||||
func (cli *CLI) provisionRemoteMachine(
|
// The remoteMachine.SSHKeyPath could be updated to the default SSH key path if it is not set and the SSH agent
|
||||||
ctx context.Context, remoteMachine RemoteMachine, version string,
|
// authentication fails.
|
||||||
|
func provisionRemoteMachine(
|
||||||
|
ctx context.Context, remoteMachine *RemoteMachine, version string,
|
||||||
) (*client.Client, error) {
|
) (*client.Client, error) {
|
||||||
// Provision the remote machine by installing the Uncloud daemon and dependencies over SSH.
|
// Provision the remote machine by installing the Uncloud daemon and dependencies over SSH.
|
||||||
sshClient, err := sshexec.Connect(remoteMachine.User, remoteMachine.Host, remoteMachine.Port, remoteMachine.KeyPath)
|
sshClient, err := sshexec.Connect(remoteMachine.User, remoteMachine.Host, remoteMachine.Port, remoteMachine.KeyPath)
|
||||||
|
// If the SSH connection using SSH agent fails and no key path is provided, try to use the default SSH key.
|
||||||
|
if err != nil && remoteMachine.KeyPath == "" {
|
||||||
|
remoteMachine.KeyPath = DefaultSSHKeyPath
|
||||||
|
sshClient, err = sshexec.Connect(
|
||||||
|
remoteMachine.User, remoteMachine.Host, remoteMachine.Port, remoteMachine.KeyPath,
|
||||||
|
)
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf(
|
return nil, fmt.Errorf(
|
||||||
"SSH login to remote machine %s: %w",
|
"SSH login to remote machine %s: %w",
|
||||||
@@ -391,31 +445,6 @@ func (cli *CLI) provisionRemoteMachine(
|
|||||||
return machineClient, nil
|
return machineClient, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cli *CLI) promptResetMachine() error {
|
|
||||||
var confirm bool
|
|
||||||
form := huh.NewForm(
|
|
||||||
huh.NewGroup(
|
|
||||||
huh.NewConfirm().
|
|
||||||
Title(
|
|
||||||
"The remote machine is already initialised as a cluster member. Do you want to reset it first?",
|
|
||||||
).
|
|
||||||
Affirmative("Yes!").
|
|
||||||
Negative("No").
|
|
||||||
Value(&confirm),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
if err := form.Run(); err != nil {
|
|
||||||
return fmt.Errorf("prompt user to confirm: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !confirm {
|
|
||||||
return fmt.Errorf("remote machine is already initialised as a cluster member")
|
|
||||||
}
|
|
||||||
// TODO: implement resetting the remote machine.
|
|
||||||
return fmt.Errorf("resetting the remote machine is not implemented yet. " +
|
|
||||||
"Please manually run 'uncloud-uninstall' on the remote machine to fully uninstall Uncloud from it")
|
|
||||||
}
|
|
||||||
|
|
||||||
// ProgressOut returns an output stream for progress writer.
|
// ProgressOut returns an output stream for progress writer.
|
||||||
func (cli *CLI) ProgressOut() *streams.Out {
|
func (cli *CLI) ProgressOut() *streams.Out {
|
||||||
return streams.NewOut(os.Stdout)
|
return streams.NewOut(os.Stdout)
|
||||||
|
|||||||
@@ -53,11 +53,11 @@ func (c *Config) Read() error {
|
|||||||
|
|
||||||
func (c *Config) Save() error {
|
func (c *Config) Save() error {
|
||||||
dir, _ := filepath.Split(c.path)
|
dir, _ := filepath.Split(c.path)
|
||||||
if err := os.MkdirAll(dir, 0700); err != nil {
|
if err := os.MkdirAll(dir, 0o700); err != nil {
|
||||||
return fmt.Errorf("create config directory '%s': %w", dir, err)
|
return fmt.Errorf("create config directory '%s': %w", dir, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
f, err := os.OpenFile(c.path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
|
f, err := os.OpenFile(c.path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o600)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("write config file '%s': %w", c.path, err)
|
return fmt.Errorf("write config file '%s': %w", c.path, err)
|
||||||
}
|
}
|
||||||
|
|||||||
+84
-3
@@ -5,12 +5,20 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/cenkalti/backoff/v4"
|
||||||
|
"github.com/charmbracelet/huh"
|
||||||
|
"github.com/psviderski/uncloud/internal/machine/api/pb"
|
||||||
"github.com/psviderski/uncloud/internal/sshexec"
|
"github.com/psviderski/uncloud/internal/sshexec"
|
||||||
|
"google.golang.org/protobuf/types/known/emptypb"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TODO: support pinning the script version to the CLI version.
|
const (
|
||||||
const installScriptURL = "https://raw.githubusercontent.com/psviderski/uncloud/refs/heads/main/scripts/install.sh"
|
// TODO: support pinning the script version to the CLI version.
|
||||||
|
installScriptURL = "https://raw.githubusercontent.com/psviderski/uncloud/refs/heads/main/scripts/install.sh"
|
||||||
|
rootUser = "root"
|
||||||
|
)
|
||||||
|
|
||||||
type RemoteMachine struct {
|
type RemoteMachine struct {
|
||||||
User string
|
User string
|
||||||
@@ -24,7 +32,7 @@ func installCmd(user string, version string) string {
|
|||||||
var env []string
|
var env []string
|
||||||
|
|
||||||
// Add the SSH user (non-root) to the uncloud group to allow access to the Uncloud daemon unix socket.
|
// Add the SSH user (non-root) to the uncloud group to allow access to the Uncloud daemon unix socket.
|
||||||
if user != "root" {
|
if user != rootUser {
|
||||||
sudoPrefix = "sudo"
|
sudoPrefix = "sudo"
|
||||||
env = append(env, "UNCLOUD_GROUP_ADD_USER="+sshexec.Quote(user))
|
env = append(env, "UNCLOUD_GROUP_ADD_USER="+sshexec.Quote(user))
|
||||||
}
|
}
|
||||||
@@ -46,6 +54,26 @@ func provisionMachine(ctx context.Context, exec sshexec.Executor, version string
|
|||||||
return fmt.Errorf("run whoami: %w", err)
|
return fmt.Errorf("run whoami: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if user != rootUser {
|
||||||
|
// 'sudo -n' is not used because it fails with 'sudo: a password is required' when the user has no password
|
||||||
|
// in /etc/shadow even though it may have valid sudo access.
|
||||||
|
out, err := exec.Run(ctx, "sudo true")
|
||||||
|
if err != nil {
|
||||||
|
if strings.Contains(out, "password is required") {
|
||||||
|
return fmt.Errorf(
|
||||||
|
"user '%[1]s' requires a password for sudo, but Uncloud needs passwordless sudo or root access "+
|
||||||
|
"to install and configure the uncloudd daemon on the remote machine.\n\n"+
|
||||||
|
"Possible solutions:\n"+
|
||||||
|
"1. Use root user or a user with passwordless sudo instead.\n"+
|
||||||
|
"2. Configure passwordless sudo for the user '%[1]s' by running on the remote machine:\n"+
|
||||||
|
" echo '%[1]s ALL=(ALL) NOPASSWD:ALL' | sudo tee /etc/sudoers.d/%[1]s",
|
||||||
|
user)
|
||||||
|
}
|
||||||
|
return fmt.Errorf("sudo command failed for user '%s': %w. "+
|
||||||
|
"Please ensure the user has sudo privileges or use root user instead", user, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
cmd := installCmd(user, version)
|
cmd := installCmd(user, version)
|
||||||
|
|
||||||
fmt.Println("Downloading Uncloud install script:", installScriptURL)
|
fmt.Println("Downloading Uncloud install script:", installScriptURL)
|
||||||
@@ -56,3 +84,56 @@ func provisionMachine(ctx context.Context, exec sshexec.Executor, version string
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func promptResetMachine(ctx context.Context, machineClient pb.MachineClient) error {
|
||||||
|
var confirm bool
|
||||||
|
form := huh.NewForm(
|
||||||
|
huh.NewGroup(
|
||||||
|
huh.NewConfirm().
|
||||||
|
Title(
|
||||||
|
"The remote machine is already initialised as a cluster member. Do you want to reset it first?\n" +
|
||||||
|
"This will:\n" +
|
||||||
|
"- Remove all service containers from the machine\n" +
|
||||||
|
"- Reset the machine to the uninitialised state",
|
||||||
|
).
|
||||||
|
Affirmative("Yes!").
|
||||||
|
Negative("No").
|
||||||
|
Value(&confirm),
|
||||||
|
),
|
||||||
|
).WithAccessible(true)
|
||||||
|
if err := form.Run(); err != nil {
|
||||||
|
return fmt.Errorf("prompt user to confirm: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !confirm {
|
||||||
|
return fmt.Errorf("remote machine is already initialised as a cluster member")
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := machineClient.Reset(ctx, &pb.ResetRequest{}); err != nil {
|
||||||
|
return fmt.Errorf("reset remote machine: %w. You can also manually run 'uncloud-uninstall' "+
|
||||||
|
"on the remote machine to fully uninstall Uncloud from it", err)
|
||||||
|
}
|
||||||
|
fmt.Println("Resetting the remote machine...")
|
||||||
|
if err := waitMachineReady(ctx, machineClient, 1*time.Minute); err != nil {
|
||||||
|
return fmt.Errorf("wait for machine to be ready after reset: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// waitMachineReady waits for the machine to be ready to serve requests.
|
||||||
|
func waitMachineReady(ctx context.Context, machineClient pb.MachineClient, timeout time.Duration) error {
|
||||||
|
boff := backoff.WithContext(backoff.NewExponentialBackOff(
|
||||||
|
backoff.WithMaxInterval(1*time.Second),
|
||||||
|
backoff.WithMaxElapsedTime(timeout),
|
||||||
|
), ctx)
|
||||||
|
|
||||||
|
inspect := func() error {
|
||||||
|
_, err := machineClient.Inspect(ctx, &emptypb.Empty{})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("inspect machine: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return backoff.Retry(inspect, boff)
|
||||||
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ func Confirm() (bool, error) {
|
|||||||
Negative("No").
|
Negative("No").
|
||||||
Value(&confirmed),
|
Value(&confirmed),
|
||||||
),
|
),
|
||||||
)
|
).WithAccessible(true)
|
||||||
if err := form.Run(); err != nil {
|
if err := form.Run(); err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,14 +5,15 @@ import (
|
|||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/cenkalti/backoff/v4"
|
|
||||||
"golang.org/x/net/http2"
|
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"net/url"
|
"net/url"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/cenkalti/backoff/v4"
|
||||||
|
"golang.org/x/net/http2"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|||||||
@@ -6,11 +6,12 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/cenkalti/backoff/v4"
|
|
||||||
"io"
|
"io"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/cenkalti/backoff/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ChangeType string
|
type ChangeType string
|
||||||
|
|||||||
@@ -3,8 +3,9 @@ package daemon
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
systemd "github.com/coreos/go-systemd/daemon"
|
|
||||||
"log/slog"
|
"log/slog"
|
||||||
|
|
||||||
|
systemd "github.com/coreos/go-systemd/daemon"
|
||||||
"github.com/psviderski/uncloud/internal/machine"
|
"github.com/psviderski/uncloud/internal/machine"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -3,10 +3,11 @@ package daemon
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/psviderski/uncloud/internal/machine"
|
|
||||||
"github.com/psviderski/uncloud/internal/machine/network"
|
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
"github.com/psviderski/uncloud/internal/machine"
|
||||||
|
"github.com/psviderski/uncloud/internal/machine/network"
|
||||||
)
|
)
|
||||||
|
|
||||||
// MachineToken returns the local machine's token that can be used for adding the machine to a cluster.
|
// MachineToken returns the local machine's token that can be used for adding the machine to a cluster.
|
||||||
|
|||||||
@@ -4,10 +4,11 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/cenkalti/backoff/v4"
|
|
||||||
"github.com/docker/docker/client"
|
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/cenkalti/backoff/v4"
|
||||||
|
"github.com/docker/docker/client"
|
||||||
)
|
)
|
||||||
|
|
||||||
// WaitDaemonReady waits for the Docker daemon to start and be ready to serve requests.
|
// WaitDaemonReady waits for the Docker daemon to start and be ready to serve requests.
|
||||||
|
|||||||
+10
-2
@@ -45,11 +45,19 @@ func (h *SlogTextHandler) Enabled(ctx context.Context, level slog.Level) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *SlogTextHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
|
func (h *SlogTextHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
|
||||||
return h.TextHandler.WithAttrs(attrs)
|
return &SlogTextHandler{
|
||||||
|
TextHandler: h.TextHandler.WithAttrs(attrs).(*slog.TextHandler),
|
||||||
|
opts: h.opts,
|
||||||
|
w: h.w,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *SlogTextHandler) WithGroup(name string) slog.Handler {
|
func (h *SlogTextHandler) WithGroup(name string) slog.Handler {
|
||||||
return h.TextHandler.WithGroup(name)
|
return &SlogTextHandler{
|
||||||
|
TextHandler: h.TextHandler.WithGroup(name).(*slog.TextHandler),
|
||||||
|
opts: h.opts,
|
||||||
|
w: h.w,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *SlogTextHandler) Handle(ctx context.Context, r slog.Record) error {
|
func (h *SlogTextHandler) Handle(ctx context.Context, r slog.Record) error {
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ func (x DNSRecord_RecordType) Number() protoreflect.EnumNumber {
|
|||||||
|
|
||||||
// Deprecated: Use DNSRecord_RecordType.Descriptor instead.
|
// Deprecated: Use DNSRecord_RecordType.Descriptor instead.
|
||||||
func (DNSRecord_RecordType) EnumDescriptor() ([]byte, []int) {
|
func (DNSRecord_RecordType) EnumDescriptor() ([]byte, []int) {
|
||||||
return file_internal_machine_api_pb_cluster_proto_rawDescGZIP(), []int{8, 0}
|
return file_internal_machine_api_pb_cluster_proto_rawDescGZIP(), []int{11, 0}
|
||||||
}
|
}
|
||||||
|
|
||||||
type AddMachineRequest struct {
|
type AddMachineRequest struct {
|
||||||
@@ -339,6 +339,173 @@ func (x *ListMachinesResponse) GetMachines() []*MachineMember {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type UpdateMachineRequest struct {
|
||||||
|
state protoimpl.MessageState
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
|
||||||
|
// Machine to update
|
||||||
|
MachineId string `protobuf:"bytes,1,opt,name=machine_id,json=machineId,proto3" json:"machine_id,omitempty"`
|
||||||
|
// Updated machine information
|
||||||
|
Name *string `protobuf:"bytes,2,opt,name=name,proto3,oneof" json:"name,omitempty"`
|
||||||
|
PublicIp *IP `protobuf:"bytes,3,opt,name=public_ip,json=publicIp,proto3,oneof" json:"public_ip,omitempty"`
|
||||||
|
Endpoints []*IPPort `protobuf:"bytes,4,rep,name=endpoints,proto3" json:"endpoints,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *UpdateMachineRequest) Reset() {
|
||||||
|
*x = UpdateMachineRequest{}
|
||||||
|
if protoimpl.UnsafeEnabled {
|
||||||
|
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[4]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *UpdateMachineRequest) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*UpdateMachineRequest) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *UpdateMachineRequest) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[4]
|
||||||
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use UpdateMachineRequest.ProtoReflect.Descriptor instead.
|
||||||
|
func (*UpdateMachineRequest) Descriptor() ([]byte, []int) {
|
||||||
|
return file_internal_machine_api_pb_cluster_proto_rawDescGZIP(), []int{4}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *UpdateMachineRequest) GetMachineId() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.MachineId
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *UpdateMachineRequest) GetName() string {
|
||||||
|
if x != nil && x.Name != nil {
|
||||||
|
return *x.Name
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *UpdateMachineRequest) GetPublicIp() *IP {
|
||||||
|
if x != nil {
|
||||||
|
return x.PublicIp
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *UpdateMachineRequest) GetEndpoints() []*IPPort {
|
||||||
|
if x != nil {
|
||||||
|
return x.Endpoints
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdateMachineResponse struct {
|
||||||
|
state protoimpl.MessageState
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
|
||||||
|
Machine *MachineInfo `protobuf:"bytes,1,opt,name=machine,proto3" json:"machine,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *UpdateMachineResponse) Reset() {
|
||||||
|
*x = UpdateMachineResponse{}
|
||||||
|
if protoimpl.UnsafeEnabled {
|
||||||
|
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[5]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *UpdateMachineResponse) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*UpdateMachineResponse) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *UpdateMachineResponse) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[5]
|
||||||
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use UpdateMachineResponse.ProtoReflect.Descriptor instead.
|
||||||
|
func (*UpdateMachineResponse) Descriptor() ([]byte, []int) {
|
||||||
|
return file_internal_machine_api_pb_cluster_proto_rawDescGZIP(), []int{5}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *UpdateMachineResponse) GetMachine() *MachineInfo {
|
||||||
|
if x != nil {
|
||||||
|
return x.Machine
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type RemoveMachineRequest struct {
|
||||||
|
state protoimpl.MessageState
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
|
||||||
|
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *RemoveMachineRequest) Reset() {
|
||||||
|
*x = RemoveMachineRequest{}
|
||||||
|
if protoimpl.UnsafeEnabled {
|
||||||
|
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[6]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *RemoveMachineRequest) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*RemoveMachineRequest) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *RemoveMachineRequest) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[6]
|
||||||
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use RemoveMachineRequest.ProtoReflect.Descriptor instead.
|
||||||
|
func (*RemoveMachineRequest) Descriptor() ([]byte, []int) {
|
||||||
|
return file_internal_machine_api_pb_cluster_proto_rawDescGZIP(), []int{6}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *RemoveMachineRequest) GetId() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Id
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
type Domain struct {
|
type Domain struct {
|
||||||
state protoimpl.MessageState
|
state protoimpl.MessageState
|
||||||
sizeCache protoimpl.SizeCache
|
sizeCache protoimpl.SizeCache
|
||||||
@@ -350,7 +517,7 @@ type Domain struct {
|
|||||||
func (x *Domain) Reset() {
|
func (x *Domain) Reset() {
|
||||||
*x = Domain{}
|
*x = Domain{}
|
||||||
if protoimpl.UnsafeEnabled {
|
if protoimpl.UnsafeEnabled {
|
||||||
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[4]
|
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[7]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@@ -363,7 +530,7 @@ func (x *Domain) String() string {
|
|||||||
func (*Domain) ProtoMessage() {}
|
func (*Domain) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *Domain) ProtoReflect() protoreflect.Message {
|
func (x *Domain) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[4]
|
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[7]
|
||||||
if protoimpl.UnsafeEnabled && x != nil {
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
@@ -376,7 +543,7 @@ func (x *Domain) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use Domain.ProtoReflect.Descriptor instead.
|
// Deprecated: Use Domain.ProtoReflect.Descriptor instead.
|
||||||
func (*Domain) Descriptor() ([]byte, []int) {
|
func (*Domain) Descriptor() ([]byte, []int) {
|
||||||
return file_internal_machine_api_pb_cluster_proto_rawDescGZIP(), []int{4}
|
return file_internal_machine_api_pb_cluster_proto_rawDescGZIP(), []int{7}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *Domain) GetName() string {
|
func (x *Domain) GetName() string {
|
||||||
@@ -397,7 +564,7 @@ type ReserveDomainRequest struct {
|
|||||||
func (x *ReserveDomainRequest) Reset() {
|
func (x *ReserveDomainRequest) Reset() {
|
||||||
*x = ReserveDomainRequest{}
|
*x = ReserveDomainRequest{}
|
||||||
if protoimpl.UnsafeEnabled {
|
if protoimpl.UnsafeEnabled {
|
||||||
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[5]
|
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[8]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@@ -410,7 +577,7 @@ func (x *ReserveDomainRequest) String() string {
|
|||||||
func (*ReserveDomainRequest) ProtoMessage() {}
|
func (*ReserveDomainRequest) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *ReserveDomainRequest) ProtoReflect() protoreflect.Message {
|
func (x *ReserveDomainRequest) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[5]
|
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[8]
|
||||||
if protoimpl.UnsafeEnabled && x != nil {
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
@@ -423,7 +590,7 @@ func (x *ReserveDomainRequest) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use ReserveDomainRequest.ProtoReflect.Descriptor instead.
|
// Deprecated: Use ReserveDomainRequest.ProtoReflect.Descriptor instead.
|
||||||
func (*ReserveDomainRequest) Descriptor() ([]byte, []int) {
|
func (*ReserveDomainRequest) Descriptor() ([]byte, []int) {
|
||||||
return file_internal_machine_api_pb_cluster_proto_rawDescGZIP(), []int{5}
|
return file_internal_machine_api_pb_cluster_proto_rawDescGZIP(), []int{8}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *ReserveDomainRequest) GetEndpoint() string {
|
func (x *ReserveDomainRequest) GetEndpoint() string {
|
||||||
@@ -444,7 +611,7 @@ type CreateDomainRecordsRequest struct {
|
|||||||
func (x *CreateDomainRecordsRequest) Reset() {
|
func (x *CreateDomainRecordsRequest) Reset() {
|
||||||
*x = CreateDomainRecordsRequest{}
|
*x = CreateDomainRecordsRequest{}
|
||||||
if protoimpl.UnsafeEnabled {
|
if protoimpl.UnsafeEnabled {
|
||||||
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[6]
|
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[9]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@@ -457,7 +624,7 @@ func (x *CreateDomainRecordsRequest) String() string {
|
|||||||
func (*CreateDomainRecordsRequest) ProtoMessage() {}
|
func (*CreateDomainRecordsRequest) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *CreateDomainRecordsRequest) ProtoReflect() protoreflect.Message {
|
func (x *CreateDomainRecordsRequest) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[6]
|
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[9]
|
||||||
if protoimpl.UnsafeEnabled && x != nil {
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
@@ -470,7 +637,7 @@ func (x *CreateDomainRecordsRequest) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use CreateDomainRecordsRequest.ProtoReflect.Descriptor instead.
|
// Deprecated: Use CreateDomainRecordsRequest.ProtoReflect.Descriptor instead.
|
||||||
func (*CreateDomainRecordsRequest) Descriptor() ([]byte, []int) {
|
func (*CreateDomainRecordsRequest) Descriptor() ([]byte, []int) {
|
||||||
return file_internal_machine_api_pb_cluster_proto_rawDescGZIP(), []int{6}
|
return file_internal_machine_api_pb_cluster_proto_rawDescGZIP(), []int{9}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *CreateDomainRecordsRequest) GetRecords() []*DNSRecord {
|
func (x *CreateDomainRecordsRequest) GetRecords() []*DNSRecord {
|
||||||
@@ -491,7 +658,7 @@ type CreateDomainRecordsResponse struct {
|
|||||||
func (x *CreateDomainRecordsResponse) Reset() {
|
func (x *CreateDomainRecordsResponse) Reset() {
|
||||||
*x = CreateDomainRecordsResponse{}
|
*x = CreateDomainRecordsResponse{}
|
||||||
if protoimpl.UnsafeEnabled {
|
if protoimpl.UnsafeEnabled {
|
||||||
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[7]
|
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[10]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@@ -504,7 +671,7 @@ func (x *CreateDomainRecordsResponse) String() string {
|
|||||||
func (*CreateDomainRecordsResponse) ProtoMessage() {}
|
func (*CreateDomainRecordsResponse) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *CreateDomainRecordsResponse) ProtoReflect() protoreflect.Message {
|
func (x *CreateDomainRecordsResponse) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[7]
|
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[10]
|
||||||
if protoimpl.UnsafeEnabled && x != nil {
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
@@ -517,7 +684,7 @@ func (x *CreateDomainRecordsResponse) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use CreateDomainRecordsResponse.ProtoReflect.Descriptor instead.
|
// Deprecated: Use CreateDomainRecordsResponse.ProtoReflect.Descriptor instead.
|
||||||
func (*CreateDomainRecordsResponse) Descriptor() ([]byte, []int) {
|
func (*CreateDomainRecordsResponse) Descriptor() ([]byte, []int) {
|
||||||
return file_internal_machine_api_pb_cluster_proto_rawDescGZIP(), []int{7}
|
return file_internal_machine_api_pb_cluster_proto_rawDescGZIP(), []int{10}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *CreateDomainRecordsResponse) GetRecords() []*DNSRecord {
|
func (x *CreateDomainRecordsResponse) GetRecords() []*DNSRecord {
|
||||||
@@ -540,7 +707,7 @@ type DNSRecord struct {
|
|||||||
func (x *DNSRecord) Reset() {
|
func (x *DNSRecord) Reset() {
|
||||||
*x = DNSRecord{}
|
*x = DNSRecord{}
|
||||||
if protoimpl.UnsafeEnabled {
|
if protoimpl.UnsafeEnabled {
|
||||||
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[8]
|
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[11]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@@ -553,7 +720,7 @@ func (x *DNSRecord) String() string {
|
|||||||
func (*DNSRecord) ProtoMessage() {}
|
func (*DNSRecord) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *DNSRecord) ProtoReflect() protoreflect.Message {
|
func (x *DNSRecord) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[8]
|
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[11]
|
||||||
if protoimpl.UnsafeEnabled && x != nil {
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
@@ -566,7 +733,7 @@ func (x *DNSRecord) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use DNSRecord.ProtoReflect.Descriptor instead.
|
// Deprecated: Use DNSRecord.ProtoReflect.Descriptor instead.
|
||||||
func (*DNSRecord) Descriptor() ([]byte, []int) {
|
func (*DNSRecord) Descriptor() ([]byte, []int) {
|
||||||
return file_internal_machine_api_pb_cluster_proto_rawDescGZIP(), []int{8}
|
return file_internal_machine_api_pb_cluster_proto_rawDescGZIP(), []int{11}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *DNSRecord) GetName() string {
|
func (x *DNSRecord) GetName() string {
|
||||||
@@ -630,59 +797,87 @@ var file_internal_machine_api_pb_cluster_proto_rawDesc = []byte{
|
|||||||
0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2e, 0x0a, 0x08, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73,
|
0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2e, 0x0a, 0x08, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73,
|
||||||
0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d, 0x61, 0x63,
|
0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d, 0x61, 0x63,
|
||||||
0x68, 0x69, 0x6e, 0x65, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x52, 0x08, 0x6d, 0x61, 0x63, 0x68,
|
0x68, 0x69, 0x6e, 0x65, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x52, 0x08, 0x6d, 0x61, 0x63, 0x68,
|
||||||
0x69, 0x6e, 0x65, 0x73, 0x22, 0x1c, 0x0a, 0x06, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x12,
|
0x69, 0x6e, 0x65, 0x73, 0x22, 0xbb, 0x01, 0x0a, 0x14, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d,
|
||||||
0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61,
|
0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a,
|
||||||
0x6d, 0x65, 0x22, 0x32, 0x0a, 0x14, 0x52, 0x65, 0x73, 0x65, 0x72, 0x76, 0x65, 0x44, 0x6f, 0x6d,
|
0x0a, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28,
|
||||||
0x61, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x65, 0x6e,
|
0x09, 0x52, 0x09, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x64, 0x12, 0x17, 0x0a, 0x04,
|
||||||
0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x65, 0x6e,
|
0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x04, 0x6e, 0x61,
|
||||||
0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x22, 0x46, 0x0a, 0x1a, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65,
|
0x6d, 0x65, 0x88, 0x01, 0x01, 0x12, 0x29, 0x0a, 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f,
|
||||||
0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x52, 0x65, 0x71,
|
0x69, 0x70, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x07, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49,
|
||||||
0x75, 0x65, 0x73, 0x74, 0x12, 0x28, 0x0a, 0x07, 0x72, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x18,
|
0x50, 0x48, 0x01, 0x52, 0x08, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x49, 0x70, 0x88, 0x01, 0x01,
|
||||||
0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x44, 0x4e, 0x53, 0x52,
|
0x12, 0x29, 0x0a, 0x09, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x18, 0x04, 0x20,
|
||||||
0x65, 0x63, 0x6f, 0x72, 0x64, 0x52, 0x07, 0x72, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x22, 0x47,
|
0x03, 0x28, 0x0b, 0x32, 0x0b, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x50, 0x50, 0x6f, 0x72, 0x74,
|
||||||
0x0a, 0x1b, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x52, 0x65,
|
0x52, 0x09, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x42, 0x07, 0x0a, 0x05, 0x5f,
|
||||||
0x63, 0x6f, 0x72, 0x64, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x28, 0x0a,
|
0x6e, 0x61, 0x6d, 0x65, 0x42, 0x0c, 0x0a, 0x0a, 0x5f, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f,
|
||||||
0x07, 0x72, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0e,
|
0x69, 0x70, 0x22, 0x43, 0x0a, 0x15, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x61, 0x63, 0x68,
|
||||||
0x2e, 0x61, 0x70, 0x69, 0x2e, 0x44, 0x4e, 0x53, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x52, 0x07,
|
0x69, 0x6e, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2a, 0x0a, 0x07, 0x6d,
|
||||||
0x72, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x22, 0x96, 0x01, 0x0a, 0x09, 0x44, 0x4e, 0x53, 0x52,
|
0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x61,
|
||||||
0x65, 0x63, 0x6f, 0x72, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20,
|
0x70, 0x69, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x07,
|
||||||
0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x2d, 0x0a, 0x04, 0x74, 0x79, 0x70,
|
0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x22, 0x26, 0x0a, 0x14, 0x52, 0x65, 0x6d, 0x6f, 0x76,
|
||||||
0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x19, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x44, 0x4e,
|
0x65, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12,
|
||||||
0x53, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x54, 0x79,
|
0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x22,
|
||||||
0x70, 0x65, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x76, 0x61, 0x6c, 0x75,
|
0x1c, 0x0a, 0x06, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d,
|
||||||
0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73,
|
0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x32, 0x0a,
|
||||||
0x22, 0x2e, 0x0a, 0x0a, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x54, 0x79, 0x70, 0x65, 0x12, 0x0f,
|
0x14, 0x52, 0x65, 0x73, 0x65, 0x72, 0x76, 0x65, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x52, 0x65,
|
||||||
0x0a, 0x0b, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12,
|
0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e,
|
||||||
0x05, 0x0a, 0x01, 0x41, 0x10, 0x01, 0x12, 0x08, 0x0a, 0x04, 0x41, 0x41, 0x41, 0x41, 0x10, 0x02,
|
0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e,
|
||||||
0x32, 0x86, 0x03, 0x0a, 0x07, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x12, 0x3d, 0x0a, 0x0a,
|
0x74, 0x22, 0x46, 0x0a, 0x1a, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x44, 0x6f, 0x6d, 0x61, 0x69,
|
||||||
0x41, 0x64, 0x64, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x12, 0x16, 0x2e, 0x61, 0x70, 0x69,
|
0x6e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12,
|
||||||
0x2e, 0x41, 0x64, 0x64, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65,
|
0x28, 0x0a, 0x07, 0x72, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b,
|
||||||
0x73, 0x74, 0x1a, 0x17, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x41, 0x64, 0x64, 0x4d, 0x61, 0x63, 0x68,
|
0x32, 0x0e, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x44, 0x4e, 0x53, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64,
|
||||||
0x69, 0x6e, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x41, 0x0a, 0x0c, 0x4c,
|
0x52, 0x07, 0x72, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x22, 0x47, 0x0a, 0x1b, 0x43, 0x72, 0x65,
|
||||||
0x69, 0x73, 0x74, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x12, 0x16, 0x2e, 0x67, 0x6f,
|
|
||||||
0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d,
|
|
||||||
0x70, 0x74, 0x79, 0x1a, 0x19, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x61,
|
|
||||||
0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x37,
|
|
||||||
0x0a, 0x0d, 0x52, 0x65, 0x73, 0x65, 0x72, 0x76, 0x65, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12,
|
|
||||||
0x19, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x52, 0x65, 0x73, 0x65, 0x72, 0x76, 0x65, 0x44, 0x6f, 0x6d,
|
|
||||||
0x61, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0b, 0x2e, 0x61, 0x70, 0x69,
|
|
||||||
0x2e, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x30, 0x0a, 0x09, 0x47, 0x65, 0x74, 0x44, 0x6f,
|
|
||||||
0x6d, 0x61, 0x69, 0x6e, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72,
|
|
||||||
0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x0b, 0x2e, 0x61,
|
|
||||||
0x70, 0x69, 0x2e, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x34, 0x0a, 0x0d, 0x52, 0x65, 0x6c,
|
|
||||||
0x65, 0x61, 0x73, 0x65, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f,
|
|
||||||
0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70,
|
|
||||||
0x74, 0x79, 0x1a, 0x0b, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12,
|
|
||||||
0x58, 0x0a, 0x13, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x52,
|
|
||||||
0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x12, 0x1f, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x72, 0x65,
|
|
||||||
0x61, 0x74, 0x65, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73,
|
0x61, 0x74, 0x65, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73,
|
||||||
0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x72,
|
0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x28, 0x0a, 0x07, 0x72, 0x65, 0x63, 0x6f,
|
||||||
0x65, 0x61, 0x74, 0x65, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64,
|
0x72, 0x64, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x61, 0x70, 0x69, 0x2e,
|
||||||
0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x37, 0x5a, 0x35, 0x67, 0x69, 0x74,
|
0x44, 0x4e, 0x53, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x52, 0x07, 0x72, 0x65, 0x63, 0x6f, 0x72,
|
||||||
0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x70, 0x73, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73,
|
0x64, 0x73, 0x22, 0x96, 0x01, 0x0a, 0x09, 0x44, 0x4e, 0x53, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64,
|
||||||
0x6b, 0x69, 0x2f, 0x75, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72,
|
0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04,
|
||||||
0x6e, 0x61, 0x6c, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f,
|
0x6e, 0x61, 0x6d, 0x65, 0x12, 0x2d, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01,
|
||||||
0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
0x28, 0x0e, 0x32, 0x19, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x44, 0x4e, 0x53, 0x52, 0x65, 0x63, 0x6f,
|
||||||
|
0x72, 0x64, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74,
|
||||||
|
0x79, 0x70, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x03, 0x20,
|
||||||
|
0x03, 0x28, 0x09, 0x52, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x22, 0x2e, 0x0a, 0x0a, 0x52,
|
||||||
|
0x65, 0x63, 0x6f, 0x72, 0x64, 0x54, 0x79, 0x70, 0x65, 0x12, 0x0f, 0x0a, 0x0b, 0x55, 0x4e, 0x53,
|
||||||
|
0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x05, 0x0a, 0x01, 0x41, 0x10,
|
||||||
|
0x01, 0x12, 0x08, 0x0a, 0x04, 0x41, 0x41, 0x41, 0x41, 0x10, 0x02, 0x32, 0x92, 0x04, 0x0a, 0x07,
|
||||||
|
0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x12, 0x3d, 0x0a, 0x0a, 0x41, 0x64, 0x64, 0x4d, 0x61,
|
||||||
|
0x63, 0x68, 0x69, 0x6e, 0x65, 0x12, 0x16, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x41, 0x64, 0x64, 0x4d,
|
||||||
|
0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x17, 0x2e,
|
||||||
|
0x61, 0x70, 0x69, 0x2e, 0x41, 0x64, 0x64, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65,
|
||||||
|
0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x41, 0x0a, 0x0c, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x61,
|
||||||
|
0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e,
|
||||||
|
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x19,
|
||||||
|
0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65,
|
||||||
|
0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x46, 0x0a, 0x0d, 0x55, 0x70, 0x64,
|
||||||
|
0x61, 0x74, 0x65, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x12, 0x19, 0x2e, 0x61, 0x70, 0x69,
|
||||||
|
0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65,
|
||||||
|
0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x55, 0x70, 0x64, 0x61,
|
||||||
|
0x74, 0x65, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
|
||||||
|
0x65, 0x12, 0x42, 0x0a, 0x0d, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x4d, 0x61, 0x63, 0x68, 0x69,
|
||||||
|
0x6e, 0x65, 0x12, 0x19, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x4d,
|
||||||
|
0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e,
|
||||||
|
0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e,
|
||||||
|
0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x37, 0x0a, 0x0d, 0x52, 0x65, 0x73, 0x65, 0x72, 0x76, 0x65,
|
||||||
|
0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x19, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x52, 0x65, 0x73,
|
||||||
|
0x65, 0x72, 0x76, 0x65, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
|
||||||
|
0x74, 0x1a, 0x0b, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x30,
|
||||||
|
0x0a, 0x09, 0x47, 0x65, 0x74, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x16, 0x2e, 0x67, 0x6f,
|
||||||
|
0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d,
|
||||||
|
0x70, 0x74, 0x79, 0x1a, 0x0b, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e,
|
||||||
|
0x12, 0x34, 0x0a, 0x0d, 0x52, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x44, 0x6f, 0x6d, 0x61, 0x69,
|
||||||
|
0x6e, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f,
|
||||||
|
0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x0b, 0x2e, 0x61, 0x70, 0x69, 0x2e,
|
||||||
|
0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x58, 0x0a, 0x13, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65,
|
||||||
|
0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x12, 0x1f, 0x2e,
|
||||||
|
0x61, 0x70, 0x69, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e,
|
||||||
|
0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20,
|
||||||
|
0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x44, 0x6f, 0x6d, 0x61, 0x69,
|
||||||
|
0x6e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
|
||||||
|
0x42, 0x37, 0x5a, 0x35, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x70,
|
||||||
|
0x73, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x6b, 0x69, 0x2f, 0x75, 0x6e, 0x63, 0x6c, 0x6f, 0x75,
|
||||||
|
0x64, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69,
|
||||||
|
0x6e, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f,
|
||||||
|
0x33,
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -698,7 +893,7 @@ func file_internal_machine_api_pb_cluster_proto_rawDescGZIP() []byte {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var file_internal_machine_api_pb_cluster_proto_enumTypes = make([]protoimpl.EnumInfo, 2)
|
var file_internal_machine_api_pb_cluster_proto_enumTypes = make([]protoimpl.EnumInfo, 2)
|
||||||
var file_internal_machine_api_pb_cluster_proto_msgTypes = make([]protoimpl.MessageInfo, 9)
|
var file_internal_machine_api_pb_cluster_proto_msgTypes = make([]protoimpl.MessageInfo, 12)
|
||||||
var file_internal_machine_api_pb_cluster_proto_goTypes = []any{
|
var file_internal_machine_api_pb_cluster_proto_goTypes = []any{
|
||||||
(MachineMember_MembershipState)(0), // 0: api.MachineMember.MembershipState
|
(MachineMember_MembershipState)(0), // 0: api.MachineMember.MembershipState
|
||||||
(DNSRecord_RecordType)(0), // 1: api.DNSRecord.RecordType
|
(DNSRecord_RecordType)(0), // 1: api.DNSRecord.RecordType
|
||||||
@@ -706,43 +901,54 @@ var file_internal_machine_api_pb_cluster_proto_goTypes = []any{
|
|||||||
(*AddMachineResponse)(nil), // 3: api.AddMachineResponse
|
(*AddMachineResponse)(nil), // 3: api.AddMachineResponse
|
||||||
(*MachineMember)(nil), // 4: api.MachineMember
|
(*MachineMember)(nil), // 4: api.MachineMember
|
||||||
(*ListMachinesResponse)(nil), // 5: api.ListMachinesResponse
|
(*ListMachinesResponse)(nil), // 5: api.ListMachinesResponse
|
||||||
(*Domain)(nil), // 6: api.Domain
|
(*UpdateMachineRequest)(nil), // 6: api.UpdateMachineRequest
|
||||||
(*ReserveDomainRequest)(nil), // 7: api.ReserveDomainRequest
|
(*UpdateMachineResponse)(nil), // 7: api.UpdateMachineResponse
|
||||||
(*CreateDomainRecordsRequest)(nil), // 8: api.CreateDomainRecordsRequest
|
(*RemoveMachineRequest)(nil), // 8: api.RemoveMachineRequest
|
||||||
(*CreateDomainRecordsResponse)(nil), // 9: api.CreateDomainRecordsResponse
|
(*Domain)(nil), // 9: api.Domain
|
||||||
(*DNSRecord)(nil), // 10: api.DNSRecord
|
(*ReserveDomainRequest)(nil), // 10: api.ReserveDomainRequest
|
||||||
(*NetworkConfig)(nil), // 11: api.NetworkConfig
|
(*CreateDomainRecordsRequest)(nil), // 11: api.CreateDomainRecordsRequest
|
||||||
(*IP)(nil), // 12: api.IP
|
(*CreateDomainRecordsResponse)(nil), // 12: api.CreateDomainRecordsResponse
|
||||||
(*MachineInfo)(nil), // 13: api.MachineInfo
|
(*DNSRecord)(nil), // 13: api.DNSRecord
|
||||||
(*emptypb.Empty)(nil), // 14: google.protobuf.Empty
|
(*NetworkConfig)(nil), // 14: api.NetworkConfig
|
||||||
|
(*IP)(nil), // 15: api.IP
|
||||||
|
(*MachineInfo)(nil), // 16: api.MachineInfo
|
||||||
|
(*IPPort)(nil), // 17: api.IPPort
|
||||||
|
(*emptypb.Empty)(nil), // 18: google.protobuf.Empty
|
||||||
}
|
}
|
||||||
var file_internal_machine_api_pb_cluster_proto_depIdxs = []int32{
|
var file_internal_machine_api_pb_cluster_proto_depIdxs = []int32{
|
||||||
11, // 0: api.AddMachineRequest.network:type_name -> api.NetworkConfig
|
14, // 0: api.AddMachineRequest.network:type_name -> api.NetworkConfig
|
||||||
12, // 1: api.AddMachineRequest.public_ip:type_name -> api.IP
|
15, // 1: api.AddMachineRequest.public_ip:type_name -> api.IP
|
||||||
13, // 2: api.AddMachineResponse.machine:type_name -> api.MachineInfo
|
16, // 2: api.AddMachineResponse.machine:type_name -> api.MachineInfo
|
||||||
13, // 3: api.MachineMember.machine:type_name -> api.MachineInfo
|
16, // 3: api.MachineMember.machine:type_name -> api.MachineInfo
|
||||||
0, // 4: api.MachineMember.state:type_name -> api.MachineMember.MembershipState
|
0, // 4: api.MachineMember.state:type_name -> api.MachineMember.MembershipState
|
||||||
4, // 5: api.ListMachinesResponse.machines:type_name -> api.MachineMember
|
4, // 5: api.ListMachinesResponse.machines:type_name -> api.MachineMember
|
||||||
10, // 6: api.CreateDomainRecordsRequest.records:type_name -> api.DNSRecord
|
15, // 6: api.UpdateMachineRequest.public_ip:type_name -> api.IP
|
||||||
10, // 7: api.CreateDomainRecordsResponse.records:type_name -> api.DNSRecord
|
17, // 7: api.UpdateMachineRequest.endpoints:type_name -> api.IPPort
|
||||||
1, // 8: api.DNSRecord.type:type_name -> api.DNSRecord.RecordType
|
16, // 8: api.UpdateMachineResponse.machine:type_name -> api.MachineInfo
|
||||||
2, // 9: api.Cluster.AddMachine:input_type -> api.AddMachineRequest
|
13, // 9: api.CreateDomainRecordsRequest.records:type_name -> api.DNSRecord
|
||||||
14, // 10: api.Cluster.ListMachines:input_type -> google.protobuf.Empty
|
13, // 10: api.CreateDomainRecordsResponse.records:type_name -> api.DNSRecord
|
||||||
7, // 11: api.Cluster.ReserveDomain:input_type -> api.ReserveDomainRequest
|
1, // 11: api.DNSRecord.type:type_name -> api.DNSRecord.RecordType
|
||||||
14, // 12: api.Cluster.GetDomain:input_type -> google.protobuf.Empty
|
2, // 12: api.Cluster.AddMachine:input_type -> api.AddMachineRequest
|
||||||
14, // 13: api.Cluster.ReleaseDomain:input_type -> google.protobuf.Empty
|
18, // 13: api.Cluster.ListMachines:input_type -> google.protobuf.Empty
|
||||||
8, // 14: api.Cluster.CreateDomainRecords:input_type -> api.CreateDomainRecordsRequest
|
6, // 14: api.Cluster.UpdateMachine:input_type -> api.UpdateMachineRequest
|
||||||
3, // 15: api.Cluster.AddMachine:output_type -> api.AddMachineResponse
|
8, // 15: api.Cluster.RemoveMachine:input_type -> api.RemoveMachineRequest
|
||||||
5, // 16: api.Cluster.ListMachines:output_type -> api.ListMachinesResponse
|
10, // 16: api.Cluster.ReserveDomain:input_type -> api.ReserveDomainRequest
|
||||||
6, // 17: api.Cluster.ReserveDomain:output_type -> api.Domain
|
18, // 17: api.Cluster.GetDomain:input_type -> google.protobuf.Empty
|
||||||
6, // 18: api.Cluster.GetDomain:output_type -> api.Domain
|
18, // 18: api.Cluster.ReleaseDomain:input_type -> google.protobuf.Empty
|
||||||
6, // 19: api.Cluster.ReleaseDomain:output_type -> api.Domain
|
11, // 19: api.Cluster.CreateDomainRecords:input_type -> api.CreateDomainRecordsRequest
|
||||||
9, // 20: api.Cluster.CreateDomainRecords:output_type -> api.CreateDomainRecordsResponse
|
3, // 20: api.Cluster.AddMachine:output_type -> api.AddMachineResponse
|
||||||
15, // [15:21] is the sub-list for method output_type
|
5, // 21: api.Cluster.ListMachines:output_type -> api.ListMachinesResponse
|
||||||
9, // [9:15] is the sub-list for method input_type
|
7, // 22: api.Cluster.UpdateMachine:output_type -> api.UpdateMachineResponse
|
||||||
9, // [9:9] is the sub-list for extension type_name
|
18, // 23: api.Cluster.RemoveMachine:output_type -> google.protobuf.Empty
|
||||||
9, // [9:9] is the sub-list for extension extendee
|
9, // 24: api.Cluster.ReserveDomain:output_type -> api.Domain
|
||||||
0, // [0:9] is the sub-list for field type_name
|
9, // 25: api.Cluster.GetDomain:output_type -> api.Domain
|
||||||
|
9, // 26: api.Cluster.ReleaseDomain:output_type -> api.Domain
|
||||||
|
12, // 27: api.Cluster.CreateDomainRecords:output_type -> api.CreateDomainRecordsResponse
|
||||||
|
20, // [20:28] is the sub-list for method output_type
|
||||||
|
12, // [12:20] is the sub-list for method input_type
|
||||||
|
12, // [12:12] is the sub-list for extension type_name
|
||||||
|
12, // [12:12] is the sub-list for extension extendee
|
||||||
|
0, // [0:12] is the sub-list for field type_name
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() { file_internal_machine_api_pb_cluster_proto_init() }
|
func init() { file_internal_machine_api_pb_cluster_proto_init() }
|
||||||
@@ -802,7 +1008,7 @@ func file_internal_machine_api_pb_cluster_proto_init() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
file_internal_machine_api_pb_cluster_proto_msgTypes[4].Exporter = func(v any, i int) any {
|
file_internal_machine_api_pb_cluster_proto_msgTypes[4].Exporter = func(v any, i int) any {
|
||||||
switch v := v.(*Domain); i {
|
switch v := v.(*UpdateMachineRequest); i {
|
||||||
case 0:
|
case 0:
|
||||||
return &v.state
|
return &v.state
|
||||||
case 1:
|
case 1:
|
||||||
@@ -814,7 +1020,7 @@ func file_internal_machine_api_pb_cluster_proto_init() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
file_internal_machine_api_pb_cluster_proto_msgTypes[5].Exporter = func(v any, i int) any {
|
file_internal_machine_api_pb_cluster_proto_msgTypes[5].Exporter = func(v any, i int) any {
|
||||||
switch v := v.(*ReserveDomainRequest); i {
|
switch v := v.(*UpdateMachineResponse); i {
|
||||||
case 0:
|
case 0:
|
||||||
return &v.state
|
return &v.state
|
||||||
case 1:
|
case 1:
|
||||||
@@ -826,7 +1032,7 @@ func file_internal_machine_api_pb_cluster_proto_init() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
file_internal_machine_api_pb_cluster_proto_msgTypes[6].Exporter = func(v any, i int) any {
|
file_internal_machine_api_pb_cluster_proto_msgTypes[6].Exporter = func(v any, i int) any {
|
||||||
switch v := v.(*CreateDomainRecordsRequest); i {
|
switch v := v.(*RemoveMachineRequest); i {
|
||||||
case 0:
|
case 0:
|
||||||
return &v.state
|
return &v.state
|
||||||
case 1:
|
case 1:
|
||||||
@@ -838,7 +1044,7 @@ func file_internal_machine_api_pb_cluster_proto_init() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
file_internal_machine_api_pb_cluster_proto_msgTypes[7].Exporter = func(v any, i int) any {
|
file_internal_machine_api_pb_cluster_proto_msgTypes[7].Exporter = func(v any, i int) any {
|
||||||
switch v := v.(*CreateDomainRecordsResponse); i {
|
switch v := v.(*Domain); i {
|
||||||
case 0:
|
case 0:
|
||||||
return &v.state
|
return &v.state
|
||||||
case 1:
|
case 1:
|
||||||
@@ -850,6 +1056,42 @@ func file_internal_machine_api_pb_cluster_proto_init() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
file_internal_machine_api_pb_cluster_proto_msgTypes[8].Exporter = func(v any, i int) any {
|
file_internal_machine_api_pb_cluster_proto_msgTypes[8].Exporter = func(v any, i int) any {
|
||||||
|
switch v := v.(*ReserveDomainRequest); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file_internal_machine_api_pb_cluster_proto_msgTypes[9].Exporter = func(v any, i int) any {
|
||||||
|
switch v := v.(*CreateDomainRecordsRequest); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file_internal_machine_api_pb_cluster_proto_msgTypes[10].Exporter = func(v any, i int) any {
|
||||||
|
switch v := v.(*CreateDomainRecordsResponse); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file_internal_machine_api_pb_cluster_proto_msgTypes[11].Exporter = func(v any, i int) any {
|
||||||
switch v := v.(*DNSRecord); i {
|
switch v := v.(*DNSRecord); i {
|
||||||
case 0:
|
case 0:
|
||||||
return &v.state
|
return &v.state
|
||||||
@@ -862,13 +1104,14 @@ func file_internal_machine_api_pb_cluster_proto_init() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
file_internal_machine_api_pb_cluster_proto_msgTypes[4].OneofWrappers = []any{}
|
||||||
type x struct{}
|
type x struct{}
|
||||||
out := protoimpl.TypeBuilder{
|
out := protoimpl.TypeBuilder{
|
||||||
File: protoimpl.DescBuilder{
|
File: protoimpl.DescBuilder{
|
||||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||||
RawDescriptor: file_internal_machine_api_pb_cluster_proto_rawDesc,
|
RawDescriptor: file_internal_machine_api_pb_cluster_proto_rawDesc,
|
||||||
NumEnums: 2,
|
NumEnums: 2,
|
||||||
NumMessages: 9,
|
NumMessages: 12,
|
||||||
NumExtensions: 0,
|
NumExtensions: 0,
|
||||||
NumServices: 1,
|
NumServices: 1,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ import "internal/machine/api/pb/machine.proto";
|
|||||||
service Cluster {
|
service Cluster {
|
||||||
rpc AddMachine(AddMachineRequest) returns (AddMachineResponse);
|
rpc AddMachine(AddMachineRequest) returns (AddMachineResponse);
|
||||||
rpc ListMachines(google.protobuf.Empty) returns (ListMachinesResponse);
|
rpc ListMachines(google.protobuf.Empty) returns (ListMachinesResponse);
|
||||||
|
rpc UpdateMachine(UpdateMachineRequest) returns (UpdateMachineResponse);
|
||||||
|
rpc RemoveMachine(RemoveMachineRequest) returns (google.protobuf.Empty);
|
||||||
|
|
||||||
rpc ReserveDomain(ReserveDomainRequest) returns (Domain);
|
rpc ReserveDomain(ReserveDomainRequest) returns (Domain);
|
||||||
rpc GetDomain(google.protobuf.Empty) returns (Domain);
|
rpc GetDomain(google.protobuf.Empty) returns (Domain);
|
||||||
@@ -49,6 +51,24 @@ message ListMachinesResponse {
|
|||||||
repeated MachineMember machines = 1;
|
repeated MachineMember machines = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message UpdateMachineRequest {
|
||||||
|
// Machine to update
|
||||||
|
string machine_id = 1;
|
||||||
|
|
||||||
|
// Updated machine information
|
||||||
|
optional string name = 2;
|
||||||
|
optional IP public_ip = 3;
|
||||||
|
repeated IPPort endpoints = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message UpdateMachineResponse {
|
||||||
|
MachineInfo machine = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message RemoveMachineRequest {
|
||||||
|
string id = 1;
|
||||||
|
}
|
||||||
|
|
||||||
message Domain {
|
message Domain {
|
||||||
string name = 1;
|
string name = 1;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ const _ = grpc.SupportPackageIsVersion9
|
|||||||
const (
|
const (
|
||||||
Cluster_AddMachine_FullMethodName = "/api.Cluster/AddMachine"
|
Cluster_AddMachine_FullMethodName = "/api.Cluster/AddMachine"
|
||||||
Cluster_ListMachines_FullMethodName = "/api.Cluster/ListMachines"
|
Cluster_ListMachines_FullMethodName = "/api.Cluster/ListMachines"
|
||||||
|
Cluster_UpdateMachine_FullMethodName = "/api.Cluster/UpdateMachine"
|
||||||
|
Cluster_RemoveMachine_FullMethodName = "/api.Cluster/RemoveMachine"
|
||||||
Cluster_ReserveDomain_FullMethodName = "/api.Cluster/ReserveDomain"
|
Cluster_ReserveDomain_FullMethodName = "/api.Cluster/ReserveDomain"
|
||||||
Cluster_GetDomain_FullMethodName = "/api.Cluster/GetDomain"
|
Cluster_GetDomain_FullMethodName = "/api.Cluster/GetDomain"
|
||||||
Cluster_ReleaseDomain_FullMethodName = "/api.Cluster/ReleaseDomain"
|
Cluster_ReleaseDomain_FullMethodName = "/api.Cluster/ReleaseDomain"
|
||||||
@@ -34,6 +36,8 @@ const (
|
|||||||
type ClusterClient interface {
|
type ClusterClient interface {
|
||||||
AddMachine(ctx context.Context, in *AddMachineRequest, opts ...grpc.CallOption) (*AddMachineResponse, error)
|
AddMachine(ctx context.Context, in *AddMachineRequest, opts ...grpc.CallOption) (*AddMachineResponse, error)
|
||||||
ListMachines(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*ListMachinesResponse, error)
|
ListMachines(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*ListMachinesResponse, error)
|
||||||
|
UpdateMachine(ctx context.Context, in *UpdateMachineRequest, opts ...grpc.CallOption) (*UpdateMachineResponse, error)
|
||||||
|
RemoveMachine(ctx context.Context, in *RemoveMachineRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
|
||||||
ReserveDomain(ctx context.Context, in *ReserveDomainRequest, opts ...grpc.CallOption) (*Domain, error)
|
ReserveDomain(ctx context.Context, in *ReserveDomainRequest, opts ...grpc.CallOption) (*Domain, error)
|
||||||
GetDomain(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*Domain, error)
|
GetDomain(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*Domain, error)
|
||||||
ReleaseDomain(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*Domain, error)
|
ReleaseDomain(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*Domain, error)
|
||||||
@@ -68,6 +72,26 @@ func (c *clusterClient) ListMachines(ctx context.Context, in *emptypb.Empty, opt
|
|||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *clusterClient) UpdateMachine(ctx context.Context, in *UpdateMachineRequest, opts ...grpc.CallOption) (*UpdateMachineResponse, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(UpdateMachineResponse)
|
||||||
|
err := c.cc.Invoke(ctx, Cluster_UpdateMachine_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *clusterClient) RemoveMachine(ctx context.Context, in *RemoveMachineRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(emptypb.Empty)
|
||||||
|
err := c.cc.Invoke(ctx, Cluster_RemoveMachine_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (c *clusterClient) ReserveDomain(ctx context.Context, in *ReserveDomainRequest, opts ...grpc.CallOption) (*Domain, error) {
|
func (c *clusterClient) ReserveDomain(ctx context.Context, in *ReserveDomainRequest, opts ...grpc.CallOption) (*Domain, error) {
|
||||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
out := new(Domain)
|
out := new(Domain)
|
||||||
@@ -114,6 +138,8 @@ func (c *clusterClient) CreateDomainRecords(ctx context.Context, in *CreateDomai
|
|||||||
type ClusterServer interface {
|
type ClusterServer interface {
|
||||||
AddMachine(context.Context, *AddMachineRequest) (*AddMachineResponse, error)
|
AddMachine(context.Context, *AddMachineRequest) (*AddMachineResponse, error)
|
||||||
ListMachines(context.Context, *emptypb.Empty) (*ListMachinesResponse, error)
|
ListMachines(context.Context, *emptypb.Empty) (*ListMachinesResponse, error)
|
||||||
|
UpdateMachine(context.Context, *UpdateMachineRequest) (*UpdateMachineResponse, error)
|
||||||
|
RemoveMachine(context.Context, *RemoveMachineRequest) (*emptypb.Empty, error)
|
||||||
ReserveDomain(context.Context, *ReserveDomainRequest) (*Domain, error)
|
ReserveDomain(context.Context, *ReserveDomainRequest) (*Domain, error)
|
||||||
GetDomain(context.Context, *emptypb.Empty) (*Domain, error)
|
GetDomain(context.Context, *emptypb.Empty) (*Domain, error)
|
||||||
ReleaseDomain(context.Context, *emptypb.Empty) (*Domain, error)
|
ReleaseDomain(context.Context, *emptypb.Empty) (*Domain, error)
|
||||||
@@ -134,6 +160,12 @@ func (UnimplementedClusterServer) AddMachine(context.Context, *AddMachineRequest
|
|||||||
func (UnimplementedClusterServer) ListMachines(context.Context, *emptypb.Empty) (*ListMachinesResponse, error) {
|
func (UnimplementedClusterServer) ListMachines(context.Context, *emptypb.Empty) (*ListMachinesResponse, error) {
|
||||||
return nil, status.Errorf(codes.Unimplemented, "method ListMachines not implemented")
|
return nil, status.Errorf(codes.Unimplemented, "method ListMachines not implemented")
|
||||||
}
|
}
|
||||||
|
func (UnimplementedClusterServer) UpdateMachine(context.Context, *UpdateMachineRequest) (*UpdateMachineResponse, error) {
|
||||||
|
return nil, status.Errorf(codes.Unimplemented, "method UpdateMachine not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedClusterServer) RemoveMachine(context.Context, *RemoveMachineRequest) (*emptypb.Empty, error) {
|
||||||
|
return nil, status.Errorf(codes.Unimplemented, "method RemoveMachine not implemented")
|
||||||
|
}
|
||||||
func (UnimplementedClusterServer) ReserveDomain(context.Context, *ReserveDomainRequest) (*Domain, error) {
|
func (UnimplementedClusterServer) ReserveDomain(context.Context, *ReserveDomainRequest) (*Domain, error) {
|
||||||
return nil, status.Errorf(codes.Unimplemented, "method ReserveDomain not implemented")
|
return nil, status.Errorf(codes.Unimplemented, "method ReserveDomain not implemented")
|
||||||
}
|
}
|
||||||
@@ -203,6 +235,42 @@ func _Cluster_ListMachines_Handler(srv interface{}, ctx context.Context, dec fun
|
|||||||
return interceptor(ctx, in, info, handler)
|
return interceptor(ctx, in, info, handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func _Cluster_UpdateMachine_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(UpdateMachineRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(ClusterServer).UpdateMachine(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: Cluster_UpdateMachine_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(ClusterServer).UpdateMachine(ctx, req.(*UpdateMachineRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _Cluster_RemoveMachine_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(RemoveMachineRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(ClusterServer).RemoveMachine(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: Cluster_RemoveMachine_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(ClusterServer).RemoveMachine(ctx, req.(*RemoveMachineRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
func _Cluster_ReserveDomain_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
func _Cluster_ReserveDomain_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
in := new(ReserveDomainRequest)
|
in := new(ReserveDomainRequest)
|
||||||
if err := dec(in); err != nil {
|
if err := dec(in); err != nil {
|
||||||
@@ -290,6 +358,14 @@ var Cluster_ServiceDesc = grpc.ServiceDesc{
|
|||||||
MethodName: "ListMachines",
|
MethodName: "ListMachines",
|
||||||
Handler: _Cluster_ListMachines_Handler,
|
Handler: _Cluster_ListMachines_Handler,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
MethodName: "UpdateMachine",
|
||||||
|
Handler: _Cluster_UpdateMachine_Handler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MethodName: "RemoveMachine",
|
||||||
|
Handler: _Cluster_RemoveMachine_Handler,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
MethodName: "ReserveDomain",
|
MethodName: "ReserveDomain",
|
||||||
Handler: _Cluster_ReserveDomain_Handler,
|
Handler: _Cluster_ReserveDomain_Handler,
|
||||||
|
|||||||
@@ -163,6 +163,63 @@ func (x *NetworkConfig) GetPublicKey() []byte {
|
|||||||
return nil
|
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 {
|
type InitClusterRequest struct {
|
||||||
state protoimpl.MessageState
|
state protoimpl.MessageState
|
||||||
sizeCache protoimpl.SizeCache
|
sizeCache protoimpl.SizeCache
|
||||||
@@ -180,7 +237,7 @@ type InitClusterRequest struct {
|
|||||||
func (x *InitClusterRequest) Reset() {
|
func (x *InitClusterRequest) Reset() {
|
||||||
*x = InitClusterRequest{}
|
*x = InitClusterRequest{}
|
||||||
if protoimpl.UnsafeEnabled {
|
if protoimpl.UnsafeEnabled {
|
||||||
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[2]
|
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[3]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@@ -193,7 +250,7 @@ func (x *InitClusterRequest) String() string {
|
|||||||
func (*InitClusterRequest) ProtoMessage() {}
|
func (*InitClusterRequest) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *InitClusterRequest) ProtoReflect() protoreflect.Message {
|
func (x *InitClusterRequest) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[2]
|
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[3]
|
||||||
if protoimpl.UnsafeEnabled && x != nil {
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
@@ -206,7 +263,7 @@ func (x *InitClusterRequest) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use InitClusterRequest.ProtoReflect.Descriptor instead.
|
// Deprecated: Use InitClusterRequest.ProtoReflect.Descriptor instead.
|
||||||
func (*InitClusterRequest) Descriptor() ([]byte, []int) {
|
func (*InitClusterRequest) Descriptor() ([]byte, []int) {
|
||||||
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{2}
|
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{3}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *InitClusterRequest) GetMachineName() string {
|
func (x *InitClusterRequest) GetMachineName() string {
|
||||||
@@ -271,7 +328,7 @@ type InitClusterResponse struct {
|
|||||||
func (x *InitClusterResponse) Reset() {
|
func (x *InitClusterResponse) Reset() {
|
||||||
*x = InitClusterResponse{}
|
*x = InitClusterResponse{}
|
||||||
if protoimpl.UnsafeEnabled {
|
if protoimpl.UnsafeEnabled {
|
||||||
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[3]
|
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[4]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@@ -284,7 +341,7 @@ func (x *InitClusterResponse) String() string {
|
|||||||
func (*InitClusterResponse) ProtoMessage() {}
|
func (*InitClusterResponse) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *InitClusterResponse) ProtoReflect() protoreflect.Message {
|
func (x *InitClusterResponse) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[3]
|
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[4]
|
||||||
if protoimpl.UnsafeEnabled && x != nil {
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
@@ -297,7 +354,7 @@ func (x *InitClusterResponse) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use InitClusterResponse.ProtoReflect.Descriptor instead.
|
// Deprecated: Use InitClusterResponse.ProtoReflect.Descriptor instead.
|
||||||
func (*InitClusterResponse) Descriptor() ([]byte, []int) {
|
func (*InitClusterResponse) Descriptor() ([]byte, []int) {
|
||||||
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{3}
|
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{4}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *InitClusterResponse) GetMachine() *MachineInfo {
|
func (x *InitClusterResponse) GetMachine() *MachineInfo {
|
||||||
@@ -319,7 +376,7 @@ type JoinClusterRequest struct {
|
|||||||
func (x *JoinClusterRequest) Reset() {
|
func (x *JoinClusterRequest) Reset() {
|
||||||
*x = JoinClusterRequest{}
|
*x = JoinClusterRequest{}
|
||||||
if protoimpl.UnsafeEnabled {
|
if protoimpl.UnsafeEnabled {
|
||||||
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[4]
|
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[5]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@@ -332,7 +389,7 @@ func (x *JoinClusterRequest) String() string {
|
|||||||
func (*JoinClusterRequest) ProtoMessage() {}
|
func (*JoinClusterRequest) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *JoinClusterRequest) ProtoReflect() protoreflect.Message {
|
func (x *JoinClusterRequest) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[4]
|
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[5]
|
||||||
if protoimpl.UnsafeEnabled && x != nil {
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
@@ -345,7 +402,7 @@ func (x *JoinClusterRequest) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use JoinClusterRequest.ProtoReflect.Descriptor instead.
|
// Deprecated: Use JoinClusterRequest.ProtoReflect.Descriptor instead.
|
||||||
func (*JoinClusterRequest) Descriptor() ([]byte, []int) {
|
func (*JoinClusterRequest) Descriptor() ([]byte, []int) {
|
||||||
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{4}
|
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{5}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *JoinClusterRequest) GetMachine() *MachineInfo {
|
func (x *JoinClusterRequest) GetMachine() *MachineInfo {
|
||||||
@@ -373,7 +430,7 @@ type TokenResponse struct {
|
|||||||
func (x *TokenResponse) Reset() {
|
func (x *TokenResponse) Reset() {
|
||||||
*x = TokenResponse{}
|
*x = TokenResponse{}
|
||||||
if protoimpl.UnsafeEnabled {
|
if protoimpl.UnsafeEnabled {
|
||||||
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[5]
|
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[6]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@@ -386,7 +443,7 @@ func (x *TokenResponse) String() string {
|
|||||||
func (*TokenResponse) ProtoMessage() {}
|
func (*TokenResponse) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *TokenResponse) ProtoReflect() protoreflect.Message {
|
func (x *TokenResponse) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[5]
|
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[6]
|
||||||
if protoimpl.UnsafeEnabled && x != nil {
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
@@ -399,7 +456,7 @@ func (x *TokenResponse) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use TokenResponse.ProtoReflect.Descriptor instead.
|
// Deprecated: Use TokenResponse.ProtoReflect.Descriptor instead.
|
||||||
func (*TokenResponse) Descriptor() ([]byte, []int) {
|
func (*TokenResponse) Descriptor() ([]byte, []int) {
|
||||||
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{5}
|
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{6}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *TokenResponse) GetToken() string {
|
func (x *TokenResponse) GetToken() string {
|
||||||
@@ -409,6 +466,44 @@ func (x *TokenResponse) GetToken() string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ResetRequest struct {
|
||||||
|
state protoimpl.MessageState
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ResetRequest) Reset() {
|
||||||
|
*x = ResetRequest{}
|
||||||
|
if protoimpl.UnsafeEnabled {
|
||||||
|
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[7]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ResetRequest) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*ResetRequest) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *ResetRequest) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[7]
|
||||||
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use ResetRequest.ProtoReflect.Descriptor instead.
|
||||||
|
func (*ResetRequest) Descriptor() ([]byte, []int) {
|
||||||
|
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{7}
|
||||||
|
}
|
||||||
|
|
||||||
type Service struct {
|
type Service struct {
|
||||||
state protoimpl.MessageState
|
state protoimpl.MessageState
|
||||||
sizeCache protoimpl.SizeCache
|
sizeCache protoimpl.SizeCache
|
||||||
@@ -423,7 +518,7 @@ type Service struct {
|
|||||||
func (x *Service) Reset() {
|
func (x *Service) Reset() {
|
||||||
*x = Service{}
|
*x = Service{}
|
||||||
if protoimpl.UnsafeEnabled {
|
if protoimpl.UnsafeEnabled {
|
||||||
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[6]
|
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[8]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@@ -436,7 +531,7 @@ func (x *Service) String() string {
|
|||||||
func (*Service) ProtoMessage() {}
|
func (*Service) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *Service) ProtoReflect() protoreflect.Message {
|
func (x *Service) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[6]
|
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[8]
|
||||||
if protoimpl.UnsafeEnabled && x != nil {
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
@@ -449,7 +544,7 @@ func (x *Service) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use Service.ProtoReflect.Descriptor instead.
|
// Deprecated: Use Service.ProtoReflect.Descriptor instead.
|
||||||
func (*Service) Descriptor() ([]byte, []int) {
|
func (*Service) Descriptor() ([]byte, []int) {
|
||||||
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{6}
|
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{8}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *Service) GetId() string {
|
func (x *Service) GetId() string {
|
||||||
@@ -491,7 +586,7 @@ type InspectServiceRequest struct {
|
|||||||
func (x *InspectServiceRequest) Reset() {
|
func (x *InspectServiceRequest) Reset() {
|
||||||
*x = InspectServiceRequest{}
|
*x = InspectServiceRequest{}
|
||||||
if protoimpl.UnsafeEnabled {
|
if protoimpl.UnsafeEnabled {
|
||||||
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[7]
|
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[9]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@@ -504,7 +599,7 @@ func (x *InspectServiceRequest) String() string {
|
|||||||
func (*InspectServiceRequest) ProtoMessage() {}
|
func (*InspectServiceRequest) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *InspectServiceRequest) ProtoReflect() protoreflect.Message {
|
func (x *InspectServiceRequest) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[7]
|
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[9]
|
||||||
if protoimpl.UnsafeEnabled && x != nil {
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
@@ -517,7 +612,7 @@ func (x *InspectServiceRequest) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use InspectServiceRequest.ProtoReflect.Descriptor instead.
|
// Deprecated: Use InspectServiceRequest.ProtoReflect.Descriptor instead.
|
||||||
func (*InspectServiceRequest) Descriptor() ([]byte, []int) {
|
func (*InspectServiceRequest) Descriptor() ([]byte, []int) {
|
||||||
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{7}
|
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{9}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *InspectServiceRequest) GetId() string {
|
func (x *InspectServiceRequest) GetId() string {
|
||||||
@@ -538,7 +633,7 @@ type InspectServiceResponse struct {
|
|||||||
func (x *InspectServiceResponse) Reset() {
|
func (x *InspectServiceResponse) Reset() {
|
||||||
*x = InspectServiceResponse{}
|
*x = InspectServiceResponse{}
|
||||||
if protoimpl.UnsafeEnabled {
|
if protoimpl.UnsafeEnabled {
|
||||||
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[8]
|
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[10]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@@ -551,7 +646,7 @@ func (x *InspectServiceResponse) String() string {
|
|||||||
func (*InspectServiceResponse) ProtoMessage() {}
|
func (*InspectServiceResponse) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *InspectServiceResponse) ProtoReflect() protoreflect.Message {
|
func (x *InspectServiceResponse) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[8]
|
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[10]
|
||||||
if protoimpl.UnsafeEnabled && x != nil {
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
@@ -564,7 +659,7 @@ func (x *InspectServiceResponse) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use InspectServiceResponse.ProtoReflect.Descriptor instead.
|
// Deprecated: Use InspectServiceResponse.ProtoReflect.Descriptor instead.
|
||||||
func (*InspectServiceResponse) Descriptor() ([]byte, []int) {
|
func (*InspectServiceResponse) Descriptor() ([]byte, []int) {
|
||||||
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{8}
|
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{10}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *InspectServiceResponse) GetService() *Service {
|
func (x *InspectServiceResponse) GetService() *Service {
|
||||||
@@ -587,7 +682,7 @@ type Service_Container struct {
|
|||||||
func (x *Service_Container) Reset() {
|
func (x *Service_Container) Reset() {
|
||||||
*x = Service_Container{}
|
*x = Service_Container{}
|
||||||
if protoimpl.UnsafeEnabled {
|
if protoimpl.UnsafeEnabled {
|
||||||
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[9]
|
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[11]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@@ -600,7 +695,7 @@ func (x *Service_Container) String() string {
|
|||||||
func (*Service_Container) ProtoMessage() {}
|
func (*Service_Container) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *Service_Container) ProtoReflect() protoreflect.Message {
|
func (x *Service_Container) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[9]
|
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[11]
|
||||||
if protoimpl.UnsafeEnabled && x != nil {
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
@@ -613,7 +708,7 @@ func (x *Service_Container) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use Service_Container.ProtoReflect.Descriptor instead.
|
// Deprecated: Use Service_Container.ProtoReflect.Descriptor instead.
|
||||||
func (*Service_Container) Descriptor() ([]byte, []int) {
|
func (*Service_Container) Descriptor() ([]byte, []int) {
|
||||||
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{6, 0}
|
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{8, 0}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *Service_Container) GetMachineId() string {
|
func (x *Service_Container) GetMachineId() string {
|
||||||
@@ -659,77 +754,91 @@ 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,
|
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,
|
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,
|
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,
|
0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x22, 0x50, 0x0a, 0x1a, 0x43, 0x68, 0x65, 0x63,
|
||||||
0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12,
|
0x6b, 0x50, 0x72, 0x65, 0x72, 0x65, 0x71, 0x75, 0x69, 0x73, 0x69, 0x74, 0x65, 0x73, 0x52, 0x65,
|
||||||
0x20, 0x0a, 0x0b, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x01,
|
0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x61, 0x74, 0x69, 0x73, 0x66,
|
||||||
0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x4e, 0x61, 0x6d,
|
0x69, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x73, 0x61, 0x74, 0x69, 0x73,
|
||||||
0x65, 0x12, 0x27, 0x0a, 0x07, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x18, 0x02, 0x20, 0x01,
|
0x66, 0x69, 0x65, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x02, 0x20,
|
||||||
0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x50, 0x50, 0x72, 0x65, 0x66, 0x69,
|
0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x22, 0xc3, 0x01, 0x0a, 0x12, 0x49,
|
||||||
0x78, 0x52, 0x07, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x12, 0x26, 0x0a, 0x09, 0x70, 0x75,
|
0x6e, 0x69, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
|
||||||
0x62, 0x6c, 0x69, 0x63, 0x5f, 0x69, 0x70, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x07, 0x2e,
|
0x74, 0x12, 0x20, 0x0a, 0x0b, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x4e, 0x61, 0x6d, 0x65,
|
||||||
0x61, 0x70, 0x69, 0x2e, 0x49, 0x50, 0x48, 0x00, 0x52, 0x08, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63,
|
0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x4e,
|
||||||
0x49, 0x70, 0x12, 0x26, 0x0a, 0x0e, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x69, 0x70, 0x5f,
|
0x61, 0x6d, 0x65, 0x12, 0x27, 0x0a, 0x07, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x18, 0x02,
|
||||||
0x61, 0x75, 0x74, 0x6f, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x48, 0x00, 0x52, 0x0c, 0x70, 0x75,
|
0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x50, 0x50, 0x72, 0x65,
|
||||||
0x62, 0x6c, 0x69, 0x63, 0x49, 0x70, 0x41, 0x75, 0x74, 0x6f, 0x42, 0x12, 0x0a, 0x10, 0x70, 0x75,
|
0x66, 0x69, 0x78, 0x52, 0x07, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x12, 0x26, 0x0a, 0x09,
|
||||||
0x62, 0x6c, 0x69, 0x63, 0x5f, 0x69, 0x70, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0x41,
|
0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x69, 0x70, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32,
|
||||||
0x0a, 0x13, 0x49, 0x6e, 0x69, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x73,
|
0x07, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x50, 0x48, 0x00, 0x52, 0x08, 0x70, 0x75, 0x62, 0x6c,
|
||||||
0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2a, 0x0a, 0x07, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65,
|
0x69, 0x63, 0x49, 0x70, 0x12, 0x26, 0x0a, 0x0e, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x69,
|
||||||
0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d, 0x61, 0x63,
|
0x70, 0x5f, 0x61, 0x75, 0x74, 0x6f, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x48, 0x00, 0x52, 0x0c,
|
||||||
0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x07, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e,
|
0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x49, 0x70, 0x41, 0x75, 0x74, 0x6f, 0x42, 0x12, 0x0a, 0x10,
|
||||||
0x65, 0x22, 0x79, 0x0a, 0x12, 0x4a, 0x6f, 0x69, 0x6e, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72,
|
0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x69, 0x70, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67,
|
||||||
0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2a, 0x0a, 0x07, 0x6d, 0x61, 0x63, 0x68, 0x69,
|
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,
|
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,
|
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,
|
0x69, 0x6e, 0x65, 0x22, 0x79, 0x0a, 0x12, 0x4a, 0x6f, 0x69, 0x6e, 0x43, 0x6c, 0x75, 0x73, 0x74,
|
||||||
0x68, 0x69, 0x6e, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x61, 0x70,
|
0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2a, 0x0a, 0x07, 0x6d, 0x61, 0x63,
|
||||||
0x69, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x0d, 0x6f,
|
0x68, 0x69, 0x6e, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x61, 0x70, 0x69,
|
||||||
0x74, 0x68, 0x65, 0x72, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x22, 0x25, 0x0a, 0x0d,
|
0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x07, 0x6d, 0x61,
|
||||||
0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x14, 0x0a,
|
0x63, 0x68, 0x69, 0x6e, 0x65, 0x12, 0x37, 0x0a, 0x0e, 0x6f, 0x74, 0x68, 0x65, 0x72, 0x5f, 0x6d,
|
||||||
0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x6f,
|
0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x10, 0x2e,
|
||||||
0x6b, 0x65, 0x6e, 0x22, 0xc3, 0x01, 0x0a, 0x07, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12,
|
0x61, 0x70, 0x69, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52,
|
||||||
0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12,
|
0x0d, 0x6f, 0x74, 0x68, 0x65, 0x72, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x22, 0x25,
|
||||||
0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e,
|
0x0a, 0x0d, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12,
|
||||||
0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28,
|
0x14, 0x0a, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05,
|
||||||
0x09, 0x52, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x12, 0x36, 0x0a, 0x0a, 0x63, 0x6f, 0x6e, 0x74, 0x61,
|
0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0x0e, 0x0a, 0x0c, 0x52, 0x65, 0x73, 0x65, 0x74, 0x52, 0x65,
|
||||||
0x69, 0x6e, 0x65, 0x72, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x61, 0x70,
|
0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0xc3, 0x01, 0x0a, 0x07, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63,
|
||||||
0x69, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69,
|
0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69,
|
||||||
0x6e, 0x65, 0x72, 0x52, 0x0a, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x1a,
|
0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52,
|
||||||
0x48, 0x0a, 0x09, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x1d, 0x0a, 0x0a,
|
0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x18, 0x03, 0x20,
|
||||||
0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
|
0x01, 0x28, 0x09, 0x52, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x12, 0x36, 0x0a, 0x0a, 0x63, 0x6f, 0x6e,
|
||||||
0x52, 0x09, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x64, 0x12, 0x1c, 0x0a, 0x09, 0x63,
|
0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e,
|
||||||
0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09,
|
0x61, 0x70, 0x69, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x43, 0x6f, 0x6e, 0x74,
|
||||||
0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x22, 0x27, 0x0a, 0x15, 0x49, 0x6e, 0x73,
|
0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x0a, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72,
|
||||||
0x70, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65,
|
0x73, 0x1a, 0x48, 0x0a, 0x09, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x1d,
|
||||||
0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02,
|
0x0a, 0x0a, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01,
|
||||||
0x69, 0x64, 0x22, 0x40, 0x0a, 0x16, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72,
|
0x28, 0x09, 0x52, 0x09, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x64, 0x12, 0x1c, 0x0a,
|
||||||
0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x26, 0x0a, 0x07,
|
0x09, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c,
|
||||||
0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0c, 0x2e,
|
0x52, 0x09, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x22, 0x27, 0x0a, 0x15, 0x49,
|
||||||
0x61, 0x70, 0x69, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x07, 0x73, 0x65, 0x72,
|
0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x71,
|
||||||
0x76, 0x69, 0x63, 0x65, 0x32, 0xc0, 0x02, 0x0a, 0x07, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65,
|
0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
|
||||||
0x12, 0x40, 0x0a, 0x0b, 0x49, 0x6e, 0x69, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x12,
|
0x52, 0x02, 0x69, 0x64, 0x22, 0x40, 0x0a, 0x16, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53,
|
||||||
0x17, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x69, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65,
|
0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x26,
|
||||||
0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49,
|
0x0a, 0x07, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32,
|
||||||
0x6e, 0x69, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
|
0x0c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x07, 0x73,
|
||||||
0x73, 0x65, 0x12, 0x3e, 0x0a, 0x0b, 0x4a, 0x6f, 0x69, 0x6e, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65,
|
0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x32, 0xc3, 0x03, 0x0a, 0x07, 0x4d, 0x61, 0x63, 0x68, 0x69,
|
||||||
0x72, 0x12, 0x17, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4a, 0x6f, 0x69, 0x6e, 0x43, 0x6c, 0x75, 0x73,
|
0x6e, 0x65, 0x12, 0x4d, 0x0a, 0x12, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x50, 0x72, 0x65, 0x72, 0x65,
|
||||||
0x74, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f,
|
0x71, 0x75, 0x69, 0x73, 0x69, 0x74, 0x65, 0x73, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c,
|
||||||
0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70,
|
0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79,
|
||||||
0x74, 0x79, 0x12, 0x33, 0x0a, 0x05, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x16, 0x2e, 0x67, 0x6f,
|
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,
|
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,
|
0x70, 0x74, 0x79, 0x12, 0x33, 0x0a, 0x05, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x16, 0x2e, 0x67,
|
||||||
0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x33, 0x0a, 0x07, 0x49, 0x6e, 0x73, 0x70, 0x65,
|
0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45,
|
||||||
0x63, 0x74, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74,
|
0x6d, 0x70, 0x74, 0x79, 0x1a, 0x12, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x54, 0x6f, 0x6b, 0x65, 0x6e,
|
||||||
0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x10, 0x2e, 0x61, 0x70, 0x69,
|
0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x33, 0x0a, 0x07, 0x49, 0x6e, 0x73, 0x70,
|
||||||
0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x49, 0x0a, 0x0e,
|
0x65, 0x63, 0x74, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f,
|
||||||
0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x1a,
|
0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x10, 0x2e, 0x61, 0x70,
|
||||||
0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72, 0x76,
|
0x69, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x32, 0x0a,
|
||||||
0x69, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x61, 0x70, 0x69,
|
0x05, 0x52, 0x65, 0x73, 0x65, 0x74, 0x12, 0x11, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x52, 0x65, 0x73,
|
||||||
0x2e, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52,
|
0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67,
|
||||||
0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x37, 0x5a, 0x35, 0x67, 0x69, 0x74, 0x68, 0x75,
|
0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74,
|
||||||
0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x70, 0x73, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x6b, 0x69,
|
0x79, 0x12, 0x49, 0x0a, 0x0e, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72, 0x76,
|
||||||
0x2f, 0x75, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61,
|
0x69, 0x63, 0x65, 0x12, 0x1a, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63,
|
||||||
0x6c, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x62,
|
0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a,
|
||||||
0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
0x1b, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72,
|
||||||
|
0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x37, 0x5a, 0x35,
|
||||||
|
0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x70, 0x73, 0x76, 0x69, 0x64,
|
||||||
|
0x65, 0x72, 0x73, 0x6b, 0x69, 0x2f, 0x75, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2f, 0x69, 0x6e,
|
||||||
|
0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x2f, 0x61,
|
||||||
|
0x70, 0x69, 0x2f, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -744,48 +853,54 @@ func file_internal_machine_api_pb_machine_proto_rawDescGZIP() []byte {
|
|||||||
return file_internal_machine_api_pb_machine_proto_rawDescData
|
return file_internal_machine_api_pb_machine_proto_rawDescData
|
||||||
}
|
}
|
||||||
|
|
||||||
var file_internal_machine_api_pb_machine_proto_msgTypes = make([]protoimpl.MessageInfo, 10)
|
var file_internal_machine_api_pb_machine_proto_msgTypes = make([]protoimpl.MessageInfo, 12)
|
||||||
var file_internal_machine_api_pb_machine_proto_goTypes = []any{
|
var file_internal_machine_api_pb_machine_proto_goTypes = []any{
|
||||||
(*MachineInfo)(nil), // 0: api.MachineInfo
|
(*MachineInfo)(nil), // 0: api.MachineInfo
|
||||||
(*NetworkConfig)(nil), // 1: api.NetworkConfig
|
(*NetworkConfig)(nil), // 1: api.NetworkConfig
|
||||||
(*InitClusterRequest)(nil), // 2: api.InitClusterRequest
|
(*CheckPrerequisitesResponse)(nil), // 2: api.CheckPrerequisitesResponse
|
||||||
(*InitClusterResponse)(nil), // 3: api.InitClusterResponse
|
(*InitClusterRequest)(nil), // 3: api.InitClusterRequest
|
||||||
(*JoinClusterRequest)(nil), // 4: api.JoinClusterRequest
|
(*InitClusterResponse)(nil), // 4: api.InitClusterResponse
|
||||||
(*TokenResponse)(nil), // 5: api.TokenResponse
|
(*JoinClusterRequest)(nil), // 5: api.JoinClusterRequest
|
||||||
(*Service)(nil), // 6: api.Service
|
(*TokenResponse)(nil), // 6: api.TokenResponse
|
||||||
(*InspectServiceRequest)(nil), // 7: api.InspectServiceRequest
|
(*ResetRequest)(nil), // 7: api.ResetRequest
|
||||||
(*InspectServiceResponse)(nil), // 8: api.InspectServiceResponse
|
(*Service)(nil), // 8: api.Service
|
||||||
(*Service_Container)(nil), // 9: api.Service.Container
|
(*InspectServiceRequest)(nil), // 9: api.InspectServiceRequest
|
||||||
(*IP)(nil), // 10: api.IP
|
(*InspectServiceResponse)(nil), // 10: api.InspectServiceResponse
|
||||||
(*IPPrefix)(nil), // 11: api.IPPrefix
|
(*Service_Container)(nil), // 11: api.Service.Container
|
||||||
(*IPPort)(nil), // 12: api.IPPort
|
(*IP)(nil), // 12: api.IP
|
||||||
(*emptypb.Empty)(nil), // 13: google.protobuf.Empty
|
(*IPPrefix)(nil), // 13: api.IPPrefix
|
||||||
|
(*IPPort)(nil), // 14: api.IPPort
|
||||||
|
(*emptypb.Empty)(nil), // 15: google.protobuf.Empty
|
||||||
}
|
}
|
||||||
var file_internal_machine_api_pb_machine_proto_depIdxs = []int32{
|
var file_internal_machine_api_pb_machine_proto_depIdxs = []int32{
|
||||||
1, // 0: api.MachineInfo.network:type_name -> api.NetworkConfig
|
1, // 0: api.MachineInfo.network:type_name -> api.NetworkConfig
|
||||||
10, // 1: api.MachineInfo.public_ip:type_name -> api.IP
|
12, // 1: api.MachineInfo.public_ip:type_name -> api.IP
|
||||||
11, // 2: api.NetworkConfig.subnet:type_name -> api.IPPrefix
|
13, // 2: api.NetworkConfig.subnet:type_name -> api.IPPrefix
|
||||||
10, // 3: api.NetworkConfig.management_ip:type_name -> api.IP
|
12, // 3: api.NetworkConfig.management_ip:type_name -> api.IP
|
||||||
12, // 4: api.NetworkConfig.endpoints:type_name -> api.IPPort
|
14, // 4: api.NetworkConfig.endpoints:type_name -> api.IPPort
|
||||||
11, // 5: api.InitClusterRequest.network:type_name -> api.IPPrefix
|
13, // 5: api.InitClusterRequest.network:type_name -> api.IPPrefix
|
||||||
10, // 6: api.InitClusterRequest.public_ip:type_name -> api.IP
|
12, // 6: api.InitClusterRequest.public_ip:type_name -> api.IP
|
||||||
0, // 7: api.InitClusterResponse.machine:type_name -> api.MachineInfo
|
0, // 7: api.InitClusterResponse.machine:type_name -> api.MachineInfo
|
||||||
0, // 8: api.JoinClusterRequest.machine:type_name -> api.MachineInfo
|
0, // 8: api.JoinClusterRequest.machine:type_name -> api.MachineInfo
|
||||||
0, // 9: api.JoinClusterRequest.other_machines:type_name -> api.MachineInfo
|
0, // 9: api.JoinClusterRequest.other_machines:type_name -> api.MachineInfo
|
||||||
9, // 10: api.Service.containers:type_name -> api.Service.Container
|
11, // 10: api.Service.containers:type_name -> api.Service.Container
|
||||||
6, // 11: api.InspectServiceResponse.service:type_name -> api.Service
|
8, // 11: api.InspectServiceResponse.service:type_name -> api.Service
|
||||||
2, // 12: api.Machine.InitCluster:input_type -> api.InitClusterRequest
|
15, // 12: api.Machine.CheckPrerequisites:input_type -> google.protobuf.Empty
|
||||||
4, // 13: api.Machine.JoinCluster:input_type -> api.JoinClusterRequest
|
3, // 13: api.Machine.InitCluster:input_type -> api.InitClusterRequest
|
||||||
13, // 14: api.Machine.Token:input_type -> google.protobuf.Empty
|
5, // 14: api.Machine.JoinCluster:input_type -> api.JoinClusterRequest
|
||||||
13, // 15: api.Machine.Inspect:input_type -> google.protobuf.Empty
|
15, // 15: api.Machine.Token:input_type -> google.protobuf.Empty
|
||||||
7, // 16: api.Machine.InspectService:input_type -> api.InspectServiceRequest
|
15, // 16: api.Machine.Inspect:input_type -> google.protobuf.Empty
|
||||||
3, // 17: api.Machine.InitCluster:output_type -> api.InitClusterResponse
|
7, // 17: api.Machine.Reset:input_type -> api.ResetRequest
|
||||||
13, // 18: api.Machine.JoinCluster:output_type -> google.protobuf.Empty
|
9, // 18: api.Machine.InspectService:input_type -> api.InspectServiceRequest
|
||||||
5, // 19: api.Machine.Token:output_type -> api.TokenResponse
|
2, // 19: api.Machine.CheckPrerequisites:output_type -> api.CheckPrerequisitesResponse
|
||||||
0, // 20: api.Machine.Inspect:output_type -> api.MachineInfo
|
4, // 20: api.Machine.InitCluster:output_type -> api.InitClusterResponse
|
||||||
8, // 21: api.Machine.InspectService:output_type -> api.InspectServiceResponse
|
15, // 21: api.Machine.JoinCluster:output_type -> google.protobuf.Empty
|
||||||
17, // [17:22] is the sub-list for method output_type
|
6, // 22: api.Machine.Token:output_type -> api.TokenResponse
|
||||||
12, // [12:17] is the sub-list for method input_type
|
0, // 23: api.Machine.Inspect:output_type -> api.MachineInfo
|
||||||
|
15, // 24: api.Machine.Reset:output_type -> google.protobuf.Empty
|
||||||
|
10, // 25: api.Machine.InspectService:output_type -> api.InspectServiceResponse
|
||||||
|
19, // [19:26] is the sub-list for method output_type
|
||||||
|
12, // [12:19] 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 type_name
|
||||||
12, // [12:12] is the sub-list for extension extendee
|
12, // [12:12] is the sub-list for extension extendee
|
||||||
0, // [0:12] is the sub-list for field type_name
|
0, // [0:12] is the sub-list for field type_name
|
||||||
@@ -823,7 +938,7 @@ func file_internal_machine_api_pb_machine_proto_init() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
file_internal_machine_api_pb_machine_proto_msgTypes[2].Exporter = func(v any, i int) any {
|
file_internal_machine_api_pb_machine_proto_msgTypes[2].Exporter = func(v any, i int) any {
|
||||||
switch v := v.(*InitClusterRequest); i {
|
switch v := v.(*CheckPrerequisitesResponse); i {
|
||||||
case 0:
|
case 0:
|
||||||
return &v.state
|
return &v.state
|
||||||
case 1:
|
case 1:
|
||||||
@@ -835,7 +950,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 {
|
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:
|
case 0:
|
||||||
return &v.state
|
return &v.state
|
||||||
case 1:
|
case 1:
|
||||||
@@ -847,7 +962,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 {
|
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:
|
case 0:
|
||||||
return &v.state
|
return &v.state
|
||||||
case 1:
|
case 1:
|
||||||
@@ -859,7 +974,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 {
|
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:
|
case 0:
|
||||||
return &v.state
|
return &v.state
|
||||||
case 1:
|
case 1:
|
||||||
@@ -871,7 +986,7 @@ func file_internal_machine_api_pb_machine_proto_init() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
file_internal_machine_api_pb_machine_proto_msgTypes[6].Exporter = func(v any, i int) any {
|
file_internal_machine_api_pb_machine_proto_msgTypes[6].Exporter = func(v any, i int) any {
|
||||||
switch v := v.(*Service); i {
|
switch v := v.(*TokenResponse); i {
|
||||||
case 0:
|
case 0:
|
||||||
return &v.state
|
return &v.state
|
||||||
case 1:
|
case 1:
|
||||||
@@ -883,7 +998,7 @@ func file_internal_machine_api_pb_machine_proto_init() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
file_internal_machine_api_pb_machine_proto_msgTypes[7].Exporter = func(v any, i int) any {
|
file_internal_machine_api_pb_machine_proto_msgTypes[7].Exporter = func(v any, i int) any {
|
||||||
switch v := v.(*InspectServiceRequest); i {
|
switch v := v.(*ResetRequest); i {
|
||||||
case 0:
|
case 0:
|
||||||
return &v.state
|
return &v.state
|
||||||
case 1:
|
case 1:
|
||||||
@@ -895,7 +1010,7 @@ func file_internal_machine_api_pb_machine_proto_init() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
file_internal_machine_api_pb_machine_proto_msgTypes[8].Exporter = func(v any, i int) any {
|
file_internal_machine_api_pb_machine_proto_msgTypes[8].Exporter = func(v any, i int) any {
|
||||||
switch v := v.(*InspectServiceResponse); i {
|
switch v := v.(*Service); i {
|
||||||
case 0:
|
case 0:
|
||||||
return &v.state
|
return &v.state
|
||||||
case 1:
|
case 1:
|
||||||
@@ -907,6 +1022,30 @@ func file_internal_machine_api_pb_machine_proto_init() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
file_internal_machine_api_pb_machine_proto_msgTypes[9].Exporter = func(v any, i int) any {
|
file_internal_machine_api_pb_machine_proto_msgTypes[9].Exporter = func(v any, i int) any {
|
||||||
|
switch v := v.(*InspectServiceRequest); i {
|
||||||
|
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.(*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[11].Exporter = func(v any, i int) any {
|
||||||
switch v := v.(*Service_Container); i {
|
switch v := v.(*Service_Container); i {
|
||||||
case 0:
|
case 0:
|
||||||
return &v.state
|
return &v.state
|
||||||
@@ -919,7 +1058,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_PublicIp)(nil),
|
||||||
(*InitClusterRequest_PublicIpAuto)(nil),
|
(*InitClusterRequest_PublicIpAuto)(nil),
|
||||||
}
|
}
|
||||||
@@ -929,7 +1068,7 @@ func file_internal_machine_api_pb_machine_proto_init() {
|
|||||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||||
RawDescriptor: file_internal_machine_api_pb_machine_proto_rawDesc,
|
RawDescriptor: file_internal_machine_api_pb_machine_proto_rawDesc,
|
||||||
NumEnums: 0,
|
NumEnums: 0,
|
||||||
NumMessages: 10,
|
NumMessages: 12,
|
||||||
NumExtensions: 0,
|
NumExtensions: 0,
|
||||||
NumServices: 1,
|
NumServices: 1,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -8,10 +8,15 @@ import "google/protobuf/empty.proto";
|
|||||||
import "internal/machine/api/pb/common.proto";
|
import "internal/machine/api/pb/common.proto";
|
||||||
|
|
||||||
service Machine {
|
service Machine {
|
||||||
|
// CheckPrerequisites verifies if the machine meets all necessary system requirements to participate in the cluster.
|
||||||
|
rpc CheckPrerequisites(google.protobuf.Empty) returns (CheckPrerequisitesResponse);
|
||||||
rpc InitCluster(InitClusterRequest) returns (InitClusterResponse);
|
rpc InitCluster(InitClusterRequest) returns (InitClusterResponse);
|
||||||
rpc JoinCluster(JoinClusterRequest) returns (google.protobuf.Empty);
|
rpc JoinCluster(JoinClusterRequest) returns (google.protobuf.Empty);
|
||||||
rpc Token(google.protobuf.Empty) returns (TokenResponse);
|
rpc Token(google.protobuf.Empty) returns (TokenResponse);
|
||||||
rpc Inspect(google.protobuf.Empty) returns (MachineInfo);
|
rpc Inspect(google.protobuf.Empty) returns (MachineInfo);
|
||||||
|
// Reset restores the machine to a clean state, removing all cluster-related configuration and data.
|
||||||
|
rpc Reset(ResetRequest) returns (google.protobuf.Empty);
|
||||||
|
|
||||||
rpc InspectService(InspectServiceRequest) returns (InspectServiceResponse);
|
rpc InspectService(InspectServiceRequest) returns (InspectServiceResponse);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,6 +34,13 @@ message NetworkConfig {
|
|||||||
bytes public_key = 4;
|
bytes public_key = 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message CheckPrerequisitesResponse {
|
||||||
|
// Overall status of the checks.
|
||||||
|
bool satisfied = 1;
|
||||||
|
// Error message if not satisfied (empty if all checks pass).
|
||||||
|
string error = 2;
|
||||||
|
}
|
||||||
|
|
||||||
message InitClusterRequest {
|
message InitClusterRequest {
|
||||||
string machineName = 1;
|
string machineName = 1;
|
||||||
IPPrefix network = 2;
|
IPPrefix network = 2;
|
||||||
@@ -52,6 +64,9 @@ message TokenResponse {
|
|||||||
string token = 1;
|
string token = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message ResetRequest {
|
||||||
|
}
|
||||||
|
|
||||||
message Service {
|
message Service {
|
||||||
string id = 1;
|
string id = 1;
|
||||||
string name = 2;
|
string name = 2;
|
||||||
|
|||||||
@@ -20,21 +20,27 @@ import (
|
|||||||
const _ = grpc.SupportPackageIsVersion9
|
const _ = grpc.SupportPackageIsVersion9
|
||||||
|
|
||||||
const (
|
const (
|
||||||
Machine_InitCluster_FullMethodName = "/api.Machine/InitCluster"
|
Machine_CheckPrerequisites_FullMethodName = "/api.Machine/CheckPrerequisites"
|
||||||
Machine_JoinCluster_FullMethodName = "/api.Machine/JoinCluster"
|
Machine_InitCluster_FullMethodName = "/api.Machine/InitCluster"
|
||||||
Machine_Token_FullMethodName = "/api.Machine/Token"
|
Machine_JoinCluster_FullMethodName = "/api.Machine/JoinCluster"
|
||||||
Machine_Inspect_FullMethodName = "/api.Machine/Inspect"
|
Machine_Token_FullMethodName = "/api.Machine/Token"
|
||||||
Machine_InspectService_FullMethodName = "/api.Machine/InspectService"
|
Machine_Inspect_FullMethodName = "/api.Machine/Inspect"
|
||||||
|
Machine_Reset_FullMethodName = "/api.Machine/Reset"
|
||||||
|
Machine_InspectService_FullMethodName = "/api.Machine/InspectService"
|
||||||
)
|
)
|
||||||
|
|
||||||
// MachineClient is the client API for Machine service.
|
// MachineClient is the client API for Machine service.
|
||||||
//
|
//
|
||||||
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
|
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
|
||||||
type MachineClient interface {
|
type MachineClient interface {
|
||||||
|
// CheckPrerequisites 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)
|
InitCluster(ctx context.Context, in *InitClusterRequest, opts ...grpc.CallOption) (*InitClusterResponse, error)
|
||||||
JoinCluster(ctx context.Context, in *JoinClusterRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
|
JoinCluster(ctx context.Context, in *JoinClusterRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
|
||||||
Token(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*TokenResponse, error)
|
Token(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*TokenResponse, error)
|
||||||
Inspect(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*MachineInfo, error)
|
Inspect(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*MachineInfo, error)
|
||||||
|
// Reset restores the machine to a clean state, removing all cluster-related configuration and data.
|
||||||
|
Reset(ctx context.Context, in *ResetRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
|
||||||
InspectService(ctx context.Context, in *InspectServiceRequest, opts ...grpc.CallOption) (*InspectServiceResponse, error)
|
InspectService(ctx context.Context, in *InspectServiceRequest, opts ...grpc.CallOption) (*InspectServiceResponse, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,6 +52,16 @@ func NewMachineClient(cc grpc.ClientConnInterface) MachineClient {
|
|||||||
return &machineClient{cc}
|
return &machineClient{cc}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *machineClient) CheckPrerequisites(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*CheckPrerequisitesResponse, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(CheckPrerequisitesResponse)
|
||||||
|
err := c.cc.Invoke(ctx, Machine_CheckPrerequisites_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (c *machineClient) InitCluster(ctx context.Context, in *InitClusterRequest, opts ...grpc.CallOption) (*InitClusterResponse, error) {
|
func (c *machineClient) InitCluster(ctx context.Context, in *InitClusterRequest, opts ...grpc.CallOption) (*InitClusterResponse, error) {
|
||||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
out := new(InitClusterResponse)
|
out := new(InitClusterResponse)
|
||||||
@@ -86,6 +102,16 @@ func (c *machineClient) Inspect(ctx context.Context, in *emptypb.Empty, opts ...
|
|||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *machineClient) Reset(ctx context.Context, in *ResetRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(emptypb.Empty)
|
||||||
|
err := c.cc.Invoke(ctx, Machine_Reset_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (c *machineClient) InspectService(ctx context.Context, in *InspectServiceRequest, opts ...grpc.CallOption) (*InspectServiceResponse, error) {
|
func (c *machineClient) InspectService(ctx context.Context, in *InspectServiceRequest, opts ...grpc.CallOption) (*InspectServiceResponse, error) {
|
||||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
out := new(InspectServiceResponse)
|
out := new(InspectServiceResponse)
|
||||||
@@ -100,10 +126,14 @@ func (c *machineClient) InspectService(ctx context.Context, in *InspectServiceRe
|
|||||||
// All implementations must embed UnimplementedMachineServer
|
// All implementations must embed UnimplementedMachineServer
|
||||||
// for forward compatibility.
|
// for forward compatibility.
|
||||||
type MachineServer interface {
|
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)
|
InitCluster(context.Context, *InitClusterRequest) (*InitClusterResponse, error)
|
||||||
JoinCluster(context.Context, *JoinClusterRequest) (*emptypb.Empty, error)
|
JoinCluster(context.Context, *JoinClusterRequest) (*emptypb.Empty, error)
|
||||||
Token(context.Context, *emptypb.Empty) (*TokenResponse, error)
|
Token(context.Context, *emptypb.Empty) (*TokenResponse, error)
|
||||||
Inspect(context.Context, *emptypb.Empty) (*MachineInfo, error)
|
Inspect(context.Context, *emptypb.Empty) (*MachineInfo, error)
|
||||||
|
// Reset restores the machine to a clean state, removing all cluster-related configuration and data.
|
||||||
|
Reset(context.Context, *ResetRequest) (*emptypb.Empty, error)
|
||||||
InspectService(context.Context, *InspectServiceRequest) (*InspectServiceResponse, error)
|
InspectService(context.Context, *InspectServiceRequest) (*InspectServiceResponse, error)
|
||||||
mustEmbedUnimplementedMachineServer()
|
mustEmbedUnimplementedMachineServer()
|
||||||
}
|
}
|
||||||
@@ -115,6 +145,9 @@ type MachineServer interface {
|
|||||||
// pointer dereference when methods are called.
|
// pointer dereference when methods are called.
|
||||||
type UnimplementedMachineServer struct{}
|
type UnimplementedMachineServer struct{}
|
||||||
|
|
||||||
|
func (UnimplementedMachineServer) CheckPrerequisites(context.Context, *emptypb.Empty) (*CheckPrerequisitesResponse, error) {
|
||||||
|
return nil, status.Errorf(codes.Unimplemented, "method CheckPrerequisites not implemented")
|
||||||
|
}
|
||||||
func (UnimplementedMachineServer) InitCluster(context.Context, *InitClusterRequest) (*InitClusterResponse, error) {
|
func (UnimplementedMachineServer) InitCluster(context.Context, *InitClusterRequest) (*InitClusterResponse, error) {
|
||||||
return nil, status.Errorf(codes.Unimplemented, "method InitCluster not implemented")
|
return nil, status.Errorf(codes.Unimplemented, "method InitCluster not implemented")
|
||||||
}
|
}
|
||||||
@@ -127,6 +160,9 @@ func (UnimplementedMachineServer) Token(context.Context, *emptypb.Empty) (*Token
|
|||||||
func (UnimplementedMachineServer) Inspect(context.Context, *emptypb.Empty) (*MachineInfo, error) {
|
func (UnimplementedMachineServer) Inspect(context.Context, *emptypb.Empty) (*MachineInfo, error) {
|
||||||
return nil, status.Errorf(codes.Unimplemented, "method Inspect not implemented")
|
return nil, status.Errorf(codes.Unimplemented, "method Inspect not implemented")
|
||||||
}
|
}
|
||||||
|
func (UnimplementedMachineServer) Reset(context.Context, *ResetRequest) (*emptypb.Empty, error) {
|
||||||
|
return nil, status.Errorf(codes.Unimplemented, "method Reset not implemented")
|
||||||
|
}
|
||||||
func (UnimplementedMachineServer) InspectService(context.Context, *InspectServiceRequest) (*InspectServiceResponse, error) {
|
func (UnimplementedMachineServer) InspectService(context.Context, *InspectServiceRequest) (*InspectServiceResponse, error) {
|
||||||
return nil, status.Errorf(codes.Unimplemented, "method InspectService not implemented")
|
return nil, status.Errorf(codes.Unimplemented, "method InspectService not implemented")
|
||||||
}
|
}
|
||||||
@@ -151,6 +187,24 @@ func RegisterMachineServer(s grpc.ServiceRegistrar, srv MachineServer) {
|
|||||||
s.RegisterService(&Machine_ServiceDesc, srv)
|
s.RegisterService(&Machine_ServiceDesc, srv)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func _Machine_CheckPrerequisites_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(emptypb.Empty)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(MachineServer).CheckPrerequisites(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: Machine_CheckPrerequisites_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(MachineServer).CheckPrerequisites(ctx, req.(*emptypb.Empty))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
func _Machine_InitCluster_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
func _Machine_InitCluster_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
in := new(InitClusterRequest)
|
in := new(InitClusterRequest)
|
||||||
if err := dec(in); err != nil {
|
if err := dec(in); err != nil {
|
||||||
@@ -223,6 +277,24 @@ func _Machine_Inspect_Handler(srv interface{}, ctx context.Context, dec func(int
|
|||||||
return interceptor(ctx, in, info, handler)
|
return interceptor(ctx, in, info, handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func _Machine_Reset_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(ResetRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(MachineServer).Reset(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: Machine_Reset_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(MachineServer).Reset(ctx, req.(*ResetRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
func _Machine_InspectService_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
func _Machine_InspectService_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
in := new(InspectServiceRequest)
|
in := new(InspectServiceRequest)
|
||||||
if err := dec(in); err != nil {
|
if err := dec(in); err != nil {
|
||||||
@@ -248,6 +320,10 @@ var Machine_ServiceDesc = grpc.ServiceDesc{
|
|||||||
ServiceName: "api.Machine",
|
ServiceName: "api.Machine",
|
||||||
HandlerType: (*MachineServer)(nil),
|
HandlerType: (*MachineServer)(nil),
|
||||||
Methods: []grpc.MethodDesc{
|
Methods: []grpc.MethodDesc{
|
||||||
|
{
|
||||||
|
MethodName: "CheckPrerequisites",
|
||||||
|
Handler: _Machine_CheckPrerequisites_Handler,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
MethodName: "InitCluster",
|
MethodName: "InitCluster",
|
||||||
Handler: _Machine_InitCluster_Handler,
|
Handler: _Machine_InitCluster_Handler,
|
||||||
@@ -264,6 +340,10 @@ var Machine_ServiceDesc = grpc.ServiceDesc{
|
|||||||
MethodName: "Inspect",
|
MethodName: "Inspect",
|
||||||
Handler: _Machine_Inspect_Handler,
|
Handler: _Machine_Inspect_Handler,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
MethodName: "Reset",
|
||||||
|
Handler: _Machine_Reset_Handler,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
MethodName: "InspectService",
|
MethodName: "InspectService",
|
||||||
Handler: _Machine_InspectService_Handler,
|
Handler: _Machine_InspectService_Handler,
|
||||||
|
|||||||
@@ -2,10 +2,11 @@ package proxy
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/psviderski/uncloud/internal/machine/api/pb"
|
||||||
"google.golang.org/grpc/status"
|
"google.golang.org/grpc/status"
|
||||||
"google.golang.org/protobuf/encoding/protowire"
|
"google.golang.org/protobuf/encoding/protowire"
|
||||||
"google.golang.org/protobuf/proto"
|
"google.golang.org/protobuf/proto"
|
||||||
"github.com/psviderski/uncloud/internal/machine/api/pb"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// One2ManyResponder converts upstream responses into messages from upstreams, so that multiple
|
// One2ManyResponder converts upstream responses into messages from upstreams, so that multiple
|
||||||
|
|||||||
@@ -2,11 +2,12 @@ package proxy
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"github.com/siderolabs/grpc-proxy/proxy"
|
"github.com/siderolabs/grpc-proxy/proxy"
|
||||||
"google.golang.org/grpc/codes"
|
"google.golang.org/grpc/codes"
|
||||||
"google.golang.org/grpc/metadata"
|
"google.golang.org/grpc/metadata"
|
||||||
"google.golang.org/grpc/status"
|
"google.golang.org/grpc/status"
|
||||||
"sync"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Director manages routing of gRPC requests between local and remote backends.
|
// Director manages routing of gRPC requests between local and remote backends.
|
||||||
|
|||||||
@@ -2,11 +2,12 @@ package proxy
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"github.com/siderolabs/grpc-proxy/proxy"
|
"github.com/siderolabs/grpc-proxy/proxy"
|
||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
"google.golang.org/grpc/credentials/insecure"
|
"google.golang.org/grpc/credentials/insecure"
|
||||||
"google.golang.org/grpc/metadata"
|
"google.golang.org/grpc/metadata"
|
||||||
"sync"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// LocalBackend is a proxy.One2ManyResponder implementation that proxies to a local gRPC server listening on a Unix socket.
|
// LocalBackend is a proxy.One2ManyResponder implementation that proxies to a local gRPC server listening on a Unix socket.
|
||||||
|
|||||||
@@ -3,14 +3,15 @@ package proxy
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/netip"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/siderolabs/grpc-proxy/proxy"
|
"github.com/siderolabs/grpc-proxy/proxy"
|
||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
"google.golang.org/grpc/backoff"
|
"google.golang.org/grpc/backoff"
|
||||||
"google.golang.org/grpc/credentials/insecure"
|
"google.golang.org/grpc/credentials/insecure"
|
||||||
"google.golang.org/grpc/metadata"
|
"google.golang.org/grpc/metadata"
|
||||||
"net/netip"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// RemoteBackend is a proxy.One2ManyResponder implementation that proxies to a remote gRPC server, injecting machine metadata
|
// RemoteBackend is a proxy.One2ManyResponder implementation that proxies to a remote gRPC server, injecting machine metadata
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package caddyconfig
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GenerateCaddyfile(containers []api.ServiceContainer, verifyResponse string) (string, error) {
|
||||||
|
return fmt.Sprintf(`http:// {
|
||||||
|
handle %s {
|
||||||
|
respond "%s" 200
|
||||||
|
}
|
||||||
|
log
|
||||||
|
}
|
||||||
|
`, VerifyPath, verifyResponse), nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
package caddyconfig
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/psviderski/uncloud/internal/fs"
|
||||||
|
"github.com/psviderski/uncloud/internal/machine/store"
|
||||||
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
CaddyGroup = "uncloud"
|
||||||
|
VerifyPath = "/.uncloud-verify"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Controller monitors container changes in the cluster store and generates a configuration file for Caddy reverse
|
||||||
|
// proxy. The generated configuration allows Caddy to route external traffic to service containers across the internal
|
||||||
|
// network.
|
||||||
|
type Controller struct {
|
||||||
|
store *store.Store
|
||||||
|
configDir string
|
||||||
|
verifyResponse string
|
||||||
|
log *slog.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewController(store *store.Store, configDir string, verifyResponse string) (*Controller, error) {
|
||||||
|
if err := os.MkdirAll(configDir, 0o750); err != nil {
|
||||||
|
return nil, fmt.Errorf("create directory for Caddy configuration '%s': %w", configDir, err)
|
||||||
|
}
|
||||||
|
if err := fs.Chown(configDir, "", CaddyGroup); err != nil {
|
||||||
|
return nil, fmt.Errorf("change owner of directory for Caddy configuration '%s': %w", configDir, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Controller{
|
||||||
|
store: store,
|
||||||
|
configDir: configDir,
|
||||||
|
verifyResponse: verifyResponse,
|
||||||
|
log: slog.With("component", "caddy-controller"),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Controller) Run(ctx context.Context) error {
|
||||||
|
containerRecords, changes, err := c.store.SubscribeContainers(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("subscribe to container changes: %w", err)
|
||||||
|
}
|
||||||
|
c.log.Info("Subscribed to container changes in the cluster to generate Caddy configuration.")
|
||||||
|
|
||||||
|
containers, err := c.filterAvailableContainers(containerRecords)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("filter available containers: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = c.generateCaddyfile(containers); err != nil {
|
||||||
|
return fmt.Errorf("generate Caddyfile configuration: %w", err)
|
||||||
|
}
|
||||||
|
if err = c.generateJSONConfig(containers); err != nil {
|
||||||
|
return fmt.Errorf("generate Caddy JSON configuration: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case _, ok := <-changes:
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("containers subscription failed")
|
||||||
|
}
|
||||||
|
c.log.Info("Cluster containers changed, updating Caddy configuration.")
|
||||||
|
|
||||||
|
containerRecords, err = c.store.ListContainers(ctx, store.ListOptions{})
|
||||||
|
if err != nil {
|
||||||
|
c.log.Info("Failed to list containers.", "err", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
containers, err = c.filterAvailableContainers(containerRecords)
|
||||||
|
if err != nil {
|
||||||
|
c.log.Info("Failed to filter available containers.", "err", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = c.generateCaddyfile(containers); err != nil {
|
||||||
|
c.log.Info("Failed to generate Caddyfile configuration.", "err", err)
|
||||||
|
}
|
||||||
|
if err = c.generateJSONConfig(containers); err != nil {
|
||||||
|
c.log.Info("Failed to generate Caddy JSON configuration.", "err", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.log.Info("Updated Caddy configuration.", "dir", c.configDir)
|
||||||
|
case <-ctx.Done():
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// filterAvailableContainers filters out containers from this machine that are likely unavailable. The availability
|
||||||
|
// is determined by the cluster membership state of the machine that the container is running on.
|
||||||
|
// TODO: implement machine membership check using Corrossion Admin client.
|
||||||
|
func (c *Controller) filterAvailableContainers(
|
||||||
|
containerRecords []store.ContainerRecord,
|
||||||
|
) ([]api.ServiceContainer, error) {
|
||||||
|
containers := make([]api.ServiceContainer, len(containerRecords))
|
||||||
|
for i, cr := range containerRecords {
|
||||||
|
containers[i] = cr.Container
|
||||||
|
}
|
||||||
|
return containers, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Controller) generateCaddyfile(containers []api.ServiceContainer) error {
|
||||||
|
caddyfile, err := GenerateCaddyfile(containers, c.verifyResponse)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("generate Caddyfile: %w", err)
|
||||||
|
}
|
||||||
|
caddyfilePath := filepath.Join(c.configDir, "Caddyfile")
|
||||||
|
|
||||||
|
if err = os.WriteFile(caddyfilePath, []byte(caddyfile), 0o640); err != nil {
|
||||||
|
return fmt.Errorf("write Caddyfile to file '%s': %w", caddyfilePath, err)
|
||||||
|
}
|
||||||
|
if err = fs.Chown(caddyfilePath, "", CaddyGroup); err != nil {
|
||||||
|
return fmt.Errorf("change owner of Caddyfile '%s': %w", caddyfilePath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Controller) generateJSONConfig(containers []api.ServiceContainer) error {
|
||||||
|
config, err := GenerateJSONConfig(containers, c.verifyResponse)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
configBytes, err := json.MarshalIndent(config, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("marshal Caddy configuration: %w", err)
|
||||||
|
}
|
||||||
|
configPath := filepath.Join(c.configDir, "caddy.json")
|
||||||
|
|
||||||
|
if err = os.WriteFile(configPath, configBytes, 0o640); err != nil {
|
||||||
|
return fmt.Errorf("write Caddy configuration to file '%s': %w", configPath, err)
|
||||||
|
}
|
||||||
|
if err = fs.Chown(configPath, "", CaddyGroup); err != nil {
|
||||||
|
return fmt.Errorf("change owner of Caddy configuration file '%s': %w", configPath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package caddyfile
|
package caddyconfig
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
@@ -10,48 +10,52 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"slices"
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/caddyserver/caddy/v2"
|
"github.com/caddyserver/caddy/v2"
|
||||||
"github.com/caddyserver/caddy/v2/caddyconfig"
|
"github.com/caddyserver/caddy/v2/caddyconfig"
|
||||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp/reverseproxy"
|
"github.com/caddyserver/caddy/v2/modules/caddyhttp/reverseproxy"
|
||||||
"github.com/psviderski/uncloud/internal/machine/docker"
|
|
||||||
"github.com/psviderski/uncloud/pkg/api"
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
func GenerateConfig(containers []api.ServiceContainer, verifyResponse string) (*caddy.Config, error) {
|
func GenerateJSONConfig(containers []api.ServiceContainer, verifyResponse string) (*caddy.Config, error) {
|
||||||
// Maps hostnames to lists of upstreams (container IP:port pairs).
|
// Maps hostnames to lists of upstreams (container IP:port pairs).
|
||||||
httpHostUpstreams := make(map[string][]string)
|
httpHostUpstreams := make(map[string][]string)
|
||||||
httpsHostUpstreams := make(map[string][]string)
|
httpsHostUpstreams := make(map[string][]string)
|
||||||
for _, ctr := range containers {
|
for _, ctr := range containers {
|
||||||
logger := slog.With("container", ctr.ID)
|
if !ctr.Healthy() {
|
||||||
network, ok := ctr.NetworkSettings.Networks[docker.NetworkName]
|
|
||||||
if !ok {
|
|
||||||
// Container is not connected to the uncloud Docker network (could be host network).
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if network.IPAddress == "" {
|
|
||||||
logger.Error("Container has no IPv4 address.")
|
|
||||||
continue
|
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()
|
ports, err := ctr.ServicePorts()
|
||||||
if err != nil {
|
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
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, port := range ports {
|
for _, port := range ports {
|
||||||
|
if port.Mode != api.PortModeIngress {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
switch port.Protocol {
|
switch port.Protocol {
|
||||||
case api.ProtocolHTTP:
|
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)
|
httpHostUpstreams[port.Hostname] = append(httpHostUpstreams[port.Hostname], upstream)
|
||||||
case api.ProtocolHTTPS:
|
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)
|
httpsHostUpstreams[port.Hostname] = append(httpsHostUpstreams[port.Hostname], upstream)
|
||||||
default:
|
default:
|
||||||
// TODO: implement L4 ingress routing for TCP and UDP.
|
// 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
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -61,6 +65,8 @@ func GenerateConfig(containers []api.ServiceContainer, verifyResponse string) (*
|
|||||||
servers := make(map[string]*caddyhttp.Server)
|
servers := make(map[string]*caddyhttp.Server)
|
||||||
servers["http"] = &caddyhttp.Server{
|
servers["http"] = &caddyhttp.Server{
|
||||||
Listen: []string{fmt.Sprintf(":%d", caddyhttp.DefaultHTTPPort)},
|
Listen: []string{fmt.Sprintf(":%d", caddyhttp.DefaultHTTPPort)},
|
||||||
|
// All http requests to this server are logged to the default logger.
|
||||||
|
Logs: &caddyhttp.ServerLogConfig{},
|
||||||
Routes: append(
|
Routes: append(
|
||||||
hostUpstreamsToRoutes(httpHostUpstreams, &warnings),
|
hostUpstreamsToRoutes(httpHostUpstreams, &warnings),
|
||||||
// Add a route to respond with a static verification response at the /.uncloud-verify path.
|
// Add a route to respond with a static verification response at the /.uncloud-verify path.
|
||||||
@@ -69,6 +75,8 @@ func GenerateConfig(containers []api.ServiceContainer, verifyResponse string) (*
|
|||||||
}
|
}
|
||||||
servers["https"] = &caddyhttp.Server{
|
servers["https"] = &caddyhttp.Server{
|
||||||
Listen: []string{fmt.Sprintf(":%d", caddyhttp.DefaultHTTPSPort)},
|
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),
|
Routes: hostUpstreamsToRoutes(httpsHostUpstreams, &warnings),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,6 +117,16 @@ func hostUpstreamsToRoutes(hostUpstreams map[string][]string, warnings *[]caddyc
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
handler := &reverseproxy.Handler{
|
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,
|
Upstreams: upstreamPool,
|
||||||
}
|
}
|
||||||
|
|
||||||
+168
-77
@@ -1,4 +1,4 @@
|
|||||||
package caddyfile
|
package caddyconfig
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"strings"
|
"strings"
|
||||||
@@ -14,6 +14,27 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestGenerateConfig(t *testing.T) {
|
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 {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
containers []api.ServiceContainer
|
containers []api.ServiceContainer
|
||||||
@@ -23,25 +44,8 @@ func TestGenerateConfig(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "empty containers",
|
name: "empty containers",
|
||||||
containers: []api.ServiceContainer{},
|
containers: []api.ServiceContainer{},
|
||||||
want: `{
|
want: configWithoutServices,
|
||||||
"servers": {
|
wantErr: false,
|
||||||
"http": {
|
|
||||||
"listen": [":80"],
|
|
||||||
"routes": [{
|
|
||||||
"match": [{"path": ["/.uncloud-verify"]}],
|
|
||||||
"handle": [{
|
|
||||||
"body": "verification-response-body",
|
|
||||||
"handler": "static_response",
|
|
||||||
"status_code": 200
|
|
||||||
}]
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
"https": {
|
|
||||||
"listen": [":443"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}`,
|
|
||||||
wantErr: false,
|
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -58,6 +62,14 @@ func TestGenerateConfig(t *testing.T) {
|
|||||||
"match": [{"host": ["app.example.com"]}],
|
"match": [{"host": ["app.example.com"]}],
|
||||||
"handle": [{
|
"handle": [{
|
||||||
"handler": "reverse_proxy",
|
"handler": "reverse_proxy",
|
||||||
|
"health_checks": {
|
||||||
|
"passive": {
|
||||||
|
"fail_duration": 30000000000
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"load_balancing": {
|
||||||
|
"retries": 3
|
||||||
|
},
|
||||||
"upstreams": [{"dial": "10.210.0.2:8080"}]
|
"upstreams": [{"dial": "10.210.0.2:8080"}]
|
||||||
}]
|
}]
|
||||||
},
|
},
|
||||||
@@ -69,10 +81,12 @@ func TestGenerateConfig(t *testing.T) {
|
|||||||
"status_code": 200
|
"status_code": 200
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"logs": {}
|
||||||
},
|
},
|
||||||
"https": {
|
"https": {
|
||||||
"listen": [":443"]
|
"listen": [":443"],
|
||||||
|
"logs": {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}`,
|
}`,
|
||||||
@@ -93,6 +107,14 @@ func TestGenerateConfig(t *testing.T) {
|
|||||||
"match": [{"host": ["app.example.com"]}],
|
"match": [{"host": ["app.example.com"]}],
|
||||||
"handle": [{
|
"handle": [{
|
||||||
"handler": "reverse_proxy",
|
"handler": "reverse_proxy",
|
||||||
|
"health_checks": {
|
||||||
|
"passive": {
|
||||||
|
"fail_duration": 30000000000
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"load_balancing": {
|
||||||
|
"retries": 3
|
||||||
|
},
|
||||||
"upstreams": [
|
"upstreams": [
|
||||||
{"dial": "10.210.0.2:8080"},
|
{"dial": "10.210.0.2:8080"},
|
||||||
{"dial": "10.210.0.3:8080"}
|
{"dial": "10.210.0.3:8080"}
|
||||||
@@ -107,10 +129,12 @@ func TestGenerateConfig(t *testing.T) {
|
|||||||
"status_code": 200
|
"status_code": 200
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"logs": {}
|
||||||
},
|
},
|
||||||
"https": {
|
"https": {
|
||||||
"listen": [":443"]
|
"listen": [":443"],
|
||||||
|
"logs": {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}`,
|
}`,
|
||||||
@@ -134,7 +158,8 @@ func TestGenerateConfig(t *testing.T) {
|
|||||||
"status_code": 200
|
"status_code": 200
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"logs": {}
|
||||||
},
|
},
|
||||||
"https": {
|
"https": {
|
||||||
"listen": [":443"],
|
"listen": [":443"],
|
||||||
@@ -143,10 +168,19 @@ func TestGenerateConfig(t *testing.T) {
|
|||||||
"match": [{"host": ["secure.example.com"]}],
|
"match": [{"host": ["secure.example.com"]}],
|
||||||
"handle": [{
|
"handle": [{
|
||||||
"handler": "reverse_proxy",
|
"handler": "reverse_proxy",
|
||||||
|
"health_checks": {
|
||||||
|
"passive": {
|
||||||
|
"fail_duration": 30000000000
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"load_balancing": {
|
||||||
|
"retries": 3
|
||||||
|
},
|
||||||
"upstreams": [{"dial": "10.210.0.2:8000"}]
|
"upstreams": [{"dial": "10.210.0.2:8000"}]
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"logs": {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}`,
|
}`,
|
||||||
@@ -178,6 +212,14 @@ func TestGenerateConfig(t *testing.T) {
|
|||||||
"match": [{"host": ["app.example.com"]}],
|
"match": [{"host": ["app.example.com"]}],
|
||||||
"handle": [{
|
"handle": [{
|
||||||
"handler": "reverse_proxy",
|
"handler": "reverse_proxy",
|
||||||
|
"health_checks": {
|
||||||
|
"passive": {
|
||||||
|
"fail_duration": 30000000000
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"load_balancing": {
|
||||||
|
"retries": 3
|
||||||
|
},
|
||||||
"upstreams": [
|
"upstreams": [
|
||||||
{"dial": "10.210.0.2:8080"},
|
{"dial": "10.210.0.2:8080"},
|
||||||
{"dial": "10.210.0.3:8080"},
|
{"dial": "10.210.0.3:8080"},
|
||||||
@@ -189,6 +231,14 @@ func TestGenerateConfig(t *testing.T) {
|
|||||||
"match": [{"host": ["web.example.com"]}],
|
"match": [{"host": ["web.example.com"]}],
|
||||||
"handle": [{
|
"handle": [{
|
||||||
"handler": "reverse_proxy",
|
"handler": "reverse_proxy",
|
||||||
|
"health_checks": {
|
||||||
|
"passive": {
|
||||||
|
"fail_duration": 30000000000
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"load_balancing": {
|
||||||
|
"retries": 3
|
||||||
|
},
|
||||||
"upstreams": [
|
"upstreams": [
|
||||||
{"dial": "10.210.0.2:8000"},
|
{"dial": "10.210.0.2:8000"},
|
||||||
{"dial": "10.210.0.4:8000"},
|
{"dial": "10.210.0.4:8000"},
|
||||||
@@ -204,7 +254,8 @@ func TestGenerateConfig(t *testing.T) {
|
|||||||
"status_code": 200
|
"status_code": 200
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"logs": {}
|
||||||
},
|
},
|
||||||
"https": {
|
"https": {
|
||||||
"listen": [":443"],
|
"listen": [":443"],
|
||||||
@@ -213,6 +264,14 @@ func TestGenerateConfig(t *testing.T) {
|
|||||||
"match": [{"host": ["secure.example.com"]}],
|
"match": [{"host": ["secure.example.com"]}],
|
||||||
"handle": [{
|
"handle": [{
|
||||||
"handler": "reverse_proxy",
|
"handler": "reverse_proxy",
|
||||||
|
"health_checks": {
|
||||||
|
"passive": {
|
||||||
|
"fail_duration": 30000000000
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"load_balancing": {
|
||||||
|
"retries": 3
|
||||||
|
},
|
||||||
"upstreams": [
|
"upstreams": [
|
||||||
{"dial": "10.210.0.3:8888"},
|
{"dial": "10.210.0.3:8888"},
|
||||||
{"dial": "10.210.0.4:8888"},
|
{"dial": "10.210.0.4:8888"},
|
||||||
@@ -220,7 +279,8 @@ func TestGenerateConfig(t *testing.T) {
|
|||||||
]
|
]
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"logs": {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}`,
|
}`,
|
||||||
@@ -231,24 +291,7 @@ func TestGenerateConfig(t *testing.T) {
|
|||||||
containers: []api.ServiceContainer{
|
containers: []api.ServiceContainer{
|
||||||
newContainerWithoutNetwork("ignored.example.com:8080/http"),
|
newContainerWithoutNetwork("ignored.example.com:8080/http"),
|
||||||
},
|
},
|
||||||
want: `{
|
want: configWithoutServices,
|
||||||
"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,
|
wantErr: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -256,48 +299,76 @@ func TestGenerateConfig(t *testing.T) {
|
|||||||
containers: []api.ServiceContainer{
|
containers: []api.ServiceContainer{
|
||||||
newContainer("10.210.0.2", "invalid-port"),
|
newContainer("10.210.0.2", "invalid-port"),
|
||||||
},
|
},
|
||||||
want: `{
|
want: configWithoutServices,
|
||||||
"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,
|
wantErr: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "containers with unsupported protocols ignored",
|
name: "containers with unsupported protocols and host mode ignored",
|
||||||
containers: []api.ServiceContainer{
|
containers: []api.ServiceContainer{
|
||||||
newContainer("10.210.0.2", "5000/tcp"),
|
newContainer("10.210.0.2", "5000/tcp"),
|
||||||
newContainer("10.210.0.3", "5000/udp"),
|
newContainer("10.210.0.3", "5000/udp"),
|
||||||
newContainer("10.210.0.4", "80:8080/tcp@host"),
|
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: `{
|
want: `{
|
||||||
"servers": {
|
"servers": {
|
||||||
"http": {
|
"http": {
|
||||||
"listen": [":80"],
|
"listen": [":80"],
|
||||||
"routes": [{
|
"routes": [
|
||||||
"match": [{"path": ["/.uncloud-verify"]}],
|
{
|
||||||
"handle": [{
|
"match": [{"host": ["app.example.com"]}],
|
||||||
"body": "verification-response-body",
|
"handle": [{
|
||||||
"handler": "static_response",
|
"handler": "reverse_proxy",
|
||||||
"status_code": 200
|
"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": {
|
"https": {
|
||||||
"listen": [":443"]
|
"listen": [":443"],
|
||||||
|
"logs": {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}`,
|
}`,
|
||||||
@@ -307,7 +378,7 @@ func TestGenerateConfig(t *testing.T) {
|
|||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
config, err := GenerateConfig(tt.containers, "verification-response-body")
|
config, err := GenerateJSONConfig(tt.containers, "verification-response-body")
|
||||||
|
|
||||||
if tt.wantErr {
|
if tt.wantErr {
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
@@ -326,7 +397,11 @@ func TestGenerateConfig(t *testing.T) {
|
|||||||
func newContainer(ip string, ports ...string) api.ServiceContainer {
|
func newContainer(ip string, ports ...string) api.ServiceContainer {
|
||||||
portsLabel := strings.Join(ports, ",")
|
portsLabel := strings.Join(ports, ",")
|
||||||
return api.ServiceContainer{Container: api.Container{ContainerJSON: types.ContainerJSON{
|
return api.ServiceContainer{Container: api.Container{ContainerJSON: types.ContainerJSON{
|
||||||
ContainerJSONBase: &types.ContainerJSONBase{},
|
ContainerJSONBase: &types.ContainerJSONBase{
|
||||||
|
State: &types.ContainerState{
|
||||||
|
Running: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
NetworkSettings: &types.NetworkSettings{
|
NetworkSettings: &types.NetworkSettings{
|
||||||
Networks: map[string]*network.EndpointSettings{
|
Networks: map[string]*network.EndpointSettings{
|
||||||
docker.NetworkName: {
|
docker.NetworkName: {
|
||||||
@@ -345,7 +420,11 @@ func newContainer(ip string, ports ...string) api.ServiceContainer {
|
|||||||
func newContainerWithoutNetwork(ports ...string) api.ServiceContainer {
|
func newContainerWithoutNetwork(ports ...string) api.ServiceContainer {
|
||||||
portsLabel := strings.Join(ports, ",")
|
portsLabel := strings.Join(ports, ",")
|
||||||
return api.ServiceContainer{Container: api.Container{ContainerJSON: types.ContainerJSON{
|
return api.ServiceContainer{Container: api.Container{ContainerJSON: types.ContainerJSON{
|
||||||
ContainerJSONBase: &types.ContainerJSONBase{},
|
ContainerJSONBase: &types.ContainerJSONBase{
|
||||||
|
State: &types.ContainerState{
|
||||||
|
Running: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
NetworkSettings: &types.NetworkSettings{
|
NetworkSettings: &types.NetworkSettings{
|
||||||
Networks: map[string]*network.EndpointSettings{
|
Networks: map[string]*network.EndpointSettings{
|
||||||
"other-network": {
|
"other-network": {
|
||||||
@@ -360,3 +439,15 @@ func newContainerWithoutNetwork(ports ...string) api.ServiceContainer {
|
|||||||
},
|
},
|
||||||
}}}
|
}}}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
@@ -1,125 +0,0 @@
|
|||||||
package caddyfile
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"log/slog"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
|
|
||||||
"github.com/psviderski/uncloud/internal/fs"
|
|
||||||
"github.com/psviderski/uncloud/internal/machine/store"
|
|
||||||
"github.com/psviderski/uncloud/pkg/api"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
CaddyGroup = "uncloud"
|
|
||||||
VerifyPath = "/.uncloud-verify"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Controller monitors container changes in the cluster store and generates a configuration file for Caddy reverse
|
|
||||||
// proxy. The generated Caddyfile allows Caddy to route external traffic to service containers across the internal
|
|
||||||
// network.
|
|
||||||
type Controller struct {
|
|
||||||
store *store.Store
|
|
||||||
path string
|
|
||||||
verifyResponse string
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewController(store *store.Store, path string, verifyResponse string) (*Controller, error) {
|
|
||||||
dir := filepath.Dir(path)
|
|
||||||
if err := os.MkdirAll(dir, 0750); err != nil {
|
|
||||||
return nil, fmt.Errorf("create parent directory for Caddy configuration '%s': %w", dir, err)
|
|
||||||
}
|
|
||||||
if err := fs.Chown(dir, "", CaddyGroup); err != nil {
|
|
||||||
return nil, fmt.Errorf("change owner of parent directory for Caddy configuration '%s': %w", dir, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &Controller{
|
|
||||||
store: store,
|
|
||||||
path: path,
|
|
||||||
verifyResponse: verifyResponse,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Controller) Run(ctx context.Context) error {
|
|
||||||
containerRecords, changes, err := c.store.SubscribeContainers(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("subscribe to container changes: %w", err)
|
|
||||||
}
|
|
||||||
slog.Info("Subscribed to container changes in the cluster to generate Caddy configuration.")
|
|
||||||
|
|
||||||
containers, err := c.filterAvailableContainers(containerRecords)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("filter available containers: %w", err)
|
|
||||||
}
|
|
||||||
if err = c.generateConfig(containers); err != nil {
|
|
||||||
return fmt.Errorf("generate Caddy configuration: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case _, ok := <-changes:
|
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf("containers subscription failed")
|
|
||||||
}
|
|
||||||
slog.Debug("Cluster containers changed, updating Caddy configuration.")
|
|
||||||
|
|
||||||
containerRecords, err = c.store.ListContainers(ctx, store.ListOptions{})
|
|
||||||
if err != nil {
|
|
||||||
slog.Error("Failed to list containers.", "err", err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
containers, err = c.filterAvailableContainers(containerRecords)
|
|
||||||
if err != nil {
|
|
||||||
slog.Error("Failed to filter available containers.", "err", err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if err = c.generateConfig(containers); err != nil {
|
|
||||||
slog.Error("Failed to generate Caddy configuration.", "err", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
slog.Debug("Updated Caddy configuration.", "path", c.path)
|
|
||||||
case <-ctx.Done():
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// filterAvailableContainers filters out containers from this machine that are likely unavailable. The availability
|
|
||||||
// is determined by the cluster membership state of the machine that the container is running on.
|
|
||||||
// TODO: implement machine membership check using Corrossion Admin client.
|
|
||||||
func (c *Controller) filterAvailableContainers(
|
|
||||||
containerRecords []store.ContainerRecord,
|
|
||||||
) ([]api.ServiceContainer, error) {
|
|
||||||
containers := make([]api.ServiceContainer, len(containerRecords))
|
|
||||||
for i, cr := range containerRecords {
|
|
||||||
containers[i] = api.ServiceContainer{
|
|
||||||
Container: cr.Container,
|
|
||||||
// TODO: restore ServiceSpec from the container record once it's saved in the store.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return containers, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Controller) generateConfig(containers []api.ServiceContainer) error {
|
|
||||||
config, err := GenerateConfig(containers, c.verifyResponse)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
configBytes, err := json.MarshalIndent(config, "", " ")
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("marshal Caddy configuration: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = os.WriteFile(c.path, configBytes, 0640); err != nil {
|
|
||||||
return fmt.Errorf("write Caddy configuration to file '%s': %w", c.path, err)
|
|
||||||
}
|
|
||||||
if err = fs.Chown(c.path, "", CaddyGroup); err != nil {
|
|
||||||
return fmt.Errorf("change owner of Caddy configuration file '%s': %w", c.path, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,425 @@
|
|||||||
|
package machine
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"net"
|
||||||
|
"net/netip"
|
||||||
|
"slices"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/cenkalti/backoff/v4"
|
||||||
|
"github.com/psviderski/uncloud/internal/machine/api/pb"
|
||||||
|
"github.com/psviderski/uncloud/internal/machine/caddyconfig"
|
||||||
|
"github.com/psviderski/uncloud/internal/machine/constants"
|
||||||
|
"github.com/psviderski/uncloud/internal/machine/corroservice"
|
||||||
|
"github.com/psviderski/uncloud/internal/machine/dns"
|
||||||
|
"github.com/psviderski/uncloud/internal/machine/docker"
|
||||||
|
"github.com/psviderski/uncloud/internal/machine/firewall"
|
||||||
|
"github.com/psviderski/uncloud/internal/machine/network"
|
||||||
|
"github.com/psviderski/uncloud/internal/machine/store"
|
||||||
|
"golang.org/x/sync/errgroup"
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
)
|
||||||
|
|
||||||
|
// clusterController is the main controller for the machine that is a cluster member. It manages components such as
|
||||||
|
// the WireGuard network, API server listening the WireGuard network, Corrosion service, Docker network and containers,
|
||||||
|
// and others.
|
||||||
|
type clusterController struct {
|
||||||
|
state *State
|
||||||
|
store *store.Store
|
||||||
|
|
||||||
|
wgnet *network.WireGuardNetwork
|
||||||
|
endpointChanges <-chan network.EndpointChangeEvent
|
||||||
|
|
||||||
|
server *grpc.Server
|
||||||
|
corroService corroservice.Service
|
||||||
|
dockerCtrl *docker.Controller
|
||||||
|
// dockerReady is signalled when Docker is configured and ready for containers.
|
||||||
|
dockerReady chan<- struct{}
|
||||||
|
caddyconfigCtrl *caddyconfig.Controller
|
||||||
|
|
||||||
|
// dnsServer is the embedded internal DNS server for the cluster listening on the machine IP.
|
||||||
|
dnsServer *dns.Server
|
||||||
|
dnsResolver *dns.ClusterResolver
|
||||||
|
|
||||||
|
// stopped is a channel that is closed when the controller is stopped.
|
||||||
|
stopped chan struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newClusterController(
|
||||||
|
state *State,
|
||||||
|
store *store.Store,
|
||||||
|
server *grpc.Server,
|
||||||
|
corroService corroservice.Service,
|
||||||
|
dockerService *docker.Service,
|
||||||
|
dockerReady chan<- struct{},
|
||||||
|
caddyfileCtrl *caddyconfig.Controller,
|
||||||
|
dnsServer *dns.Server,
|
||||||
|
dnsResolver *dns.ClusterResolver,
|
||||||
|
) (*clusterController, error) {
|
||||||
|
slog.Info("Starting WireGuard network.")
|
||||||
|
wgnet, err := network.NewWireGuardNetwork()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("create WireGuard network: %w", err)
|
||||||
|
}
|
||||||
|
endpointChanges := wgnet.WatchEndpoints()
|
||||||
|
|
||||||
|
return &clusterController{
|
||||||
|
state: state,
|
||||||
|
store: store,
|
||||||
|
wgnet: wgnet,
|
||||||
|
endpointChanges: endpointChanges,
|
||||||
|
server: server,
|
||||||
|
corroService: corroService,
|
||||||
|
dockerCtrl: docker.NewController(state.ID, dockerService, store),
|
||||||
|
dockerReady: dockerReady,
|
||||||
|
caddyconfigCtrl: caddyfileCtrl,
|
||||||
|
dnsServer: dnsServer,
|
||||||
|
dnsResolver: dnsResolver,
|
||||||
|
stopped: make(chan struct{}),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cc *clusterController) Run(ctx context.Context) error {
|
||||||
|
defer close(cc.stopped)
|
||||||
|
|
||||||
|
if err := firewall.ConfigureIptablesChains(); err != nil {
|
||||||
|
return fmt.Errorf("configure iptables chains: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := cc.ensureDockerNetwork(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
slog.Info("Docker network configured.")
|
||||||
|
|
||||||
|
if err := cc.wgnet.Configure(*cc.state.Network); err != nil {
|
||||||
|
return fmt.Errorf("configure WireGuard network: %w", err)
|
||||||
|
}
|
||||||
|
slog.Info("WireGuard network configured.")
|
||||||
|
|
||||||
|
if cc.corroService.Running() {
|
||||||
|
// Corrosion service was running before the WireGuard network was configured so we need to restart it.
|
||||||
|
slog.Info("Restarting corrosion service to apply new configuration with WireGuard network.")
|
||||||
|
if err := cc.corroService.Restart(ctx); err != nil {
|
||||||
|
return fmt.Errorf("restart corrosion service: %w", err)
|
||||||
|
}
|
||||||
|
slog.Info("Corrosion service restarted.")
|
||||||
|
} else {
|
||||||
|
slog.Info("Starting corrosion service.")
|
||||||
|
if err := cc.corroService.Start(ctx); err != nil {
|
||||||
|
return fmt.Errorf("start corrosion service: %w", err)
|
||||||
|
}
|
||||||
|
slog.Info("Corrosion service started.")
|
||||||
|
}
|
||||||
|
|
||||||
|
errGroup, ctx := errgroup.WithContext(ctx)
|
||||||
|
|
||||||
|
// Start the network API server. Assume the management IP can't be changed when the network is running.
|
||||||
|
apiAddr := net.JoinHostPort(cc.state.Network.ManagementIP.String(), strconv.Itoa(constants.MachineAPIPort))
|
||||||
|
listener, err := net.Listen("tcp", apiAddr)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("listen API port: %w", err)
|
||||||
|
}
|
||||||
|
errGroup.Go(func() error {
|
||||||
|
slog.Info("Starting network API server.", "addr", apiAddr)
|
||||||
|
if err := cc.server.Serve(listener); err != nil {
|
||||||
|
return fmt.Errorf("network API server failed: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
errGroup.Go(func() error {
|
||||||
|
slog.Info("Starting embedded DNS resolver.")
|
||||||
|
if err := cc.dnsResolver.Run(ctx); err != nil {
|
||||||
|
return fmt.Errorf("embedded DNS resolver failed: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
// The Docker network must be created before starting the DNS server because it listens on the machine IP.
|
||||||
|
errGroup.Go(func() error {
|
||||||
|
slog.Info("Starting embedded DNS server.")
|
||||||
|
if err := cc.dnsServer.Run(ctx); err != nil {
|
||||||
|
return fmt.Errorf("embedded DNS server failed: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
// Synchronise Docker containers to the cluster store.
|
||||||
|
errGroup.Go(func() error {
|
||||||
|
slog.Info("Watching Docker containers and syncing them to cluster store.")
|
||||||
|
return cc.syncDockerContainers(ctx)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Handle machine changes in the cluster. Handling machine and endpoint changes should be done
|
||||||
|
// in separate goroutines to avoid a deadlock when reconfiguring the network.
|
||||||
|
errGroup.Go(func() error {
|
||||||
|
if err := cc.handleMachineChanges(ctx); err != nil {
|
||||||
|
return fmt.Errorf("handle new machines: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
// Watch for endpoint changes and update the machine state accordingly.
|
||||||
|
errGroup.Go(func() error {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case e, ok := <-cc.endpointChanges:
|
||||||
|
if !ok {
|
||||||
|
// The channel was closed, stop watching for changes.
|
||||||
|
cc.endpointChanges = nil
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
cc.state.mu.Lock()
|
||||||
|
for i := range cc.state.Network.Peers {
|
||||||
|
if cc.state.Network.Peers[i].PublicKey.Equal(e.PublicKey) {
|
||||||
|
cc.state.Network.Peers[i].Endpoint = &e.Endpoint
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := cc.state.Save(); err != nil {
|
||||||
|
slog.Error("Failed to save machine state.", "err", err)
|
||||||
|
}
|
||||||
|
cc.state.mu.Unlock()
|
||||||
|
|
||||||
|
slog.Debug("Preserved endpoint change in the machine state.",
|
||||||
|
"public_key", e.PublicKey, "endpoint", e.Endpoint)
|
||||||
|
case <-ctx.Done():
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
errGroup.Go(func() error {
|
||||||
|
if err := cc.wgnet.Run(ctx); err != nil {
|
||||||
|
return fmt.Errorf("WireGuard network failed: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
errGroup.Go(func() error {
|
||||||
|
slog.Info("Starting caddyconfig controller.")
|
||||||
|
if err := cc.caddyconfigCtrl.Run(ctx); err != nil {
|
||||||
|
return fmt.Errorf("caddyconfig controller failed: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
// Wait for the context to be done and stop the network API server.
|
||||||
|
<-ctx.Done()
|
||||||
|
slog.Info("Stopping network API server.")
|
||||||
|
// TODO: implement timeout for graceful shutdown.
|
||||||
|
cc.server.GracefulStop()
|
||||||
|
slog.Info("Network API server stopped.")
|
||||||
|
|
||||||
|
// Wait for all controllers to finish.
|
||||||
|
err = errGroup.Wait()
|
||||||
|
|
||||||
|
// It's safe to stop the Corrosion service after the controllers depending on it and API server are stopped.
|
||||||
|
// Use a new context with a timeout as the current context is already canceled.
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
if corroErr := cc.corroService.Stop(ctx); corroErr != nil {
|
||||||
|
err = errors.Join(err, fmt.Errorf("stop corrosion service: %w", corroErr))
|
||||||
|
} else {
|
||||||
|
slog.Info("Corrosion service stopped.")
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensureDockerNetwork ensures that the Docker network is configured and ready for containers.
|
||||||
|
func (cc *clusterController) ensureDockerNetwork(ctx context.Context) error {
|
||||||
|
if err := cc.dockerCtrl.WaitDaemonReady(ctx); err != nil {
|
||||||
|
return fmt.Errorf("wait for Docker daemon: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := cc.dockerCtrl.EnsureUncloudNetwork(
|
||||||
|
ctx,
|
||||||
|
cc.state.Network.Subnet,
|
||||||
|
cc.dnsServer.ListenAddr(),
|
||||||
|
); err != nil {
|
||||||
|
return fmt.Errorf("ensure Docker network: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Signal that Docker is ready for containers.
|
||||||
|
close(cc.dockerReady)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// syncDockerContainers watches local Docker containers and syncs them to the cluster store.
|
||||||
|
// TODO: move this to the Docker controller.
|
||||||
|
func (cc *clusterController) syncDockerContainers(ctx context.Context) error {
|
||||||
|
// Retry to watch and sync containers until the context is done.
|
||||||
|
boff := backoff.WithContext(backoff.NewExponentialBackOff(
|
||||||
|
backoff.WithInitialInterval(100*time.Millisecond),
|
||||||
|
backoff.WithMaxInterval(5*time.Second),
|
||||||
|
backoff.WithMaxElapsedTime(0),
|
||||||
|
), ctx)
|
||||||
|
watchAndSync := func() error {
|
||||||
|
if wErr := cc.dockerCtrl.WatchAndSyncContainers(ctx); wErr != nil {
|
||||||
|
slog.Error("Failed to watch and sync containers to cluster store, retrying.", "err", wErr)
|
||||||
|
return wErr
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err := backoff.Retry(watchAndSync, boff); err != nil {
|
||||||
|
if errors.Is(err, context.Canceled) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return fmt.Errorf("watch and sync containers to cluster store: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleMachineChanges subscribes to machine changes in the cluster and reconfigures the network peers accordingly
|
||||||
|
// when changes occur.
|
||||||
|
func (cc *clusterController) handleMachineChanges(ctx context.Context) error {
|
||||||
|
for {
|
||||||
|
// Retry to subscribe to machine changes indefinitely until the context is done.
|
||||||
|
boff := backoff.WithContext(backoff.NewExponentialBackOff(
|
||||||
|
backoff.WithInitialInterval(1*time.Second),
|
||||||
|
backoff.WithMaxInterval(60*time.Second),
|
||||||
|
backoff.WithMaxElapsedTime(0),
|
||||||
|
), ctx)
|
||||||
|
|
||||||
|
var (
|
||||||
|
machines []*pb.MachineInfo
|
||||||
|
changes <-chan struct{}
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
subscribe := func() error {
|
||||||
|
if machines, changes, err = cc.store.SubscribeMachines(ctx); err != nil {
|
||||||
|
slog.Info("Failed to subscribe to machine changes, retrying.", "err", err)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err = backoff.Retry(subscribe, boff); err != nil {
|
||||||
|
if errors.Is(err, context.Canceled) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
slog.Error("Unexpected error while retrying to subscribe to machine changes.", "err", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
slog.Info("Subscribed to machine changes in the cluster to reconfigure network peers.")
|
||||||
|
|
||||||
|
// The machine store may be empty when a machine first joins the cluster, before store synchronization
|
||||||
|
// completes. Skip configuration now and apply it when the store changes are received.
|
||||||
|
if len(machines) > 0 {
|
||||||
|
slog.Info("Reconfiguring network peers with the current machines.", "machines", len(machines))
|
||||||
|
if err = cc.configurePeers(machines); err != nil {
|
||||||
|
slog.Error("Failed to configure peers.", "err", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// For simplicity, reconfigure all peers on any change.
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
// TODO: test when Corrosion fails and the subscription fails to resubscribe (after 1 minute). It seems
|
||||||
|
// the changes channel will be closed and this will become a busy loop. Perhaps, the outer for loop should
|
||||||
|
// be reworked as well.
|
||||||
|
case <-changes:
|
||||||
|
slog.Info("Cluster machines changed, reconfiguring network peers.")
|
||||||
|
if machines, err = cc.store.ListMachines(ctx); err != nil {
|
||||||
|
slog.Error("Failed to list machines.", "err", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err = cc.configurePeers(machines); err != nil {
|
||||||
|
slog.Error("Failed to configure peers.", "err", err)
|
||||||
|
}
|
||||||
|
case <-ctx.Done():
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cc *clusterController) configurePeers(machines []*pb.MachineInfo) error {
|
||||||
|
if len(machines) == 0 {
|
||||||
|
return fmt.Errorf("no machines to configure peers")
|
||||||
|
}
|
||||||
|
|
||||||
|
cc.state.mu.RLock()
|
||||||
|
currentPeerEndpoints := make(map[string]*netip.AddrPort, len(cc.state.Network.Peers))
|
||||||
|
for _, p := range cc.state.Network.Peers {
|
||||||
|
currentPeerEndpoints[p.PublicKey.String()] = p.Endpoint
|
||||||
|
}
|
||||||
|
cc.state.mu.RUnlock()
|
||||||
|
|
||||||
|
// Construct the list of peers from the machine configurations ensuring that the current endpoint is preserved.
|
||||||
|
peers := make([]network.PeerConfig, 0, len(machines)-1)
|
||||||
|
for _, m := range machines {
|
||||||
|
// Skip the current machine.
|
||||||
|
if m.Id == cc.state.ID {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := m.Network.Validate(); err != nil {
|
||||||
|
slog.Error("Invalid machine network configuration.", "machine", m.Name, "err", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Ignore errors as they are already validated.
|
||||||
|
subnet, _ := m.Network.Subnet.ToPrefix()
|
||||||
|
manageIP, _ := m.Network.ManagementIp.ToAddr()
|
||||||
|
endpoints := make([]netip.AddrPort, len(m.Network.Endpoints))
|
||||||
|
for i, ep := range m.Network.Endpoints {
|
||||||
|
addrPort, _ := ep.ToAddrPort()
|
||||||
|
endpoints[i] = addrPort
|
||||||
|
}
|
||||||
|
peer := network.PeerConfig{
|
||||||
|
Subnet: &subnet,
|
||||||
|
ManagementIP: manageIP,
|
||||||
|
AllEndpoints: endpoints,
|
||||||
|
PublicKey: m.Network.PublicKey,
|
||||||
|
}
|
||||||
|
|
||||||
|
currentEndpoint := currentPeerEndpoints[peer.PublicKey.String()]
|
||||||
|
if currentEndpoint != nil && slices.Contains(endpoints, *currentEndpoint) {
|
||||||
|
peer.Endpoint = currentEndpoint
|
||||||
|
} else if len(endpoints) > 0 {
|
||||||
|
peer.Endpoint = &endpoints[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
peers = append(peers, peer)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Preserve the new list of peers in the machine state.
|
||||||
|
cc.state.mu.Lock()
|
||||||
|
cc.state.Network.Peers = peers
|
||||||
|
err := cc.state.Save()
|
||||||
|
cc.state.mu.Unlock()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("save machine state: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cc.state.mu.RLock()
|
||||||
|
defer cc.state.mu.RUnlock()
|
||||||
|
if err = cc.wgnet.Configure(*cc.state.Network); err != nil {
|
||||||
|
return fmt.Errorf("configure network peers: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup cleans up the cluster resources such as the WireGuard network, iptables rules, Docker network and containers.
|
||||||
|
func (cc *clusterController) Cleanup() error {
|
||||||
|
// Wait for the controller to stop before cleaning up.
|
||||||
|
<-cc.stopped
|
||||||
|
|
||||||
|
var errs []error
|
||||||
|
if err := cc.dockerCtrl.Cleanup(); err != nil {
|
||||||
|
errs = append(errs, fmt.Errorf("cleanup Docker resources: %w", err))
|
||||||
|
}
|
||||||
|
if err := cc.wgnet.Cleanup(); err != nil {
|
||||||
|
errs = append(errs, fmt.Errorf("cleanup WireGuard network: %w", err))
|
||||||
|
}
|
||||||
|
if err := firewall.CleanupIptablesChains(); err != nil {
|
||||||
|
errs = append(errs, fmt.Errorf("cleanup iptables chains: %w", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors.Join(errs...)
|
||||||
|
}
|
||||||
@@ -5,17 +5,18 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"google.golang.org/grpc/codes"
|
|
||||||
"google.golang.org/grpc/status"
|
|
||||||
"google.golang.org/protobuf/types/known/emptypb"
|
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/psviderski/uncloud/internal/corrosion"
|
"github.com/psviderski/uncloud/internal/corrosion"
|
||||||
"github.com/psviderski/uncloud/internal/machine/api/pb"
|
"github.com/psviderski/uncloud/internal/machine/api/pb"
|
||||||
"github.com/psviderski/uncloud/internal/machine/network"
|
"github.com/psviderski/uncloud/internal/machine/network"
|
||||||
"github.com/psviderski/uncloud/internal/machine/store"
|
"github.com/psviderski/uncloud/internal/machine/store"
|
||||||
"github.com/psviderski/uncloud/internal/secret"
|
"github.com/psviderski/uncloud/internal/secret"
|
||||||
|
"google.golang.org/grpc/codes"
|
||||||
|
"google.golang.org/grpc/status"
|
||||||
|
"google.golang.org/protobuf/types/known/emptypb"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Cluster struct {
|
type Cluster struct {
|
||||||
@@ -197,6 +198,89 @@ func (c *Cluster) AddMachine(ctx context.Context, req *pb.AddMachineRequest) (*p
|
|||||||
return resp, nil
|
return resp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UpdateMachine updates machine configuration in the cluster.
|
||||||
|
func (c *Cluster) UpdateMachine(ctx context.Context, req *pb.UpdateMachineRequest) (*pb.UpdateMachineResponse, error) {
|
||||||
|
if err := c.checkInitialised(ctx); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.MachineId == "" {
|
||||||
|
return nil, status.Error(codes.InvalidArgument, "machine_id not set")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the current machine info
|
||||||
|
currentMachine, err := c.store.GetMachine(ctx, req.MachineId)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, store.ErrMachineNotFound) {
|
||||||
|
return nil, status.Errorf(codes.NotFound, "machine not found: %s", req.MachineId)
|
||||||
|
}
|
||||||
|
return nil, status.Errorf(codes.Internal, "failed to get machine: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a copy of the current machine for updating
|
||||||
|
updatedMachine := &pb.MachineInfo{
|
||||||
|
Id: currentMachine.Id,
|
||||||
|
Name: currentMachine.Name,
|
||||||
|
Network: currentMachine.Network,
|
||||||
|
PublicIp: currentMachine.PublicIp,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply updates from the request
|
||||||
|
if req.Name != nil {
|
||||||
|
// Check for empty name
|
||||||
|
if *req.Name == "" {
|
||||||
|
return nil, status.Error(codes.InvalidArgument, "machine name cannot be empty")
|
||||||
|
}
|
||||||
|
// Check for duplicate names (excluding the current machine)
|
||||||
|
if *req.Name != currentMachine.Name {
|
||||||
|
machines, err := c.store.ListMachines(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, status.Errorf(codes.Internal, "list machines: %v", err)
|
||||||
|
}
|
||||||
|
for _, m := range machines {
|
||||||
|
if m.Id != req.MachineId && m.Name == *req.Name {
|
||||||
|
return nil, status.Errorf(codes.AlreadyExists, "machine with name %q already exists", *req.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
updatedMachine.Name = *req.Name
|
||||||
|
}
|
||||||
|
if req.PublicIp != nil {
|
||||||
|
// Check if this is an empty IP (used to signal removal)
|
||||||
|
if len(req.PublicIp.Ip) == 0 {
|
||||||
|
// User wants to remove public IP
|
||||||
|
updatedMachine.PublicIp = nil
|
||||||
|
} else {
|
||||||
|
// Validate and set the new IP
|
||||||
|
ip, err := req.PublicIp.ToAddr()
|
||||||
|
if err != nil {
|
||||||
|
return nil, status.Errorf(codes.InvalidArgument, "invalid public IP: %v", err)
|
||||||
|
}
|
||||||
|
if !ip.IsValid() {
|
||||||
|
return nil, status.Error(codes.InvalidArgument, "invalid public IP")
|
||||||
|
}
|
||||||
|
updatedMachine.PublicIp = req.PublicIp
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if req.Endpoints != nil {
|
||||||
|
updatedMachine.Network.Endpoints = req.Endpoints
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the machine in the store
|
||||||
|
if err = c.store.UpdateMachine(ctx, updatedMachine); err != nil {
|
||||||
|
if errors.Is(err, store.ErrMachineNotFound) {
|
||||||
|
return nil, status.Errorf(codes.NotFound, "machine not found: %s", req.MachineId)
|
||||||
|
}
|
||||||
|
return nil, status.Errorf(codes.Internal, "update machine: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
slog.Info("Machine configuration updated in the cluster.",
|
||||||
|
"id", updatedMachine.Id, "name", updatedMachine.Name)
|
||||||
|
|
||||||
|
resp := &pb.UpdateMachineResponse{Machine: updatedMachine}
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
// ListMachines lists all machines in the cluster including their membership states.
|
// ListMachines lists all machines in the cluster including their membership states.
|
||||||
func (c *Cluster) ListMachines(ctx context.Context, _ *emptypb.Empty) (*pb.ListMachinesResponse, error) {
|
func (c *Cluster) ListMachines(ctx context.Context, _ *emptypb.Empty) (*pb.ListMachinesResponse, error) {
|
||||||
if err := c.checkInitialised(ctx); err != nil {
|
if err := c.checkInitialised(ctx); err != nil {
|
||||||
@@ -242,3 +326,24 @@ func (c *Cluster) ListMachines(ctx context.Context, _ *emptypb.Empty) (*pb.ListM
|
|||||||
|
|
||||||
return &pb.ListMachinesResponse{Machines: members}, nil
|
return &pb.ListMachinesResponse{Machines: members}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RemoveMachine removes a machine from the cluster.
|
||||||
|
func (c *Cluster) RemoveMachine(ctx context.Context, req *pb.RemoveMachineRequest) (*emptypb.Empty, error) {
|
||||||
|
if err := c.checkInitialised(ctx); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Id == "" {
|
||||||
|
return nil, status.Error(codes.InvalidArgument, "machine ID not set")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.store.DeleteMachine(ctx, req.Id); err != nil {
|
||||||
|
if errors.Is(err, store.ErrMachineNotFound) {
|
||||||
|
return nil, status.Errorf(codes.NotFound, "machine not found: %s", req.Id)
|
||||||
|
}
|
||||||
|
return nil, status.Errorf(codes.Internal, "delete machine from store: %v", err)
|
||||||
|
}
|
||||||
|
slog.Info("Machine removed from the cluster.", "id", req.Id)
|
||||||
|
|
||||||
|
return &emptypb.Empty{}, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
|
|
||||||
"github.com/psviderski/uncloud/internal/dns"
|
"github.com/psviderski/uncloud/internal/dns"
|
||||||
"github.com/psviderski/uncloud/internal/machine/api/pb"
|
"github.com/psviderski/uncloud/internal/machine/api/pb"
|
||||||
"github.com/psviderski/uncloud/internal/machine/store"
|
"github.com/psviderski/uncloud/internal/machine/store"
|
||||||
|
|||||||
@@ -3,8 +3,9 @@ package cluster
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"go4.org/netipx"
|
|
||||||
"net/netip"
|
"net/netip"
|
||||||
|
|
||||||
|
"go4.org/netipx"
|
||||||
)
|
)
|
||||||
|
|
||||||
const DefaultSubnetBits = 24
|
const DefaultSubnetBits = 24
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package cluster
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/psviderski/uncloud/internal/secret"
|
"github.com/psviderski/uncloud/internal/secret"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package constants
|
||||||
|
|
||||||
|
const (
|
||||||
|
// MachineAPIPort is the port for the Machine API service on the management WireGuard network.
|
||||||
|
MachineAPIPort = 51000
|
||||||
|
)
|
||||||
@@ -3,10 +3,11 @@ package corroservice
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/BurntSushi/toml"
|
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/BurntSushi/toml"
|
||||||
"github.com/psviderski/uncloud/internal/fs"
|
"github.com/psviderski/uncloud/internal/fs"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -50,7 +51,7 @@ func (c *Config) Write(path, owner string) error {
|
|||||||
if err := encoder.Encode(c); err != nil {
|
if err := encoder.Encode(c); err != nil {
|
||||||
return fmt.Errorf("encode config: %w", err)
|
return fmt.Errorf("encode config: %w", err)
|
||||||
}
|
}
|
||||||
if err := os.WriteFile(path, data.Bytes(), 0600); err != nil {
|
if err := os.WriteFile(path, data.Bytes(), 0o600); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := fs.Chown(path, owner, owner); err != nil {
|
if err := fs.Chown(path, owner, owner); err != nil {
|
||||||
@@ -62,10 +63,10 @@ func (c *Config) Write(path, owner string) error {
|
|||||||
func MkDataDir(dir, owner string) error {
|
func MkDataDir(dir, owner string) error {
|
||||||
parent, _ := filepath.Split(dir)
|
parent, _ := filepath.Split(dir)
|
||||||
// Use 0711 for parent directories to allow `owner` to access its nested data directory.
|
// Use 0711 for parent directories to allow `owner` to access its nested data directory.
|
||||||
if err := os.MkdirAll(parent, 0711); err != nil {
|
if err := os.MkdirAll(parent, 0o711); err != nil {
|
||||||
return fmt.Errorf("create directory %q: %w", parent, err)
|
return fmt.Errorf("create directory %q: %w", parent, err)
|
||||||
}
|
}
|
||||||
if err := os.Mkdir(dir, 0700); err != nil {
|
if err := os.Mkdir(dir, 0o700); err != nil {
|
||||||
if !os.IsExist(err) {
|
if !os.IsExist(err) {
|
||||||
return fmt.Errorf("create directory %q: %w", dir, err)
|
return fmt.Errorf("create directory %q: %w", dir, err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,15 +3,16 @@ package corroservice
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log/slog"
|
||||||
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/docker/docker/api/types/container"
|
"github.com/docker/docker/api/types/container"
|
||||||
"github.com/docker/docker/api/types/image"
|
"github.com/docker/docker/api/types/image"
|
||||||
"github.com/docker/docker/api/types/mount"
|
"github.com/docker/docker/api/types/mount"
|
||||||
"github.com/docker/docker/api/types/network"
|
"github.com/docker/docker/api/types/network"
|
||||||
"github.com/docker/docker/client"
|
"github.com/docker/docker/client"
|
||||||
"io"
|
|
||||||
"log/slog"
|
|
||||||
"path/filepath"
|
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -26,29 +27,47 @@ type DockerService struct {
|
|||||||
User string
|
User string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewDockerService(cli *client.Client, image, name, dataDir string) *DockerService {
|
|
||||||
return &DockerService{
|
|
||||||
Client: cli,
|
|
||||||
Image: image,
|
|
||||||
Name: name,
|
|
||||||
DataDir: dataDir,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *DockerService) Start(ctx context.Context) error {
|
func (s *DockerService) Start(ctx context.Context) error {
|
||||||
_, err := s.Client.ContainerInspect(ctx, s.Name)
|
_, err := s.Client.ContainerInspect(ctx, s.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if client.IsErrNotFound(err) {
|
if !client.IsErrNotFound(err) {
|
||||||
return s.startNewContainer(ctx)
|
return fmt.Errorf("inspect container %q: %w", s.Name, err)
|
||||||
|
}
|
||||||
|
if err = s.startNewContainer(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Container already exists.
|
||||||
|
// TODO: recreate only if the container configuration has to be changed.
|
||||||
|
if err = s.Client.ContainerRemove(ctx, s.Name, container.RemoveOptions{Force: true}); err != nil {
|
||||||
|
return fmt.Errorf("remove container %q: %w", s.Name, err)
|
||||||
|
}
|
||||||
|
if err = s.startNewContainer(ctx); err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
return fmt.Errorf("inspect container %q: %w", s.Name, err)
|
|
||||||
}
|
|
||||||
// TODO: recreate only if the container configuration has to be changed.
|
|
||||||
if err = s.Client.ContainerRemove(ctx, s.Name, container.RemoveOptions{Force: true}); err != nil {
|
|
||||||
return fmt.Errorf("remove container %q: %w", s.Name, err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return s.startNewContainer(ctx)
|
slog.Debug("Waiting for corrosion service to be ready.")
|
||||||
|
if err = WaitReady(ctx, s.DataDir); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
slog.Debug("Corrosion service is ready.")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *DockerService) Stop(ctx context.Context) error {
|
||||||
|
if err := s.Client.ContainerStop(ctx, s.Name, container.StopOptions{}); err != nil {
|
||||||
|
return fmt.Errorf("stop container %q: %w", s.Name, err)
|
||||||
|
}
|
||||||
|
slog.Debug("Corrosion Docker container stopped.", "name", s.Name)
|
||||||
|
|
||||||
|
if err := s.Client.ContainerRemove(ctx, s.Name, container.RemoveOptions{}); err != nil {
|
||||||
|
return fmt.Errorf("remove container %q: %w", s.Name, err)
|
||||||
|
}
|
||||||
|
slog.Debug("Corrosion Docker container removed.", "name", s.Name)
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *DockerService) Restart(ctx context.Context) error {
|
func (s *DockerService) Restart(ctx context.Context) error {
|
||||||
|
|||||||
@@ -1,9 +1,62 @@
|
|||||||
package corroservice
|
package corroservice
|
||||||
|
|
||||||
import "context"
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/BurntSushi/toml"
|
||||||
|
"github.com/cenkalti/backoff/v4"
|
||||||
|
"github.com/psviderski/uncloud/internal/corrosion"
|
||||||
|
)
|
||||||
|
|
||||||
type Service interface {
|
type Service interface {
|
||||||
Start(ctx context.Context) error
|
Start(ctx context.Context) error
|
||||||
|
Stop(ctx context.Context) error
|
||||||
Restart(ctx context.Context) error
|
Restart(ctx context.Context) error
|
||||||
Running() bool
|
Running() bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WaitReady waits for the Corrosion service to be ready with the uncloud schema applied.
|
||||||
|
func WaitReady(ctx context.Context, dataDir string) error {
|
||||||
|
// Read the config file to get the API address.
|
||||||
|
configPath := filepath.Join(dataDir, "config.toml")
|
||||||
|
configData, err := os.ReadFile(configPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("read config file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var config Config
|
||||||
|
if err = toml.Unmarshal(configData, &config); err != nil {
|
||||||
|
return fmt.Errorf("unmarshal config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
corro, err := corrosion.NewAPIClient(config.API.Addr)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("create corrosion API client: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Corrosion starts serving the API before applying the schema. Query the cluster table with exponential backoff
|
||||||
|
// to check if the uncloud schema has been applied.
|
||||||
|
checkReady := func() error {
|
||||||
|
rows, err := corro.QueryContext(ctx, "SELECT 1 FROM cluster LIMIT 1")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("query cluster table: %w", err)
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
b := backoff.NewExponentialBackOff(
|
||||||
|
backoff.WithInitialInterval(50*time.Millisecond),
|
||||||
|
backoff.WithMaxInterval(1*time.Second),
|
||||||
|
backoff.WithMaxElapsedTime(15*time.Second),
|
||||||
|
)
|
||||||
|
if err = backoff.Retry(checkReady, backoff.WithContext(b, ctx)); err != nil {
|
||||||
|
return fmt.Errorf("corrosion service did not become ready: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,157 +0,0 @@
|
|||||||
package corroservice
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"log/slog"
|
|
||||||
"os/exec"
|
|
||||||
"path/filepath"
|
|
||||||
"sync"
|
|
||||||
"syscall"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
DefaultCommand = "corrosion"
|
|
||||||
DefaultDataDir = "/var/lib/uncloud/corrosion"
|
|
||||||
)
|
|
||||||
|
|
||||||
// SubprocessService implements the Service interface by running the service as a subprocess.
|
|
||||||
type SubprocessService struct {
|
|
||||||
Command string
|
|
||||||
DataDir string
|
|
||||||
|
|
||||||
cmd *exec.Cmd
|
|
||||||
running bool
|
|
||||||
mu sync.Mutex
|
|
||||||
cancelWatch context.CancelFunc
|
|
||||||
}
|
|
||||||
|
|
||||||
func DefaultSubprocessService() *SubprocessService {
|
|
||||||
return &SubprocessService{
|
|
||||||
Command: DefaultCommand,
|
|
||||||
DataDir: DefaultDataDir,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: maybe stop the process if this ctx is cancelled.
|
|
||||||
func (s *SubprocessService) Start(ctx context.Context) error {
|
|
||||||
s.mu.Lock()
|
|
||||||
defer s.mu.Unlock()
|
|
||||||
|
|
||||||
if s.running {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return s.startProcess(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SubprocessService) Restart(ctx context.Context) error {
|
|
||||||
s.mu.Lock()
|
|
||||||
defer s.mu.Unlock()
|
|
||||||
|
|
||||||
if s.running {
|
|
||||||
if err := s.stopProcess(); err != nil {
|
|
||||||
return fmt.Errorf("stop process: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return s.startProcess(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SubprocessService) Running() bool {
|
|
||||||
s.mu.Lock()
|
|
||||||
defer s.mu.Unlock()
|
|
||||||
return s.running
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SubprocessService) startProcess(ctx context.Context) error {
|
|
||||||
s.cmd = exec.Command(s.Command, "agent", "-c", filepath.Join(s.DataDir, "config.toml"))
|
|
||||||
|
|
||||||
// Redirect stdout and stderr to the logger.
|
|
||||||
stdout, err := s.cmd.StdoutPipe()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("create stdout pipe: %w", err)
|
|
||||||
}
|
|
||||||
stderr, err := s.cmd.StderrPipe()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("create stderr pipe: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
scanner := bufio.NewScanner(stdout)
|
|
||||||
for scanner.Scan() {
|
|
||||||
slog.Info("[corrosion]: " + scanner.Text())
|
|
||||||
}
|
|
||||||
// TODO: remove
|
|
||||||
slog.Info("######## corrosion redirect go routine end ########")
|
|
||||||
}()
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
scanner := bufio.NewScanner(stderr)
|
|
||||||
for scanner.Scan() {
|
|
||||||
slog.Error("[corrosion]: " + scanner.Text())
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
if err = s.cmd.Start(); err != nil {
|
|
||||||
return fmt.Errorf("start process: %w", err)
|
|
||||||
}
|
|
||||||
s.running = true
|
|
||||||
|
|
||||||
// Watch for process exit to update running status.
|
|
||||||
go func() {
|
|
||||||
if err := s.cmd.Wait(); err != nil {
|
|
||||||
slog.Error("corrosion process exited with error.", "code", s.cmd.ProcessState.ExitCode(), "err", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
s.mu.Lock()
|
|
||||||
s.running = false
|
|
||||||
s.mu.Unlock()
|
|
||||||
}()
|
|
||||||
|
|
||||||
// TODO: figure out the waiting process
|
|
||||||
// Wait for initialization
|
|
||||||
//timer := time.NewTimer(2 * time.Second)
|
|
||||||
//defer timer.Stop()
|
|
||||||
|
|
||||||
//select {
|
|
||||||
////case <-timer.C:
|
|
||||||
//// s.running = true
|
|
||||||
//// return nil
|
|
||||||
//case <-watchCtx.Done():
|
|
||||||
// return fmt.Errorf("process failed to start")
|
|
||||||
//case <-ctx.Done():
|
|
||||||
// s.stopProcess()
|
|
||||||
// return ctx.Err()
|
|
||||||
//}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SubprocessService) stopProcess() error {
|
|
||||||
if s.cmd == nil || s.cmd.Process == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := s.cmd.Process.Signal(syscall.SIGTERM); err != nil {
|
|
||||||
return fmt.Errorf("send SIGTERM: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait up to 5 seconds for graceful shutdown before killing the process.
|
|
||||||
done := make(chan error, 1)
|
|
||||||
go func() {
|
|
||||||
done <- s.cmd.Wait()
|
|
||||||
}()
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-time.After(5 * time.Second):
|
|
||||||
if err := s.cmd.Process.Kill(); err != nil {
|
|
||||||
return fmt.Errorf("kill process: %w", err)
|
|
||||||
}
|
|
||||||
case err := <-done:
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("process exited with error: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -5,7 +5,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const DefaultSystemdUnit = "uncloud-corrosion.service"
|
const DefaultSystemdUnit = "uncloud-corrosion.service"
|
||||||
@@ -27,6 +26,15 @@ func (s *SystemdService) Start(ctx context.Context) error {
|
|||||||
return s.startOrRestart(ctx, "start")
|
return s.startOrRestart(ctx, "start")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *SystemdService) Stop(_ context.Context) error {
|
||||||
|
if _, err := exec.Command("systemctl", "stop", s.Unit).Output(); err != nil {
|
||||||
|
return fmt.Errorf("systemctl stop %s: %w", s.Unit, err)
|
||||||
|
}
|
||||||
|
slog.Info("Corrosion systemd service stopped.", "unit", s.Unit)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *SystemdService) Restart(ctx context.Context) error {
|
func (s *SystemdService) Restart(ctx context.Context) error {
|
||||||
return s.startOrRestart(ctx, "restart")
|
return s.startOrRestart(ctx, "restart")
|
||||||
}
|
}
|
||||||
@@ -35,20 +43,15 @@ func (s *SystemdService) startOrRestart(ctx context.Context, cmd string) error {
|
|||||||
if _, err := exec.Command("systemctl", cmd, s.Unit).Output(); err != nil {
|
if _, err := exec.Command("systemctl", cmd, s.Unit).Output(); err != nil {
|
||||||
return fmt.Errorf("systemctl %s %s: %w", cmd, s.Unit, err)
|
return fmt.Errorf("systemctl %s %s: %w", cmd, s.Unit, err)
|
||||||
}
|
}
|
||||||
slog.Info(fmt.Sprintf("Corrosion systemd service %sed.", cmd), "unit", s.Unit)
|
slog.Debug(fmt.Sprintf("Corrosion systemd service %sed.", cmd), "unit", s.Unit)
|
||||||
|
|
||||||
// Optimistically wait for the corrosion service to start and initialise the database schema before proceeding.
|
slog.Debug("Waiting for corrosion service to be ready.")
|
||||||
timer := time.NewTimer(2 * time.Second)
|
if err := WaitReady(ctx, s.DataDir); err != nil {
|
||||||
defer timer.Stop()
|
return err
|
||||||
|
|
||||||
select {
|
|
||||||
case <-timer.C:
|
|
||||||
case <-ctx.Done():
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
slog.Debug("Corrosion service is ready.")
|
||||||
// TODO: run a goroutine to check the status of the service and log any errors in the uncloud log.
|
|
||||||
s.running = true
|
s.running = true
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,13 +14,13 @@ const DBFileName = "machine.db"
|
|||||||
func NewDB(path string) (*sqlx.DB, error) {
|
func NewDB(path string) (*sqlx.DB, error) {
|
||||||
// Create the database file with 0600 permissions if it doesn't exist, or update permissions if exists.
|
// 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) {
|
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||||
file, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR, 0600)
|
file, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR, 0o600)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("create SQLite database '%s': %w", path, err)
|
return nil, fmt.Errorf("create SQLite database '%s': %w", path, err)
|
||||||
}
|
}
|
||||||
file.Close()
|
file.Close()
|
||||||
} else {
|
} else {
|
||||||
if err = os.Chmod(path, 0600); err != nil {
|
if err = os.Chmod(path, 0o600); err != nil {
|
||||||
return nil, fmt.Errorf("update SQLite database permissions '%s': %w", path, err)
|
return nil, fmt.Errorf("update SQLite database permissions '%s': %w", path, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,120 @@
|
|||||||
|
package dns
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"net/netip"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/psviderski/uncloud/internal/machine/store"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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 := record.Container
|
||||||
|
if ctr.ServiceID() == "" || ctr.ServiceName() == "" {
|
||||||
|
// Container is not part of a service, skip it.
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
newServiceIPs[ctr.ServiceName()] = append(newServiceIPs[ctr.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)
|
||||||
|
}
|
||||||
@@ -196,13 +196,27 @@ func (c *Client) RemoveContainer(ctx context.Context, id string, opts container.
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PullOptions defines the options for pulling an image from a remote registry.
|
||||||
|
// This is a copy of image.PullOptions from the Docker API without the PrivilegeFunc field that is non-serialisable.
|
||||||
|
type PullOptions struct {
|
||||||
|
All bool
|
||||||
|
// RegistryAuth is the base64 encoded credentials for the registry.
|
||||||
|
RegistryAuth string
|
||||||
|
Platform string
|
||||||
|
}
|
||||||
|
|
||||||
type PullImageMessage struct {
|
type PullImageMessage struct {
|
||||||
Message jsonmessage.JSONMessage
|
Message jsonmessage.JSONMessage
|
||||||
Err error
|
Err error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) PullImage(ctx context.Context, image string) (<-chan PullImageMessage, error) {
|
func (c *Client) PullImage(ctx context.Context, image string, opts PullOptions) (<-chan PullImageMessage, error) {
|
||||||
stream, err := c.grpcClient.PullImage(ctx, &pb.PullImageRequest{Image: image})
|
optsBytes, err := json.Marshal(opts)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("marshal options: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
stream, err := c.grpcClient.PullImage(ctx, &pb.PullImageRequest{Image: image, Options: optsBytes})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,19 +4,18 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
dockercontainer "github.com/docker/docker/api/types/container"
|
"log/slog"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/docker/docker/api/types/container"
|
||||||
"github.com/docker/docker/api/types/events"
|
"github.com/docker/docker/api/types/events"
|
||||||
"github.com/docker/docker/api/types/filters"
|
"github.com/docker/docker/api/types/filters"
|
||||||
"github.com/docker/docker/client"
|
"github.com/docker/docker/client"
|
||||||
"github.com/psviderski/uncloud/internal/machine/store"
|
"github.com/psviderski/uncloud/internal/machine/store"
|
||||||
"github.com/psviderski/uncloud/pkg/api"
|
|
||||||
"log/slog"
|
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
NetworkName = "uncloud"
|
NetworkName = "uncloud"
|
||||||
UserChain = "DOCKER-USER"
|
|
||||||
// EventsDebounceInterval defines how long to wait before processing the next Docker event. Multiple events
|
// EventsDebounceInterval defines how long to wait before processing the next Docker event. Multiple events
|
||||||
// occurring within this window will be processed together as a single event to prevent system overload.
|
// occurring within this window will be processed together as a single event to prevent system overload.
|
||||||
EventsDebounceInterval = 100 * time.Millisecond
|
EventsDebounceInterval = 100 * time.Millisecond
|
||||||
@@ -24,23 +23,26 @@ const (
|
|||||||
SyncInterval = 30 * time.Second
|
SyncInterval = 30 * time.Second
|
||||||
)
|
)
|
||||||
|
|
||||||
type Manager struct {
|
// Controller monitors Docker events and synchronises service containers with the cluster store.
|
||||||
client *client.Client
|
type Controller struct {
|
||||||
// machineID is the ID of the machine where the managed Docker daemon is running.
|
// machineID is the ID of the machine where the managed Docker daemon is running.
|
||||||
machineID string
|
machineID string
|
||||||
|
client *client.Client
|
||||||
|
service *Service
|
||||||
store *store.Store
|
store *store.Store
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewManager(client *client.Client, machineID string, store *store.Store) *Manager {
|
func NewController(machineID string, service *Service, store *store.Store) *Controller {
|
||||||
return &Manager{
|
return &Controller{
|
||||||
client: client,
|
|
||||||
machineID: machineID,
|
machineID: machineID,
|
||||||
|
client: service.Client,
|
||||||
|
service: service,
|
||||||
store: store,
|
store: store,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// WaitDaemonReady waits for the Docker daemon to start and be ready to serve requests.
|
// WaitDaemonReady waits for the Docker daemon to start and be ready to serve requests.
|
||||||
func (m *Manager) WaitDaemonReady(ctx context.Context) error {
|
func (c *Controller) WaitDaemonReady(ctx context.Context) error {
|
||||||
ticker := time.NewTicker(1 * time.Second)
|
ticker := time.NewTicker(1 * time.Second)
|
||||||
defer ticker.Stop()
|
defer ticker.Stop()
|
||||||
|
|
||||||
@@ -50,7 +52,7 @@ func (m *Manager) WaitDaemonReady(ctx context.Context) error {
|
|||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return ctx.Err()
|
return ctx.Err()
|
||||||
case <-ticker.C:
|
case <-ticker.C:
|
||||||
_, err := m.client.Ping(ctx)
|
_, err := c.client.Ping(ctx)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
ready = true
|
ready = true
|
||||||
break
|
break
|
||||||
@@ -67,7 +69,7 @@ func (m *Manager) WaitDaemonReady(ctx context.Context) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) WatchAndSyncContainers(ctx context.Context) error {
|
func (c *Controller) WatchAndSyncContainers(ctx context.Context) error {
|
||||||
ctx, cancel := context.WithCancel(ctx)
|
ctx, cancel := context.WithCancel(ctx)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
// Filter only local container events.
|
// Filter only local container events.
|
||||||
@@ -79,9 +81,9 @@ func (m *Manager) WatchAndSyncContainers(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Subscribe to Docker events before running the initial sync to avoid missing any events.
|
// Subscribe to Docker events before running the initial sync to avoid missing any events.
|
||||||
eventCh, errCh := m.client.Events(ctx, opts)
|
eventCh, errCh := c.service.Client.Events(ctx, opts)
|
||||||
slog.Debug("Syncing containers to cluster store before processing Docker events.")
|
slog.Debug("Syncing containers to cluster store before processing Docker events.")
|
||||||
if err := m.syncContainersToStore(ctx); err != nil {
|
if err := c.syncContainersToStore(ctx); err != nil {
|
||||||
// The deferred cancel will stop the event subscription.
|
// The deferred cancel will stop the event subscription.
|
||||||
return fmt.Errorf("sync containers to cluster store: %w", err)
|
return fmt.Errorf("sync containers to cluster store: %w", err)
|
||||||
}
|
}
|
||||||
@@ -126,13 +128,13 @@ func (m *Manager) WatchAndSyncContainers(ctx context.Context) error {
|
|||||||
"container_name", e.Actor.Attributes["name"],
|
"container_name", e.Actor.Attributes["name"],
|
||||||
"action", e.Action)
|
"action", e.Action)
|
||||||
|
|
||||||
if err := m.syncContainersToStore(ctx); err != nil {
|
if err := c.syncContainersToStore(ctx); err != nil {
|
||||||
return fmt.Errorf("sync containers to cluster store: %w", err)
|
return fmt.Errorf("sync containers to cluster store: %w", err)
|
||||||
}
|
}
|
||||||
case <-ticker.C:
|
case <-ticker.C:
|
||||||
slog.Debug("Syncing containers to cluster store triggered by a regular interval.",
|
slog.Debug("Syncing containers to cluster store triggered by a regular interval.",
|
||||||
"interval", SyncInterval)
|
"interval", SyncInterval)
|
||||||
if err := m.syncContainersToStore(ctx); err != nil {
|
if err := c.syncContainersToStore(ctx); err != nil {
|
||||||
return fmt.Errorf("sync containers to cluster store: %w", err)
|
return fmt.Errorf("sync containers to cluster store: %w", err)
|
||||||
}
|
}
|
||||||
case err := <-errCh:
|
case err := <-errCh:
|
||||||
@@ -144,32 +146,16 @@ func (m *Manager) WatchAndSyncContainers(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) syncContainersToStore(ctx context.Context) error {
|
func (c *Controller) syncContainersToStore(ctx context.Context) error {
|
||||||
storeContainers, err := m.store.ListContainers(ctx, store.ListOptions{MachineIDs: []string{m.machineID}})
|
storeContainers, err := c.store.ListContainers(ctx, store.ListOptions{MachineIDs: []string{c.machineID}})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("list containers from store: %w", err)
|
return fmt.Errorf("list containers from store: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// List only Uncloud service containers identified by their labels.
|
containers, err := c.service.ListServiceContainers(ctx, "", container.ListOptions{})
|
||||||
containerSummaries, err := m.client.ContainerList(ctx, dockercontainer.ListOptions{
|
|
||||||
Filters: filters.NewArgs(
|
|
||||||
filters.Arg("label", api.LabelServiceID),
|
|
||||||
filters.Arg("label", api.LabelServiceName),
|
|
||||||
),
|
|
||||||
})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// TODO: mark all containers as outdated in the store.
|
// TODO: mark all containers as outdated in the store.
|
||||||
return fmt.Errorf("list Docker containers: %w", err)
|
return fmt.Errorf("list service containers: %w", err)
|
||||||
}
|
|
||||||
|
|
||||||
// Inspect each container to get the full container details.
|
|
||||||
containers := make([]api.Container, len(containerSummaries))
|
|
||||||
for i, cs := range containerSummaries {
|
|
||||||
ctr, err := m.client.ContainerInspect(ctx, cs.ID)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("inspect container '%s': %w", cs.ID, err)
|
|
||||||
}
|
|
||||||
containers[i] = api.Container{ContainerJSON: ctr}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete containers from the store that are no longer present in the Docker daemon.
|
// Delete containers from the store that are no longer present in the Docker daemon.
|
||||||
@@ -189,15 +175,15 @@ func (m *Manager) syncContainersToStore(ctx context.Context) error {
|
|||||||
|
|
||||||
var storeErr error
|
var storeErr error
|
||||||
if len(deleteIDs) > 0 {
|
if len(deleteIDs) > 0 {
|
||||||
if err = m.store.DeleteContainers(ctx, store.DeleteOptions{IDs: deleteIDs}); err != nil {
|
if err = c.store.DeleteContainers(ctx, store.DeleteOptions{IDs: deleteIDs}); err != nil {
|
||||||
storeErr = fmt.Errorf("delete containers from store: %w", err)
|
storeErr = fmt.Errorf("delete containers from store: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create or update the current Docker containers in the store.
|
// Create or update the current Docker containers in the store.
|
||||||
for _, c := range containers {
|
for _, ctr := range containers {
|
||||||
if err = m.store.CreateOrUpdateContainer(ctx, c, m.machineID); err != nil {
|
if err = c.store.CreateOrUpdateContainer(ctx, ctr, c.machineID); err != nil {
|
||||||
storeErr = errors.Join(storeErr, fmt.Errorf("create or update container %q: %w", c.ID, err))
|
storeErr = errors.Join(storeErr, fmt.Errorf("create or update container '%s': %w", ctr.ID, err))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return storeErr
|
return storeErr
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
//go:build darwin
|
||||||
|
|
||||||
|
package docker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/netip"
|
||||||
|
)
|
||||||
|
|
||||||
|
// EnsureUncloudNetwork is a stub for Darwin.
|
||||||
|
func (c *Controller) EnsureUncloudNetwork(ctx context.Context, subnet netip.Prefix, dnsServer netip.Addr) error {
|
||||||
|
return fmt.Errorf("not supported on Darwin")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup is a stub for Darwin.
|
||||||
|
func (c *Controller) Cleanup() error {
|
||||||
|
return fmt.Errorf("not supported on Darwin")
|
||||||
|
}
|
||||||
@@ -0,0 +1,234 @@
|
|||||||
|
package docker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"net/netip"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
dockercontainer "github.com/docker/docker/api/types/container"
|
||||||
|
"github.com/docker/docker/api/types/filters"
|
||||||
|
dnetwork "github.com/docker/docker/api/types/network"
|
||||||
|
"github.com/docker/docker/client"
|
||||||
|
"github.com/docker/docker/libnetwork/iptables"
|
||||||
|
"github.com/psviderski/uncloud/internal/machine/dns"
|
||||||
|
"github.com/psviderski/uncloud/internal/machine/firewall"
|
||||||
|
"github.com/psviderski/uncloud/internal/machine/network"
|
||||||
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
// EnsureUncloudNetwork creates the Docker bridge network NetworkName with the provided machine subnet
|
||||||
|
// if it doesn't exist. If the network exists but has a different subnet, it removes and recreates the network.
|
||||||
|
// It also configures iptables to allow container access from the WireGuard network.
|
||||||
|
func (c *Controller) EnsureUncloudNetwork(ctx context.Context, subnet netip.Prefix, dnsServer netip.Addr) error {
|
||||||
|
// Ensure the Docker network 'uncloud' is created with the correct subnet.
|
||||||
|
needsCreation := false
|
||||||
|
nw, err := c.client.NetworkInspect(ctx, NetworkName, dnetwork.InspectOptions{})
|
||||||
|
if err != nil {
|
||||||
|
if !client.IsErrNotFound(err) {
|
||||||
|
return fmt.Errorf("inspect Docker network '%s': %w", NetworkName, err)
|
||||||
|
}
|
||||||
|
needsCreation = true
|
||||||
|
} else if nw.IPAM.Config[0].Subnet != subnet.String() {
|
||||||
|
// Remove the Docker network if the subnet is different.
|
||||||
|
// It could be a leftover from a previous incomplete cleanup.
|
||||||
|
slog.Info(
|
||||||
|
"Removing Docker network with old subnet.", "name", NetworkName, "subnet", nw.IPAM.Config[0].Subnet,
|
||||||
|
)
|
||||||
|
if err = c.client.NetworkRemove(ctx, NetworkName); err != nil {
|
||||||
|
// It can still fail if the network is in use by a container. Leave it to the user to resolve the issue.
|
||||||
|
return fmt.Errorf("remove Docker network '%s': %w", NetworkName, err)
|
||||||
|
}
|
||||||
|
needsCreation = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if needsCreation {
|
||||||
|
if _, err = c.client.NetworkCreate(
|
||||||
|
ctx, NetworkName, dnetwork.CreateOptions{
|
||||||
|
Driver: "bridge",
|
||||||
|
Scope: "local",
|
||||||
|
IPAM: &dnetwork.IPAM{
|
||||||
|
Config: []dnetwork.IPAMConfig{
|
||||||
|
{
|
||||||
|
Subnet: subnet.String(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Labels: map[string]string{
|
||||||
|
api.LabelManaged: "",
|
||||||
|
},
|
||||||
|
Options: map[string]string{
|
||||||
|
// Starting with Docker 28.2.0 (https://github.com/moby/moby/pull/49832), we have to explicitly
|
||||||
|
// allow direct routing from the WireGuard interface to the bridge network.
|
||||||
|
"com.docker.network.bridge.trusted_host_interfaces": network.WireGuardInterfaceName,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
); err != nil {
|
||||||
|
return fmt.Errorf("create Docker network '%s': %w", NetworkName, err)
|
||||||
|
}
|
||||||
|
slog.Info("Docker network created.", "name", NetworkName, "subnet", subnet.String())
|
||||||
|
|
||||||
|
if nw, err = c.client.NetworkInspect(ctx, NetworkName, dnetwork.InspectOptions{}); err != nil {
|
||||||
|
return fmt.Errorf("inspect Docker network '%s': %w", NetworkName, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configure iptables to allow WireGuard network to access containers. The Docker daemon should have already
|
||||||
|
// created the DOCKER-USER chain at this point.
|
||||||
|
// TODO: check if this works when firewalld used instead of raw iptables. The Docker daemon has a different
|
||||||
|
// code path for firewalld.
|
||||||
|
|
||||||
|
// Bridge name doesn't seem to be documented but this is the source code where it is generated:
|
||||||
|
// https://github.com/moby/moby/blob/v27.2.1/libnetwork/drivers/bridge/bridge_linux.go#L664
|
||||||
|
bridgeName := "br-" + nw.ID[:12]
|
||||||
|
|
||||||
|
if err = configureIptables(bridgeName, subnet, dnsServer); err != nil {
|
||||||
|
return fmt.Errorf("configure iptables for Docker network '%s': %w", NetworkName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// configureIptables configures iptables rules for the uncloud Docker network.
|
||||||
|
func configureIptables(bridgeName string, subnet netip.Prefix, dnsServer netip.Addr) error {
|
||||||
|
ipt := iptables.GetIptable(iptables.IPv4)
|
||||||
|
// Allow traffic from other machines and their containers through the WG mesh to the Uncloud containers
|
||||||
|
// on the machine.
|
||||||
|
wgRule := []string{
|
||||||
|
"--in-interface", network.WireGuardInterfaceName,
|
||||||
|
"--out-interface", bridgeName,
|
||||||
|
"-j", "ACCEPT",
|
||||||
|
}
|
||||||
|
if err := ipt.ProgramRule(iptables.Filter, firewall.DockerUserChain, iptables.Insert, wgRule); err != nil {
|
||||||
|
return fmt.Errorf("insert iptables rule: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow DNS queries from Uncloud containers to the embedded DNS server.
|
||||||
|
for _, proto := range []string{"udp", "tcp"} {
|
||||||
|
dnsRule := []string{
|
||||||
|
"--in-interface", bridgeName,
|
||||||
|
"--dst", dnsServer.String(),
|
||||||
|
"--protocol", proto,
|
||||||
|
"--dport", strconv.Itoa(dns.Port),
|
||||||
|
"-j", "ACCEPT",
|
||||||
|
}
|
||||||
|
if err := ipt.ProgramRule(iptables.Filter, firewall.UncloudInputChain, iptables.Insert, dnsRule); err != nil {
|
||||||
|
return fmt.Errorf("insert iptables rule: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip masquerading for the container traffic going from the uncloud Docker network through the WG mesh.
|
||||||
|
// https://uncloud.run/blog/connect-docker-containers-across-hosts-wireguard#step-3-configure-ip-routing
|
||||||
|
skipMasqueradeRule := []string{
|
||||||
|
"--src", subnet.String(),
|
||||||
|
"--out-interface", network.WireGuardInterfaceName,
|
||||||
|
"-j", "RETURN",
|
||||||
|
}
|
||||||
|
// Delete and reinsert the rule to ensure it's at the top of the POSTROUTING chain before the MASQUERADE rule
|
||||||
|
// added by Docker: POSTROUTING -s 10.210.X.0/24 ! -o br-XXX -j MASQUERADE
|
||||||
|
if err := ipt.ProgramRule(iptables.Nat, "POSTROUTING", iptables.Delete, skipMasqueradeRule); err != nil {
|
||||||
|
return fmt.Errorf("delete iptables rule: %w", err)
|
||||||
|
}
|
||||||
|
if err := ipt.ProgramRule(iptables.Nat, "POSTROUTING", iptables.Insert, skipMasqueradeRule); err != nil {
|
||||||
|
return fmt.Errorf("insert iptables rule: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// cleanupIptables deletes the iptables rules for the uncloud Docker network.
|
||||||
|
func cleanupIptables(bridgeName string, subnet netip.Prefix) error {
|
||||||
|
ipt := iptables.GetIptable(iptables.IPv4)
|
||||||
|
// Delete the rule allowing traffic from the WireGuard network to the Docker bridge.
|
||||||
|
wgRule := []string{
|
||||||
|
"--in-interface", network.WireGuardInterfaceName,
|
||||||
|
"--out-interface", bridgeName,
|
||||||
|
"-j", "ACCEPT",
|
||||||
|
}
|
||||||
|
if err := ipt.ProgramRule(iptables.Filter, firewall.DockerUserChain, iptables.Delete, wgRule); err != nil {
|
||||||
|
return fmt.Errorf("delete iptables rule: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete the rule that skips masquerading for the container traffic going from the uncloud Docker network
|
||||||
|
// through the WG mesh.
|
||||||
|
skipMasqueradeRule := []string{
|
||||||
|
"--src", subnet.String(),
|
||||||
|
"--out-interface", network.WireGuardInterfaceName,
|
||||||
|
"-j", "RETURN",
|
||||||
|
}
|
||||||
|
if err := ipt.ProgramRule(iptables.Nat, "POSTROUTING", iptables.Delete, skipMasqueradeRule); err != nil {
|
||||||
|
return fmt.Errorf("delete iptables rule: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rules in uncloud-owned chains will be automatically cleaned up by the machine cleanup.
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup removes all uncloud-managed containers and the uncloud Docker network.
|
||||||
|
func (c *Controller) Cleanup() error {
|
||||||
|
ctx := context.Background()
|
||||||
|
var errs []error
|
||||||
|
|
||||||
|
// Remove uncloud-managed Docker containers.
|
||||||
|
containers, err := c.client.ContainerList(ctx, dockercontainer.ListOptions{
|
||||||
|
All: true, // Include stopped containers.
|
||||||
|
Filters: filters.NewArgs(
|
||||||
|
filters.Arg("label", api.LabelManaged),
|
||||||
|
),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
errs = append(errs, fmt.Errorf("list uncloud-managed Docker containers: %w", err))
|
||||||
|
} else if len(containers) > 0 {
|
||||||
|
slog.Info("Removing uncloud-managed Docker containers.", "count", len(containers))
|
||||||
|
removed := 0
|
||||||
|
|
||||||
|
for _, ctr := range containers {
|
||||||
|
err = c.client.ContainerStop(ctx, ctr.ID, dockercontainer.StopOptions{})
|
||||||
|
if err != nil && !client.IsErrNotFound(err) {
|
||||||
|
errs = append(errs, fmt.Errorf("stop container '%s': %w", ctr.ID, err))
|
||||||
|
}
|
||||||
|
|
||||||
|
err = c.client.ContainerRemove(ctx, ctr.ID, dockercontainer.RemoveOptions{
|
||||||
|
// Remove anonymous volumes created by the container.
|
||||||
|
RemoveVolumes: true,
|
||||||
|
})
|
||||||
|
if err == nil {
|
||||||
|
removed++
|
||||||
|
} else if !client.IsErrNotFound(err) {
|
||||||
|
errs = append(errs, fmt.Errorf("remove container '%s': %w", ctr.ID, err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
slog.Info("Removed uncloud-managed Docker containers.", "count", removed)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the uncloud Docker network and related iptables rules.
|
||||||
|
nw, err := c.client.NetworkInspect(ctx, NetworkName, dnetwork.InspectOptions{})
|
||||||
|
if err == nil {
|
||||||
|
bridgeName := "br-" + nw.ID[:12]
|
||||||
|
var subnet netip.Prefix
|
||||||
|
if len(nw.IPAM.Config) > 0 {
|
||||||
|
subnet, _ = netip.ParsePrefix(nw.IPAM.Config[0].Subnet)
|
||||||
|
}
|
||||||
|
|
||||||
|
if subnet.IsValid() {
|
||||||
|
if err = cleanupIptables(bridgeName, subnet); err != nil {
|
||||||
|
errs = append(errs, fmt.Errorf("cleanup iptables for Docker network '%s': %w", NetworkName, err))
|
||||||
|
} else {
|
||||||
|
slog.Info("Cleaned up iptables rules for Docker network.", "name", NetworkName, "bridge", bridgeName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = c.client.NetworkRemove(ctx, NetworkName); err == nil {
|
||||||
|
slog.Info("Docker network removed.", "name", NetworkName)
|
||||||
|
} else if !client.IsErrNotFound(err) {
|
||||||
|
errs = append(errs, fmt.Errorf("remove Docker network '%s': %w", NetworkName, err))
|
||||||
|
}
|
||||||
|
} else if !client.IsErrNotFound(err) {
|
||||||
|
errs = append(errs, fmt.Errorf("inspect Docker network '%s': %w", NetworkName, err))
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors.Join(errs...)
|
||||||
|
}
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
//go:build darwin
|
|
||||||
|
|
||||||
package docker
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"net/netip"
|
|
||||||
)
|
|
||||||
|
|
||||||
// EnsureUncloudNetwork is a stub for darwin.
|
|
||||||
func (m *Manager) EnsureUncloudNetwork(ctx context.Context, subnet netip.Prefix) error {
|
|
||||||
return fmt.Errorf("not supported on darwin")
|
|
||||||
}
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
package docker
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
dnetwork "github.com/docker/docker/api/types/network"
|
|
||||||
"github.com/docker/docker/client"
|
|
||||||
"github.com/docker/docker/libnetwork/iptables"
|
|
||||||
"log/slog"
|
|
||||||
"net/netip"
|
|
||||||
"github.com/psviderski/uncloud/internal/machine/network"
|
|
||||||
)
|
|
||||||
|
|
||||||
// EnsureUncloudNetwork creates the Docker bridge network NetworkName with the provided machine subnet
|
|
||||||
// if it doesn't exist. If the network exists but has a different subnet, it removes and recreates the network.
|
|
||||||
// It also configures iptables to allow container access from the WireGuard network.
|
|
||||||
func (m *Manager) EnsureUncloudNetwork(ctx context.Context, subnet netip.Prefix) error {
|
|
||||||
// Ensure the Docker network 'uncloud' is created with the correct subnet.
|
|
||||||
needsCreation := false
|
|
||||||
nw, err := m.client.NetworkInspect(ctx, NetworkName, dnetwork.InspectOptions{})
|
|
||||||
if err != nil {
|
|
||||||
if !client.IsErrNotFound(err) {
|
|
||||||
return fmt.Errorf("inspect Docker network %q: %w", NetworkName, err)
|
|
||||||
}
|
|
||||||
needsCreation = true
|
|
||||||
} else if nw.IPAM.Config[0].Subnet != subnet.String() {
|
|
||||||
// Remove the Docker network if the subnet is different.
|
|
||||||
// It could be a leftover from a previous incomplete cleanup.
|
|
||||||
slog.Info(
|
|
||||||
"Removing Docker network with old subnet.", "name", NetworkName, "subnet", nw.IPAM.Config[0].Subnet,
|
|
||||||
)
|
|
||||||
if err = m.client.NetworkRemove(ctx, NetworkName); err != nil {
|
|
||||||
// It can still fail if the network is in use by a container. Leave it to the user to resolve the issue.
|
|
||||||
return fmt.Errorf("remove Docker network %q: %w", NetworkName, err)
|
|
||||||
}
|
|
||||||
needsCreation = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if needsCreation {
|
|
||||||
if _, err = m.client.NetworkCreate(
|
|
||||||
ctx, NetworkName, dnetwork.CreateOptions{
|
|
||||||
Driver: "bridge",
|
|
||||||
Scope: "local",
|
|
||||||
IPAM: &dnetwork.IPAM{
|
|
||||||
Config: []dnetwork.IPAMConfig{
|
|
||||||
{
|
|
||||||
Subnet: subnet.String(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
); err != nil {
|
|
||||||
return fmt.Errorf("create Docker network %q: %w", NetworkName, err)
|
|
||||||
}
|
|
||||||
slog.Info("Docker network created.", "name", NetworkName, "subnet", subnet.String())
|
|
||||||
|
|
||||||
if nw, err = m.client.NetworkInspect(ctx, NetworkName, dnetwork.InspectOptions{}); err != nil {
|
|
||||||
return fmt.Errorf("inspect Docker network %q: %w", NetworkName, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Configure iptables to allow WireGuard network to access containers. The Docker daemon should have already
|
|
||||||
// created the DOCKER-USER chain at this point.
|
|
||||||
// TODO: check if this works when firewalld used instead of raw iptables. The Docker daemon has a different
|
|
||||||
// code path for firewalld.
|
|
||||||
|
|
||||||
// Bridge name doesn't seem to be documented but this is the source code where it is generated:
|
|
||||||
// https://github.com/moby/moby/blob/v27.2.1/libnetwork/drivers/bridge/bridge_linux.go#L664
|
|
||||||
bridgeName := "br-" + nw.ID[:12]
|
|
||||||
ipt := iptables.GetIptable(iptables.IPv4)
|
|
||||||
rule := []string{"--in-interface", network.WireGuardInterfaceName, "--out-interface", bridgeName, "-j", "ACCEPT"}
|
|
||||||
if err = ipt.ProgramRule(iptables.Filter, UserChain, iptables.Insert, rule); err != nil {
|
|
||||||
return fmt.Errorf("insert iptables rule: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -2,18 +2,21 @@ package docker
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"database/sql"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
|
"net/netip"
|
||||||
|
"os"
|
||||||
"regexp"
|
"regexp"
|
||||||
"slices"
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/distribution/reference"
|
"github.com/distribution/reference"
|
||||||
|
dockercommand "github.com/docker/cli/cli/command"
|
||||||
|
dockerconfig "github.com/docker/cli/cli/config"
|
||||||
"github.com/docker/docker/api/types"
|
"github.com/docker/docker/api/types"
|
||||||
"github.com/docker/docker/api/types/container"
|
"github.com/docker/docker/api/types/container"
|
||||||
"github.com/docker/docker/api/types/filters"
|
"github.com/docker/docker/api/types/filters"
|
||||||
@@ -30,6 +33,7 @@ import (
|
|||||||
"github.com/opencontainers/go-digest"
|
"github.com/opencontainers/go-digest"
|
||||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||||
"github.com/psviderski/uncloud/internal/machine/api/pb"
|
"github.com/psviderski/uncloud/internal/machine/api/pb"
|
||||||
|
"github.com/psviderski/uncloud/internal/machine/dns"
|
||||||
"github.com/psviderski/uncloud/internal/secret"
|
"github.com/psviderski/uncloud/internal/secret"
|
||||||
"github.com/psviderski/uncloud/pkg/api"
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
@@ -43,18 +47,51 @@ var fullDockerIDRegex = regexp.MustCompile(`^[a-f0-9]{64}$`)
|
|||||||
// Server implements the gRPC Docker service that proxies requests to the Docker daemon.
|
// Server implements the gRPC Docker service that proxies requests to the Docker daemon.
|
||||||
type Server struct {
|
type Server struct {
|
||||||
pb.UnimplementedDockerServer
|
pb.UnimplementedDockerServer
|
||||||
client *client.Client
|
client *client.Client
|
||||||
db *sqlx.DB
|
service *Service
|
||||||
|
db *sqlx.DB
|
||||||
|
// internalDNSIP is a function that returns the IP address of the internal DNS server. It may return an empty
|
||||||
|
// address if the address is unknown (e.g. when the machine is not initialised yet).
|
||||||
|
internalDNSIP func() netip.Addr
|
||||||
|
// networkReady is a function that returns true if the Docker network is ready for containers.
|
||||||
|
networkReady func() bool
|
||||||
|
// waitForNetworkReady is a function that waits for the Docker network to be ready for containers.
|
||||||
|
waitForNetworkReady func(ctx context.Context) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewServer creates a new Docker gRPC server with the provided Docker client.
|
// ServerOption configures the Docker server.
|
||||||
func NewServer(cli *client.Client, db *sqlx.DB) *Server {
|
type ServerOption func(*Server)
|
||||||
return &Server{
|
|
||||||
client: cli,
|
// WithNetworkReady sets the network readiness check function.
|
||||||
db: db,
|
func WithNetworkReady(networkReady func() bool) ServerOption {
|
||||||
|
return func(s *Server) {
|
||||||
|
s.networkReady = networkReady
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithWaitForNetworkReady sets the network readiness wait function.
|
||||||
|
func WithWaitForNetworkReady(waitForNetworkReady func(ctx context.Context) error) ServerOption {
|
||||||
|
return func(s *Server) {
|
||||||
|
s.waitForNetworkReady = waitForNetworkReady
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewServer creates a new Docker gRPC server with the provided Docker service.
|
||||||
|
func NewServer(service *Service, db *sqlx.DB, internalDNSIP func() netip.Addr, opts ...ServerOption) *Server {
|
||||||
|
s := &Server{
|
||||||
|
client: service.Client,
|
||||||
|
service: service,
|
||||||
|
db: db,
|
||||||
|
internalDNSIP: internalDNSIP,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, opt := range opts {
|
||||||
|
opt(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
// CreateContainer creates a new container based on the given configuration.
|
// CreateContainer creates a new container based on the given configuration.
|
||||||
func (s *Server) CreateContainer(ctx context.Context, req *pb.CreateContainerRequest) (*pb.CreateContainerResponse, error) {
|
func (s *Server) CreateContainer(ctx context.Context, req *pb.CreateContainerRequest) (*pb.CreateContainerResponse, error) {
|
||||||
var config container.Config
|
var config container.Config
|
||||||
@@ -112,6 +149,15 @@ func (s *Server) InspectContainer(ctx context.Context, req *pb.InspectContainerR
|
|||||||
|
|
||||||
// StartContainer starts a container with the given ID and options.
|
// StartContainer starts a container with the given ID and options.
|
||||||
func (s *Server) StartContainer(ctx context.Context, req *pb.StartContainerRequest) (*emptypb.Empty, error) {
|
func (s *Server) StartContainer(ctx context.Context, req *pb.StartContainerRequest) (*emptypb.Empty, error) {
|
||||||
|
// Wait for Docker network to be ready before starting the container
|
||||||
|
if s.waitForNetworkReady != nil {
|
||||||
|
if err := s.waitForNetworkReady(ctx); err != nil {
|
||||||
|
return nil, status.Errorf(codes.Unavailable, "Docker network not ready: %v", err)
|
||||||
|
}
|
||||||
|
} else if s.networkReady != nil && !s.networkReady() {
|
||||||
|
return nil, status.Errorf(codes.Unavailable, "Docker network not ready")
|
||||||
|
}
|
||||||
|
|
||||||
var opts container.StartOptions
|
var opts container.StartOptions
|
||||||
if len(req.Options) > 0 {
|
if len(req.Options) > 0 {
|
||||||
if err := json.Unmarshal(req.Options, &opts); err != nil {
|
if err := json.Unmarshal(req.Options, &opts); err != nil {
|
||||||
@@ -223,7 +269,6 @@ func (s *Server) RemoveContainer(ctx context.Context, req *pb.RemoveContainerReq
|
|||||||
func (s *Server) PullImage(req *pb.PullImageRequest, stream grpc.ServerStreamingServer[pb.JSONMessage]) error {
|
func (s *Server) PullImage(req *pb.PullImageRequest, stream grpc.ServerStreamingServer[pb.JSONMessage]) error {
|
||||||
ctx := stream.Context()
|
ctx := stream.Context()
|
||||||
|
|
||||||
// TODO: replace with another JSON serializable type (PullOptions.PrivilegeFunc is not serializable).
|
|
||||||
var opts image.PullOptions
|
var opts image.PullOptions
|
||||||
if len(req.Options) > 0 {
|
if len(req.Options) > 0 {
|
||||||
if err := json.Unmarshal(req.Options, &opts); err != nil {
|
if err := json.Unmarshal(req.Options, &opts); err != nil {
|
||||||
@@ -231,6 +276,14 @@ func (s *Server) PullImage(req *pb.PullImageRequest, stream grpc.ServerStreaming
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if opts.RegistryAuth == "" {
|
||||||
|
// Try to retrieve the authentication token for the image from the default local Docker config file.
|
||||||
|
dockerConfig := dockerconfig.LoadDefaultConfigFile(os.Stderr)
|
||||||
|
if encodedAuth, err := dockercommand.RetrieveAuthTokenFromImage(dockerConfig, req.Image); err == nil {
|
||||||
|
opts.RegistryAuth = encodedAuth
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
respBody, err := s.client.ImagePull(ctx, req.Image, opts)
|
respBody, err := s.client.ImagePull(ctx, req.Image, opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return status.Errorf(codes.Internal, err.Error())
|
return status.Errorf(codes.Internal, err.Error())
|
||||||
@@ -339,6 +392,12 @@ func (s *Server) CreateVolume(ctx context.Context, req *pb.CreateVolumeRequest)
|
|||||||
return nil, status.Errorf(codes.InvalidArgument, "unmarshal options: %v", err)
|
return nil, status.Errorf(codes.InvalidArgument, "unmarshal options: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Always add the managed label to the volume to indicate that it is managed by Uncloud.
|
||||||
|
if opts.Labels == nil {
|
||||||
|
opts.Labels = make(map[string]string)
|
||||||
|
}
|
||||||
|
opts.Labels[api.LabelManaged] = ""
|
||||||
|
|
||||||
vol, err := s.client.VolumeCreate(ctx, opts)
|
vol, err := s.client.VolumeCreate(ctx, opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, status.Error(codes.Internal, err.Error())
|
return nil, status.Error(codes.Internal, err.Error())
|
||||||
@@ -407,6 +466,7 @@ func (s *Server) RemoveVolume(ctx context.Context, req *pb.RemoveVolumeRequest)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CreateServiceContainer creates a new container for the service with the given specifications.
|
// CreateServiceContainer creates a new container for the service with the given specifications.
|
||||||
|
// TODO: move the main logic to the Docker service and remove db dependency from the server.
|
||||||
func (s *Server) CreateServiceContainer(
|
func (s *Server) CreateServiceContainer(
|
||||||
ctx context.Context, req *pb.CreateServiceContainerRequest,
|
ctx context.Context, req *pb.CreateServiceContainerRequest,
|
||||||
) (*pb.CreateContainerResponse, error) {
|
) (*pb.CreateContainerResponse, error) {
|
||||||
@@ -498,13 +558,23 @@ func (s *Server) CreateServiceContainer(
|
|||||||
Memory: spec.Container.Resources.Memory,
|
Memory: spec.Container.Resources.Memory,
|
||||||
MemoryReservation: spec.Container.Resources.MemoryReservation,
|
MemoryReservation: spec.Container.Resources.MemoryReservation,
|
||||||
},
|
},
|
||||||
// Always restart service containers if they exit or a machine restarts.
|
// Restart service containers if they exit or a machine restarts unless they are explicitly stopped.
|
||||||
// For one-off containers and batch jobs we plan to use a different service type/mode.
|
// For one-off containers and batch jobs we plan to use a different service type/mode.
|
||||||
RestartPolicy: container.RestartPolicy{
|
RestartPolicy: container.RestartPolicy{
|
||||||
Name: container.RestartPolicyAlways,
|
Name: container.RestartPolicyUnlessStopped,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Configure the container to use the internal DNS server if it's available.
|
||||||
|
dnsIP := s.internalDNSIP()
|
||||||
|
if dnsIP.IsValid() {
|
||||||
|
hostConfig.DNS = []string{dnsIP.String()}
|
||||||
|
// Optimize DNS resolution for service discovery by appending the search domain to names without a dot.
|
||||||
|
// For example, the first attempt for "my-service" will be "my-service.internal".
|
||||||
|
hostConfig.DNSOptions = []string{"ndots:1"}
|
||||||
|
hostConfig.DNSSearch = []string{dns.InternalDomain}
|
||||||
|
}
|
||||||
|
|
||||||
if spec.Container.LogDriver != nil {
|
if spec.Container.LogDriver != nil {
|
||||||
hostConfig.LogConfig = container.LogConfig{
|
hostConfig.LogConfig = container.LogConfig{
|
||||||
Type: spec.Container.LogDriver.Name,
|
Type: spec.Container.LogDriver.Name,
|
||||||
@@ -651,7 +721,7 @@ func (s *Server) verifyDockerVolumesExist(ctx context.Context, mounts []mount.Mo
|
|||||||
func (s *Server) InspectServiceContainer(
|
func (s *Server) InspectServiceContainer(
|
||||||
ctx context.Context, req *pb.InspectContainerRequest,
|
ctx context.Context, req *pb.InspectContainerRequest,
|
||||||
) (*pb.ServiceContainer, error) {
|
) (*pb.ServiceContainer, error) {
|
||||||
ctr, err := s.client.ContainerInspect(ctx, req.Id)
|
serviceCtr, err := s.service.InspectServiceContainer(ctx, req.Id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if client.IsErrNotFound(err) {
|
if client.IsErrNotFound(err) {
|
||||||
return nil, status.Errorf(codes.NotFound, err.Error())
|
return nil, status.Errorf(codes.NotFound, err.Error())
|
||||||
@@ -659,19 +729,14 @@ func (s *Server) InspectServiceContainer(
|
|||||||
return nil, status.Errorf(codes.Internal, err.Error())
|
return nil, status.Errorf(codes.Internal, err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
ctrBytes, err := json.Marshal(ctr)
|
ctrBytes, err := json.Marshal(serviceCtr.Container)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, status.Errorf(codes.Internal, "marshal response: %v", err)
|
return nil, status.Errorf(codes.Internal, "marshal container: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var specBytes []byte
|
specBytes, err := json.Marshal(serviceCtr.ServiceSpec)
|
||||||
err = s.db.QueryRowContext(ctx, `SELECT service_spec FROM containers WHERE id = $1`, ctr.ID).Scan(&specBytes)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, sql.ErrNoRows) {
|
return nil, status.Errorf(codes.Internal, "marshal service spec: %v", err)
|
||||||
return nil, status.Errorf(codes.NotFound, "service spec not found for container: '%s'", ctr.ID)
|
|
||||||
}
|
|
||||||
return nil, status.Errorf(codes.Internal, "get service spec for container '%s' from machine database: %v",
|
|
||||||
ctr.ID, err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return &pb.ServiceContainer{
|
return &pb.ServiceContainer{
|
||||||
@@ -703,54 +768,28 @@ func (s *Server) ListServiceContainers(
|
|||||||
return nil, status.Errorf(codes.InvalidArgument, "unmarshal filters: %v", err)
|
return nil, status.Errorf(codes.InvalidArgument, "unmarshal filters: %v", err)
|
||||||
}
|
}
|
||||||
opts.Filters = args
|
opts.Filters = args
|
||||||
} else {
|
|
||||||
opts.Filters = filters.NewArgs()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Only uncloud-managed containers that belong to some service.
|
|
||||||
opts.Filters.Add("label", api.LabelServiceID)
|
|
||||||
opts.Filters.Add("label", api.LabelManaged)
|
|
||||||
|
|
||||||
containerSummaries, err := s.client.ContainerList(ctx, opts)
|
containers, err := s.service.ListServiceContainers(ctx, req.ServiceId, opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, status.Error(codes.Internal, err.Error())
|
return nil, status.Error(codes.Internal, err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
containers := make([]*pb.ServiceContainer, 0, len(containerSummaries))
|
// Convert to protobuf format.
|
||||||
for _, cs := range containerSummaries {
|
pbContainers := make([]*pb.ServiceContainer, 0, len(containers))
|
||||||
if req.ServiceId != "" &&
|
for _, ctr := range containers {
|
||||||
cs.Labels[api.LabelServiceID] != req.ServiceId && cs.Labels[api.LabelServiceName] != req.ServiceId {
|
ctrBytes, err := json.Marshal(ctr.Container)
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
ctr, err := s.client.ContainerInspect(ctx, cs.ID)
|
|
||||||
if err != nil {
|
|
||||||
if client.IsErrNotFound(err) {
|
|
||||||
// The listed container may have been removed while we were inspecting other containers.
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
return nil, status.Errorf(codes.Internal, "inspect container %s: %v", cs.ID, err)
|
|
||||||
}
|
|
||||||
ctrBytes, err := json.Marshal(ctr)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, status.Errorf(codes.Internal, "marshal container: %v", err)
|
return nil, status.Errorf(codes.Internal, "marshal container: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var specBytes []byte
|
specBytes, err := json.Marshal(ctr.ServiceSpec)
|
||||||
err = s.db.QueryRowContext(ctx, `SELECT service_spec FROM containers WHERE id = $1`, ctr.ID).Scan(&specBytes)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, sql.ErrNoRows) {
|
return nil, status.Errorf(codes.Internal, "marshal service spec: %v", err)
|
||||||
// If this happens, there is a bug in the code, or someone manually removed the container from the DB,
|
|
||||||
// or created a managed container out of band.
|
|
||||||
slog.Error("Service container not found in machine database.", "id", ctr.ID)
|
|
||||||
// Just ignore such a container to not fail the list operation as it's not easily recoverable.
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
return nil, status.Errorf(codes.Internal, "get service spec for container '%s' from machine database: %v",
|
|
||||||
ctr.ID, err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
containers = append(containers, &pb.ServiceContainer{
|
pbContainers = append(pbContainers, &pb.ServiceContainer{
|
||||||
Container: ctrBytes,
|
Container: ctrBytes,
|
||||||
ServiceSpec: specBytes,
|
ServiceSpec: specBytes,
|
||||||
})
|
})
|
||||||
@@ -759,7 +798,7 @@ func (s *Server) ListServiceContainers(
|
|||||||
return &pb.ListServiceContainersResponse{
|
return &pb.ListServiceContainersResponse{
|
||||||
Messages: []*pb.MachineServiceContainers{
|
Messages: []*pb.MachineServiceContainers{
|
||||||
{
|
{
|
||||||
Containers: containers,
|
Containers: pbContainers,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}, nil
|
}, nil
|
||||||
|
|||||||
@@ -0,0 +1,103 @@
|
|||||||
|
package docker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
|
||||||
|
"github.com/docker/docker/api/types/container"
|
||||||
|
"github.com/docker/docker/api/types/filters"
|
||||||
|
"github.com/docker/docker/client"
|
||||||
|
"github.com/jmoiron/sqlx"
|
||||||
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Service provides higher-level Docker operations that extends Docker API with Uncloud-specific data
|
||||||
|
// from the machine database.
|
||||||
|
type Service struct {
|
||||||
|
Client *client.Client
|
||||||
|
db *sqlx.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewService creates a new Docker service instance.
|
||||||
|
func NewService(client *client.Client, db *sqlx.DB) *Service {
|
||||||
|
return &Service{
|
||||||
|
Client: client,
|
||||||
|
db: db,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// InspectServiceContainer inspects a Docker container and retrieves its associated ServiceSpec
|
||||||
|
// from the machine database, returning a complete ServiceContainer.
|
||||||
|
func (s *Service) InspectServiceContainer(ctx context.Context, nameOrID string) (api.ServiceContainer, error) {
|
||||||
|
var serviceCtr api.ServiceContainer
|
||||||
|
|
||||||
|
ctr, err := s.Client.ContainerInspect(ctx, nameOrID)
|
||||||
|
if err != nil {
|
||||||
|
return serviceCtr, err
|
||||||
|
}
|
||||||
|
if _, ok := ctr.Config.Labels[api.LabelManaged]; !ok {
|
||||||
|
return serviceCtr, fmt.Errorf("container '%s' is not managed by Uncloud", nameOrID)
|
||||||
|
}
|
||||||
|
|
||||||
|
serviceCtr.Container = api.Container{ContainerJSON: ctr}
|
||||||
|
|
||||||
|
// Retrieve ServiceSpec from the machine database.
|
||||||
|
var specBytes []byte
|
||||||
|
err = s.db.QueryRowContext(ctx, `SELECT service_spec FROM containers WHERE id = $1`, ctr.ID).Scan(&specBytes)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
|
// If this happens, there is a bug in the code, or someone manually removed the container from the DB,
|
||||||
|
// or created a managed container out of band or by previous uncloud installation.
|
||||||
|
return serviceCtr, fmt.Errorf("service spec not found for container '%s' in machine DB", ctr.ID)
|
||||||
|
}
|
||||||
|
return serviceCtr, fmt.Errorf("get service spec for container '%s' from machine DB: %w", ctr.ID, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = json.Unmarshal(specBytes, &serviceCtr.ServiceSpec); err != nil {
|
||||||
|
return serviceCtr, fmt.Errorf("unmarshal service spec for container '%s': %w", ctr.ID, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return serviceCtr, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListServiceContainers lists Docker containers that belong to the service with the given name or ID.
|
||||||
|
// If serviceIDOrName is empty, all service containers are returned. The opts parameter allows additional filtering.
|
||||||
|
func (s *Service) ListServiceContainers(
|
||||||
|
ctx context.Context, serviceNameOrID string, opts container.ListOptions,
|
||||||
|
) ([]api.ServiceContainer, error) {
|
||||||
|
if opts.Filters.Len() == 0 {
|
||||||
|
opts.Filters = filters.NewArgs()
|
||||||
|
}
|
||||||
|
// Add labels to existing filters to list only Uncloud-managed service containers.
|
||||||
|
opts.Filters.Add("label", api.LabelServiceID)
|
||||||
|
opts.Filters.Add("label", api.LabelManaged)
|
||||||
|
|
||||||
|
containerSummaries, err := s.Client.ContainerList(ctx, opts)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var containers []api.ServiceContainer
|
||||||
|
for _, cs := range containerSummaries {
|
||||||
|
// Filter by service name or ID if provided.
|
||||||
|
if serviceNameOrID != "" &&
|
||||||
|
cs.Labels[api.LabelServiceID] != serviceNameOrID &&
|
||||||
|
cs.Labels[api.LabelServiceName] != serviceNameOrID {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
ctr, err := s.InspectServiceContainer(ctx, cs.ID)
|
||||||
|
if err != nil {
|
||||||
|
// Log error but continue with other containers.
|
||||||
|
slog.Error("Failed to inspect service container.", "service", serviceNameOrID, "id", cs.ID, "err", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
containers = append(containers, ctr)
|
||||||
|
}
|
||||||
|
|
||||||
|
return containers, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package firewall
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
// ConfigureIptablesChains is a stub for Darwin.
|
||||||
|
func ConfigureIptablesChains() error {
|
||||||
|
return fmt.Errorf("not supported on Darwin")
|
||||||
|
}
|
||||||
|
|
||||||
|
// CleanupIptablesChains is a stub for Darwin.
|
||||||
|
func CleanupIptablesChains() error {
|
||||||
|
return fmt.Errorf("not supported on Darwin")
|
||||||
|
}
|
||||||
@@ -0,0 +1,163 @@
|
|||||||
|
package firewall
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/docker/docker/libnetwork/iptables"
|
||||||
|
"github.com/psviderski/uncloud/internal/machine/constants"
|
||||||
|
"github.com/psviderski/uncloud/internal/machine/corroservice"
|
||||||
|
"github.com/psviderski/uncloud/internal/machine/network"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
DockerUserChain = "DOCKER-USER"
|
||||||
|
UncloudInputChain = "UNCLOUD-INPUT"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ConfigureIptablesChains sets up custom iptables chains and initial firewall rules for Uncloud networking.
|
||||||
|
func ConfigureIptablesChains() error {
|
||||||
|
if err := createIptablesChains(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
ipt4 := iptables.GetIptable(iptables.IPv4)
|
||||||
|
ipt6 := iptables.GetIptable(iptables.IPv6)
|
||||||
|
|
||||||
|
// Allow WireGuard traffic to the machine.
|
||||||
|
acceptWireGuardRule := []string{"-p", "udp", "--dport", strconv.Itoa(network.WireGuardPort), "-j", "ACCEPT"}
|
||||||
|
err := ipt4.ProgramRule(iptables.Filter, UncloudInputChain, iptables.Insert, acceptWireGuardRule)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("insert iptables rule '%s': %w", strings.Join(acceptWireGuardRule, " "), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow cluster machines to access Machine API via the management IPv6 WireGuard network.
|
||||||
|
acceptMachineAPIRule := []string{
|
||||||
|
"-i", network.WireGuardInterfaceName,
|
||||||
|
"-s", "fdcc::/16",
|
||||||
|
"-p", "tcp",
|
||||||
|
"--dport", strconv.Itoa(constants.MachineAPIPort),
|
||||||
|
"-j", "ACCEPT",
|
||||||
|
}
|
||||||
|
// Allow Corrosion gossip traffic from cluster machines via the management IPv6 WireGuard network.
|
||||||
|
acceptCorrosionGossipRule := []string{
|
||||||
|
"-i", network.WireGuardInterfaceName,
|
||||||
|
"-s", "fdcc::/16",
|
||||||
|
"-p", "udp",
|
||||||
|
"--dport", strconv.Itoa(corroservice.DefaultGossipPort),
|
||||||
|
"-j", "ACCEPT",
|
||||||
|
}
|
||||||
|
for _, rule := range [][]string{acceptMachineAPIRule, acceptCorrosionGossipRule} {
|
||||||
|
if err = ipt6.ProgramRule(iptables.Filter, UncloudInputChain, iptables.Insert, rule); err != nil {
|
||||||
|
return fmt.Errorf("insert ip6tables rule '%s': %w", strings.Join(rule, " "), err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// createIptablesChains ensures UNCLOUD-INPUT iptables and ip6tables chains exist and
|
||||||
|
// there are jump rules from the main INPUT chains.
|
||||||
|
func createIptablesChains() error {
|
||||||
|
ipt4 := iptables.GetIptable(iptables.IPv4)
|
||||||
|
ipt6 := iptables.GetIptable(iptables.IPv6)
|
||||||
|
|
||||||
|
for i, ipt := range []*iptables.IPTable{ipt4, ipt6} {
|
||||||
|
iptBin := "iptables"
|
||||||
|
if i == 1 {
|
||||||
|
iptBin = "ip6tables"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure UNCLOUD-INPUT chain exists. All existing rules are flushed.
|
||||||
|
if _, err := ipt.NewChain(UncloudInputChain, iptables.Filter); err != nil {
|
||||||
|
return fmt.Errorf("create %s chain '%s': %w", iptBin, UncloudInputChain, err)
|
||||||
|
}
|
||||||
|
if err := ipt.RawCombinedOutput("-t", string(iptables.Filter), "-F", UncloudInputChain); err != nil {
|
||||||
|
return fmt.Errorf("flush %s chain '%s': %w", iptBin, UncloudInputChain, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure the main INPUT chain has a jump rule to the UNCLOUD-INPUT chain before any DROP/REJECT rules.
|
||||||
|
jumpRule := []string{"-m", "comment", "--comment", "Uncloud-managed", "-j", UncloudInputChain}
|
||||||
|
if !ipt.Exists(iptables.Filter, "INPUT", jumpRule...) {
|
||||||
|
// Look for the first DROP/REJECT rule in the INPUT chain.
|
||||||
|
out, err := ipt.Raw("-t", string(iptables.Filter), "-L", "INPUT", "--line-numbers")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("get %s rules for chain '%s': %w", iptBin, UncloudInputChain, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
firstRejectRuleNum := 0
|
||||||
|
for _, line := range strings.Split(string(out), "\n") {
|
||||||
|
fields := strings.Fields(line)
|
||||||
|
if len(fields) < 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if fields[1] == "DROP" || fields[1] == "REJECT" {
|
||||||
|
if ruleNum, err := strconv.Atoi(fields[0]); err == nil {
|
||||||
|
firstRejectRuleNum = ruleNum
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var addJumpRule []string
|
||||||
|
if firstRejectRuleNum > 0 {
|
||||||
|
addJumpRule = append([]string{
|
||||||
|
"-t", string(iptables.Filter),
|
||||||
|
"-I", "INPUT",
|
||||||
|
strconv.Itoa(firstRejectRuleNum),
|
||||||
|
}, jumpRule...)
|
||||||
|
} else {
|
||||||
|
addJumpRule = append([]string{
|
||||||
|
"-t", string(iptables.Filter),
|
||||||
|
"-A", "INPUT",
|
||||||
|
}, jumpRule...)
|
||||||
|
}
|
||||||
|
if err = ipt.RawCombinedOutput(addJumpRule...); err != nil {
|
||||||
|
return fmt.Errorf("add %s rule '%s': %w", iptBin, strings.Join(addJumpRule, " "), err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CleanupIptablesChains removes the custom iptables chains and rules created by ConfigureIptablesChains.
|
||||||
|
func CleanupIptablesChains() error {
|
||||||
|
ipt4 := iptables.GetIptable(iptables.IPv4)
|
||||||
|
ipt6 := iptables.GetIptable(iptables.IPv6)
|
||||||
|
|
||||||
|
for i, ipt := range []*iptables.IPTable{ipt4, ipt6} {
|
||||||
|
iptBin := "iptables"
|
||||||
|
if i == 1 {
|
||||||
|
iptBin = "ip6tables"
|
||||||
|
}
|
||||||
|
|
||||||
|
// First, remove the jump rule from INPUT chain to UNCLOUD-INPUT.
|
||||||
|
jumpRule := []string{"-m", "comment", "--comment", "Uncloud-managed", "-j", UncloudInputChain}
|
||||||
|
if err := ipt.ProgramRule(iptables.Filter, "INPUT", iptables.Delete, jumpRule); err != nil {
|
||||||
|
return fmt.Errorf("delete %s jump rule from INPUT: %w", iptBin, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flush all rules from UNCLOUD-INPUT chain as it must be empty before deletion.
|
||||||
|
if err := ipt.RawCombinedOutput("-t", string(iptables.Filter), "-F", UncloudInputChain); err != nil {
|
||||||
|
// Chain might not exist which is fine.
|
||||||
|
if !strings.Contains(err.Error(), "No chain") {
|
||||||
|
return fmt.Errorf("flush %s chain '%s': %w", iptBin, UncloudInputChain, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete the UNCLOUD-INPUT chain.
|
||||||
|
if err := ipt.RawCombinedOutput("-t", string(iptables.Filter), "-X", UncloudInputChain); err != nil {
|
||||||
|
// Chain might not exist which is fine.
|
||||||
|
if !strings.Contains(err.Error(), "No chain") {
|
||||||
|
return fmt.Errorf("delete %s chain '%s': %w", iptBin, UncloudInputChain, err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
slog.Info(fmt.Sprintf("Deleted %s chain.", iptBin), "chain", UncloudInputChain)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user