mirror of
https://github.com/psviderski/uncloud.git
synced 2026-08-27 03:23:34 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0e80f2e5b2 | ||
|
|
64f6a4f3d3 | ||
|
|
0975cbab66 | ||
|
|
615022c9ac | ||
|
|
ecbd4378e4 | ||
|
|
1becd033e6 | ||
|
|
1cba1eef0a | ||
|
|
3893005866 | ||
|
|
1911e03c82 | ||
|
|
641b11a92d | ||
|
|
7c7530f977 | ||
|
|
6e7fe405ef | ||
|
|
03c5c054e1 | ||
|
|
c42968c812 | ||
|
|
ffef41081b | ||
|
|
58b51b974c | ||
|
|
2d923380a3 | ||
|
|
3910101a88 | ||
|
|
7a9d0cf9db | ||
|
|
042dd594e0 | ||
|
|
1471a94162 | ||
|
|
b1abd07dde | ||
|
|
3757813b20 | ||
|
|
f1a150821c | ||
|
|
874af1ba10 | ||
|
|
2ad098c612 | ||
|
|
4172cb826f | ||
|
|
041d51b74f | ||
|
|
fb01604d21 | ||
|
|
bd8bcbd2f9 | ||
|
|
07501813c8 | ||
|
|
1bbcfa8519 | ||
|
|
e8715fce38 | ||
|
|
a0a66f34c4 | ||
|
|
b799b7518e | ||
|
|
2571498ed2 | ||
|
|
5bcbfc332d | ||
|
|
34574e41fc | ||
|
|
c7355afa3d | ||
|
|
a907fdb1af | ||
|
|
c9ba4adccb | ||
|
|
a0551d2d64 | ||
|
|
76129ea056 | ||
|
|
6ffd35f596 | ||
|
|
0ef776e7f1 | ||
|
|
0b22b2136a | ||
|
|
e1530bb6f3 | ||
|
|
7fc07ce4bd | ||
|
|
eb42082955 | ||
|
|
8719e37856 | ||
|
|
e1cc39b0d5 | ||
|
|
efe646f54c | ||
|
|
e130d803e4 | ||
|
|
56e7e8baa2 | ||
|
|
b963ef39d1 | ||
|
|
1df0fa290f | ||
|
|
c3bb0c9f49 | ||
|
|
cf65cecd9e | ||
|
|
fff17dbb60 | ||
|
|
586a4fa129 | ||
|
|
2a82f75cad | ||
|
|
805afd0d28 | ||
|
|
11098ea7d1 | ||
|
|
90e357a1dc | ||
|
|
91d58bc701 | ||
|
|
69d3a511d6 | ||
|
|
c3123820b2 | ||
|
|
2e49d94fad | ||
|
|
bb51e01c5c | ||
|
|
cdb2884e4e | ||
|
|
137abde808 | ||
|
|
342969b7f4 | ||
|
|
d9e350c4dd | ||
|
|
3f4f263c05 | ||
|
|
0760ba3b18 | ||
|
|
0896bd27fb | ||
|
|
a16b2d1c74 | ||
|
|
52117954a8 | ||
|
|
435a794fa5 | ||
|
|
9642863d7b | ||
|
|
6bb9bdeb5a | ||
|
|
30f1e135f1 | ||
|
|
58ecc17ac1 | ||
|
|
4c239ed79a | ||
|
|
968dd50705 | ||
|
|
d7f437107d | ||
|
|
72548fbd1b | ||
|
|
a5f6ec4a68 | ||
|
|
4cc68e3133 | ||
|
|
2bacebe900 | ||
|
|
09bea1d12d | ||
|
|
5e9b883ad5 | ||
|
|
1046396edc | ||
|
|
35804458b0 | ||
|
|
6bb64ac5dc | ||
|
|
e5f68947f9 | ||
|
|
289d964b2a | ||
|
|
d0fd9db258 | ||
|
|
0ea997e826 | ||
|
|
2d84059d7f | ||
|
|
ecec42ea60 | ||
|
|
af4629b679 | ||
|
|
f6cf5cc250 | ||
|
|
f37bfd01fe | ||
|
|
d9897e5d1e | ||
|
|
bcb67a4073 | ||
|
|
bcb5a2a173 | ||
|
|
c3b88077ac | ||
|
|
98e1bdc9b5 | ||
|
|
e13408d79f | ||
|
|
8610322682 | ||
|
|
0be42f1f59 | ||
|
|
dcbfcbd0fb | ||
|
|
d0e46186ed | ||
|
|
625a8baed3 | ||
|
|
98db5ed440 |
@@ -4,6 +4,7 @@
|
|||||||
# Allow files and directories.
|
# Allow files and directories.
|
||||||
!cmd/
|
!cmd/
|
||||||
!internal/
|
!internal/
|
||||||
|
!pkg/
|
||||||
!scripts/docker/
|
!scripts/docker/
|
||||||
!go.*
|
!go.*
|
||||||
|
|
||||||
|
|||||||
@@ -32,3 +32,6 @@ dist/
|
|||||||
|
|
||||||
# Web
|
# Web
|
||||||
node_modules/
|
node_modules/
|
||||||
|
|
||||||
|
# VS Code
|
||||||
|
.vscode
|
||||||
|
|||||||
@@ -8,6 +8,10 @@ update-dev:
|
|||||||
scp uncloudd-linux-amd64 spy@192.168.40.176:~/ && \
|
scp uncloudd-linux-amd64 spy@192.168.40.176:~/ && \
|
||||||
ssh spy@192.168.40.176 sudo install ./uncloudd-linux-amd64 /usr/local/bin/uncloudd && \
|
ssh spy@192.168.40.176 sudo install ./uncloudd-linux-amd64 /usr/local/bin/uncloudd && \
|
||||||
rm uncloudd-linux-amd64
|
rm uncloudd-linux-amd64
|
||||||
|
# GOOS=linux GOARCH=arm64 go build -o uncloudd-linux-arm64 ./cmd/uncloudd && \
|
||||||
|
# scp uncloudd-linux-arm64 ubuntu@152.67.101.197:~/ && \
|
||||||
|
# ssh ubuntu@152.67.101.197 sudo install ./uncloudd-linux-arm64 /usr/local/bin/uncloudd && \
|
||||||
|
# rm uncloudd-linux-arm64
|
||||||
|
|
||||||
update-restart-dev:
|
update-restart-dev:
|
||||||
GOOS=linux GOARCH=amd64 go build -o uncloudd-linux-amd64 ./cmd/uncloudd && \
|
GOOS=linux GOARCH=amd64 go build -o uncloudd-linux-amd64 ./cmd/uncloudd && \
|
||||||
@@ -16,12 +20,22 @@ update-restart-dev:
|
|||||||
scp uncloudd-linux-amd64 spy@192.168.40.176:~/ && \
|
scp uncloudd-linux-amd64 spy@192.168.40.176:~/ && \
|
||||||
ssh spy@192.168.40.176 "sudo install ./uncloudd-linux-amd64 /usr/local/bin/uncloudd && sudo systemctl restart uncloud" && \
|
ssh spy@192.168.40.176 "sudo install ./uncloudd-linux-amd64 /usr/local/bin/uncloudd && sudo systemctl restart uncloud" && \
|
||||||
rm uncloudd-linux-amd64
|
rm uncloudd-linux-amd64
|
||||||
|
# GOOS=linux GOARCH=arm64 go build -o uncloudd-linux-arm64 ./cmd/uncloudd && \
|
||||||
|
# scp uncloudd-linux-arm64 ubuntu@152.67.101.197:~/ && \
|
||||||
|
# ssh ubuntu@152.67.101.197 "sudo install ./uncloudd-linux-arm64 /usr/local/bin/uncloudd && sudo systemctl restart uncloud" && \
|
||||||
|
# rm uncloudd-linux-arm64
|
||||||
|
|
||||||
reset-dev:
|
reset-dev:
|
||||||
ssh spy@192.168.40.243 "sudo systemctl stop uncloud && sudo rm -rf /var/lib/uncloud"
|
ssh spy@192.168.40.243 "sudo systemctl stop uncloud && sudo rm -rf /var/lib/uncloud"
|
||||||
ssh spy@192.168.40.176 "sudo systemctl stop uncloud && sudo rm -rf /var/lib/uncloud"
|
ssh spy@192.168.40.176 "sudo systemctl stop uncloud && sudo rm -rf /var/lib/uncloud"
|
||||||
ssh ubuntu@152.67.101.197 "sudo systemctl stop uncloud && sudo rm -rf /var/lib/uncloud"
|
ssh ubuntu@152.67.101.197 "sudo systemctl stop uncloud && sudo rm -rf /var/lib/uncloud"
|
||||||
|
|
||||||
|
demo-reset:
|
||||||
|
rm -fv ~/.config/uncloud/config.toml
|
||||||
|
ssh ubuntu@152.67.101.197 "AUTO_CONFIRM=true sudo -E uncloud-uninstall && docker rmi caddy:2.9.1"
|
||||||
|
ssh root@5.223.45.199 "AUTO_CONFIRM=true sudo -E uncloud-uninstall"
|
||||||
|
ssh spy@192.168.40.243 "AUTO_CONFIRM=true sudo -E uncloud-uninstall"
|
||||||
|
|
||||||
.PHONY: 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 \
|
||||||
|
|||||||
@@ -16,6 +16,14 @@ some machines go offline.
|
|||||||
Uncloud aims to be the solution for developers who want the flexibility of self-hosted infrastructure without the
|
Uncloud aims to be the solution for developers who want the flexibility of self-hosted infrastructure without the
|
||||||
operational complexity of Kubernetes.
|
operational 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 —
|
||||||
@@ -25,7 +33,10 @@ operational complexity of Kubernetes.
|
|||||||
* **No control plane**: Fully decentralized design eliminates single points of failure and reduces operational overhead.
|
* **No control plane**: Fully decentralized 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.
|
* **Service discovery**: Built-in DNS server resolves service names to container IPs.
|
||||||
|
* **Managed DNS**: Automatic DNS records `*.<id>.cluster.uncloud.run` for services with public access via managed
|
||||||
|
[Uncloud DNS](https://github.com/psviderski/uncloud-dns) service.
|
||||||
* **Automatic HTTPS**: Built-in Caddy reverse proxy handles TLS certificate provisioning and renewal using Let's
|
* **Automatic HTTPS**: Built-in Caddy reverse proxy handles TLS certificate provisioning and renewal using Let's
|
||||||
Encrypt.
|
Encrypt.
|
||||||
* **Docker-like CLI**: Familiar commands for managing both infrastructure and applications.
|
* **Docker-like CLI**: Familiar commands for managing both infrastructure and applications.
|
||||||
@@ -33,12 +44,11 @@ operational complexity of Kubernetes.
|
|||||||
|
|
||||||
Coming soon:
|
Coming soon:
|
||||||
|
|
||||||
* Project isolation through environments/namespaces
|
|
||||||
* Infrastructure as Code using Docker Compose format
|
* Infrastructure as Code using Docker Compose format
|
||||||
|
* Project isolation through environments/namespaces
|
||||||
* Persistent volumes and secrets management
|
* Persistent volumes and secrets management
|
||||||
* Monitoring and log aggregation
|
* Monitoring and log aggregation
|
||||||
* Database deployment and management
|
* Database deployment and management
|
||||||
* Curated application catalog
|
|
||||||
|
|
||||||
Here is a diagram of an Uncloud multi-provider cluster of 3 machines:
|
Here is a diagram of an Uncloud multi-provider cluster of 3 machines:
|
||||||
|
|
||||||
@@ -66,36 +76,45 @@ platform, whether you're running on a $5 VPS, a spare Mac mini, or a rack of bar
|
|||||||
|
|
||||||
1. Install Uncloud CLI:
|
1. Install Uncloud CLI:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
brew install psviderski/tap/uncloud
|
brew install psviderski/tap/uncloud
|
||||||
|
|
||||||
# or using curl (macOS/Linux)
|
# or using curl (macOS/Linux)
|
||||||
curl -fsS https://get.uncloud.run/install.sh | sh
|
curl -fsS https://get.uncloud.run/install.sh | sh
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Initialize your first machine:
|
2. Initialize your first machine:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
uc machine init root@your-server-ip
|
uc machine init root@your-server-ip
|
||||||
```
|
```
|
||||||
|
|
||||||
3. Create a DNS A record in your DNS provider (Cloudflare, Namecheap, etc.) that points `app.example.com` to your
|
3. Deploy your app from a Docker image and publish its container port 8000 as HTTPS using `app.example.com` domain:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uc run -p app.example.com:8000/https image/my-app
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Create a DNS A record in your DNS provider (Cloudflare, Namecheap, etc.) that points `app.example.com` to your
|
||||||
server's IP address. Allow a few minutes for DNS propagation.
|
server's IP address. Allow a few minutes for DNS propagation.
|
||||||
4. Deploy your app from a Docker image:
|
|
||||||
|
|
||||||
```bash
|
That's it! Your app is now running and accessible at https://app.example.com ✨
|
||||||
uc run -p app.example.com:8000/https my-app-image
|
|
||||||
```
|
|
||||||
|
|
||||||
That's it! Your app is now running and accessible at https://app.example.com ✨
|
|
||||||
|
|
||||||
5. Clean up when you're done:
|
5. Clean up when you're done:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
uc ls
|
uc ls
|
||||||
# Copy the service ID from the output and remove it:
|
# Copy the service name from the output and run the rm command:
|
||||||
uc rm my-app-id
|
uc rm my-app-name
|
||||||
```
|
```
|
||||||
|
|
||||||
|
If you want to fully uninstall Uncloud on a machine, run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uncloud-uninstall
|
||||||
|
```
|
||||||
|
|
||||||
|
View the [user guide](docs/user_guide.md) for more information.
|
||||||
|
|
||||||
## ⚙️ How it works
|
## ⚙️ How it works
|
||||||
|
|
||||||
@@ -105,7 +124,42 @@ under the hood to see what happens when you run certain commands.
|
|||||||
**When you initialize a new cluster on a machine:**
|
**When you initialize a new cluster on a machine:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
uc machine init root@your-server-ip
|
$ uc machine init --name oracle-vm ubuntu@152.67.101.197
|
||||||
|
|
||||||
|
Downloading Uncloud install script: https://raw.githubusercontent.com/psviderski/uncloud/refs/heads/main/scripts/install.sh
|
||||||
|
⏳ Running Uncloud install script...
|
||||||
|
✓ Docker is already installed.
|
||||||
|
⏳ Installing Docker...
|
||||||
|
...
|
||||||
|
✓ Docker installed successfully.
|
||||||
|
✓ Linux user and group 'uncloud' created.
|
||||||
|
✓ Linux user 'ubuntu' added to group 'uncloud'.
|
||||||
|
⏳ Installing Uncloud binaries...
|
||||||
|
⏳ Downloading uncloudd binary: https://github.com/psviderski/uncloud/releases/latest/download/uncloudd_linux_arm64.tar.gz
|
||||||
|
✓ uncloudd binary installed: /usr/local/bin/uncloudd
|
||||||
|
⏳ Downloading uninstall script: https://raw.githubusercontent.com/psviderski/uncloud/refs/heads/main/scripts/uninstall.sh
|
||||||
|
✓ uncloud-uninstall script installed: /usr/local/bin/uncloud-uninstall
|
||||||
|
✓ Systemd unit file created: /etc/systemd/system/uncloud.service
|
||||||
|
Created symlink /etc/systemd/system/multi-user.target.wants/uncloud.service → /etc/systemd/system/uncloud.service.
|
||||||
|
⏳ Downloading uncloud-corrosion binary: https://github.com/psviderski/corrosion/releases/latest/download/corrosion-aarch64-unknown-linux-gnu.tar.gz
|
||||||
|
✓ uncloud-corrosion binary installed: /usr/local/bin/uncloud-corrosion
|
||||||
|
✓ Systemd unit file created: /etc/systemd/system/uncloud-corrosion.service
|
||||||
|
⏳ Starting Uncloud machine daemon (uncloud.service)...
|
||||||
|
✓ Uncloud machine daemon started.
|
||||||
|
✓ Uncloud installed on the machine successfully! 🎉
|
||||||
|
Cluster "default" initialised with machine "oracle-vm"
|
||||||
|
Waiting for the machine to be ready...
|
||||||
|
|
||||||
|
Reserved cluster domain: xuw3xd.cluster.uncloud.run
|
||||||
|
[+] Deploying service caddy 1/1
|
||||||
|
✔ Container caddy-c47x on oracle-vm Started 0.9s
|
||||||
|
|
||||||
|
Updating cluster domain records in Uncloud DNS to point to machines running caddy service...
|
||||||
|
[+] Verifying internet access to caddy service 1/1
|
||||||
|
✔ Machine oracle-vm (152.67.101.197) Reachable 0.1s
|
||||||
|
|
||||||
|
DNS records updated to use only the internet-reachable machines running caddy service:
|
||||||
|
*.xuw3xd.cluster.uncloud.run A → 152.67.101.197
|
||||||
```
|
```
|
||||||
|
|
||||||
1. The CLI SSHs into the machine and installs Docker, the `uncloudd` machine daemon and
|
1. The CLI SSHs into the machine and installs Docker, the `uncloudd` machine daemon and
|
||||||
@@ -121,7 +175,42 @@ uc machine init root@your-server-ip
|
|||||||
**When you add another machine:**
|
**When you add another machine:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
uc machine add ubuntu@second-server-ip
|
$ uc machine add --name hetzner-server root@5.223.45.199
|
||||||
|
Downloading Uncloud install script: https://raw.githubusercontent.com/psviderski/uncloud/refs/heads/main/scripts/install.sh
|
||||||
|
⏳ Running Uncloud install script...
|
||||||
|
✓ Docker is already installed.
|
||||||
|
✓ Linux user and group 'uncloud' created.
|
||||||
|
⏳ Installing Uncloud binaries...
|
||||||
|
⏳ Downloading uncloudd binary: https://github.com/psviderski/uncloud/releases/latest/download/uncloudd_linux_amd64.tar.gz
|
||||||
|
✓ uncloudd binary installed: /usr/local/bin/uncloudd
|
||||||
|
⏳ Downloading uninstall script: https://raw.githubusercontent.com/psviderski/uncloud/refs/heads/main/scripts/uninstall.sh
|
||||||
|
✓ uncloud-uninstall script installed: /usr/local/bin/uncloud-uninstall
|
||||||
|
✓ Systemd unit file created: /etc/systemd/system/uncloud.service
|
||||||
|
Created symlink /etc/systemd/system/multi-user.target.wants/uncloud.service → /etc/systemd/system/uncloud.service.
|
||||||
|
⏳ Downloading uncloud-corrosion binary: https://github.com/psviderski/corrosion/releases/latest/download/corrosion-x86_64-unknown-linux-gnu.tar.gz
|
||||||
|
✓ uncloud-corrosion binary installed: /usr/local/bin/uncloud-corrosion
|
||||||
|
✓ Systemd unit file created: /etc/systemd/system/uncloud-corrosion.service
|
||||||
|
⏳ Starting Uncloud machine daemon (uncloud.service)...
|
||||||
|
✓ Uncloud machine daemon started.
|
||||||
|
✓ Uncloud installed on the machine successfully! 🎉
|
||||||
|
Machine "hetzner-server" added to cluster
|
||||||
|
Waiting for the machine to be ready...
|
||||||
|
|
||||||
|
[+] Deploying service caddy 1/1
|
||||||
|
✔ Container caddy-d36c on hetzner-server Started 1.0s
|
||||||
|
|
||||||
|
Updating cluster domain records in Uncloud DNS to point to machines running caddy service...
|
||||||
|
[+] Verifying internet access to caddy service 2/2
|
||||||
|
✔ Machine hetzner-server (5.223.45.199) Reachable 0.2s
|
||||||
|
✔ Machine oracle-vm (152.67.101.197) Reachable 0.1s
|
||||||
|
|
||||||
|
DNS records updated to use only the internet-reachable machines running caddy service:
|
||||||
|
*.xuw3xd.cluster.uncloud.run A → 152.67.101.197, 5.223.45.199
|
||||||
|
|
||||||
|
$ uc machine ls
|
||||||
|
NAME STATE ADDRESS PUBLIC IP WIREGUARD ENDPOINTS
|
||||||
|
oracle-vm Up 10.210.0.1/24 152.67.101.197 10.0.0.95:51820, 152.67.101.197:51820
|
||||||
|
hetzner-server Up 10.210.1.1/24 5.223.45.199 5.223.45.199:51820, [2a01:4ff:2f0:128b::1]:51820
|
||||||
```
|
```
|
||||||
|
|
||||||
1. The second machine gets provisioned just like the first. A non-root SSH user will need `sudo` access.
|
1. The second machine gets provisioned just like the first. A non-root SSH user will need `sudo` access.
|
||||||
@@ -140,7 +229,14 @@ and automatically establish a WireGuard tunnel with it.
|
|||||||
**When you run a service:**
|
**When you run a service:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
uc run -p app.example.com:8000/https my-app-image
|
$ uc run -p app.example.com:8000/https image/my-app
|
||||||
|
|
||||||
|
[+] Running service my-app-1b3b (replicated mode) 1/1
|
||||||
|
✔ Container my-app-1b3b-tcex on oracle-vm Started
|
||||||
|
|
||||||
|
my-app-1b3b endpoints:
|
||||||
|
• https://app.example.com → :8000
|
||||||
|
• https://my-app-1b3b.xuw3xd.cluster.uncloud.run → :8000
|
||||||
```
|
```
|
||||||
|
|
||||||
1. CLI picks a machine to run your container.
|
1. CLI picks a machine to run your container.
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package cluster
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"uncloud/internal/ucind"
|
"github.com/psviderski/uncloud/internal/ucind"
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewCreateCommand() *cobra.Command {
|
func NewCreateCommand() *cobra.Command {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package cluster
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"uncloud/internal/ucind"
|
"github.com/psviderski/uncloud/internal/ucind"
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewRemoveCommand() *cobra.Command {
|
func NewRemoveCommand() *cobra.Command {
|
||||||
|
|||||||
+2
-2
@@ -7,8 +7,8 @@ import (
|
|||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"uncloud/cmd/ucind/cluster"
|
"github.com/psviderski/uncloud/cmd/ucind/cluster"
|
||||||
"uncloud/internal/ucind"
|
"github.com/psviderski/uncloud/internal/ucind"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|||||||
@@ -0,0 +1,225 @@
|
|||||||
|
package caddy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"github.com/docker/cli/cli/streams"
|
||||||
|
"github.com/docker/compose/v2/pkg/progress"
|
||||||
|
"github.com/psviderski/uncloud/internal/cli"
|
||||||
|
"github.com/psviderski/uncloud/internal/machine/api/pb"
|
||||||
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
|
"github.com/psviderski/uncloud/pkg/client"
|
||||||
|
"github.com/psviderski/uncloud/pkg/client/deploy"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"maps"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type deployOptions struct {
|
||||||
|
image string
|
||||||
|
machine string
|
||||||
|
cluster string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDeployCommand() *cobra.Command {
|
||||||
|
opts := deployOptions{}
|
||||||
|
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "deploy",
|
||||||
|
Short: "Deploy or upgrade Caddy reverse proxy across all machines in the cluster.",
|
||||||
|
Long: "Deploy or upgrade Caddy reverse proxy across all machines in the cluster.\n" +
|
||||||
|
"A rolling update is performed when updating existing containers to minimise disruption.",
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
uncli := cmd.Context().Value("cli").(*cli.CLI)
|
||||||
|
return runDeploy(cmd.Context(), uncli, opts)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Flags().StringVar(&opts.image, "image", "",
|
||||||
|
"Caddy Docker image to deploy. (default caddy:LATEST_VERSION)")
|
||||||
|
cmd.Flags().StringVarP(&opts.machine, "machine", "m", "",
|
||||||
|
"Machine names to deploy to (comma-separated). (default is all machines)")
|
||||||
|
cmd.Flags().StringVarP(
|
||||||
|
&opts.cluster, "cluster", "c", "",
|
||||||
|
"Name of the cluster to deploy to. (default is the current cluster)",
|
||||||
|
)
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func runDeploy(ctx context.Context, uncli *cli.CLI, opts deployOptions) error {
|
||||||
|
clusterClient, err := uncli.ConnectCluster(ctx, opts.cluster)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("connect to cluster: %w", err)
|
||||||
|
}
|
||||||
|
defer clusterClient.Close()
|
||||||
|
|
||||||
|
svc, err := clusterClient.InspectService(ctx, client.CaddyServiceName)
|
||||||
|
if err != nil {
|
||||||
|
if !errors.Is(err, api.ErrNotFound) {
|
||||||
|
return fmt.Errorf("inspect caddy service: %w", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("Service: %s (not running)\n", client.CaddyServiceName)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("Service: %s (%s mode)\n", svc.Name, svc.Mode)
|
||||||
|
|
||||||
|
// Collect unique images of all containers in the running caddy service.
|
||||||
|
images := make(map[string]struct{}, len(svc.Containers))
|
||||||
|
for _, c := range svc.Containers {
|
||||||
|
images[c.Container.Config.Image] = struct{}{}
|
||||||
|
}
|
||||||
|
currentImages := slices.Collect(maps.Keys(images))
|
||||||
|
|
||||||
|
if len(currentImages) > 1 {
|
||||||
|
commaSeparatedImages := strings.Join(currentImages, ", ")
|
||||||
|
fmt.Printf("Current images (multiple versions detected): %s\n", commaSeparatedImages)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("Current image: %s\n", currentImages[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if opts.image != "" {
|
||||||
|
fmt.Printf("Target image: %s\n", opts.image)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Println("Preparing a deployment plan...")
|
||||||
|
|
||||||
|
var filter deploy.MachineFilter
|
||||||
|
if opts.machine != "" {
|
||||||
|
machines := strings.Split(opts.machine, ",")
|
||||||
|
for i, m := range machines {
|
||||||
|
machines[i] = strings.TrimSpace(m)
|
||||||
|
}
|
||||||
|
filter = machineFilter(machines)
|
||||||
|
}
|
||||||
|
|
||||||
|
d, err := clusterClient.NewCaddyDeployment(opts.image, filter)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("create caddy deployment: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if opts.image == "" {
|
||||||
|
fmt.Printf("Target image: %s (latest stable)\n", d.Spec.Container.Image)
|
||||||
|
}
|
||||||
|
|
||||||
|
plan, err := d.Plan(ctx)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, deploy.ErrNoMatchingMachines) {
|
||||||
|
return fmt.Errorf("no machines found matching: %s", opts.machine)
|
||||||
|
}
|
||||||
|
return fmt.Errorf("plan caddy deployment: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(plan.Operations) == 0 {
|
||||||
|
if opts.machine != "" {
|
||||||
|
fmt.Printf("%s service is up to date on selected machines.\n", client.CaddyServiceName)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("%s service is up to date.\n", client.CaddyServiceName)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if svc.ID == "" {
|
||||||
|
if opts.machine != "" {
|
||||||
|
fmt.Println("This will run a Caddy container on selected machines.")
|
||||||
|
} else {
|
||||||
|
fmt.Println("This will run a Caddy container on each machine.")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if opts.machine != "" {
|
||||||
|
fmt.Println("This will perform a rolling update of Caddy containers on selected machines.")
|
||||||
|
} else {
|
||||||
|
fmt.Println("This will perform a rolling update of Caddy containers on each machine.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialise a machine and container name resolver to properly format the plan output.
|
||||||
|
resolver, err := clusterClient.ServiceOperationNameResolver(ctx, svc)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("create machine and container name resolver for service operations: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Println("Deployment plan:")
|
||||||
|
fmt.Println(plan.Format(resolver))
|
||||||
|
fmt.Println()
|
||||||
|
|
||||||
|
confirmed, err := cli.Confirm()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("confirm deployment: %w", err)
|
||||||
|
}
|
||||||
|
if !confirmed {
|
||||||
|
fmt.Println("Cancelled. No changes were made.")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
err = progress.RunWithTitle(ctx, func(ctx context.Context) error {
|
||||||
|
if _, err = d.Run(ctx); err != nil {
|
||||||
|
return fmt.Errorf("deploy caddy: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}, uncli.ProgressOut(), fmt.Sprintf("Deploying service %s (%s mode)", d.Spec.Name, d.Spec.Mode))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println()
|
||||||
|
return UpdateDomainRecords(ctx, clusterClient, uncli.ProgressOut())
|
||||||
|
}
|
||||||
|
|
||||||
|
func UpdateDomainRecords(ctx context.Context, clusterClient *client.Client, progressOut *streams.Out) error {
|
||||||
|
if _, err := clusterClient.GetDomain(ctx); err != nil {
|
||||||
|
if errors.Is(err, api.ErrNotFound) {
|
||||||
|
fmt.Println("Skipping DNS records update as no cluster domain is reserved (see 'uc dns').")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return fmt.Errorf("get cluster domain: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("Updating cluster domain records in Uncloud DNS to point to machines running caddy service...")
|
||||||
|
// TODO: split the method into two: one to get the records and one to update them to ask for update confirmation.
|
||||||
|
|
||||||
|
var records []*pb.DNSRecord
|
||||||
|
err := progress.RunWithTitle(ctx, func(ctx context.Context) error {
|
||||||
|
var err error
|
||||||
|
records, err = clusterClient.CreateIngressRecords(ctx, client.CaddyServiceName)
|
||||||
|
return err
|
||||||
|
}, progressOut, "Verifying internet access to caddy service")
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, client.ErrNoReachableMachines) {
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Println("DNS records could not be updated as there are no internet-reachable machines running " +
|
||||||
|
"caddy containers.")
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Println("Possible solutions:")
|
||||||
|
fmt.Println("- Ensure your machines have public IP addresses")
|
||||||
|
fmt.Println("- Use --public-ip flag when adding machines to override the automatically detected IPs")
|
||||||
|
fmt.Println("- Check firewall settings on your machines")
|
||||||
|
fmt.Println("- Configure port forwarding if behind NAT")
|
||||||
|
fmt.Println("- Retry Caddy deployment after resolving connectivity issues with 'uc caddy deploy'")
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Println("Your services will not be accessible from the internet until at least one machine " +
|
||||||
|
"becomes reachable.")
|
||||||
|
}
|
||||||
|
return fmt.Errorf("failed to update DNS records pointing to caddy service: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Println("DNS records updated to use only the internet-reachable machines running caddy service:")
|
||||||
|
for _, r := range records {
|
||||||
|
fmt.Printf(" %s %s → %s\n", r.Name, r.Type, strings.Join(r.Values, ", "))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func machineFilter(machines []string) deploy.MachineFilter {
|
||||||
|
if len(machines) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return func(m *pb.MachineInfo) bool {
|
||||||
|
return slices.Contains(machines, m.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package caddy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewRootCommand() *cobra.Command {
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "caddy",
|
||||||
|
Short: "Manage Caddy reverse proxy service.",
|
||||||
|
}
|
||||||
|
cmd.AddCommand(
|
||||||
|
NewDeployCommand(),
|
||||||
|
)
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"github.com/compose-spec/compose-go/v2/types"
|
||||||
|
"github.com/docker/compose/v2/pkg/progress"
|
||||||
|
"github.com/psviderski/uncloud/internal/cli"
|
||||||
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
|
"github.com/psviderski/uncloud/pkg/client/compose"
|
||||||
|
"github.com/psviderski/uncloud/pkg/client/deploy"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type deployOptions struct {
|
||||||
|
files []string
|
||||||
|
services []string
|
||||||
|
|
||||||
|
cluster string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDeployCommand creates a new command to deploy services from a Compose file.
|
||||||
|
func NewDeployCommand() *cobra.Command {
|
||||||
|
opts := deployOptions{}
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "deploy [FLAGS] [SERVICE...]",
|
||||||
|
// TODO: remove WIP when the command is fully implemented.
|
||||||
|
Short: "WIP: Deploy services from a Compose file.",
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
uncli := cmd.Context().Value("cli").(*cli.CLI)
|
||||||
|
|
||||||
|
if len(args) > 0 {
|
||||||
|
opts.services = args
|
||||||
|
}
|
||||||
|
|
||||||
|
return runDeploy(cmd.Context(), uncli, opts)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Flags().StringSliceVarP(&opts.files, "file", "f", nil,
|
||||||
|
"One or more Compose files to deploy services from. (default compose-ports-long.yaml)")
|
||||||
|
cmd.Flags().StringVarP(&opts.cluster, "cluster", "c", "",
|
||||||
|
"Name of the cluster to deploy to (default is the current cluster)")
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
// runDeploy parses the Compose file(s) and deploys the services.
|
||||||
|
func runDeploy(ctx context.Context, uncli *cli.CLI, opts deployOptions) error {
|
||||||
|
project, err := compose.LoadProject(ctx, opts.files)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("load compose file(s): %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(opts.services) > 0 {
|
||||||
|
// TODO: handle dependencies properly.
|
||||||
|
project, err = project.WithSelectedServices(opts.services, types.IgnoreDependencies)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("select services: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clusterClient, err := uncli.ConnectCluster(ctx, opts.cluster)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("connect to cluster: %w", err)
|
||||||
|
}
|
||||||
|
defer clusterClient.Close()
|
||||||
|
|
||||||
|
composeDeploy, err := compose.NewDeployment(ctx, clusterClient, project)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("create compose deployment: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
plan, err := composeDeploy.Plan(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("plan deployment: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(plan.Operations) == 0 {
|
||||||
|
fmt.Println("Services are up to date.")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("Deployment plan:")
|
||||||
|
|
||||||
|
for _, op := range plan.Operations {
|
||||||
|
svcPlan, ok := op.(*deploy.Plan)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("expected service Plan, got: %T", op)
|
||||||
|
}
|
||||||
|
|
||||||
|
svc, err := clusterClient.InspectService(ctx, svcPlan.ServiceID)
|
||||||
|
if err != nil {
|
||||||
|
if !errors.Is(err, api.ErrNotFound) {
|
||||||
|
return fmt.Errorf("inspect service: %w", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("- Run service [name=%s]\n", svcPlan.ServiceName)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("- Update service [name=%s]\n", svc.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialise a machine and container name resolver to properly format the service plan output.
|
||||||
|
resolver, err := clusterClient.ServiceOperationNameResolver(ctx, svc)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("create machine and container name resolver for service operations: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println(indent(svcPlan.Format(resolver), " "))
|
||||||
|
}
|
||||||
|
fmt.Println()
|
||||||
|
|
||||||
|
confirmed, err := cli.Confirm()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("confirm deployment: %w", err)
|
||||||
|
}
|
||||||
|
if !confirmed {
|
||||||
|
fmt.Println("Cancelled. No changes were made.")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return progress.RunWithTitle(ctx, func(ctx context.Context) error {
|
||||||
|
if err := plan.Execute(ctx, clusterClient); err != nil {
|
||||||
|
return fmt.Errorf("deploy services: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}, uncli.ProgressOut(), "Deploying services")
|
||||||
|
}
|
||||||
|
|
||||||
|
func indent(text, prefix string) string {
|
||||||
|
lines := strings.Split(text, "\n")
|
||||||
|
for i, line := range lines {
|
||||||
|
lines[i] = prefix + line
|
||||||
|
}
|
||||||
|
return strings.Join(lines, "\n")
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
package dns
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"google.golang.org/grpc/codes"
|
||||||
|
"google.golang.org/grpc/status"
|
||||||
|
"google.golang.org/protobuf/types/known/emptypb"
|
||||||
|
"github.com/psviderski/uncloud/internal/cli"
|
||||||
|
)
|
||||||
|
|
||||||
|
type releaseOptions struct {
|
||||||
|
cluster string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewReleaseCommand() *cobra.Command {
|
||||||
|
opts := releaseOptions{}
|
||||||
|
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "release",
|
||||||
|
Short: "Release the reserved cluster domain.",
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
uncli := cmd.Context().Value("cli").(*cli.CLI)
|
||||||
|
return release(cmd.Context(), uncli, opts)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Flags().StringVarP(
|
||||||
|
&opts.cluster, "cluster", "c", "",
|
||||||
|
"Name of the cluster. (default is the current cluster)",
|
||||||
|
)
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func release(ctx context.Context, uncli *cli.CLI, opts releaseOptions) error {
|
||||||
|
client, err := uncli.ConnectCluster(ctx, opts.cluster)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("connect to cluster: %w", err)
|
||||||
|
}
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
domain, err := client.ReleaseDomain(ctx, &emptypb.Empty{})
|
||||||
|
if err != nil {
|
||||||
|
if status.Convert(err).Code() == codes.NotFound {
|
||||||
|
return errors.New("no domain reserved")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Released cluster domain: %s\n", domain.Name)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
package dns
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"github.com/psviderski/uncloud/cmd/uncloud/caddy"
|
||||||
|
"github.com/psviderski/uncloud/internal/cli"
|
||||||
|
"github.com/psviderski/uncloud/internal/machine/api/pb"
|
||||||
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
|
"github.com/psviderski/uncloud/pkg/client"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"google.golang.org/grpc/codes"
|
||||||
|
"google.golang.org/grpc/status"
|
||||||
|
)
|
||||||
|
|
||||||
|
const DefaultUncloudDNSAPIEndpoint = "https://dns.uncloud.run/v1"
|
||||||
|
|
||||||
|
type reserveOptions struct {
|
||||||
|
endpoint string
|
||||||
|
cluster string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewReserveCommand() *cobra.Command {
|
||||||
|
opts := reserveOptions{}
|
||||||
|
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "reserve",
|
||||||
|
Short: "Reserve a cluster domain in Uncloud DNS.",
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
uncli := cmd.Context().Value("cli").(*cli.CLI)
|
||||||
|
return reserve(cmd.Context(), uncli, opts)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Flags().StringVar(&opts.endpoint, "endpoint", DefaultUncloudDNSAPIEndpoint,
|
||||||
|
"API endpoint for the Uncloud DNS service.")
|
||||||
|
cmd.Flags().StringVarP(
|
||||||
|
&opts.cluster, "cluster", "c", "",
|
||||||
|
"Name of the cluster. (default is the current cluster)",
|
||||||
|
)
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func reserve(ctx context.Context, uncli *cli.CLI, opts reserveOptions) error {
|
||||||
|
clusterClient, err := uncli.ConnectCluster(ctx, opts.cluster)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("connect to cluster: %w", err)
|
||||||
|
}
|
||||||
|
defer clusterClient.Close()
|
||||||
|
|
||||||
|
domain, err := clusterClient.ReserveDomain(ctx, &pb.ReserveDomainRequest{Endpoint: opts.endpoint})
|
||||||
|
if err != nil {
|
||||||
|
if status.Convert(err).Code() == codes.AlreadyExists {
|
||||||
|
return errors.New("domain already reserved")
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Reserved cluster domain: %s\n", domain.Name)
|
||||||
|
|
||||||
|
// Update cluster domain records in Uncloud DNS to point to machines running caddy service if it has been deployed.
|
||||||
|
if _, err = clusterClient.InspectService(ctx, client.CaddyServiceName); err != nil {
|
||||||
|
if errors.Is(err, api.ErrNotFound) {
|
||||||
|
fmt.Println("Deploy the Caddy reverse proxy service ('uc caddy deploy') to enable internet access " +
|
||||||
|
"to your services via the reserved or your custom domain.")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return fmt.Errorf("inspect caddy service: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return caddy.UpdateDomainRecords(ctx, clusterClient, uncli.ProgressOut())
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package dns
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewRootCommand() *cobra.Command {
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "dns",
|
||||||
|
Short: "Manage cluster domain in Uncloud DNS.",
|
||||||
|
Long: "Manage cluster domain in Uncloud DNS.\n" +
|
||||||
|
"DNS commands allow you to reserve or release a unique '<id>.cluster.uncloud.run' domain for your " +
|
||||||
|
"cluster. When reserved, Caddy service deployments will automatically update DNS records to route " +
|
||||||
|
"traffic to the services in the cluster.",
|
||||||
|
}
|
||||||
|
cmd.AddCommand(
|
||||||
|
NewReleaseCommand(),
|
||||||
|
NewReserveCommand(),
|
||||||
|
NewShowCommand(),
|
||||||
|
)
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
package dns
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"github.com/psviderski/uncloud/internal/cli"
|
||||||
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
type showOptions struct {
|
||||||
|
cluster string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewShowCommand() *cobra.Command {
|
||||||
|
opts := showOptions{}
|
||||||
|
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "show",
|
||||||
|
Short: "Print the cluster domain name.",
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
uncli := cmd.Context().Value("cli").(*cli.CLI)
|
||||||
|
return show(cmd.Context(), uncli, opts)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Flags().StringVarP(
|
||||||
|
&opts.cluster, "cluster", "c", "",
|
||||||
|
"Name of the cluster. (default is the current cluster)",
|
||||||
|
)
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func show(ctx context.Context, uncli *cli.CLI, opts showOptions) error {
|
||||||
|
clusterClient, err := uncli.ConnectCluster(ctx, opts.cluster)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("connect to cluster: %w", err)
|
||||||
|
}
|
||||||
|
defer clusterClient.Close()
|
||||||
|
|
||||||
|
domain, err := clusterClient.GetDomain(ctx)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, api.ErrNotFound) {
|
||||||
|
return errors.New("no domain reserved")
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println(domain)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
+135
-6
@@ -1,16 +1,31 @@
|
|||||||
package machine
|
package machine
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/cenkalti/backoff/v4"
|
||||||
|
"github.com/docker/compose/v2/pkg/progress"
|
||||||
|
"github.com/psviderski/uncloud/cmd/uncloud/caddy"
|
||||||
|
"github.com/psviderski/uncloud/internal/cli"
|
||||||
|
"github.com/psviderski/uncloud/internal/cli/config"
|
||||||
|
"github.com/psviderski/uncloud/internal/machine/api/pb"
|
||||||
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
|
"github.com/psviderski/uncloud/pkg/client"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"uncloud/internal/cli"
|
"google.golang.org/grpc/codes"
|
||||||
"uncloud/internal/cli/config"
|
"google.golang.org/grpc/status"
|
||||||
|
"google.golang.org/protobuf/types/known/emptypb"
|
||||||
|
"net/netip"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type addOptions struct {
|
type addOptions struct {
|
||||||
name string
|
name string
|
||||||
sshKey string
|
noCaddy bool
|
||||||
cluster string
|
publicIP string
|
||||||
|
sshKey string
|
||||||
|
cluster string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAddCommand() *cobra.Command {
|
func NewAddCommand() *cobra.Command {
|
||||||
@@ -33,10 +48,19 @@ func NewAddCommand() *cobra.Command {
|
|||||||
KeyPath: opts.sshKey,
|
KeyPath: opts.sshKey,
|
||||||
}
|
}
|
||||||
|
|
||||||
return uncli.AddMachine(cmd.Context(), remoteMachine, opts.cluster, opts.name)
|
return add(cmd.Context(), uncli, remoteMachine, opts)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
cmd.Flags().StringVarP(&opts.name, "name", "n", "", "Assign a name to the machine.")
|
cmd.Flags().StringVarP(&opts.name, "name", "n", "", "Assign a name to the machine.")
|
||||||
|
cmd.Flags().BoolVar(
|
||||||
|
&opts.noCaddy, "no-caddy", false,
|
||||||
|
"Don't deploy Caddy reverse proxy service to the machine.",
|
||||||
|
)
|
||||||
|
cmd.Flags().StringVar(
|
||||||
|
&opts.publicIP, "public-ip", "auto",
|
||||||
|
"Public IP address of the machine for ingress configuration. Use 'auto' for automatic detection, "+
|
||||||
|
"blank '' or 'none' to disable ingress on this machine, or specify an IP address.",
|
||||||
|
)
|
||||||
cmd.Flags().StringVarP(
|
cmd.Flags().StringVarP(
|
||||||
&opts.sshKey, "ssh-key", "i", "",
|
&opts.sshKey, "ssh-key", "i", "",
|
||||||
"path to SSH private key for SSH remote login. (default ~/.ssh/id_*)",
|
"path to SSH private key for SSH remote login. (default ~/.ssh/id_*)",
|
||||||
@@ -47,3 +71,108 @@ func NewAddCommand() *cobra.Command {
|
|||||||
)
|
)
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func add(ctx context.Context, uncli *cli.CLI, remoteMachine cli.RemoteMachine, opts addOptions) error {
|
||||||
|
var publicIP *netip.Addr
|
||||||
|
switch opts.publicIP {
|
||||||
|
case "auto":
|
||||||
|
publicIP = &netip.Addr{}
|
||||||
|
case "", "none":
|
||||||
|
publicIP = nil
|
||||||
|
default:
|
||||||
|
ip, err := netip.ParseAddr(opts.publicIP)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("parse public IP: %w", err)
|
||||||
|
}
|
||||||
|
publicIP = &ip
|
||||||
|
}
|
||||||
|
|
||||||
|
machineClient, err := uncli.AddMachine(ctx, remoteMachine, opts.cluster, opts.name, publicIP)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer machineClient.Close()
|
||||||
|
|
||||||
|
if opts.noCaddy {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for the cluster to be initialised to be able to deploy the Caddy service.
|
||||||
|
fmt.Println("Waiting for the machine to be ready...")
|
||||||
|
fmt.Println()
|
||||||
|
if err = waitClusterInitialised(ctx, machineClient); err != nil {
|
||||||
|
return fmt.Errorf("wait for cluster to be initialised on machine: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inspect the added machine to get its ID to create a filter for the Caddy deployment.
|
||||||
|
minfo, err := machineClient.Inspect(ctx, &emptypb.Empty{})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("inspect machine: %w", err)
|
||||||
|
}
|
||||||
|
filter := func(m *pb.MachineInfo) bool {
|
||||||
|
return m.Id == minfo.Id
|
||||||
|
}
|
||||||
|
// Deploy a Caddy service container to the added machine. If caddy service is already deployed on other machines,
|
||||||
|
// use the deployed image version. Otherwise, use the latest version.
|
||||||
|
caddyImage := ""
|
||||||
|
caddySvc, err := machineClient.InspectService(ctx, client.CaddyServiceName)
|
||||||
|
if err != nil {
|
||||||
|
if !errors.Is(err, api.ErrNotFound) {
|
||||||
|
return fmt.Errorf("inspect caddy service: %w", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
caddyImage = caddySvc.Containers[0].Container.Config.Image
|
||||||
|
// Find the latest created container and use its image.
|
||||||
|
var latestCreated time.Time
|
||||||
|
for _, c := range caddySvc.Containers[1:] {
|
||||||
|
created, err := time.Parse(time.RFC3339Nano, c.Container.Created)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if created.After(latestCreated) {
|
||||||
|
latestCreated = created
|
||||||
|
caddyImage = c.Container.Config.Image
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
d, err := machineClient.NewCaddyDeployment(caddyImage, filter)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("create caddy deployment: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = progress.RunWithTitle(ctx, func(ctx context.Context) error {
|
||||||
|
if _, err = d.Run(ctx); err != nil {
|
||||||
|
return fmt.Errorf("deploy caddy: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}, uncli.ProgressOut(), fmt.Sprintf("Deploying service %s", d.Spec.Name))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println()
|
||||||
|
return caddy.UpdateDomainRecords(ctx, machineClient, uncli.ProgressOut())
|
||||||
|
}
|
||||||
|
|
||||||
|
func waitClusterInitialised(ctx context.Context, client *client.Client) error {
|
||||||
|
boff := backoff.WithContext(backoff.NewExponentialBackOff(
|
||||||
|
backoff.WithMaxInterval(1*time.Second),
|
||||||
|
backoff.WithMaxElapsedTime(5*time.Minute),
|
||||||
|
), ctx)
|
||||||
|
|
||||||
|
check := func() error {
|
||||||
|
_, err := client.ListMachines(ctx)
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
statusErr := status.Convert(err)
|
||||||
|
if statusErr.Code() == codes.FailedPrecondition {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return backoff.Permanent(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return backoff.Retry(check, boff)
|
||||||
|
}
|
||||||
|
|||||||
+104
-14
@@ -1,19 +1,28 @@
|
|||||||
package machine
|
package machine
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/docker/compose/v2/pkg/progress"
|
||||||
|
"github.com/psviderski/uncloud/cmd/uncloud/caddy"
|
||||||
|
"github.com/psviderski/uncloud/cmd/uncloud/dns"
|
||||||
|
"github.com/psviderski/uncloud/internal/cli"
|
||||||
|
"github.com/psviderski/uncloud/internal/cli/config"
|
||||||
|
"github.com/psviderski/uncloud/internal/machine/api/pb"
|
||||||
|
"github.com/psviderski/uncloud/internal/machine/cluster"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"uncloud/internal/cli"
|
|
||||||
"uncloud/internal/cli/config"
|
|
||||||
"uncloud/internal/machine/cluster"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type initOptions struct {
|
type initOptions struct {
|
||||||
name string
|
dnsEndpoint string
|
||||||
network string
|
name string
|
||||||
sshKey string
|
network string
|
||||||
cluster string
|
noCaddy bool
|
||||||
|
noDNS bool
|
||||||
|
publicIP string
|
||||||
|
sshKey string
|
||||||
|
cluster string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewInitCommand() *cobra.Command {
|
func NewInitCommand() *cobra.Command {
|
||||||
@@ -39,22 +48,36 @@ func NewInitCommand() *cobra.Command {
|
|||||||
KeyPath: opts.sshKey,
|
KeyPath: opts.sshKey,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
netPrefix, err := netip.ParsePrefix(opts.network)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("parse network CIDR: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return uncli.InitCluster(cmd.Context(), remoteMachine, opts.cluster, opts.name, netPrefix)
|
return initCluster(cmd.Context(), uncli, remoteMachine, opts)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
cmd.Flags().StringVarP(&opts.name, "name", "n", "", "Assign a name to the machine.")
|
cmd.Flags().StringVar(&opts.dnsEndpoint, "dns-endpoint", dns.DefaultUncloudDNSAPIEndpoint,
|
||||||
|
"API endpoint for the Uncloud DNS service.")
|
||||||
|
cmd.Flags().StringVarP(
|
||||||
|
&opts.name, "name", "n", "",
|
||||||
|
"Assign a name to the machine.",
|
||||||
|
)
|
||||||
cmd.Flags().StringVar(
|
cmd.Flags().StringVar(
|
||||||
&opts.network, "network", cluster.DefaultNetwork.String(),
|
&opts.network, "network", cluster.DefaultNetwork.String(),
|
||||||
"IPv4 network CIDR to use for machines and services.",
|
"IPv4 network CIDR to use for machines and services.",
|
||||||
)
|
)
|
||||||
|
cmd.Flags().BoolVar(
|
||||||
|
&opts.noCaddy, "no-caddy", false,
|
||||||
|
"Don't deploy Caddy reverse proxy service to the machine.",
|
||||||
|
)
|
||||||
|
cmd.Flags().BoolVar(
|
||||||
|
&opts.noDNS, "no-dns", false,
|
||||||
|
"Don't reserve a cluster domain in Uncloud DNS.",
|
||||||
|
)
|
||||||
|
cmd.Flags().StringVar(
|
||||||
|
&opts.publicIP, "public-ip", "auto",
|
||||||
|
"Public IP address of the machine for ingress configuration. Use 'auto' for automatic detection, "+
|
||||||
|
"blank '' or 'none' to disable ingress on this machine, or specify an IP address.",
|
||||||
|
)
|
||||||
cmd.Flags().StringVarP(
|
cmd.Flags().StringVarP(
|
||||||
&opts.sshKey, "ssh-key", "i", "",
|
&opts.sshKey, "ssh-key", "i", "",
|
||||||
"path to SSH private key for SSH remote login. (default ~/.ssh/id_*)",
|
"Path to SSH private key for SSH remote login. (default ~/.ssh/id_*)",
|
||||||
)
|
)
|
||||||
cmd.Flags().StringVarP(
|
cmd.Flags().StringVarP(
|
||||||
&opts.cluster, "cluster", "c", "",
|
&opts.cluster, "cluster", "c", "",
|
||||||
@@ -63,3 +86,70 @@ func NewInitCommand() *cobra.Command {
|
|||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func initCluster(ctx context.Context, uncli *cli.CLI, remoteMachine *cli.RemoteMachine, opts initOptions) error {
|
||||||
|
netPrefix, err := netip.ParsePrefix(opts.network)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("parse network CIDR: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var publicIP *netip.Addr
|
||||||
|
switch opts.publicIP {
|
||||||
|
case "auto":
|
||||||
|
publicIP = &netip.Addr{}
|
||||||
|
case "", "none":
|
||||||
|
publicIP = nil
|
||||||
|
default:
|
||||||
|
ip, err := netip.ParseAddr(opts.publicIP)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("parse public IP: %w", err)
|
||||||
|
}
|
||||||
|
publicIP = &ip
|
||||||
|
}
|
||||||
|
|
||||||
|
client, err := uncli.InitCluster(ctx, remoteMachine, opts.cluster, opts.name, netPrefix, publicIP)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
if opts.noCaddy && opts.noDNS {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deploy the Caddy service to the initialised machine.
|
||||||
|
// The creation of a deployment plan talks to cluster API. Since the API needs a few moments to become available
|
||||||
|
// after cluster initialisation, we keep the user informed during this wait.
|
||||||
|
fmt.Println("Waiting for the machine to be ready...")
|
||||||
|
fmt.Println()
|
||||||
|
|
||||||
|
if !opts.noDNS {
|
||||||
|
domain, err := client.ReserveDomain(ctx, &pb.ReserveDomainRequest{Endpoint: opts.dnsEndpoint})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("reserve cluster domain in Uncloud DNS: %w", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("Reserved cluster domain: %s\n", domain.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !opts.noCaddy {
|
||||||
|
d, err := client.NewCaddyDeployment("", nil)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("create caddy deployment: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = progress.RunWithTitle(ctx, func(ctx context.Context) error {
|
||||||
|
if _, err = d.Run(ctx); err != nil {
|
||||||
|
return fmt.Errorf("deploy caddy: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}, uncli.ProgressOut(), fmt.Sprintf("Deploying service %s", d.Spec.Name))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println()
|
||||||
|
return caddy.UpdateDomainRecords(ctx, client, uncli.ProgressOut())
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,9 +8,8 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"text/tabwriter"
|
"text/tabwriter"
|
||||||
"uncloud/internal/cli"
|
"github.com/psviderski/uncloud/internal/cli"
|
||||||
"uncloud/internal/machine/network"
|
"github.com/psviderski/uncloud/internal/machine/network"
|
||||||
"uncloud/internal/secret"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewListCommand() *cobra.Command {
|
func NewListCommand() *cobra.Command {
|
||||||
@@ -46,7 +45,7 @@ func list(ctx context.Context, uncli *cli.CLI, clusterName string) error {
|
|||||||
// Print the list of machines in a table format.
|
// Print the list of machines in a table format.
|
||||||
tw := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', 0)
|
tw := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', 0)
|
||||||
// Print header.
|
// Print header.
|
||||||
if _, err = fmt.Fprintln(tw, "NAME\tSTATE\tADDRESS\tPUBLIC KEY\tENDPOINTS"); err != nil {
|
if _, err = fmt.Fprintln(tw, "NAME\tSTATE\tADDRESS\tPUBLIC IP\tWIREGUARD ENDPOINTS"); err != nil {
|
||||||
return fmt.Errorf("write header: %w", err)
|
return fmt.Errorf("write header: %w", err)
|
||||||
}
|
}
|
||||||
// Print rows.
|
// Print rows.
|
||||||
@@ -54,14 +53,21 @@ func list(ctx context.Context, uncli *cli.CLI, clusterName string) error {
|
|||||||
m := member.Machine
|
m := member.Machine
|
||||||
subnet, _ := m.Network.Subnet.ToPrefix()
|
subnet, _ := m.Network.Subnet.ToPrefix()
|
||||||
subnet = netip.PrefixFrom(network.MachineIP(subnet), subnet.Bits())
|
subnet = netip.PrefixFrom(network.MachineIP(subnet), subnet.Bits())
|
||||||
|
|
||||||
|
publicIP := "-"
|
||||||
|
if m.PublicIp != nil {
|
||||||
|
ip, _ := m.PublicIp.ToAddr()
|
||||||
|
publicIP = ip.String()
|
||||||
|
}
|
||||||
|
|
||||||
endpoints := make([]string, len(m.Network.Endpoints))
|
endpoints := make([]string, len(m.Network.Endpoints))
|
||||||
for i, ep := range m.Network.Endpoints {
|
for i, ep := range m.Network.Endpoints {
|
||||||
addrPort, _ := ep.ToAddrPort()
|
addrPort, _ := ep.ToAddrPort()
|
||||||
endpoints[i] = addrPort.String()
|
endpoints[i] = addrPort.String()
|
||||||
}
|
}
|
||||||
publicKey := secret.Secret(m.Network.PublicKey)
|
|
||||||
if _, err = fmt.Fprintf(
|
if _, err = fmt.Fprintf(
|
||||||
tw, "%s\t%s\t%s\t%s\t%s\n", m.Name, capitalise(member.State.String()), subnet, publicKey, strings.Join(endpoints, ", "),
|
tw, "%s\t%s\t%s\t%s\t%s\n", m.Name, capitalise(member.State.String()), subnet, publicIP, strings.Join(endpoints, ", "),
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return fmt.Errorf("write row: %w", err)
|
return fmt.Errorf("write row: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ package machine
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"uncloud/internal/daemon"
|
"github.com/psviderski/uncloud/internal/daemon"
|
||||||
"uncloud/internal/machine"
|
"github.com/psviderski/uncloud/internal/machine"
|
||||||
)
|
)
|
||||||
|
|
||||||
type tokenOptions struct {
|
type tokenOptions struct {
|
||||||
|
|||||||
+46
-14
@@ -3,31 +3,55 @@ package main
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/spf13/cobra"
|
"net/netip"
|
||||||
"os"
|
|
||||||
"strings"
|
"strings"
|
||||||
"uncloud/cmd/uncloud/machine"
|
|
||||||
"uncloud/cmd/uncloud/service"
|
"github.com/psviderski/uncloud/cmd/uncloud/caddy"
|
||||||
"uncloud/internal/cli"
|
"github.com/psviderski/uncloud/cmd/uncloud/dns"
|
||||||
|
"github.com/psviderski/uncloud/cmd/uncloud/machine"
|
||||||
|
"github.com/psviderski/uncloud/cmd/uncloud/service"
|
||||||
|
"github.com/psviderski/uncloud/internal/cli"
|
||||||
|
"github.com/psviderski/uncloud/internal/cli/config"
|
||||||
|
"github.com/psviderski/uncloud/internal/fs"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type globalOptions struct {
|
||||||
|
configPath string
|
||||||
|
connect string
|
||||||
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
var configPath string
|
opts := globalOptions{}
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "uncloud",
|
Use: "uncloud",
|
||||||
Short: "A CLI tool for managing Uncloud resources such as clusters, machines, and services.",
|
Short: "A CLI tool for managing Uncloud resources such as clusters, machines, and services.",
|
||||||
SilenceUsage: true,
|
SilenceUsage: true,
|
||||||
SilenceErrors: true,
|
SilenceErrors: true,
|
||||||
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
|
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
|
||||||
if strings.HasPrefix(configPath, "~/") {
|
var conn *config.MachineConnection
|
||||||
home, err := os.UserHomeDir()
|
if opts.connect != "" {
|
||||||
if err != nil {
|
if strings.HasPrefix(opts.connect, "tcp://") {
|
||||||
return fmt.Errorf("get user home directory to resolve %q: %w", configPath, err)
|
addrPort, err := netip.ParseAddrPort(opts.connect[len("tcp://"):])
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("parse TCP address: %w", err)
|
||||||
|
}
|
||||||
|
conn = &config.MachineConnection{
|
||||||
|
TCP: addrPort,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
dest := opts.connect
|
||||||
|
if strings.HasPrefix(dest, "ssh://") {
|
||||||
|
dest = dest[len("ssh://"):]
|
||||||
|
}
|
||||||
|
conn = &config.MachineConnection{
|
||||||
|
SSH: config.SSHDestination(dest),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
configPath = strings.Replace(configPath, "~", home, 1)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
uncli, err := cli.New(configPath)
|
configPath := fs.ExpandHomeDir(opts.configPath)
|
||||||
|
uncli, err := cli.New(configPath, conn)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("initialize CLI: %w", err)
|
return fmt.Errorf("initialize CLI: %w", err)
|
||||||
}
|
}
|
||||||
@@ -35,18 +59,26 @@ func main() {
|
|||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cmd.PersistentFlags().StringVar(&opts.connect, "connect", "",
|
||||||
|
"Connect to a remote cluster machine without using the Uncloud configuration file.\n"+
|
||||||
|
"Format: [ssh://]user@host[:port] or tcp://host:port")
|
||||||
// TODO: allow to override using UNCLOUD_CONFIG env var.
|
// TODO: allow to override using UNCLOUD_CONFIG env var.
|
||||||
cmd.PersistentFlags().StringVar(&configPath, "uncloud-config", "~/.config/uncloud/config.toml",
|
cmd.PersistentFlags().StringVar(&opts.configPath, "uncloud-config", "~/.config/uncloud/config.toml",
|
||||||
"path to the Uncloud configuration file.")
|
"Path to the Uncloud configuration file.")
|
||||||
_ = cmd.MarkPersistentFlagFilename("uncloud-config", "toml")
|
_ = cmd.MarkPersistentFlagFilename("uncloud-config", "toml")
|
||||||
|
|
||||||
cmd.AddCommand(
|
cmd.AddCommand(
|
||||||
|
NewDeployCommand(),
|
||||||
|
caddy.NewRootCommand(),
|
||||||
|
dns.NewRootCommand(),
|
||||||
machine.NewRootCommand(),
|
machine.NewRootCommand(),
|
||||||
service.NewRootCommand(),
|
service.NewRootCommand(),
|
||||||
service.NewInspectCommand(),
|
service.NewInspectCommand(),
|
||||||
service.NewListCommand(),
|
service.NewListCommand(),
|
||||||
service.NewRmCommand(),
|
service.NewRmCommand(),
|
||||||
service.NewRunCommand(),
|
service.NewRunCommand(),
|
||||||
|
service.NewScaleCommand(),
|
||||||
)
|
)
|
||||||
cobra.CheckErr(cmd.Execute())
|
cobra.CheckErr(cmd.Execute())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"uncloud/internal/cli"
|
"github.com/psviderski/uncloud/internal/cli"
|
||||||
)
|
)
|
||||||
|
|
||||||
type inspectOptions struct {
|
type inspectOptions struct {
|
||||||
@@ -89,7 +89,7 @@ func inspect(ctx context.Context, uncli *cli.CLI, opts inspectOptions) error {
|
|||||||
tw,
|
tw,
|
||||||
"%s\t%s\t%s\t%s\t%s\n",
|
"%s\t%s\t%s\t%s\t%s\n",
|
||||||
stringid.TruncateID(ctr.Container.ID),
|
stringid.TruncateID(ctr.Container.ID),
|
||||||
ctr.Container.Image,
|
ctr.Container.Config.Image,
|
||||||
created,
|
created,
|
||||||
state,
|
state,
|
||||||
machine,
|
machine,
|
||||||
|
|||||||
@@ -3,10 +3,11 @@ package service
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/psviderski/uncloud/internal/cli"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
"text/tabwriter"
|
"text/tabwriter"
|
||||||
"uncloud/internal/cli"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewListCommand() *cobra.Command {
|
func NewListCommand() *cobra.Command {
|
||||||
@@ -39,13 +40,38 @@ func list(ctx context.Context, uncli *cli.CLI, clusterName string) error {
|
|||||||
return fmt.Errorf("list services: %w", err)
|
return fmt.Errorf("list services: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
serviceNames := make(map[string]struct{}, len(services))
|
||||||
|
haveDuplicateNames := false
|
||||||
|
for _, svc := range services {
|
||||||
|
if _, exists := serviceNames[svc.Name]; exists {
|
||||||
|
haveDuplicateNames = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
serviceNames[svc.Name] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
// Print the list of services in a table format.
|
// Print the list of services in a table format.
|
||||||
tw := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', 0)
|
tw := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', 0)
|
||||||
if _, err = fmt.Fprintln(tw, "SERVICE ID\tNAME\tMODE\tREPLICAS"); err != nil {
|
|
||||||
|
// Include the ID column if there are duplicate service names to differentiate them.
|
||||||
|
if haveDuplicateNames {
|
||||||
|
if _, err = fmt.Fprintf(tw, "ID\t"); err != nil {
|
||||||
|
return fmt.Errorf("write header: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if _, err = fmt.Fprintln(tw, "NAME\tMODE\tREPLICAS\tENDPOINTS"); err != nil {
|
||||||
return fmt.Errorf("write header: %w", err)
|
return fmt.Errorf("write header: %w", err)
|
||||||
}
|
}
|
||||||
for _, s := range services {
|
for _, s := range services {
|
||||||
if _, err = fmt.Fprintf(tw, "%s\t%s\t%s\t%d\n", s.ID, s.Name, s.Mode, len(s.Containers)); err != nil {
|
endpointsSlice := s.Endpoints()
|
||||||
|
endpoints := strings.Join(endpointsSlice, ", ")
|
||||||
|
|
||||||
|
if haveDuplicateNames {
|
||||||
|
if _, err = fmt.Fprintf(tw, "%s\t", s.ID); err != nil {
|
||||||
|
return fmt.Errorf("write row: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if _, err = fmt.Fprintf(tw, "%s\t%s\t%d\t%s\n", s.Name, s.Mode, len(s.Containers), endpoints); err != nil {
|
||||||
return fmt.Errorf("write row: %w", err)
|
return fmt.Errorf("write row: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"github.com/docker/cli/cli/streams"
|
"github.com/docker/cli/cli/streams"
|
||||||
"github.com/docker/compose/v2/pkg/progress"
|
"github.com/docker/compose/v2/pkg/progress"
|
||||||
|
"github.com/psviderski/uncloud/internal/cli"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"os"
|
"os"
|
||||||
"uncloud/internal/cli"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type rmOptions struct {
|
type rmOptions struct {
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ func NewRootCommand() *cobra.Command {
|
|||||||
NewListCommand(),
|
NewListCommand(),
|
||||||
NewRmCommand(),
|
NewRmCommand(),
|
||||||
NewRunCommand(),
|
NewRunCommand(),
|
||||||
|
NewScaleCommand(),
|
||||||
)
|
)
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|||||||
+98
-28
@@ -3,19 +3,27 @@ package service
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/psviderski/uncloud/internal/cli"
|
||||||
|
"github.com/psviderski/uncloud/internal/machine/api/pb"
|
||||||
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
|
"github.com/psviderski/uncloud/pkg/client/deploy"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"uncloud/internal/api"
|
"slices"
|
||||||
"uncloud/internal/cli"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
type runOptions struct {
|
type runOptions struct {
|
||||||
command []string
|
command []string
|
||||||
image string
|
entrypoint string
|
||||||
machine string
|
entrypointChanged bool
|
||||||
mode string
|
image string
|
||||||
name string
|
machines []string
|
||||||
publish []string
|
mode string
|
||||||
volumes []string
|
name string
|
||||||
|
publish []string
|
||||||
|
pull string
|
||||||
|
replicas uint
|
||||||
|
volumes []string
|
||||||
|
|
||||||
cluster string
|
cluster string
|
||||||
}
|
}
|
||||||
@@ -30,6 +38,7 @@ func NewRunCommand() *cobra.Command {
|
|||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
uncli := cmd.Context().Value("cli").(*cli.CLI)
|
uncli := cmd.Context().Value("cli").(*cli.CLI)
|
||||||
|
|
||||||
|
opts.entrypointChanged = cmd.Flag("entrypoint").Changed
|
||||||
opts.image = args[0]
|
opts.image = args[0]
|
||||||
if len(args) > 1 {
|
if len(args) > 1 {
|
||||||
opts.command = args[1:]
|
opts.command = args[1:]
|
||||||
@@ -39,25 +48,32 @@ func NewRunCommand() *cobra.Command {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: implement placement constraints and translate --machine to a constraint.
|
cmd.Flags().StringVar(&opts.entrypoint, "entrypoint", "",
|
||||||
//cmd.Flags().StringVarP(
|
"Overwrite the default ENTRYPOINT of the image. Pass an empty string \"\" to reset it.")
|
||||||
// &opts.machine, "machine", "m", "",
|
|
||||||
// "Name or ID of the machine to run the service on. (default is first available)",
|
|
||||||
//)
|
|
||||||
cmd.Flags().StringVar(&opts.mode, "mode", api.ServiceModeReplicated,
|
cmd.Flags().StringVar(&opts.mode, "mode", api.ServiceModeReplicated,
|
||||||
fmt.Sprintf("Replication mode of the service: either %q (a specified number of containers across "+
|
fmt.Sprintf("Replication mode of the service: either '%s' (a specified number of containers across "+
|
||||||
"the machines) or %q (one container on every machine).",
|
"the machines) or '%s' (one container on every machine).",
|
||||||
api.ServiceModeReplicated, api.ServiceModeGlobal))
|
api.ServiceModeReplicated, api.ServiceModeGlobal))
|
||||||
|
cmd.Flags().StringSliceVarP(&opts.machines, "machine", "m", nil,
|
||||||
|
"Placement constraint by machine name, limiting which machines the service can run on. Can be specified "+
|
||||||
|
"multiple times or as a comma-separated list of machine names. (default is any suitable machine)")
|
||||||
cmd.Flags().StringVarP(&opts.name, "name", "n", "",
|
cmd.Flags().StringVarP(&opts.name, "name", "n", "",
|
||||||
"Assign a name to the service. A random name is generated if not specified.")
|
"Assign a name to the service. A random name is generated if not specified.")
|
||||||
cmd.Flags().StringSliceVarP(&opts.publish, "publish", "p", nil,
|
cmd.Flags().StringSliceVarP(&opts.publish, "publish", "p", nil,
|
||||||
"Publish a service port to make it accessible outside the cluster. Can be specified multiple times.\n"+
|
"Publish a service port to make it accessible outside the cluster. Can be specified multiple times.\n"+
|
||||||
"Format: [hostname:][load_balancer_port:]container_port[/protocol] or [host_ip:]:host_port:container_port[/protocol]@host\n"+
|
"Format: [hostname:][load_balancer_port:]container_port[/protocol] or [host_ip:]:host_port:container_port[/protocol]@host\n"+
|
||||||
"Supported protocols: tcp, udp, http, https (default is tcp). If a hostname for http(s) port is not specified, a random hostname is generated.\n"+
|
"Supported protocols: tcp, udp, http, https (default is tcp). If a hostname for http(s) port is not specified,\n"+
|
||||||
|
"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 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 load balancer with custom hostname\n"+
|
||||||
" -p 9000:8080 Publish port 8080 as TCP port 9000 via load balancer\n"+
|
" -p 9000:8080 Publish port 8080 as TCP port 9000 via load balancer\n"+
|
||||||
" -p 53:5353/udp@host Bind UDP port 5353 to host port 53")
|
" -p 53:5353/udp@host Bind UDP port 5353 to host port 53")
|
||||||
|
cmd.Flags().StringVar(&opts.pull, "pull", api.PullPolicyMissing,
|
||||||
|
fmt.Sprintf("Pull image from the registry before running service containers ('%s', '%s', '%s').",
|
||||||
|
api.PullPolicyAlways, api.PullPolicyMissing, api.PullPolicyNever))
|
||||||
|
cmd.Flags().UintVar(&opts.replicas, "replicas", 1,
|
||||||
|
"Number of containers to run for the service. Only valid for a replicated service.")
|
||||||
cmd.Flags().StringSliceVarP(&opts.volumes, "volume", "v", nil,
|
cmd.Flags().StringSliceVarP(&opts.volumes, "volume", "v", nil,
|
||||||
"Bind mount a host file or directory into a service container using the format "+
|
"Bind mount a host file or directory into a service container using the format "+
|
||||||
"/host/path:/container/path[:ro]. Can be specified multiple times.")
|
"/host/path:/container/path[:ro]. Can be specified multiple times.")
|
||||||
@@ -72,9 +88,38 @@ func NewRunCommand() *cobra.Command {
|
|||||||
|
|
||||||
func run(ctx context.Context, uncli *cli.CLI, opts runOptions) error {
|
func run(ctx context.Context, uncli *cli.CLI, opts runOptions) error {
|
||||||
switch opts.mode {
|
switch opts.mode {
|
||||||
case "", api.ServiceModeReplicated, api.ServiceModeGlobal:
|
case api.ServiceModeReplicated, api.ServiceModeGlobal:
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("invalid replication mode: %q", opts.mode)
|
return fmt.Errorf("invalid replication mode: '%s'", opts.mode)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch opts.pull {
|
||||||
|
case api.PullPolicyAlways, api.PullPolicyMissing, api.PullPolicyNever:
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("invalid pull policy: '%s'", opts.pull)
|
||||||
|
}
|
||||||
|
|
||||||
|
var machineFilter deploy.MachineFilter
|
||||||
|
if len(opts.machines) > 0 {
|
||||||
|
var machines []string
|
||||||
|
for _, value := range opts.machines {
|
||||||
|
if value == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
mlist := strings.Split(value, ",")
|
||||||
|
for _, m := range mlist {
|
||||||
|
if m = strings.TrimSpace(m); m != "" {
|
||||||
|
machines = append(machines, m)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(machines) > 0 {
|
||||||
|
machineFilter = func(m *pb.MachineInfo) bool {
|
||||||
|
return slices.Contains(machines, m.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ports := make([]api.PortSpec, len(opts.publish))
|
ports := make([]api.PortSpec, len(opts.publish))
|
||||||
@@ -89,27 +134,52 @@ func run(ctx context.Context, uncli *cli.CLI, opts runOptions) error {
|
|||||||
|
|
||||||
spec := api.ServiceSpec{
|
spec := api.ServiceSpec{
|
||||||
Container: api.ContainerSpec{
|
Container: api.ContainerSpec{
|
||||||
Command: opts.command,
|
Command: opts.command,
|
||||||
Image: opts.image,
|
Image: opts.image,
|
||||||
Volumes: opts.volumes,
|
PullPolicy: opts.pull,
|
||||||
|
Volumes: opts.volumes,
|
||||||
},
|
},
|
||||||
Mode: opts.mode,
|
Mode: opts.mode,
|
||||||
Name: opts.name,
|
Name: opts.name,
|
||||||
Ports: ports,
|
Ports: ports,
|
||||||
|
Replicas: opts.replicas,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Overwrite the default ENTRYPOINT of the image or reset it if an empty string is passed.
|
||||||
|
if opts.entrypoint != "" {
|
||||||
|
spec.Container.Entrypoint = []string{opts.entrypoint}
|
||||||
|
} else if opts.entrypointChanged {
|
||||||
|
spec.Container.Entrypoint = []string{""}
|
||||||
|
}
|
||||||
|
|
||||||
if err := spec.Validate(); err != nil {
|
if err := spec.Validate(); err != nil {
|
||||||
return fmt.Errorf("invalid service configuration: %w", err)
|
return fmt.Errorf("invalid service configuration: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
client, err := uncli.ConnectCluster(ctx, opts.cluster)
|
clusterClient, err := uncli.ConnectCluster(ctx, opts.cluster)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("connect to cluster: %w", err)
|
return fmt.Errorf("connect to cluster: %w", err)
|
||||||
}
|
}
|
||||||
defer client.Close()
|
defer clusterClient.Close()
|
||||||
|
|
||||||
if _, err = client.RunService(ctx, spec); err != nil {
|
resp, err := clusterClient.RunService(ctx, spec, machineFilter)
|
||||||
|
if err != nil {
|
||||||
return fmt.Errorf("run service: %w", err)
|
return fmt.Errorf("run service: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
svc, err := clusterClient.InspectService(ctx, resp.ID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("inspect service: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
endpoints := svc.Endpoints()
|
||||||
|
if len(endpoints) > 0 {
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Printf("%s endpoints:\n", svc.Name)
|
||||||
|
for _, endpoint := range endpoints {
|
||||||
|
fmt.Printf(" • %s\n", endpoint)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,152 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/huh"
|
||||||
|
"github.com/docker/compose/v2/pkg/progress"
|
||||||
|
"github.com/psviderski/uncloud/internal/cli"
|
||||||
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
type scaleOptions struct {
|
||||||
|
service string
|
||||||
|
replicas uint
|
||||||
|
cluster string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewScaleCommand() *cobra.Command {
|
||||||
|
opts := scaleOptions{}
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "scale SERVICE REPLICAS",
|
||||||
|
Short: "Scale a replicated service by changing the number of replicas.",
|
||||||
|
Long: "Scale a replicated service by changing the number of replicas. Scaling down requires confirmation.",
|
||||||
|
Args: cobra.ExactArgs(2),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
uncli := cmd.Context().Value("cli").(*cli.CLI)
|
||||||
|
|
||||||
|
opts.service = args[0]
|
||||||
|
replicas, err := strconv.ParseUint(args[1], 10, 0)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid number of replicas: %w", err)
|
||||||
|
}
|
||||||
|
opts.replicas = uint(replicas)
|
||||||
|
|
||||||
|
return scale(cmd.Context(), uncli, opts)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Flags().StringVarP(
|
||||||
|
&opts.cluster, "cluster", "c", "",
|
||||||
|
"Name of the cluster. (default is the current cluster)",
|
||||||
|
)
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func scale(ctx context.Context, uncli *cli.CLI, opts scaleOptions) error {
|
||||||
|
if opts.replicas == 0 {
|
||||||
|
return fmt.Errorf(
|
||||||
|
"scaling to zero replicas is not supported. This would effectively remove the service without preserving "+
|
||||||
|
"its configuration, making it impossible to scale back up. Uncloud derives the service configuration "+
|
||||||
|
"from existing containers. Use 'uc rm %s' instead if you want to remove the service",
|
||||||
|
opts.service,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
clusterClient, err := uncli.ConnectCluster(ctx, opts.cluster)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("connect to cluster: %w", err)
|
||||||
|
}
|
||||||
|
defer clusterClient.Close()
|
||||||
|
|
||||||
|
svc, err := clusterClient.InspectService(ctx, opts.service)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("inspect service '%s': %w", opts.service, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if svc.Mode != api.ServiceModeReplicated {
|
||||||
|
return fmt.Errorf("scaling is only supported for services in %s mode, service '%s' is in %s mode",
|
||||||
|
api.ServiceModeReplicated, svc.Name, svc.Mode)
|
||||||
|
}
|
||||||
|
|
||||||
|
currentReplicas := uint(len(svc.Containers))
|
||||||
|
|
||||||
|
if currentReplicas == opts.replicas {
|
||||||
|
fmt.Printf("Service '%s' already has %d replicas. No changes required.\n", svc.Name, currentReplicas)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Check if all containers have the same spec. If not, prompt user to choose which one to scale.
|
||||||
|
// This can happen if a service deployment failed midway and some containers were not updated.
|
||||||
|
spec := svc.Containers[0].Container.ServiceSpec
|
||||||
|
spec.Replicas = opts.replicas
|
||||||
|
deployment := clusterClient.NewDeployment(spec, nil)
|
||||||
|
plan, err := deployment.Plan(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("plan deployment: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(plan.Operations) == 0 {
|
||||||
|
fmt.Printf("Service '%s' is already scaled to %d replicas.\n", svc.Name, opts.replicas)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if opts.replicas < currentReplicas {
|
||||||
|
// Initialise a machine and container name resolver to properly format the plan output.
|
||||||
|
resolver, err := clusterClient.ServiceOperationNameResolver(ctx, svc)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("create machine and container name resolver for service operations: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Scaling plan for service %s (%d → %d replicas):\n", svc.Name, currentReplicas, opts.replicas)
|
||||||
|
fmt.Println(plan.Format(resolver))
|
||||||
|
fmt.Println()
|
||||||
|
|
||||||
|
// Ask for confirmation before scaling down as it may cause data loss.
|
||||||
|
confirmed, err := confirm()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("confirm scaling: %w", err)
|
||||||
|
}
|
||||||
|
if !confirmed {
|
||||||
|
fmt.Println("Cancelled. No changes were made.")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
title := fmt.Sprintf("Scaling service %s (%d → %d replicas)", svc.Name, currentReplicas, opts.replicas)
|
||||||
|
err = progress.RunWithTitle(ctx, func(ctx context.Context) error {
|
||||||
|
if _, err = deployment.Run(ctx); err != nil {
|
||||||
|
return fmt.Errorf("deploy service: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}, uncli.ProgressOut(), title)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func confirm() (bool, error) {
|
||||||
|
var confirmed bool
|
||||||
|
form := huh.NewForm(
|
||||||
|
huh.NewGroup(
|
||||||
|
huh.NewConfirm().
|
||||||
|
Title(
|
||||||
|
"Do you want to continue?",
|
||||||
|
).
|
||||||
|
Affirmative("Yes!").
|
||||||
|
Negative("No").
|
||||||
|
Value(&confirmed),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
if err := form.Run(); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return confirmed, nil
|
||||||
|
}
|
||||||
@@ -7,9 +7,9 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"syscall"
|
"syscall"
|
||||||
"uncloud/internal/daemon"
|
"github.com/psviderski/uncloud/internal/daemon"
|
||||||
"uncloud/internal/log"
|
"github.com/psviderski/uncloud/internal/log"
|
||||||
"uncloud/internal/machine"
|
"github.com/psviderski/uncloud/internal/machine"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
# User Guide
|
||||||
|
|
||||||
|
## Initialising a cluster
|
||||||
|
|
||||||
|
To begin setting up a new Uncloud cluster, create the desired nodes. Ensure that their firewall allows the required ports.
|
||||||
|
|
||||||
|
### Ports
|
||||||
|
|
||||||
|
Nodes running Uncloud with a standard configuration must have the following inbound ports allowed in the firewall:
|
||||||
|
|
||||||
|
* 51820/udp (WireGuard meshnet)
|
||||||
|
* 22/tcp (default SSH management port)
|
||||||
|
* 80/tcp (required for challenge if uncloud.run DNS is enabled, pass --no-dns during init to disable)
|
||||||
|
|
||||||
|
In addition, any ports for any workloads you want to expose will need to be open.
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
|
||||||
|
Uncloud stores its configuration in `~/.config/uncloud/config.toml`. If you wish to reinitialise a cluster, simply remove it from this config.
|
||||||
|
|
||||||
|
### Initialisation
|
||||||
|
|
||||||
|
Begin by initialising the first node in your cluster with `uc machine init [USER@HOST:PORT]`. If you do not have a need for Caddy reverse proxy, you may disable this feature with `--no-caddy`.
|
||||||
|
|
||||||
|
This command will idempotently install Docker, uncloudd, uncloud-corrosion. If Caddy is enabled, it will set up a reverse proxy. If Uncloud DNS is enabled, it will create a DNS A record for the machine's public IP address under `*.[CLUSTER ID].cluster.uncloud.run`.
|
||||||
|
|
||||||
|
If you wish to uninstall Uncloud and its components, run `uncloud-uninstall`.
|
||||||
|
|
||||||
|
### Adding a node
|
||||||
|
|
||||||
|
Just like the initialisation of the first node, a node can be added to the cluster with `uc machine add`.
|
||||||
|
|
||||||
|
### DNS
|
||||||
|
|
||||||
|
Uncloud (uncloud.run) DNS can be managed with the `uc dns` subcommand.
|
||||||
|
|
||||||
|
* To reserve a domain name, run `uc dns reserve`
|
||||||
|
* To release a domain name, run `uc dns release`.
|
||||||
|
* To see the domain name, run `uc dns show`
|
||||||
|
|
||||||
|
### Running a service
|
||||||
|
|
||||||
|
Services on an Uncloud cluster can be managed with `uc service`.
|
||||||
|
|
||||||
|
You can run a service with two replicas that expose port 80 like the following:
|
||||||
|
|
||||||
|
```
|
||||||
|
uc service run -p 80/http --replicas 2 nginxdemos/hello
|
||||||
|
```
|
||||||
|
|
||||||
|
This requires the Caddy reverse proxy to be deployed. If it wasn't previously, it can be deployed with `uc caddy deploy`.
|
||||||
|
|
||||||
|
Since this doesn't specify a service name, a random one will be generated.
|
||||||
|
|
||||||
|
The service can be deleted by its service name:
|
||||||
|
|
||||||
|
```
|
||||||
|
uc service rm hello-gsdo
|
||||||
|
```
|
||||||
@@ -11,7 +11,7 @@ import (
|
|||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"time"
|
"time"
|
||||||
"uncloud/internal/machine/network"
|
"github.com/psviderski/uncloud/internal/machine/network"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|||||||
@@ -1,23 +1,27 @@
|
|||||||
module uncloud
|
module github.com/psviderski/uncloud
|
||||||
|
|
||||||
go 1.23
|
go 1.23.0
|
||||||
|
|
||||||
toolchain go1.23.2
|
toolchain go1.23.2
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/BurntSushi/toml v1.4.0
|
github.com/BurntSushi/toml v1.4.0
|
||||||
|
github.com/Masterminds/semver v1.5.0
|
||||||
github.com/Masterminds/squirrel v1.5.4
|
github.com/Masterminds/squirrel v1.5.4
|
||||||
github.com/caddyserver/caddy/v2 v2.8.4
|
github.com/caddyserver/caddy/v2 v2.8.4
|
||||||
github.com/cenkalti/backoff/v4 v4.3.0
|
github.com/cenkalti/backoff/v4 v4.3.0
|
||||||
github.com/charmbracelet/huh v0.6.0
|
github.com/charmbracelet/huh v0.6.0
|
||||||
|
github.com/compose-spec/compose-go/v2 v2.4.5
|
||||||
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf
|
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf
|
||||||
|
github.com/deckarep/golang-set/v2 v2.8.0
|
||||||
github.com/dgraph-io/badger/v3 v3.2103.5
|
github.com/dgraph-io/badger/v3 v3.2103.5
|
||||||
github.com/distribution/reference v0.6.0
|
github.com/distribution/reference v0.6.0
|
||||||
github.com/docker/cli v27.4.0-rc.4+incompatible
|
github.com/docker/cli v27.5.0+incompatible
|
||||||
github.com/docker/compose/v2 v2.31.0
|
github.com/docker/compose/v2 v2.31.0
|
||||||
github.com/docker/docker v27.4.0-rc.2+incompatible
|
github.com/docker/docker v27.4.0-rc.2+incompatible
|
||||||
github.com/docker/go-connections v0.5.0
|
github.com/docker/go-connections v0.5.0
|
||||||
github.com/docker/go-units v0.5.0
|
github.com/docker/go-units v0.5.0
|
||||||
|
github.com/google/go-containerregistry v0.20.2
|
||||||
github.com/hashicorp/memberlist v0.5.1
|
github.com/hashicorp/memberlist v0.5.1
|
||||||
github.com/hashicorp/serf v0.10.1
|
github.com/hashicorp/serf v0.10.1
|
||||||
github.com/ipfs/boxo v0.21.0
|
github.com/ipfs/boxo v0.21.0
|
||||||
@@ -27,7 +31,9 @@ require (
|
|||||||
github.com/ipfs/go-ds-crdt v0.5.2
|
github.com/ipfs/go-ds-crdt v0.5.2
|
||||||
github.com/ipfs/go-ipld-format v0.6.0
|
github.com/ipfs/go-ipld-format v0.6.0
|
||||||
github.com/ipfs/go-log/v2 v2.5.1
|
github.com/ipfs/go-log/v2 v2.5.1
|
||||||
|
github.com/jmoiron/sqlx v1.4.0
|
||||||
github.com/lmittmann/tint v1.0.5
|
github.com/lmittmann/tint v1.0.5
|
||||||
|
github.com/opencontainers/go-digest v1.0.0
|
||||||
github.com/opencontainers/image-spec v1.1.0
|
github.com/opencontainers/image-spec v1.1.0
|
||||||
github.com/siderolabs/discovery-api v0.1.4
|
github.com/siderolabs/discovery-api v0.1.4
|
||||||
github.com/siderolabs/discovery-client v0.1.9
|
github.com/siderolabs/discovery-client v0.1.9
|
||||||
@@ -37,15 +43,16 @@ require (
|
|||||||
github.com/vishvananda/netlink v1.3.0
|
github.com/vishvananda/netlink v1.3.0
|
||||||
go.uber.org/zap v1.27.0
|
go.uber.org/zap v1.27.0
|
||||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba
|
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba
|
||||||
golang.org/x/crypto v0.31.0
|
golang.org/x/crypto v0.32.0
|
||||||
golang.org/x/net v0.32.0
|
golang.org/x/net v0.34.0
|
||||||
golang.org/x/sync v0.10.0
|
golang.org/x/sync v0.10.0
|
||||||
golang.org/x/sys v0.28.0
|
golang.org/x/sys v0.31.0
|
||||||
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173
|
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173
|
||||||
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6
|
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20241007155032-5fefd90f89a9
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576
|
||||||
google.golang.org/grpc v1.68.0
|
google.golang.org/grpc v1.68.1
|
||||||
google.golang.org/protobuf v1.36.0
|
google.golang.org/protobuf v1.36.3
|
||||||
|
modernc.org/sqlite v1.36.3
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
@@ -81,7 +88,6 @@ require (
|
|||||||
github.com/charmbracelet/x/term v0.2.0 // indirect
|
github.com/charmbracelet/x/term v0.2.0 // indirect
|
||||||
github.com/chzyer/readline v1.5.1 // indirect
|
github.com/chzyer/readline v1.5.1 // indirect
|
||||||
github.com/cloudflare/cfssl v1.6.4 // indirect
|
github.com/cloudflare/cfssl v1.6.4 // indirect
|
||||||
github.com/compose-spec/compose-go/v2 v2.4.5 // indirect
|
|
||||||
github.com/containerd/console v1.0.4 // indirect
|
github.com/containerd/console v1.0.4 // indirect
|
||||||
github.com/containerd/containerd v1.7.24 // indirect
|
github.com/containerd/containerd v1.7.24 // indirect
|
||||||
github.com/containerd/containerd/api v1.7.19 // indirect
|
github.com/containerd/containerd/api v1.7.19 // indirect
|
||||||
@@ -89,9 +95,10 @@ require (
|
|||||||
github.com/containerd/errdefs v0.3.0 // indirect
|
github.com/containerd/errdefs v0.3.0 // indirect
|
||||||
github.com/containerd/log v0.1.0 // indirect
|
github.com/containerd/log v0.1.0 // indirect
|
||||||
github.com/containerd/platforms v0.2.1 // indirect
|
github.com/containerd/platforms v0.2.1 // indirect
|
||||||
|
github.com/containerd/stargz-snapshotter/estargz v0.16.3 // indirect
|
||||||
github.com/containerd/ttrpc v1.2.5 // indirect
|
github.com/containerd/ttrpc v1.2.5 // indirect
|
||||||
github.com/containerd/typeurl/v2 v2.2.0 // indirect
|
github.com/containerd/typeurl/v2 v2.2.0 // indirect
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect
|
github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect
|
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect
|
||||||
github.com/dgraph-io/badger v1.6.2 // indirect
|
github.com/dgraph-io/badger v1.6.2 // indirect
|
||||||
@@ -115,7 +122,7 @@ require (
|
|||||||
github.com/go-logfmt/logfmt v0.6.0 // indirect
|
github.com/go-logfmt/logfmt v0.6.0 // indirect
|
||||||
github.com/go-logr/logr v1.4.2 // indirect
|
github.com/go-logr/logr v1.4.2 // indirect
|
||||||
github.com/go-logr/stdr v1.2.2 // indirect
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
github.com/go-sql-driver/mysql v1.7.1 // indirect
|
github.com/go-sql-driver/mysql v1.8.1 // indirect
|
||||||
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
|
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
|
||||||
github.com/go-viper/mapstructure/v2 v2.0.0 // indirect
|
github.com/go-viper/mapstructure/v2 v2.0.0 // indirect
|
||||||
github.com/godbus/dbus/v5 v5.1.0 // indirect
|
github.com/godbus/dbus/v5 v5.1.0 // indirect
|
||||||
@@ -129,11 +136,11 @@ require (
|
|||||||
github.com/google/cel-go v0.20.1 // indirect
|
github.com/google/cel-go v0.20.1 // indirect
|
||||||
github.com/google/flatbuffers v24.3.25+incompatible // indirect
|
github.com/google/flatbuffers v24.3.25+incompatible // indirect
|
||||||
github.com/google/go-cmp v0.6.0 // indirect
|
github.com/google/go-cmp v0.6.0 // indirect
|
||||||
github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad // indirect
|
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e // indirect
|
||||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
|
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
github.com/gorilla/mux v1.8.1 // indirect
|
github.com/gorilla/mux v1.8.1 // indirect
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0 // indirect
|
||||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||||
github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
|
github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
|
||||||
github.com/hashicorp/go-msgpack v0.5.5 // indirect
|
github.com/hashicorp/go-msgpack v0.5.5 // indirect
|
||||||
@@ -194,6 +201,7 @@ require (
|
|||||||
github.com/minio/sha256-simd v1.0.1 // indirect
|
github.com/minio/sha256-simd v1.0.1 // indirect
|
||||||
github.com/mitchellh/cli v1.1.5 // indirect
|
github.com/mitchellh/cli v1.1.5 // indirect
|
||||||
github.com/mitchellh/copystructure v1.2.0 // indirect
|
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||||
|
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||||
github.com/mitchellh/go-ps v1.0.0 // indirect
|
github.com/mitchellh/go-ps v1.0.0 // indirect
|
||||||
github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect
|
github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect
|
||||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||||
@@ -220,8 +228,8 @@ require (
|
|||||||
github.com/multiformats/go-multistream v0.5.0 // indirect
|
github.com/multiformats/go-multistream v0.5.0 // indirect
|
||||||
github.com/multiformats/go-varint v0.0.7 // indirect
|
github.com/multiformats/go-varint v0.0.7 // indirect
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||||
|
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||||
github.com/onsi/ginkgo/v2 v2.22.0 // indirect
|
github.com/onsi/ginkgo/v2 v2.22.0 // indirect
|
||||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
|
||||||
github.com/pelletier/go-toml v1.9.5 // indirect
|
github.com/pelletier/go-toml v1.9.5 // indirect
|
||||||
github.com/pires/go-proxyproto v0.7.0 // indirect
|
github.com/pires/go-proxyproto v0.7.0 // indirect
|
||||||
github.com/pkg/errors v0.9.1 // indirect
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
@@ -235,6 +243,7 @@ require (
|
|||||||
github.com/prometheus/procfs v0.15.1 // indirect
|
github.com/prometheus/procfs v0.15.1 // indirect
|
||||||
github.com/quic-go/qpack v0.5.1 // indirect
|
github.com/quic-go/qpack v0.5.1 // indirect
|
||||||
github.com/quic-go/quic-go v0.48.2 // indirect
|
github.com/quic-go/quic-go v0.48.2 // indirect
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
github.com/rivo/uniseg v0.4.7 // indirect
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
github.com/rs/xid v1.5.0 // indirect
|
github.com/rs/xid v1.5.0 // indirect
|
||||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||||
@@ -263,22 +272,27 @@ require (
|
|||||||
github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea // indirect
|
github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea // indirect
|
||||||
github.com/tonistiigi/vt100 v0.0.0-20240514184818-90bafcd6abab // indirect
|
github.com/tonistiigi/vt100 v0.0.0-20240514184818-90bafcd6abab // indirect
|
||||||
github.com/urfave/cli v1.22.16 // indirect
|
github.com/urfave/cli v1.22.16 // indirect
|
||||||
|
github.com/vbatts/tar-split v0.11.6 // indirect
|
||||||
github.com/vishvananda/netns v0.0.4 // indirect
|
github.com/vishvananda/netns v0.0.4 // indirect
|
||||||
|
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
|
||||||
|
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
|
||||||
|
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
|
||||||
github.com/zeebo/blake3 v0.2.4 // indirect
|
github.com/zeebo/blake3 v0.2.4 // indirect
|
||||||
go.etcd.io/bbolt v1.3.11 // indirect
|
go.etcd.io/bbolt v1.3.11 // indirect
|
||||||
go.opencensus.io v0.24.0 // indirect
|
go.opencensus.io v0.24.0 // indirect
|
||||||
|
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 // indirect
|
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 // indirect
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.46.1 // indirect
|
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.46.1 // indirect
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.52.0 // indirect
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 // indirect
|
||||||
go.opentelemetry.io/otel v1.32.0 // indirect
|
go.opentelemetry.io/otel v1.33.0 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.44.0 // indirect
|
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.44.0 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.27.0 // indirect
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0 // indirect
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0 // indirect
|
||||||
go.opentelemetry.io/otel/metric v1.32.0 // indirect
|
go.opentelemetry.io/otel/metric v1.33.0 // indirect
|
||||||
go.opentelemetry.io/otel/sdk v1.32.0 // indirect
|
go.opentelemetry.io/otel/sdk v1.33.0 // indirect
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.32.0 // indirect
|
go.opentelemetry.io/otel/sdk/metric v1.32.0 // indirect
|
||||||
go.opentelemetry.io/otel/trace v1.32.0 // indirect
|
go.opentelemetry.io/otel/trace v1.33.0 // indirect
|
||||||
go.opentelemetry.io/proto/otlp v1.3.1 // indirect
|
go.opentelemetry.io/proto/otlp v1.4.0 // indirect
|
||||||
go.step.sm/cli-utils v0.9.0 // indirect
|
go.step.sm/cli-utils v0.9.0 // indirect
|
||||||
go.step.sm/crypto v0.45.0 // indirect
|
go.step.sm/crypto v0.45.0 // indirect
|
||||||
go.step.sm/linkedca v0.20.1 // indirect
|
go.step.sm/linkedca v0.20.1 // indirect
|
||||||
@@ -289,15 +303,18 @@ require (
|
|||||||
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.22.0 // indirect
|
||||||
golang.org/x/term v0.27.0 // indirect
|
golang.org/x/term v0.28.0 // indirect
|
||||||
golang.org/x/text v0.21.0 // indirect
|
golang.org/x/text v0.21.0 // indirect
|
||||||
golang.org/x/time v0.8.0 // indirect
|
golang.org/x/time v0.8.0 // indirect
|
||||||
golang.org/x/tools v0.28.0 // indirect
|
golang.org/x/tools v0.29.0 // indirect
|
||||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
|
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
|
||||||
google.golang.org/genproto v0.0.0-20240401170217-c3f982113cda // indirect
|
google.golang.org/genproto v0.0.0-20240401170217-c3f982113cda // indirect
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 // indirect
|
google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259 // indirect
|
gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259 // indirect
|
||||||
howett.net/plist v1.0.0 // indirect
|
howett.net/plist v1.0.0 // indirect
|
||||||
lukechampine.com/blake3 v1.3.0 // indirect
|
lukechampine.com/blake3 v1.3.0 // indirect
|
||||||
|
modernc.org/libc v1.61.13 // indirect
|
||||||
|
modernc.org/mathutil v1.7.1 // indirect
|
||||||
|
modernc.org/memory v1.8.2 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ
|
|||||||
github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
|
github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
|
||||||
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
|
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
|
||||||
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
|
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
|
||||||
|
github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww=
|
||||||
|
github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
|
||||||
github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
|
github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
|
||||||
github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
|
github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
|
||||||
github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0=
|
github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0=
|
||||||
@@ -199,9 +201,8 @@ github.com/containerd/nydus-snapshotter v0.14.0 h1:6/eAi6d7MjaeLLuMO8Udfe5GVsDud
|
|||||||
github.com/containerd/nydus-snapshotter v0.14.0/go.mod h1:TT4jv2SnIDxEBu4H2YOvWQHPOap031ydTaHTuvc5VQk=
|
github.com/containerd/nydus-snapshotter v0.14.0/go.mod h1:TT4jv2SnIDxEBu4H2YOvWQHPOap031ydTaHTuvc5VQk=
|
||||||
github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A=
|
github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A=
|
||||||
github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw=
|
github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw=
|
||||||
github.com/containerd/stargz-snapshotter v0.15.1 h1:fpsP4kf/Z4n2EYnU0WT8ZCE3eiKDwikDhL6VwxIlgeA=
|
github.com/containerd/stargz-snapshotter/estargz v0.16.3 h1:7evrXtoh1mSbGj/pfRccTampEyKpjpOnS3CyiV1Ebr8=
|
||||||
github.com/containerd/stargz-snapshotter/estargz v0.15.1 h1:eXJjw9RbkLFgioVaTG+G/ZW/0kEe2oEKCdS/ZxIyoCU=
|
github.com/containerd/stargz-snapshotter/estargz v0.16.3/go.mod h1:uyr4BfYfOj3G9WBVE8cOlQmXAbPN9VEQpBBeJIuOipU=
|
||||||
github.com/containerd/stargz-snapshotter/estargz v0.15.1/go.mod h1:gr2RNwukQ/S9Nv33Lt6UC7xEx58C+LHRdoqbEKjz1Kk=
|
|
||||||
github.com/containerd/ttrpc v1.2.5 h1:IFckT1EFQoFBMG4c3sMdT8EP3/aKfumK1msY+Ze4oLU=
|
github.com/containerd/ttrpc v1.2.5 h1:IFckT1EFQoFBMG4c3sMdT8EP3/aKfumK1msY+Ze4oLU=
|
||||||
github.com/containerd/ttrpc v1.2.5/go.mod h1:YCXHsb32f+Sq5/72xHubdiJRQY9inL4a4ZQrAbN1q9o=
|
github.com/containerd/ttrpc v1.2.5/go.mod h1:YCXHsb32f+Sq5/72xHubdiJRQY9inL4a4ZQrAbN1q9o=
|
||||||
github.com/containerd/typeurl/v2 v2.2.0 h1:6NBDbQzr7I5LHgp34xAXYF5DOTQDn05X58lsPEmzLso=
|
github.com/containerd/typeurl/v2 v2.2.0 h1:6NBDbQzr7I5LHgp34xAXYF5DOTQDn05X58lsPEmzLso=
|
||||||
@@ -218,8 +219,9 @@ github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSV
|
|||||||
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
|
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc=
|
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||||
|
github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0=
|
||||||
|
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||||
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
|
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
|
||||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||||
github.com/creack/pty v1.1.21 h1:1/QdRyBaHHJP61QkWMXlOIBfsgdDeeKfK8SYVUWJKf0=
|
github.com/creack/pty v1.1.21 h1:1/QdRyBaHHJP61QkWMXlOIBfsgdDeeKfK8SYVUWJKf0=
|
||||||
@@ -231,6 +233,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
|
|||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c h1:pFUpOrbxDR6AkioZ1ySsx5yxlDQZ8stG2b88gTPxgJU=
|
github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c h1:pFUpOrbxDR6AkioZ1ySsx5yxlDQZ8stG2b88gTPxgJU=
|
||||||
github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c/go.mod h1:6UhI8N9EjYm1c2odKpFpAYeR8dsBeM7PtzQhRgxRr9U=
|
github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c/go.mod h1:6UhI8N9EjYm1c2odKpFpAYeR8dsBeM7PtzQhRgxRr9U=
|
||||||
|
github.com/deckarep/golang-set/v2 v2.8.0 h1:swm0rlPCmdWn9mESxKOjWk8hXSqoxOp+ZlfuyaAdFlQ=
|
||||||
|
github.com/deckarep/golang-set/v2 v2.8.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4=
|
||||||
github.com/decred/dcrd/crypto/blake256 v1.0.1 h1:7PltbUIQB7u/FfZ39+DGa/ShuMyJ5ilcvdfma9wOH6Y=
|
github.com/decred/dcrd/crypto/blake256 v1.0.1 h1:7PltbUIQB7u/FfZ39+DGa/ShuMyJ5ilcvdfma9wOH6Y=
|
||||||
github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo=
|
github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo=
|
||||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnNEcHYvcCuK6dPZSg=
|
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnNEcHYvcCuK6dPZSg=
|
||||||
@@ -253,8 +257,8 @@ github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5Qvfr
|
|||||||
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||||
github.com/docker/buildx v0.18.0 h1:rSauXHeJt90NvtXrLK5J992Eb0UPJZs2vV3u1zTf1nE=
|
github.com/docker/buildx v0.18.0 h1:rSauXHeJt90NvtXrLK5J992Eb0UPJZs2vV3u1zTf1nE=
|
||||||
github.com/docker/buildx v0.18.0/go.mod h1:JGNSshOhHs5FhG3u51jXUf4lLOeD2QBIlJ2vaRB67p4=
|
github.com/docker/buildx v0.18.0/go.mod h1:JGNSshOhHs5FhG3u51jXUf4lLOeD2QBIlJ2vaRB67p4=
|
||||||
github.com/docker/cli v27.4.0-rc.4+incompatible h1:nnrHCfd5eODviSGzCpIyYFcJovrq+/dVW4K4oqQu7WA=
|
github.com/docker/cli v27.5.0+incompatible h1:aMphQkcGtpHixwwhAXJT1rrK/detk2JIvDaFkLctbGM=
|
||||||
github.com/docker/cli v27.4.0-rc.4+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
github.com/docker/cli v27.5.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||||
github.com/docker/compose/v2 v2.31.0 h1:8Sm0c4MjIhksguxIA5koYMXoTJDAp/CaZ1cdZrMvMdw=
|
github.com/docker/compose/v2 v2.31.0 h1:8Sm0c4MjIhksguxIA5koYMXoTJDAp/CaZ1cdZrMvMdw=
|
||||||
github.com/docker/compose/v2 v2.31.0/go.mod h1:oQq3UDEdsnB3AUO72AxaoeLbkCgmUu1+8tLzvmphmXA=
|
github.com/docker/compose/v2 v2.31.0/go.mod h1:oQq3UDEdsnB3AUO72AxaoeLbkCgmUu1+8tLzvmphmXA=
|
||||||
github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
|
github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
|
||||||
@@ -330,8 +334,8 @@ github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ4
|
|||||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||||
github.com/go-sql-driver/mysql v1.3.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
github.com/go-sql-driver/mysql v1.3.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
||||||
github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI=
|
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
|
||||||
github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
||||||
github.com/go-stack/stack v1.6.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
github.com/go-stack/stack v1.6.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||||
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
|
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
|
||||||
@@ -395,6 +399,8 @@ github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
|||||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
|
github.com/google/go-containerregistry v0.20.2 h1:B1wPJ1SN/S7pB+ZAimcciVD+r+yV/l/DSArMxlbwseo=
|
||||||
|
github.com/google/go-containerregistry v0.20.2/go.mod h1:z38EKdKh4h7IP2gSfUUqEvalZBqs6AoLeWfUy34nQC8=
|
||||||
github.com/google/go-tpm v0.9.0 h1:sQF6YqWMi+SCXpsmS3fd21oPy/vSddwZry4JnmltHVk=
|
github.com/google/go-tpm v0.9.0 h1:sQF6YqWMi+SCXpsmS3fd21oPy/vSddwZry4JnmltHVk=
|
||||||
github.com/google/go-tpm v0.9.0/go.mod h1:FkNVkc6C+IsvDI9Jw1OveJmxGZUUaKxtrpOS47QWKfU=
|
github.com/google/go-tpm v0.9.0/go.mod h1:FkNVkc6C+IsvDI9Jw1OveJmxGZUUaKxtrpOS47QWKfU=
|
||||||
github.com/google/go-tpm-tools v0.4.4 h1:oiQfAIkc6xTy9Fl5NKTeTJkBTlXdHsxAofmQyxBKY98=
|
github.com/google/go-tpm-tools v0.4.4 h1:oiQfAIkc6xTy9Fl5NKTeTJkBTlXdHsxAofmQyxBKY98=
|
||||||
@@ -404,8 +410,8 @@ github.com/google/go-tspi v0.3.0/go.mod h1:xfMGI3G0PhxCdNVcYr1C4C+EizojDg/TXuX5b
|
|||||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8=
|
github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8=
|
||||||
github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo=
|
github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo=
|
||||||
github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad h1:a6HEuzUHeKH6hwfN/ZoQgRgVIWFJljSWa/zetS2WTvg=
|
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
|
||||||
github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
|
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
||||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||||
github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o=
|
github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o=
|
||||||
github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw=
|
github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw=
|
||||||
@@ -427,8 +433,8 @@ github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
|||||||
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
||||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0=
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0 h1:TmHmbvxPmaegwhDubVz0lICL0J5Ka2vwTzhoePEXsGE=
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k=
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0/go.mod h1:qztMSjm835F2bXf+5HKAPIS5qsmQDqZna/PgVt4rWtI=
|
||||||
github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed h1:5upAirOpQc1Q53c0bnx2ufif5kANL7bfZWcc6VJWJd8=
|
github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed h1:5upAirOpQc1Q53c0bnx2ufif5kANL7bfZWcc6VJWJd8=
|
||||||
github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4=
|
github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4=
|
||||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||||
@@ -593,8 +599,8 @@ github.com/jinzhu/gorm v0.0.0-20170222002820-5409931a1bb8/go.mod h1:Vla75njaFJ8c
|
|||||||
github.com/jinzhu/inflection v0.0.0-20170102125226-1c35d901db3d h1:jRQLvyVGL+iVtDElaEIDdKwpPqUIZJfzkNLV34htpEc=
|
github.com/jinzhu/inflection v0.0.0-20170102125226-1c35d901db3d h1:jRQLvyVGL+iVtDElaEIDdKwpPqUIZJfzkNLV34htpEc=
|
||||||
github.com/jinzhu/inflection v0.0.0-20170102125226-1c35d901db3d/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
github.com/jinzhu/inflection v0.0.0-20170102125226-1c35d901db3d/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||||
github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||||
github.com/jmoiron/sqlx v1.3.3 h1:j82X0bf7oQ27XeqxicSZsTU5suPwKElg3oyxNn43iTk=
|
github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
|
||||||
github.com/jmoiron/sqlx v1.3.3/go.mod h1:2BljVx/86SuTyjE+aPYlHCTNvZrnJXghYGpNiXLBMCQ=
|
github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
|
||||||
github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=
|
github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=
|
||||||
github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
|
github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
|
||||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||||
@@ -635,8 +641,9 @@ github.com/lib/pq v0.0.0-20150723085316-0dad96c0b94f/go.mod h1:5WUZQaWbwv1U+lTRe
|
|||||||
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||||
github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||||
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||||
github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8=
|
|
||||||
github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||||
|
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||||
|
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||||
github.com/libdns/libdns v0.2.2 h1:O6ws7bAfRPaBsgAYt8MDe2HcNBGC29hkZ9MX2eUSX3s=
|
github.com/libdns/libdns v0.2.2 h1:O6ws7bAfRPaBsgAYt8MDe2HcNBGC29hkZ9MX2eUSX3s=
|
||||||
github.com/libdns/libdns v0.2.2/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ=
|
github.com/libdns/libdns v0.2.2/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ=
|
||||||
github.com/libp2p/go-buffer-pool v0.1.0 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6cdF0Y8=
|
github.com/libp2p/go-buffer-pool v0.1.0 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6cdF0Y8=
|
||||||
@@ -697,6 +704,8 @@ github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh
|
|||||||
github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk=
|
github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk=
|
||||||
github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
|
github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
|
||||||
github.com/mattn/go-sqlite3 v1.6.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
github.com/mattn/go-sqlite3 v1.6.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||||
github.com/mdlayher/genetlink v1.3.2 h1:KdrNKe+CTu+IbZnm/GVUMXSqBBLqcGpRDa0xkQy56gw=
|
github.com/mdlayher/genetlink v1.3.2 h1:KdrNKe+CTu+IbZnm/GVUMXSqBBLqcGpRDa0xkQy56gw=
|
||||||
github.com/mdlayher/genetlink v1.3.2/go.mod h1:tcC3pkCrPUGIKKsCsp0B3AdaaKuHtaxoJRz3cc+528o=
|
github.com/mdlayher/genetlink v1.3.2/go.mod h1:tcC3pkCrPUGIKKsCsp0B3AdaaKuHtaxoJRz3cc+528o=
|
||||||
@@ -731,6 +740,7 @@ github.com/mitchellh/cli v1.1.5/go.mod h1:v8+iFts2sPIKUV1ltktPXMCC8fumSKFItNcD2c
|
|||||||
github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
|
github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
|
||||||
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
|
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
|
||||||
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
|
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
|
||||||
|
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||||
github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc=
|
github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc=
|
||||||
github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg=
|
github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg=
|
||||||
@@ -804,6 +814,8 @@ github.com/multiformats/go-varint v0.0.7/go.mod h1:r8PUYw/fD/SjBCiKOoDlGF6QawOEL
|
|||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||||
|
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
|
||||||
|
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
github.com/onsi/ginkgo v1.12.0/go.mod h1:oUhWkIvk5aDxtKvDDuw8gItl8pKl42LzjC9KZE0HfGg=
|
github.com/onsi/ginkgo v1.12.0/go.mod h1:oUhWkIvk5aDxtKvDDuw8gItl8pKl42LzjC9KZE0HfGg=
|
||||||
@@ -920,12 +932,14 @@ github.com/quic-go/webtransport-go v0.8.0 h1:HxSrwun11U+LlmwpgM1kEqIqH90IT4N8auv
|
|||||||
github.com/quic-go/webtransport-go v0.8.0/go.mod h1:N99tjprW432Ut5ONql/aUhSLT0YVSlwHohQsuac9WaM=
|
github.com/quic-go/webtransport-go v0.8.0/go.mod h1:N99tjprW432Ut5ONql/aUhSLT0YVSlwHohQsuac9WaM=
|
||||||
github.com/raulk/go-watchdog v1.3.0 h1:oUmdlHxdkXRJlwfG0O9omj8ukerm8MEQavSiDTEtBsk=
|
github.com/raulk/go-watchdog v1.3.0 h1:oUmdlHxdkXRJlwfG0O9omj8ukerm8MEQavSiDTEtBsk=
|
||||||
github.com/raulk/go-watchdog v1.3.0/go.mod h1:fIvOnLbF0b0ZwkB9YU4mOW9Did//4vPZtDqv66NfsMU=
|
github.com/raulk/go-watchdog v1.3.0/go.mod h1:fIvOnLbF0b0ZwkB9YU4mOW9Did//4vPZtDqv66NfsMU=
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||||
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
|
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||||
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
|
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
||||||
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
|
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
|
||||||
github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc=
|
github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc=
|
||||||
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||||
@@ -1052,8 +1066,8 @@ github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljT
|
|||||||
github.com/urfave/cli v1.22.10/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
|
github.com/urfave/cli v1.22.10/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
|
||||||
github.com/urfave/cli v1.22.16 h1:MH0k6uJxdwdeWQTwhSO42Pwr4YLrNLwBtg1MRgTqPdQ=
|
github.com/urfave/cli v1.22.16 h1:MH0k6uJxdwdeWQTwhSO42Pwr4YLrNLwBtg1MRgTqPdQ=
|
||||||
github.com/urfave/cli v1.22.16/go.mod h1:EeJR6BKodywf4zciqrdw6hpCPk68JO9z5LazXZMn5Po=
|
github.com/urfave/cli v1.22.16/go.mod h1:EeJR6BKodywf4zciqrdw6hpCPk68JO9z5LazXZMn5Po=
|
||||||
github.com/vbatts/tar-split v0.11.5 h1:3bHCTIheBm1qFTcgh9oPu+nNBtX+XJIupG/vacinCts=
|
github.com/vbatts/tar-split v0.11.6 h1:4SjTW5+PU11n6fZenf2IPoV8/tz3AaYHMWjf23envGs=
|
||||||
github.com/vbatts/tar-split v0.11.5/go.mod h1:yZbwRsSeGjusneWgA781EKej9HF8vme8okylkAeNKLk=
|
github.com/vbatts/tar-split v0.11.6/go.mod h1:dqKNtesIOr2j2Qv3W/cHjnvk9I8+G7oAkFDFN6TCBEI=
|
||||||
github.com/vishvananda/netlink v1.3.0 h1:X7l42GfcV4S6E4vHTsw48qbrV+9PVojNfIhZcwQdrZk=
|
github.com/vishvananda/netlink v1.3.0 h1:X7l42GfcV4S6E4vHTsw48qbrV+9PVojNfIhZcwQdrZk=
|
||||||
github.com/vishvananda/netlink v1.3.0/go.mod h1:i6NetklAujEcC6fK0JPjT8qSwWyO0HLn4UKG+hGqeJs=
|
github.com/vishvananda/netlink v1.3.0/go.mod h1:i6NetklAujEcC6fK0JPjT8qSwWyO0HLn4UKG+hGqeJs=
|
||||||
github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8=
|
github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8=
|
||||||
@@ -1064,6 +1078,13 @@ github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0 h1:GDDkbFiaK8jsSD
|
|||||||
github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0/go.mod h1:x6AKhvSSexNrVSrViXSHUEbICjmGXhtgABaHIySUSGw=
|
github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0/go.mod h1:x6AKhvSSexNrVSrViXSHUEbICjmGXhtgABaHIySUSGw=
|
||||||
github.com/weppos/publicsuffix-go v0.15.1-0.20210511084619-b1f36a2d6c0b h1:FsyNrX12e5BkplJq7wKOLk0+C6LZ+KGXvuEcKUYm5ss=
|
github.com/weppos/publicsuffix-go v0.15.1-0.20210511084619-b1f36a2d6c0b h1:FsyNrX12e5BkplJq7wKOLk0+C6LZ+KGXvuEcKUYm5ss=
|
||||||
github.com/weppos/publicsuffix-go v0.15.1-0.20210511084619-b1f36a2d6c0b/go.mod h1:HYux0V0Zi04bHNwOHy4cXJVz/TQjYonnF6aoYhj+3QE=
|
github.com/weppos/publicsuffix-go v0.15.1-0.20210511084619-b1f36a2d6c0b/go.mod h1:HYux0V0Zi04bHNwOHy4cXJVz/TQjYonnF6aoYhj+3QE=
|
||||||
|
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
|
||||||
|
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo=
|
||||||
|
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
|
||||||
|
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
|
||||||
|
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
|
||||||
|
github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
|
||||||
|
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
|
||||||
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
||||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
@@ -1085,32 +1106,34 @@ go.etcd.io/bbolt v1.3.11/go.mod h1:dksAq7YMXoljX0xu6VF5DMZGbhYYoLUalEiSySYAS4I=
|
|||||||
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
|
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
|
||||||
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
|
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
|
||||||
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
|
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
|
||||||
|
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||||
|
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 h1:4Pp6oUg3+e/6M4C0A/3kJ2VYa++dsWVTtGgLVj5xtHg=
|
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 h1:4Pp6oUg3+e/6M4C0A/3kJ2VYa++dsWVTtGgLVj5xtHg=
|
||||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0/go.mod h1:Mjt1i1INqiaoZOMGR1RIUJN+i3ChKoFRqzrRQhlkbs0=
|
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0/go.mod h1:Mjt1i1INqiaoZOMGR1RIUJN+i3ChKoFRqzrRQhlkbs0=
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.46.1 h1:gbhw/u49SS3gkPWiYweQNJGm/uJN5GkI/FrosxSHT7A=
|
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.46.1 h1:gbhw/u49SS3gkPWiYweQNJGm/uJN5GkI/FrosxSHT7A=
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.46.1/go.mod h1:GnOaBaFQ2we3b9AGWJpsBa7v1S5RlQzlC3O7dRMxZhM=
|
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.46.1/go.mod h1:GnOaBaFQ2we3b9AGWJpsBa7v1S5RlQzlC3O7dRMxZhM=
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.52.0 h1:9l89oX4ba9kHbBol3Xin3leYJ+252h0zszDtBwyKe2A=
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 h1:yd02MEjBdJkG3uabWP9apV+OuWRIXGDuJEUJbOHmCFU=
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.52.0/go.mod h1:XLZfZboOJWHNKUv7eH0inh0E9VV6eWDFB/9yJyTLPp0=
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0/go.mod h1:umTcuxiv1n/s/S6/c2AT/g2CQ7u5C59sHDNmfSwgz7Q=
|
||||||
go.opentelemetry.io/otel v1.32.0 h1:WnBN+Xjcteh0zdk01SVqV55d/m62NJLJdIyb4y/WO5U=
|
go.opentelemetry.io/otel v1.33.0 h1:/FerN9bax5LoK51X/sI0SVYrjSE0/yUL7DpxW4K3FWw=
|
||||||
go.opentelemetry.io/otel v1.32.0/go.mod h1:00DCVSB0RQcnzlwyTfqtxSm+DRr9hpYrHjNGiBHVQIg=
|
go.opentelemetry.io/otel v1.33.0/go.mod h1:SUUkR6csvUQl+yjReHu5uM3EtVV7MBm5FHKRlNx4I8I=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.44.0 h1:jd0+5t/YynESZqsSyPz+7PAFdEop0dlN0+PkyHYo8oI=
|
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.44.0 h1:jd0+5t/YynESZqsSyPz+7PAFdEop0dlN0+PkyHYo8oI=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.44.0/go.mod h1:U707O40ee1FpQGyhvqnzmCJm1Wh6OX6GGBVn0E6Uyyk=
|
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.44.0/go.mod h1:U707O40ee1FpQGyhvqnzmCJm1Wh6OX6GGBVn0E6Uyyk=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.27.0 h1:R9DE4kQ4k+YtfLI2ULwX82VtNQ2J8yZmA7ZIF/D+7Mc=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0 h1:Vh5HayB/0HHfOQA7Ctx69E/Y/DcQSMPpKANYVMQ7fBA=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.27.0/go.mod h1:OQFyQVrDlbe+R7xrEyDr/2Wr67Ol0hRUgsfA+V5A95s=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0/go.mod h1:cpgtDBaqD/6ok/UG0jT15/uKjAY8mRA53diogHBg3UI=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0 h1:qFffATk0X+HD+f1Z8lswGiOQYKHRlzfmdJm0wEaVrFA=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0 h1:qFffATk0X+HD+f1Z8lswGiOQYKHRlzfmdJm0wEaVrFA=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0/go.mod h1:MOiCmryaYtc+V0Ei+Tx9o5S1ZjA7kzLucuVuyzBZloQ=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0/go.mod h1:MOiCmryaYtc+V0Ei+Tx9o5S1ZjA7kzLucuVuyzBZloQ=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.27.0 h1:QY7/0NeRPKlzusf40ZE4t1VlMKbqSNT7cJRYzWuja0s=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.27.0 h1:QY7/0NeRPKlzusf40ZE4t1VlMKbqSNT7cJRYzWuja0s=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.27.0/go.mod h1:HVkSiDhTM9BoUJU8qE6j2eSWLLXvi1USXjyd2BXT8PY=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.27.0/go.mod h1:HVkSiDhTM9BoUJU8qE6j2eSWLLXvi1USXjyd2BXT8PY=
|
||||||
go.opentelemetry.io/otel/metric v1.32.0 h1:xV2umtmNcThh2/a/aCP+h64Xx5wsj8qqnkYZktzNa0M=
|
go.opentelemetry.io/otel/metric v1.33.0 h1:r+JOocAyeRVXD8lZpjdQjzMadVZp2M4WmQ+5WtEnklQ=
|
||||||
go.opentelemetry.io/otel/metric v1.32.0/go.mod h1:jH7CIbbK6SH2V2wE16W05BHCtIDzauciCRLoc/SyMv8=
|
go.opentelemetry.io/otel/metric v1.33.0/go.mod h1:L9+Fyctbp6HFTddIxClbQkjtubW6O9QS3Ann/M82u6M=
|
||||||
go.opentelemetry.io/otel/sdk v1.32.0 h1:RNxepc9vK59A8XsgZQouW8ue8Gkb4jpWtJm9ge5lEG4=
|
go.opentelemetry.io/otel/sdk v1.33.0 h1:iax7M131HuAm9QkZotNHEfstof92xM+N8sr3uHXc2IM=
|
||||||
go.opentelemetry.io/otel/sdk v1.32.0/go.mod h1:LqgegDBjKMmb2GC6/PrTnteJG39I8/vJCAP9LlJXEjU=
|
go.opentelemetry.io/otel/sdk v1.33.0/go.mod h1:A1Q5oi7/9XaMlIWzPSxLRWOI8nG3FnzHJNbiENQuihM=
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.32.0 h1:rZvFnvmvawYb0alrYkjraqJq0Z4ZUJAiyYCU9snn1CU=
|
go.opentelemetry.io/otel/sdk/metric v1.32.0 h1:rZvFnvmvawYb0alrYkjraqJq0Z4ZUJAiyYCU9snn1CU=
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.32.0/go.mod h1:PWeZlq0zt9YkYAp3gjKZ0eicRYvOh1Gd+X99x6GHpCQ=
|
go.opentelemetry.io/otel/sdk/metric v1.32.0/go.mod h1:PWeZlq0zt9YkYAp3gjKZ0eicRYvOh1Gd+X99x6GHpCQ=
|
||||||
go.opentelemetry.io/otel/trace v1.32.0 h1:WIC9mYrXf8TmY/EXuULKc8hR17vE+Hjv2cssQDe03fM=
|
go.opentelemetry.io/otel/trace v1.33.0 h1:cCJuF7LRjUFso9LPnEAHJDB2pqzp+hbO8eu1qqW2d/s=
|
||||||
go.opentelemetry.io/otel/trace v1.32.0/go.mod h1:+i4rkvCraA+tG6AzwloGaCtkx53Fa+L+V8e9a7YvhT8=
|
go.opentelemetry.io/otel/trace v1.33.0/go.mod h1:uIcdVUZMpTAmz0tI1z04GoVSezK37CbGV4fr1f2nBck=
|
||||||
go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0=
|
go.opentelemetry.io/proto/otlp v1.4.0 h1:TA9WRvW6zMwP+Ssb6fLoUIuirti1gGbP28GcKG1jgeg=
|
||||||
go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8=
|
go.opentelemetry.io/proto/otlp v1.4.0/go.mod h1:PPBWZIP98o2ElSqI35IHfu7hIhSwvc5N38Jw8pXuGFY=
|
||||||
go.step.sm/cli-utils v0.9.0 h1:55jYcsQbnArNqepZyAwcato6Zy2MoZDRkWW+jF+aPfQ=
|
go.step.sm/cli-utils v0.9.0 h1:55jYcsQbnArNqepZyAwcato6Zy2MoZDRkWW+jF+aPfQ=
|
||||||
go.step.sm/cli-utils v0.9.0/go.mod h1:Y/CRoWl1FVR9j+7PnAewufAwKmBOTzR6l9+7EYGAnp8=
|
go.step.sm/cli-utils v0.9.0/go.mod h1:Y/CRoWl1FVR9j+7PnAewufAwKmBOTzR6l9+7EYGAnp8=
|
||||||
go.step.sm/crypto v0.45.0 h1:Z0WYAaaOYrJmKP9sJkPW+6wy3pgN3Ija8ek/D4serjc=
|
go.step.sm/crypto v0.45.0 h1:Z0WYAaaOYrJmKP9sJkPW+6wy3pgN3Ija8ek/D4serjc=
|
||||||
@@ -1171,8 +1194,8 @@ golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5y
|
|||||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
|
golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
|
||||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||||
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
|
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
|
||||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
|
||||||
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=
|
||||||
@@ -1212,8 +1235,8 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug
|
|||||||
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
|
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
|
||||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||||
golang.org/x/net v0.32.0 h1:ZqPmj8Kzc+Y6e0+skZsuACbx+wzMgo5MQsJh9Qd6aYI=
|
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
|
||||||
golang.org/x/net v0.32.0/go.mod h1:CwU0IoeOlnQQWJ6ioyFrfRuomB8GKF6KbYXZVyeXNfs=
|
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
|
||||||
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=
|
||||||
@@ -1276,8 +1299,8 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|||||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
|
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
|
||||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
@@ -1285,8 +1308,8 @@ golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
|
|||||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||||
golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
|
golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg=
|
||||||
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
|
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
|
||||||
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=
|
||||||
@@ -1320,8 +1343,8 @@ golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4f
|
|||||||
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||||
golang.org/x/tools v0.28.0 h1:WuB6qZ4RPCQo5aP3WdKZS7i595EdWqWR8vqJTlwTVK8=
|
golang.org/x/tools v0.29.0 h1:Xx0h3TtM9rzQpQuR4dKLrdglAmCEN5Oi+P74JdhdzXE=
|
||||||
golang.org/x/tools v0.28.0/go.mod h1:dcIOrVd3mfQKTgrDVQHqCPMWy6lnhfhtX3hLXYVLfRw=
|
golang.org/x/tools v0.29.0/go.mod h1:KMQVMRsVxU6nHCFXrBPhDB8XncLNLM0lIy/F14RP588=
|
||||||
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=
|
||||||
@@ -1344,10 +1367,10 @@ google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98
|
|||||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||||
google.golang.org/genproto v0.0.0-20240401170217-c3f982113cda h1:wu/KJm9KJwpfHWhkkZGohVC6KRrc1oJNr4jwtQMOQXw=
|
google.golang.org/genproto v0.0.0-20240401170217-c3f982113cda h1:wu/KJm9KJwpfHWhkkZGohVC6KRrc1oJNr4jwtQMOQXw=
|
||||||
google.golang.org/genproto v0.0.0-20240401170217-c3f982113cda/go.mod h1:g2LLCvCeCSir/JJSWosk19BR4NVxGqHUC6rxIRsd7Aw=
|
google.golang.org/genproto v0.0.0-20240401170217-c3f982113cda/go.mod h1:g2LLCvCeCSir/JJSWosk19BR4NVxGqHUC6rxIRsd7Aw=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 h1:hjSy6tcFQZ171igDaN5QHOw2n6vx40juYbC/x67CEhc=
|
google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 h1:CkkIfIt50+lT6NHAVoRYEyAvQGFM7xEwXUUywFvEb3Q=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:qpvKtACPCQhAdu3PyQgV4l3LMXZEtft7y8QcarRsp9I=
|
google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576/go.mod h1:1R3kvZ1dtP3+4p4d3G8uJ8rFk/fWlScl38vanWACI08=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20241007155032-5fefd90f89a9 h1:QCqS/PdaHTSWGvupk2F/ehwHtGc0/GYkT+3GAcR1CCc=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576 h1:8ZmaLZE4XWrtU3MyClkYqqtl6Oegr3235h7jxsDyqCY=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20241007155032-5fefd90f89a9/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU=
|
||||||
google.golang.org/grpc v1.0.5/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
|
google.golang.org/grpc v1.0.5/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
|
||||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||||
@@ -1355,8 +1378,8 @@ google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyac
|
|||||||
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||||
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
|
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
|
||||||
google.golang.org/grpc v1.68.0 h1:aHQeeJbo8zAkAa3pRzrVjZlbz6uSfeOXlJNQM0RAbz0=
|
google.golang.org/grpc v1.68.1 h1:oI5oTa11+ng8r8XMMN7jAOmWfPZWbYpCFaMUTACxkM0=
|
||||||
google.golang.org/grpc v1.68.0/go.mod h1:fmSPC5AsjSBCK54MyHRx48kpOti1/jRfOlwEWywNjWA=
|
google.golang.org/grpc v1.68.1/go.mod h1:+q1XYFJjShcqn0QZHvCyeR4CXPA+llXIeUIfIe00waw=
|
||||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||||
@@ -1366,8 +1389,8 @@ google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2
|
|||||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||||
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||||
google.golang.org/protobuf v1.36.0 h1:mjIs9gYtt56AzC4ZaffQuh88TZurBGhIJMBZGSxNerQ=
|
google.golang.org/protobuf v1.36.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU=
|
||||||
google.golang.org/protobuf v1.36.0/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||||
gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U=
|
gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U=
|
||||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||||
gopkg.in/cenkalti/backoff.v2 v2.2.1 h1:eJ9UAg01/HIHG987TwxvnzK2MgxXq97YY6rYDpY9aII=
|
gopkg.in/cenkalti/backoff.v2 v2.2.1 h1:eJ9UAg01/HIHG987TwxvnzK2MgxXq97YY6rYDpY9aII=
|
||||||
@@ -1413,3 +1436,27 @@ k8s.io/klog/v2 v2.110.1 h1:U/Af64HJf7FcwMcXyKm2RPM22WZzyR7OSpYj5tg3cL0=
|
|||||||
k8s.io/klog/v2 v2.110.1/go.mod h1:YGtd1984u+GgbuZ7e08/yBuAfKLSO0+uR1Fhi6ExXjo=
|
k8s.io/klog/v2 v2.110.1/go.mod h1:YGtd1984u+GgbuZ7e08/yBuAfKLSO0+uR1Fhi6ExXjo=
|
||||||
lukechampine.com/blake3 v1.3.0 h1:sJ3XhFINmHSrYCgl958hscfIa3bw8x4DqMP3u1YvoYE=
|
lukechampine.com/blake3 v1.3.0 h1:sJ3XhFINmHSrYCgl958hscfIa3bw8x4DqMP3u1YvoYE=
|
||||||
lukechampine.com/blake3 v1.3.0/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k=
|
lukechampine.com/blake3 v1.3.0/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k=
|
||||||
|
modernc.org/cc/v4 v4.24.4 h1:TFkx1s6dCkQpd6dKurBNmpo+G8Zl4Sq/ztJ+2+DEsh0=
|
||||||
|
modernc.org/cc/v4 v4.24.4/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
||||||
|
modernc.org/ccgo/v4 v4.23.16 h1:Z2N+kk38b7SfySC1ZkpGLN2vthNJP1+ZzGZIlH7uBxo=
|
||||||
|
modernc.org/ccgo/v4 v4.23.16/go.mod h1:nNma8goMTY7aQZQNTyN9AIoJfxav4nvTnvKThAeMDdo=
|
||||||
|
modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
|
||||||
|
modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=
|
||||||
|
modernc.org/gc/v2 v2.6.3 h1:aJVhcqAte49LF+mGveZ5KPlsp4tdGdAOT4sipJXADjw=
|
||||||
|
modernc.org/gc/v2 v2.6.3/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
||||||
|
modernc.org/libc v1.61.13 h1:3LRd6ZO1ezsFiX1y+bHd1ipyEHIJKvuprv0sLTBwLW8=
|
||||||
|
modernc.org/libc v1.61.13/go.mod h1:8F/uJWL/3nNil0Lgt1Dpz+GgkApWh04N3el3hxJcA6E=
|
||||||
|
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||||
|
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||||
|
modernc.org/memory v1.8.2 h1:cL9L4bcoAObu4NkxOlKWBWtNHIsnnACGF/TbqQ6sbcI=
|
||||||
|
modernc.org/memory v1.8.2/go.mod h1:ZbjSvMO5NQ1A2i3bWeDiVMxIorXwdClKE/0SZ+BMotU=
|
||||||
|
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
|
||||||
|
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
||||||
|
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
||||||
|
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
||||||
|
modernc.org/sqlite v1.36.3 h1:qYMYlFR+rtLDUzuXoST1SDIdEPbX8xzuhdF90WsX1ss=
|
||||||
|
modernc.org/sqlite v1.36.3/go.mod h1:ADySlx7K4FdY5MaJcEv86hTJ0PjedAloTUuif0YS3ws=
|
||||||
|
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||||
|
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||||
|
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||||
|
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||||
|
|||||||
@@ -1,103 +0,0 @@
|
|||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"github.com/distribution/reference"
|
|
||||||
"reflect"
|
|
||||||
"uncloud/internal/machine/api/pb"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
ServiceModeReplicated = "replicated"
|
|
||||||
ServiceModeGlobal = "global"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ServiceSpec struct {
|
|
||||||
Container ContainerSpec
|
|
||||||
// Mode is the replication mode of the service. Default is ServiceModeReplicated if empty.
|
|
||||||
Mode string
|
|
||||||
Name string
|
|
||||||
// Ports defines what service ports to publish to make the service accessible outside the cluster.
|
|
||||||
Ports []PortSpec
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ServiceSpec) Validate() error {
|
|
||||||
if err := s.Container.Validate(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
switch s.Mode {
|
|
||||||
case "", ServiceModeGlobal, ServiceModeReplicated:
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("invalid mode: %q", s.Mode)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: validate there is no conflict between ports.
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ServiceSpec) Equals(spec ServiceSpec) bool {
|
|
||||||
// TODO: ignore order of ports.
|
|
||||||
return reflect.DeepEqual(*s, spec)
|
|
||||||
}
|
|
||||||
|
|
||||||
type ContainerSpec struct {
|
|
||||||
Command []string
|
|
||||||
Image string
|
|
||||||
// Run a custom init inside the container. If nil, use the daemon's configured settings.
|
|
||||||
Init *bool
|
|
||||||
// List of volumes to bind mount into the container.
|
|
||||||
Volumes []string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ContainerSpec) Validate() error {
|
|
||||||
if _, err := reference.ParseDockerRef(s.Image); err != nil {
|
|
||||||
return fmt.Errorf("invalid image: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type Service struct {
|
|
||||||
ID string
|
|
||||||
Name string
|
|
||||||
Mode string
|
|
||||||
Containers []MachineContainer
|
|
||||||
}
|
|
||||||
|
|
||||||
type MachineContainer struct {
|
|
||||||
MachineID string
|
|
||||||
Container Container
|
|
||||||
}
|
|
||||||
|
|
||||||
func ServiceFromProto(s *pb.Service) (Service, error) {
|
|
||||||
var err error
|
|
||||||
containers := make([]MachineContainer, len(s.Containers))
|
|
||||||
for i, sc := range s.Containers {
|
|
||||||
containers[i], err = machineContainerFromProto(sc)
|
|
||||||
if err != nil {
|
|
||||||
return Service{}, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Service{
|
|
||||||
ID: s.Id,
|
|
||||||
Name: s.Name,
|
|
||||||
Mode: s.Mode,
|
|
||||||
Containers: containers,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func machineContainerFromProto(sc *pb.Service_Container) (MachineContainer, error) {
|
|
||||||
var c Container
|
|
||||||
if err := json.Unmarshal(sc.Container, &c); err != nil {
|
|
||||||
return MachineContainer{}, fmt.Errorf("unmarshal container: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return MachineContainer{
|
|
||||||
MachineID: sc.MachineId,
|
|
||||||
Container: c,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
+117
-42
@@ -4,14 +4,17 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/docker/cli/cli/streams"
|
||||||
|
"github.com/psviderski/uncloud/internal/cli/config"
|
||||||
|
"github.com/psviderski/uncloud/internal/fs"
|
||||||
|
"github.com/psviderski/uncloud/internal/machine"
|
||||||
|
"github.com/psviderski/uncloud/internal/machine/api/pb"
|
||||||
|
"github.com/psviderski/uncloud/internal/sshexec"
|
||||||
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
|
"github.com/psviderski/uncloud/pkg/client"
|
||||||
|
"github.com/psviderski/uncloud/pkg/client/connector"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"uncloud/internal/cli/client"
|
"os"
|
||||||
"uncloud/internal/cli/client/connector"
|
|
||||||
"uncloud/internal/cli/config"
|
|
||||||
"uncloud/internal/fs"
|
|
||||||
"uncloud/internal/machine"
|
|
||||||
"uncloud/internal/machine/api/pb"
|
|
||||||
"uncloud/internal/sshexec"
|
|
||||||
|
|
||||||
"github.com/charmbracelet/huh"
|
"github.com/charmbracelet/huh"
|
||||||
"google.golang.org/protobuf/types/known/emptypb"
|
"google.golang.org/protobuf/types/known/emptypb"
|
||||||
@@ -21,13 +24,22 @@ const defaultClusterName = "default"
|
|||||||
|
|
||||||
type CLI struct {
|
type CLI struct {
|
||||||
config *config.Config
|
config *config.Config
|
||||||
|
conn *config.MachineConnection
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(configPath string) (*CLI, error) {
|
// New creates a new CLI instance with the given config path or remote machine connection.
|
||||||
|
// If the connection is provided, the config is ignored for all operations which is useful for interacting with
|
||||||
|
// a cluster without creating a config.
|
||||||
|
func New(configPath string, conn *config.MachineConnection) (*CLI, error) {
|
||||||
|
if conn != nil {
|
||||||
|
return &CLI{conn: conn}, nil
|
||||||
|
}
|
||||||
|
|
||||||
cfg, err := config.NewFromFile(configPath)
|
cfg, err := config.NewFromFile(configPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("read Uncloud config: %w", err)
|
return nil, fmt.Errorf("read Uncloud config: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &CLI{
|
return &CLI{
|
||||||
config: cfg,
|
config: cfg,
|
||||||
}, nil
|
}, nil
|
||||||
@@ -45,13 +57,19 @@ func (cli *CLI) CreateCluster(name string) error {
|
|||||||
|
|
||||||
func (cli *CLI) SetCurrentCluster(name string) error {
|
func (cli *CLI) SetCurrentCluster(name string) error {
|
||||||
if _, ok := cli.config.Clusters[name]; !ok {
|
if _, ok := cli.config.Clusters[name]; !ok {
|
||||||
return client.ErrNotFound
|
return api.ErrNotFound
|
||||||
}
|
}
|
||||||
cli.config.CurrentCluster = name
|
cli.config.CurrentCluster = name
|
||||||
return cli.config.Save()
|
return cli.config.Save()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ConnectCluster connects to a cluster using the given cluster name or the current cluster if not specified.
|
||||||
|
// If the CLI was initialised with a machine connection, the config is ignored and the connection is used instead.
|
||||||
func (cli *CLI) ConnectCluster(ctx context.Context, clusterName string) (*client.Client, error) {
|
func (cli *CLI) ConnectCluster(ctx context.Context, clusterName string) (*client.Client, error) {
|
||||||
|
if cli.conn != nil {
|
||||||
|
return connectCluster(ctx, *cli.conn)
|
||||||
|
}
|
||||||
|
|
||||||
if len(cli.config.Clusters) == 0 {
|
if len(cli.config.Clusters) == 0 {
|
||||||
return nil, errors.New(
|
return nil, errors.New(
|
||||||
"no clusters found in the Uncloud config. " +
|
"no clusters found in the Uncloud config. " +
|
||||||
@@ -86,6 +104,16 @@ func (cli *CLI) ConnectCluster(ctx context.Context, clusterName string) (*client
|
|||||||
|
|
||||||
// TODO: iterate over all connections and try to connect to the cluster using the first successful connection.
|
// TODO: iterate over all connections and try to connect to the cluster using the first successful connection.
|
||||||
conn := cfg.Connections[0]
|
conn := cfg.Connections[0]
|
||||||
|
|
||||||
|
c, err := connectCluster(ctx, conn)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.New("no valid connection configuration found for the cluster")
|
||||||
|
}
|
||||||
|
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func connectCluster(ctx context.Context, conn config.MachineConnection) (*client.Client, error) {
|
||||||
if conn.SSH != "" {
|
if conn.SSH != "" {
|
||||||
user, host, port, err := conn.SSH.Parse()
|
user, host, port, err := conn.SSH.Parse()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -104,43 +132,61 @@ func (cli *CLI) ConnectCluster(ctx context.Context, clusterName string) (*client
|
|||||||
} else if conn.TCP.IsValid() {
|
} else if conn.TCP.IsValid() {
|
||||||
return client.New(ctx, connector.NewTCPConnector(conn.TCP))
|
return client.New(ctx, connector.NewTCPConnector(conn.TCP))
|
||||||
}
|
}
|
||||||
return nil, errors.New("no valid connection configuration found for the cluster")
|
|
||||||
|
return nil, errors.New("connection configuration is invalid")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// InitCluster initialises a new cluster on a remote machine and returns a client to interact with the cluster.
|
||||||
|
// The client should be closed after use by the caller.
|
||||||
func (cli *CLI) InitCluster(
|
func (cli *CLI) InitCluster(
|
||||||
ctx context.Context, remoteMachine *RemoteMachine, clusterName, machineName string, netPrefix netip.Prefix,
|
ctx context.Context,
|
||||||
) error {
|
remoteMachine *RemoteMachine,
|
||||||
|
clusterName,
|
||||||
|
machineName string,
|
||||||
|
netPrefix netip.Prefix,
|
||||||
|
publicIP *netip.Addr,
|
||||||
|
) (*client.Client, error) {
|
||||||
if remoteMachine != nil {
|
if remoteMachine != nil {
|
||||||
return cli.initRemoteMachine(ctx, *remoteMachine, clusterName, machineName, netPrefix)
|
return cli.initRemoteMachine(ctx, *remoteMachine, clusterName, machineName, netPrefix, publicIP)
|
||||||
}
|
}
|
||||||
// TODO: implement local machine initialisation
|
// TODO: implement local machine initialisation
|
||||||
return fmt.Errorf("local machine initialisation is not implemented yet")
|
return nil, fmt.Errorf("local machine initialisation is not implemented yet")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cli *CLI) initRemoteMachine(
|
func (cli *CLI) initRemoteMachine(
|
||||||
ctx context.Context, remoteMachine RemoteMachine, clusterName, machineName string, netPrefix netip.Prefix,
|
ctx context.Context,
|
||||||
) error {
|
remoteMachine RemoteMachine,
|
||||||
|
clusterName,
|
||||||
|
machineName string,
|
||||||
|
netPrefix netip.Prefix,
|
||||||
|
publicIP *netip.Addr,
|
||||||
|
) (*client.Client, error) {
|
||||||
if clusterName == "" {
|
if clusterName == "" {
|
||||||
clusterName = defaultClusterName
|
clusterName = defaultClusterName
|
||||||
}
|
}
|
||||||
if _, ok := cli.config.Clusters[clusterName]; ok {
|
if _, ok := cli.config.Clusters[clusterName]; ok {
|
||||||
return fmt.Errorf("cluster %q already exists", clusterName)
|
return nil, fmt.Errorf("cluster %q already exists", clusterName)
|
||||||
}
|
}
|
||||||
|
|
||||||
machineClient, err := cli.provisionRemoteMachine(ctx, remoteMachine)
|
machineClient, err := cli.provisionRemoteMachine(ctx, remoteMachine)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer machineClient.Close()
|
// Ensure machineClient is closed on error.
|
||||||
|
defer func() {
|
||||||
|
if err != nil {
|
||||||
|
machineClient.Close()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
// Check if the machine is already initialised as a cluster member and prompt the user to reset it first.
|
// Check if the machine is already initialised as a cluster member and prompt the user to reset it first.
|
||||||
minfo, err := machineClient.Inspect(ctx, &emptypb.Empty{})
|
minfo, err := machineClient.Inspect(ctx, &emptypb.Empty{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("inspect machine: %w", err)
|
return nil, fmt.Errorf("inspect machine: %w", err)
|
||||||
}
|
}
|
||||||
if minfo.Id != "" {
|
if minfo.Id != "" {
|
||||||
if err = cli.promptResetMachine(); err != nil {
|
if err = cli.promptResetMachine(); err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,19 +194,28 @@ func (cli *CLI) initRemoteMachine(
|
|||||||
MachineName: machineName,
|
MachineName: machineName,
|
||||||
Network: pb.NewIPPrefix(netPrefix),
|
Network: pb.NewIPPrefix(netPrefix),
|
||||||
}
|
}
|
||||||
|
if publicIP != nil {
|
||||||
|
if publicIP.IsValid() {
|
||||||
|
req.PublicIpConfig = &pb.InitClusterRequest_PublicIp{PublicIp: pb.NewIP(*publicIP)}
|
||||||
|
} else {
|
||||||
|
// Invalid or in other words zero IP means to automatically detect the public IP.
|
||||||
|
req.PublicIpConfig = &pb.InitClusterRequest_PublicIpAuto{PublicIpAuto: true}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
resp, err := machineClient.InitCluster(ctx, req)
|
resp, err := machineClient.InitCluster(ctx, req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("init cluster: %w", err)
|
return nil, fmt.Errorf("init cluster: %w", err)
|
||||||
}
|
}
|
||||||
fmt.Printf("Cluster %q initialised with machine %q\n", clusterName, resp.Machine.Name)
|
fmt.Printf("Cluster %q initialised with machine %q\n", clusterName, resp.Machine.Name)
|
||||||
|
|
||||||
if err = cli.CreateCluster(clusterName); err != nil {
|
if err = cli.CreateCluster(clusterName); err != nil {
|
||||||
return fmt.Errorf("save cluster to config: %w", err)
|
return nil, fmt.Errorf("save cluster to config: %w", err)
|
||||||
}
|
}
|
||||||
// Set the current cluster to the just created one if it is the only cluster in the config.
|
// Set the current cluster to the just created one if it is the only cluster in the config.
|
||||||
if len(cli.config.Clusters) == 1 {
|
if len(cli.config.Clusters) == 1 {
|
||||||
if err = cli.SetCurrentCluster(clusterName); err != nil {
|
if err = cli.SetCurrentCluster(clusterName); err != nil {
|
||||||
return fmt.Errorf("set current cluster: %w", err)
|
return nil, fmt.Errorf("set current cluster: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,44 +226,50 @@ func (cli *CLI) initRemoteMachine(
|
|||||||
}
|
}
|
||||||
cli.config.Clusters[clusterName].Connections = append(cli.config.Clusters[clusterName].Connections, connCfg)
|
cli.config.Clusters[clusterName].Connections = append(cli.config.Clusters[clusterName].Connections, connCfg)
|
||||||
if err = cli.config.Save(); err != nil {
|
if err = cli.config.Save(); err != nil {
|
||||||
return fmt.Errorf("save config: %w", err)
|
return nil, fmt.Errorf("save config: %w", err)
|
||||||
}
|
}
|
||||||
return nil
|
return machineClient, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cli *CLI) AddMachine(ctx context.Context, remoteMachine RemoteMachine, clusterName, machineName string) error {
|
// AddMachine provisions a remote machine and adds it to the cluster. It returns a client to interact with the machine
|
||||||
|
// which should be closed after use by the caller.
|
||||||
|
func (cli *CLI) AddMachine(
|
||||||
|
ctx context.Context, remoteMachine RemoteMachine, clusterName, machineName string, publicIP *netip.Addr,
|
||||||
|
) (*client.Client, error) {
|
||||||
c, err := cli.ConnectCluster(ctx, clusterName)
|
c, err := cli.ConnectCluster(ctx, clusterName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("connect to cluster: %w", err)
|
return nil, fmt.Errorf("connect to cluster: %w", err)
|
||||||
}
|
}
|
||||||
defer func() {
|
defer c.Close()
|
||||||
_ = c.Close()
|
|
||||||
}()
|
|
||||||
|
|
||||||
machineClient, err := cli.provisionRemoteMachine(ctx, remoteMachine)
|
machineClient, err := cli.provisionRemoteMachine(ctx, remoteMachine)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer machineClient.Close()
|
defer func() {
|
||||||
|
if err != nil {
|
||||||
|
machineClient.Close()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
// Check if the machine is already initialised as a cluster member and prompt the user to reset it first.
|
// Check if the machine is already initialised as a cluster member and prompt the user to reset it first.
|
||||||
minfo, err := machineClient.Inspect(ctx, &emptypb.Empty{})
|
minfo, err := machineClient.Inspect(ctx, &emptypb.Empty{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("inspect machine: %w", err)
|
return nil, fmt.Errorf("inspect machine: %w", err)
|
||||||
}
|
}
|
||||||
if minfo.Id != "" {
|
if minfo.Id != "" {
|
||||||
if err = cli.promptResetMachine(); err != nil {
|
if err = cli.promptResetMachine(); err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tokenResp, err := machineClient.Token(ctx, &emptypb.Empty{})
|
tokenResp, err := machineClient.Token(ctx, &emptypb.Empty{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("get remote machine token: %w", err)
|
return nil, fmt.Errorf("get remote machine token: %w", err)
|
||||||
}
|
}
|
||||||
token, err := machine.ParseToken(tokenResp.Token)
|
token, err := machine.ParseToken(tokenResp.Token)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("parse remote machine token: %w", err)
|
return nil, fmt.Errorf("parse remote machine token: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register the machine in the cluster using its public key and endpoints from the token.
|
// Register the machine in the cluster using its public key and endpoints from the token.
|
||||||
@@ -223,15 +284,24 @@ func (cli *CLI) AddMachine(ctx context.Context, remoteMachine RemoteMachine, clu
|
|||||||
PublicKey: token.PublicKey,
|
PublicKey: token.PublicKey,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
if publicIP != nil {
|
||||||
|
if publicIP.IsValid() {
|
||||||
|
addReq.PublicIp = pb.NewIP(*publicIP)
|
||||||
|
} else if token.PublicIP.IsValid() {
|
||||||
|
// Invalid or in other words zero IP means to use an automatically detected public IP from the token.
|
||||||
|
addReq.PublicIp = pb.NewIP(token.PublicIP)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
addResp, err := c.AddMachine(ctx, addReq)
|
addResp, err := c.AddMachine(ctx, addReq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("add machine to cluster: %w", err)
|
return nil, fmt.Errorf("add machine to cluster: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// List other machines in the cluster to include them in the join request.
|
// List other machines in the cluster to include them in the join request.
|
||||||
machines, err := c.ListMachines(ctx)
|
machines, err := c.ListMachines(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("list cluster machines: %w", err)
|
return nil, fmt.Errorf("list cluster machines: %w", err)
|
||||||
}
|
}
|
||||||
otherMachines := make([]*pb.MachineInfo, 0, len(machines)-1)
|
otherMachines := make([]*pb.MachineInfo, 0, len(machines)-1)
|
||||||
for _, m := range machines {
|
for _, m := range machines {
|
||||||
@@ -246,7 +316,7 @@ func (cli *CLI) AddMachine(ctx context.Context, remoteMachine RemoteMachine, clu
|
|||||||
OtherMachines: otherMachines,
|
OtherMachines: otherMachines,
|
||||||
}
|
}
|
||||||
if _, err = machineClient.JoinCluster(ctx, joinReq); err != nil {
|
if _, err = machineClient.JoinCluster(ctx, joinReq); err != nil {
|
||||||
return fmt.Errorf("join cluster: %w", err)
|
return nil, fmt.Errorf("join cluster: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("Machine %q added to cluster\n", addResp.Machine.Name)
|
fmt.Printf("Machine %q added to cluster\n", addResp.Machine.Name)
|
||||||
@@ -261,10 +331,10 @@ func (cli *CLI) AddMachine(ctx context.Context, remoteMachine RemoteMachine, clu
|
|||||||
}
|
}
|
||||||
cli.config.Clusters[clusterName].Connections = append(cli.config.Clusters[clusterName].Connections, connCfg)
|
cli.config.Clusters[clusterName].Connections = append(cli.config.Clusters[clusterName].Connections, connCfg)
|
||||||
if err = cli.config.Save(); err != nil {
|
if err = cli.config.Save(); err != nil {
|
||||||
return fmt.Errorf("save config: %w", err)
|
return nil, fmt.Errorf("save config: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return machineClient, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// provisionRemoteMachine installs the Uncloud daemon and dependencies on the remote machine over SSH and returns
|
// provisionRemoteMachine installs the Uncloud daemon and dependencies on the remote machine over SSH and returns
|
||||||
@@ -328,3 +398,8 @@ func (cli *CLI) promptResetMachine() error {
|
|||||||
// TODO: implement resetting the remote machine.
|
// TODO: implement resetting the remote machine.
|
||||||
return fmt.Errorf("resetting the remote machine is not implemented yet")
|
return fmt.Errorf("resetting the remote machine is not implemented yet")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ProgressOut returns an output stream for progress writer.
|
||||||
|
func (cli *CLI) ProgressOut() *streams.Out {
|
||||||
|
return streams.NewOut(os.Stdout)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,186 +0,0 @@
|
|||||||
package client
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"uncloud/internal/api"
|
|
||||||
"uncloud/internal/machine/api/pb"
|
|
||||||
"uncloud/internal/secret"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Strategy defines how a service should be deployed or updated. Different implementations can provide various
|
|
||||||
// deployment patterns such as rolling updates, blue/green deployments, etc.
|
|
||||||
type Strategy interface {
|
|
||||||
// Plan returns the operation to reconcile the service to the desired state.
|
|
||||||
// If the service does not exist (new deployment), svc will be nil.
|
|
||||||
Plan(ctx context.Context, cli *Client, svc *api.Service, spec api.ServiceSpec) (Plan, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// RollingStrategy implements a rolling update deployment pattern where containers are updated one at a time
|
|
||||||
// to minimize service disruption.
|
|
||||||
type RollingStrategy struct{}
|
|
||||||
|
|
||||||
func (s *RollingStrategy) Plan(
|
|
||||||
ctx context.Context, cli *Client, svc *api.Service, spec api.ServiceSpec,
|
|
||||||
) (Plan, error) {
|
|
||||||
switch spec.Mode {
|
|
||||||
case "", api.ServiceModeReplicated:
|
|
||||||
return s.planReplicated(ctx, cli, svc, spec)
|
|
||||||
case api.ServiceModeGlobal:
|
|
||||||
return s.planGlobal(ctx, cli, svc, spec)
|
|
||||||
default:
|
|
||||||
return Plan{}, fmt.Errorf("unsupported service mode: %s", spec.Mode)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// planReplicated creates a plan for a replicated service deployment.
|
|
||||||
func (s *RollingStrategy) planReplicated(
|
|
||||||
ctx context.Context, cli *Client, svc *api.Service, spec api.ServiceSpec,
|
|
||||||
) (Plan, error) {
|
|
||||||
return Plan{}, errors.New("not implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
// planGlobal creates a plan for a global service deployment, ensuring one container runs on each available machine.
|
|
||||||
// For machines with an existing container, it attempts to start a new container before removing the old one if
|
|
||||||
// possible. If the new container would have port conflicts with the existing one, the old container is removed first.
|
|
||||||
// It handles multiple containers per machine (though this should not occur in normal operation) and skips machines
|
|
||||||
// that are down.
|
|
||||||
func (s *RollingStrategy) planGlobal(
|
|
||||||
ctx context.Context, cli *Client, svc *api.Service, spec api.ServiceSpec,
|
|
||||||
) (Plan, error) {
|
|
||||||
var plan Plan
|
|
||||||
// Map machineID to service containers on that machine. For the global mode, there should be at most one
|
|
||||||
// container per machine but we use a slice to handle multiple containers that may exist due to a bug
|
|
||||||
// or interruption in the previous deployment.
|
|
||||||
containersOnMachine := make(map[string][]api.MachineContainer)
|
|
||||||
if svc != nil {
|
|
||||||
plan.ServiceID = svc.ID
|
|
||||||
for _, c := range svc.Containers {
|
|
||||||
containersOnMachine[c.MachineID] = append(containersOnMachine[c.MachineID], c)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Generate a new service ID for the first service deployment.
|
|
||||||
var err error
|
|
||||||
plan.ServiceID, err = secret.NewID()
|
|
||||||
if err != nil {
|
|
||||||
return plan, fmt.Errorf("generate service ID: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
machines, err := cli.ListMachines(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return plan, fmt.Errorf("list machines: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
seqOp := &SequenceOperation{}
|
|
||||||
// TODO: figure out how to return a warning if there are machines down. Embed the machinesDown in the plan?
|
|
||||||
// WARNING: failed to run a service container on machine '%s' which is Down.
|
|
||||||
var machinesDown []*pb.MachineInfo
|
|
||||||
for _, m := range machines {
|
|
||||||
// Skip machines that are down but collect them to report a warning later.
|
|
||||||
if m.State == pb.MachineMember_DOWN {
|
|
||||||
machinesDown = append(machinesDown, m.Machine)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
containers := containersOnMachine[m.Machine.Id]
|
|
||||||
ops, err := reconcileGlobalContainer(containers, spec, plan.ServiceID, m.Machine.Id)
|
|
||||||
if err != nil {
|
|
||||||
return plan, err
|
|
||||||
}
|
|
||||||
seqOp.Operations = append(seqOp.Operations, ops...)
|
|
||||||
}
|
|
||||||
plan.Operation = seqOp
|
|
||||||
|
|
||||||
return plan, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// reconcileGlobalContainer returns a sequence of operations to reconcile containers on a machine for a global service.
|
|
||||||
// It ensures exactly one container with the desired spec is running on the machine by creating a new container and
|
|
||||||
// removing old ones. If there is a host port conflict, it stops the old container before starting a new one.
|
|
||||||
func reconcileGlobalContainer(
|
|
||||||
containers []api.MachineContainer, spec api.ServiceSpec, serviceID, machineID string,
|
|
||||||
) ([]Operation, error) {
|
|
||||||
var ops []Operation
|
|
||||||
|
|
||||||
if len(containers) == 0 {
|
|
||||||
// No containers on this machine, create a new one.
|
|
||||||
ops = append(ops, &RunContainerOperation{
|
|
||||||
ServiceID: serviceID,
|
|
||||||
Spec: spec,
|
|
||||||
MachineID: machineID,
|
|
||||||
})
|
|
||||||
return ops, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if there is a container with the same spec already running. If so, remove the rest.
|
|
||||||
upToDate := false
|
|
||||||
for i, c := range containers {
|
|
||||||
if !c.Container.State.Running || c.Container.State.Paused {
|
|
||||||
// Skip containers that are not running.
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
svcSpec, err := c.Container.ServiceSpec()
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("get service spec: %w", err)
|
|
||||||
}
|
|
||||||
if svcSpec.Equals(spec) {
|
|
||||||
// The container is already running with the same spec.
|
|
||||||
upToDate = true
|
|
||||||
for j, old := range containers {
|
|
||||||
if i == j {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
ops = append(ops, &RemoveContainerOperation{
|
|
||||||
ServiceID: serviceID,
|
|
||||||
ContainerID: old.Container.ID,
|
|
||||||
MachineID: old.MachineID,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if upToDate {
|
|
||||||
return ops, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// The machine has containers but none of them match the new spec.
|
|
||||||
// Stop the old running containers that have conflicting ports with the new spec before running a new one.
|
|
||||||
for _, c := range containers {
|
|
||||||
if c.Container.State.Running {
|
|
||||||
conflictingPorts, err := c.Container.ConflictingServicePorts(spec.Ports)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("check conflicting ports: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(conflictingPorts) > 0 {
|
|
||||||
// Stop the running container with conflicting ports.
|
|
||||||
ops = append(ops, &StopContainerOperation{
|
|
||||||
ServiceID: serviceID,
|
|
||||||
ContainerID: c.Container.ID,
|
|
||||||
MachineID: c.MachineID,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run a new container.
|
|
||||||
ops = append(ops, &RunContainerOperation{
|
|
||||||
ServiceID: serviceID,
|
|
||||||
Spec: spec,
|
|
||||||
MachineID: machineID,
|
|
||||||
})
|
|
||||||
|
|
||||||
// Remove the old containers.
|
|
||||||
for _, c := range containers {
|
|
||||||
ops = append(ops, &RemoveContainerOperation{
|
|
||||||
ServiceID: serviceID,
|
|
||||||
ContainerID: c.Container.ID,
|
|
||||||
MachineID: c.MachineID,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return ops, nil
|
|
||||||
}
|
|
||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
"net/netip"
|
"net/netip"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"uncloud/internal/secret"
|
"github.com/psviderski/uncloud/internal/secret"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -15,10 +15,10 @@ const (
|
|||||||
|
|
||||||
type MachineConnection struct {
|
type MachineConnection struct {
|
||||||
SSH SSHDestination `toml:"ssh,omitempty"`
|
SSH SSHDestination `toml:"ssh,omitempty"`
|
||||||
|
SSHKeyFile string `toml:"ssh_key_file,omitempty"`
|
||||||
TCP netip.AddrPort `toml:"tcp,omitempty"`
|
TCP netip.AddrPort `toml:"tcp,omitempty"`
|
||||||
Host string `toml:"host,omitempty"`
|
Host string `toml:"host,omitempty"`
|
||||||
PublicKey secret.Secret `toml:"public_key,omitempty"`
|
PublicKey secret.Secret `toml:"public_key,omitempty"`
|
||||||
SSHKeyFile string `toml:"ssh_key_file,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SSHDestination represents an SSH destination string in the canonical form of "user@host:port".
|
// SSHDestination represents an SSH destination string in the canonical form of "user@host:port".
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"uncloud/internal/sshexec"
|
"github.com/psviderski/uncloud/internal/sshexec"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TODO: support pinning the script version to the CLI version.
|
// TODO: support pinning the script version to the CLI version.
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import "github.com/charmbracelet/huh"
|
||||||
|
|
||||||
|
func Confirm() (bool, error) {
|
||||||
|
var confirmed bool
|
||||||
|
form := huh.NewForm(
|
||||||
|
huh.NewGroup(
|
||||||
|
huh.NewConfirm().
|
||||||
|
Title(
|
||||||
|
"Do you want to continue?",
|
||||||
|
).
|
||||||
|
Affirmative("Yes!").
|
||||||
|
Negative("No").
|
||||||
|
Value(&confirmed),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
if err := form.Run(); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return confirmed, nil
|
||||||
|
}
|
||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
systemd "github.com/coreos/go-systemd/daemon"
|
systemd "github.com/coreos/go-systemd/daemon"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"uncloud/internal/machine"
|
"github.com/psviderski/uncloud/internal/machine"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Daemon struct {
|
type Daemon struct {
|
||||||
|
|||||||
@@ -3,10 +3,10 @@ package daemon
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/psviderski/uncloud/internal/machine"
|
||||||
|
"github.com/psviderski/uncloud/internal/machine/network"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"os"
|
"os"
|
||||||
"uncloud/internal/machine"
|
|
||||||
"uncloud/internal/machine/network"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// MachineToken returns the local machine's token that can be used for adding the machine to a cluster.
|
// MachineToken returns the local machine's token that can be used for adding the machine to a cluster.
|
||||||
@@ -37,5 +37,5 @@ func MachineToken(dataDir string) (machine.Token, error) {
|
|||||||
for i, ip := range ips {
|
for i, ip := range ips {
|
||||||
endpoints[i] = netip.AddrPortFrom(ip, network.WireGuardPort)
|
endpoints[i] = netip.AddrPortFrom(ip, network.WireGuardPort)
|
||||||
}
|
}
|
||||||
return machine.NewToken(state.Network.PublicKey, endpoints), nil
|
return machine.NewToken(state.Network.PublicKey, publicIP, endpoints), nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package dns
|
||||||
|
|
||||||
|
const (
|
||||||
|
RecordTypeA RecordType = "A"
|
||||||
|
RecordTypeAAAA RecordType = "AAAA"
|
||||||
|
)
|
||||||
|
|
||||||
|
type RecordType string
|
||||||
|
|
||||||
|
type DomainResponse struct {
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
Token string `json:"token,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RecordRequest struct {
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
Type RecordType `json:"type,omitempty"`
|
||||||
|
Values []string `json:"values,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RecordResponse struct {
|
||||||
|
RecordRequest
|
||||||
|
FQDN string `json:"fqdn,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AuthErrorResponse struct {
|
||||||
|
Status int `json:"status,omitempty"`
|
||||||
|
Message string `json:"msg,omitempty"`
|
||||||
|
Data authErrorData `json:"data,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type authErrorData struct {
|
||||||
|
NoDomain bool `json:"noDomain,omitempty"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
package dns
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log/slog"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
// The dns package code is based on https://github.com/acorn-io/runtime/blob/main/pkg/dns.
|
||||||
|
|
||||||
|
// Client handles interactions with the Uncloud DNS API service.
|
||||||
|
type Client interface {
|
||||||
|
// ReserveDomain calls Uncloud DNS to reserve a new domain. It returns the domain, a token for authentication,
|
||||||
|
// and an error.
|
||||||
|
ReserveDomain(endpoint string) (string, string, error)
|
||||||
|
|
||||||
|
// CreateRecords calls Uncloud DNS to create or update DNS records based on the supplied RecordRequests
|
||||||
|
// for the specified domain.
|
||||||
|
CreateRecords(endpoint, domain, token string, records []RecordRequest) ([]RecordResponse, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrAuthNoDomain indicates that a request failed authentication because the domain was not found.
|
||||||
|
// If encountered, a new domain needs to be reserved.
|
||||||
|
var ErrAuthNoDomain = errors.New("the supplied domain failed authentication")
|
||||||
|
|
||||||
|
// NewClient creates a new AcornDNS client
|
||||||
|
func NewClient() Client {
|
||||||
|
return &client{
|
||||||
|
c: http.DefaultClient,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type client struct {
|
||||||
|
c *http.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *client) ReserveDomain(endpoint string) (string, string, error) {
|
||||||
|
url := fmt.Sprintf("%s/%s", endpoint, "domains")
|
||||||
|
|
||||||
|
req, err := c.request(http.MethodPost, url, nil, "")
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := &DomainResponse{}
|
||||||
|
err = c.do(req, resp)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp.Name, resp.Token, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *client) CreateRecords(endpoint, domain, token string, records []RecordRequest) ([]RecordResponse, error) {
|
||||||
|
url := fmt.Sprintf("%s/domains/%s/records", endpoint, domain)
|
||||||
|
|
||||||
|
var resp []RecordResponse
|
||||||
|
for _, recordRequest := range records {
|
||||||
|
body, err := jsonBody(recordRequest)
|
||||||
|
if err != nil {
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := c.request(http.MethodPost, url, body, token)
|
||||||
|
if err != nil {
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var recordResp RecordResponse
|
||||||
|
if err = c.do(req, &recordResp); err != nil {
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
resp = append(resp, recordResp)
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *client) request(method string, url string, body io.Reader, token string) (*http.Request, error) {
|
||||||
|
req, err := http.NewRequest(method, url, body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
req.Header.Add("Content-Type", "application/json")
|
||||||
|
|
||||||
|
if token != "" {
|
||||||
|
bearer := "Bearer " + token
|
||||||
|
req.Header.Add("Authorization", bearer)
|
||||||
|
}
|
||||||
|
|
||||||
|
return req, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *client) do(req *http.Request, responseBody any) error {
|
||||||
|
slog.Debug("Making request to DNS service.", "method", req.Method, "url", req.URL)
|
||||||
|
|
||||||
|
resp, err := c.c.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
slog.Debug("Response code for request to DNS service.",
|
||||||
|
"method", req.Method, "url", req.URL, "code", resp.StatusCode)
|
||||||
|
// When err is nil, resp contains a non-nil resp.Body which must be closed.
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("read response body: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode == http.StatusUnauthorized {
|
||||||
|
var authError AuthErrorResponse
|
||||||
|
|
||||||
|
err = json.Unmarshal(body, &authError)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unmarshal auth error response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if authError.Data.NoDomain {
|
||||||
|
return ErrAuthNoDomain
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors.New("authentication failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
if code := resp.StatusCode; code < 200 || code > 300 {
|
||||||
|
return fmt.Errorf("unexpected response status code: %d", code)
|
||||||
|
}
|
||||||
|
|
||||||
|
if responseBody != nil {
|
||||||
|
err = json.Unmarshal(body, responseBody)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unmarshal response body (%s): %w", string(body), err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func jsonBody(payload any) (io.Reader, error) {
|
||||||
|
buf := &bytes.Buffer{}
|
||||||
|
err := json.NewEncoder(buf).Encode(payload)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return buf, nil
|
||||||
|
}
|
||||||
@@ -78,13 +78,63 @@ func (MachineMember_MembershipState) EnumDescriptor() ([]byte, []int) {
|
|||||||
return file_internal_machine_api_pb_cluster_proto_rawDescGZIP(), []int{2, 0}
|
return file_internal_machine_api_pb_cluster_proto_rawDescGZIP(), []int{2, 0}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type DNSRecord_RecordType int32
|
||||||
|
|
||||||
|
const (
|
||||||
|
DNSRecord_UNSPECIFIED DNSRecord_RecordType = 0
|
||||||
|
DNSRecord_A DNSRecord_RecordType = 1
|
||||||
|
DNSRecord_AAAA DNSRecord_RecordType = 2
|
||||||
|
)
|
||||||
|
|
||||||
|
// Enum value maps for DNSRecord_RecordType.
|
||||||
|
var (
|
||||||
|
DNSRecord_RecordType_name = map[int32]string{
|
||||||
|
0: "UNSPECIFIED",
|
||||||
|
1: "A",
|
||||||
|
2: "AAAA",
|
||||||
|
}
|
||||||
|
DNSRecord_RecordType_value = map[string]int32{
|
||||||
|
"UNSPECIFIED": 0,
|
||||||
|
"A": 1,
|
||||||
|
"AAAA": 2,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func (x DNSRecord_RecordType) Enum() *DNSRecord_RecordType {
|
||||||
|
p := new(DNSRecord_RecordType)
|
||||||
|
*p = x
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x DNSRecord_RecordType) String() string {
|
||||||
|
return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (DNSRecord_RecordType) Descriptor() protoreflect.EnumDescriptor {
|
||||||
|
return file_internal_machine_api_pb_cluster_proto_enumTypes[1].Descriptor()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (DNSRecord_RecordType) Type() protoreflect.EnumType {
|
||||||
|
return &file_internal_machine_api_pb_cluster_proto_enumTypes[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x DNSRecord_RecordType) Number() protoreflect.EnumNumber {
|
||||||
|
return protoreflect.EnumNumber(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use DNSRecord_RecordType.Descriptor instead.
|
||||||
|
func (DNSRecord_RecordType) EnumDescriptor() ([]byte, []int) {
|
||||||
|
return file_internal_machine_api_pb_cluster_proto_rawDescGZIP(), []int{8, 0}
|
||||||
|
}
|
||||||
|
|
||||||
type AddMachineRequest struct {
|
type AddMachineRequest struct {
|
||||||
state protoimpl.MessageState
|
state protoimpl.MessageState
|
||||||
sizeCache protoimpl.SizeCache
|
sizeCache protoimpl.SizeCache
|
||||||
unknownFields protoimpl.UnknownFields
|
unknownFields protoimpl.UnknownFields
|
||||||
|
|
||||||
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
|
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
|
||||||
Network *NetworkConfig `protobuf:"bytes,2,opt,name=network,proto3" json:"network,omitempty"`
|
Network *NetworkConfig `protobuf:"bytes,2,opt,name=network,proto3" json:"network,omitempty"`
|
||||||
|
PublicIp *IP `protobuf:"bytes,3,opt,name=public_ip,json=publicIp,proto3" json:"public_ip,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *AddMachineRequest) Reset() {
|
func (x *AddMachineRequest) Reset() {
|
||||||
@@ -133,6 +183,13 @@ func (x *AddMachineRequest) GetNetwork() *NetworkConfig {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (x *AddMachineRequest) GetPublicIp() *IP {
|
||||||
|
if x != nil {
|
||||||
|
return x.PublicIp
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
type AddMachineResponse struct {
|
type AddMachineResponse struct {
|
||||||
state protoimpl.MessageState
|
state protoimpl.MessageState
|
||||||
sizeCache protoimpl.SizeCache
|
sizeCache protoimpl.SizeCache
|
||||||
@@ -282,6 +339,257 @@ func (x *ListMachinesResponse) GetMachines() []*MachineMember {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Domain struct {
|
||||||
|
state protoimpl.MessageState
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
|
||||||
|
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Domain) Reset() {
|
||||||
|
*x = Domain{}
|
||||||
|
if protoimpl.UnsafeEnabled {
|
||||||
|
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[4]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Domain) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*Domain) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *Domain) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[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 Domain.ProtoReflect.Descriptor instead.
|
||||||
|
func (*Domain) Descriptor() ([]byte, []int) {
|
||||||
|
return file_internal_machine_api_pb_cluster_proto_rawDescGZIP(), []int{4}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Domain) GetName() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Name
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
type ReserveDomainRequest struct {
|
||||||
|
state protoimpl.MessageState
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
|
||||||
|
Endpoint string `protobuf:"bytes,1,opt,name=endpoint,proto3" json:"endpoint,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ReserveDomainRequest) Reset() {
|
||||||
|
*x = ReserveDomainRequest{}
|
||||||
|
if protoimpl.UnsafeEnabled {
|
||||||
|
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[5]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ReserveDomainRequest) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*ReserveDomainRequest) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *ReserveDomainRequest) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[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 ReserveDomainRequest.ProtoReflect.Descriptor instead.
|
||||||
|
func (*ReserveDomainRequest) Descriptor() ([]byte, []int) {
|
||||||
|
return file_internal_machine_api_pb_cluster_proto_rawDescGZIP(), []int{5}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ReserveDomainRequest) GetEndpoint() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Endpoint
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateDomainRecordsRequest struct {
|
||||||
|
state protoimpl.MessageState
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
|
||||||
|
Records []*DNSRecord `protobuf:"bytes,1,rep,name=records,proto3" json:"records,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *CreateDomainRecordsRequest) Reset() {
|
||||||
|
*x = CreateDomainRecordsRequest{}
|
||||||
|
if protoimpl.UnsafeEnabled {
|
||||||
|
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[6]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *CreateDomainRecordsRequest) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*CreateDomainRecordsRequest) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *CreateDomainRecordsRequest) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[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 CreateDomainRecordsRequest.ProtoReflect.Descriptor instead.
|
||||||
|
func (*CreateDomainRecordsRequest) Descriptor() ([]byte, []int) {
|
||||||
|
return file_internal_machine_api_pb_cluster_proto_rawDescGZIP(), []int{6}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *CreateDomainRecordsRequest) GetRecords() []*DNSRecord {
|
||||||
|
if x != nil {
|
||||||
|
return x.Records
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateDomainRecordsResponse struct {
|
||||||
|
state protoimpl.MessageState
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
|
||||||
|
Records []*DNSRecord `protobuf:"bytes,1,rep,name=records,proto3" json:"records,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *CreateDomainRecordsResponse) Reset() {
|
||||||
|
*x = CreateDomainRecordsResponse{}
|
||||||
|
if protoimpl.UnsafeEnabled {
|
||||||
|
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[7]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *CreateDomainRecordsResponse) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*CreateDomainRecordsResponse) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *CreateDomainRecordsResponse) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[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 CreateDomainRecordsResponse.ProtoReflect.Descriptor instead.
|
||||||
|
func (*CreateDomainRecordsResponse) Descriptor() ([]byte, []int) {
|
||||||
|
return file_internal_machine_api_pb_cluster_proto_rawDescGZIP(), []int{7}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *CreateDomainRecordsResponse) GetRecords() []*DNSRecord {
|
||||||
|
if x != nil {
|
||||||
|
return x.Records
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type DNSRecord struct {
|
||||||
|
state protoimpl.MessageState
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
|
||||||
|
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
|
||||||
|
Type DNSRecord_RecordType `protobuf:"varint,2,opt,name=type,proto3,enum=api.DNSRecord_RecordType" json:"type,omitempty"`
|
||||||
|
Values []string `protobuf:"bytes,3,rep,name=values,proto3" json:"values,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *DNSRecord) Reset() {
|
||||||
|
*x = DNSRecord{}
|
||||||
|
if protoimpl.UnsafeEnabled {
|
||||||
|
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[8]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *DNSRecord) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*DNSRecord) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *DNSRecord) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[8]
|
||||||
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use DNSRecord.ProtoReflect.Descriptor instead.
|
||||||
|
func (*DNSRecord) Descriptor() ([]byte, []int) {
|
||||||
|
return file_internal_machine_api_pb_cluster_proto_rawDescGZIP(), []int{8}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *DNSRecord) GetName() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Name
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *DNSRecord) GetType() DNSRecord_RecordType {
|
||||||
|
if x != nil {
|
||||||
|
return x.Type
|
||||||
|
}
|
||||||
|
return DNSRecord_UNSPECIFIED
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *DNSRecord) GetValues() []string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Values
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
var File_internal_machine_api_pb_cluster_proto protoreflect.FileDescriptor
|
var File_internal_machine_api_pb_cluster_proto protoreflect.FileDescriptor
|
||||||
|
|
||||||
var file_internal_machine_api_pb_cluster_proto_rawDesc = []byte{
|
var file_internal_machine_api_pb_cluster_proto_rawDesc = []byte{
|
||||||
@@ -289,48 +597,92 @@ var file_internal_machine_api_pb_cluster_proto_rawDesc = []byte{
|
|||||||
0x6e, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x62, 0x2f, 0x63, 0x6c, 0x75, 0x73, 0x74, 0x65,
|
0x6e, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x62, 0x2f, 0x63, 0x6c, 0x75, 0x73, 0x74, 0x65,
|
||||||
0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x03, 0x61, 0x70, 0x69, 0x1a, 0x1b, 0x67, 0x6f,
|
0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x03, 0x61, 0x70, 0x69, 0x1a, 0x1b, 0x67, 0x6f,
|
||||||
0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x65, 0x6d,
|
0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x65, 0x6d,
|
||||||
0x70, 0x74, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x25, 0x69, 0x6e, 0x74, 0x65, 0x72,
|
0x70, 0x74, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x24, 0x69, 0x6e, 0x74, 0x65, 0x72,
|
||||||
0x6e, 0x61, 0x6c, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f,
|
0x6e, 0x61, 0x6c, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f,
|
||||||
0x70, 0x62, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f,
|
0x70, 0x62, 0x2f, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a,
|
||||||
0x22, 0x55, 0x0a, 0x11, 0x41, 0x64, 0x64, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65,
|
0x25, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e,
|
||||||
0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20,
|
0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x62, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65,
|
||||||
0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x2c, 0x0a, 0x07, 0x6e, 0x65, 0x74,
|
0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x7b, 0x0a, 0x11, 0x41, 0x64, 0x64, 0x4d, 0x61, 0x63,
|
||||||
0x77, 0x6f, 0x72, 0x6b, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x61, 0x70, 0x69,
|
0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x6e,
|
||||||
0x2e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x07,
|
0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12,
|
||||||
0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x22, 0x40, 0x0a, 0x12, 0x41, 0x64, 0x64, 0x4d, 0x61,
|
0x2c, 0x0a, 0x07, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b,
|
||||||
0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2a, 0x0a,
|
0x32, 0x12, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x43, 0x6f,
|
||||||
0x07, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10,
|
0x6e, 0x66, 0x69, 0x67, 0x52, 0x07, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x12, 0x24, 0x0a,
|
||||||
0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f,
|
0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x69, 0x70, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b,
|
||||||
0x52, 0x07, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x22, 0xb4, 0x01, 0x0a, 0x0d, 0x4d, 0x61,
|
0x32, 0x07, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x50, 0x52, 0x08, 0x70, 0x75, 0x62, 0x6c, 0x69,
|
||||||
0x63, 0x68, 0x69, 0x6e, 0x65, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x12, 0x2a, 0x0a, 0x07, 0x6d,
|
0x63, 0x49, 0x70, 0x22, 0x40, 0x0a, 0x12, 0x41, 0x64, 0x64, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e,
|
||||||
0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x61,
|
0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2a, 0x0a, 0x07, 0x6d, 0x61, 0x63,
|
||||||
0x70, 0x69, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x07,
|
0x68, 0x69, 0x6e, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x61, 0x70, 0x69,
|
||||||
0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x12, 0x38, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65,
|
0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x07, 0x6d, 0x61,
|
||||||
0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x22, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d, 0x61, 0x63,
|
0x63, 0x68, 0x69, 0x6e, 0x65, 0x22, 0xb4, 0x01, 0x0a, 0x0d, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e,
|
||||||
0x68, 0x69, 0x6e, 0x65, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x2e, 0x4d, 0x65, 0x6d, 0x62, 0x65,
|
0x65, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x12, 0x2a, 0x0a, 0x07, 0x6d, 0x61, 0x63, 0x68, 0x69,
|
||||||
0x72, 0x73, 0x68, 0x69, 0x70, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74,
|
0x6e, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d,
|
||||||
0x65, 0x22, 0x3d, 0x0a, 0x0f, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, 0x68, 0x69, 0x70, 0x53,
|
0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x07, 0x6d, 0x61, 0x63, 0x68,
|
||||||
0x74, 0x61, 0x74, 0x65, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10,
|
0x69, 0x6e, 0x65, 0x12, 0x38, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01,
|
||||||
0x00, 0x12, 0x06, 0x0a, 0x02, 0x55, 0x50, 0x10, 0x01, 0x12, 0x0b, 0x0a, 0x07, 0x53, 0x55, 0x53,
|
0x28, 0x0e, 0x32, 0x22, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65,
|
||||||
0x50, 0x45, 0x43, 0x54, 0x10, 0x02, 0x12, 0x08, 0x0a, 0x04, 0x44, 0x4f, 0x57, 0x4e, 0x10, 0x03,
|
0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x2e, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, 0x68, 0x69,
|
||||||
0x22, 0x46, 0x0a, 0x14, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73,
|
0x70, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x22, 0x3d, 0x0a,
|
||||||
0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2e, 0x0a, 0x08, 0x6d, 0x61, 0x63, 0x68,
|
0x0f, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, 0x68, 0x69, 0x70, 0x53, 0x74, 0x61, 0x74, 0x65,
|
||||||
0x69, 0x6e, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x61, 0x70, 0x69,
|
0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x06, 0x0a,
|
||||||
0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x52, 0x08,
|
0x02, 0x55, 0x50, 0x10, 0x01, 0x12, 0x0b, 0x0a, 0x07, 0x53, 0x55, 0x53, 0x50, 0x45, 0x43, 0x54,
|
||||||
0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x32, 0x8b, 0x01, 0x0a, 0x07, 0x43, 0x6c, 0x75,
|
0x10, 0x02, 0x12, 0x08, 0x0a, 0x04, 0x44, 0x4f, 0x57, 0x4e, 0x10, 0x03, 0x22, 0x46, 0x0a, 0x14,
|
||||||
0x73, 0x74, 0x65, 0x72, 0x12, 0x3d, 0x0a, 0x0a, 0x41, 0x64, 0x64, 0x4d, 0x61, 0x63, 0x68, 0x69,
|
0x4c, 0x69, 0x73, 0x74, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70,
|
||||||
0x6e, 0x65, 0x12, 0x16, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x41, 0x64, 0x64, 0x4d, 0x61, 0x63, 0x68,
|
0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2e, 0x0a, 0x08, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73,
|
||||||
0x69, 0x6e, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x17, 0x2e, 0x61, 0x70, 0x69,
|
0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d, 0x61, 0x63,
|
||||||
0x2e, 0x41, 0x64, 0x64, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f,
|
0x68, 0x69, 0x6e, 0x65, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x52, 0x08, 0x6d, 0x61, 0x63, 0x68,
|
||||||
0x6e, 0x73, 0x65, 0x12, 0x41, 0x0a, 0x0c, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x61, 0x63, 0x68, 0x69,
|
0x69, 0x6e, 0x65, 0x73, 0x22, 0x1c, 0x0a, 0x06, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x12,
|
||||||
0x6e, 0x65, 0x73, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f,
|
0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61,
|
||||||
0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x19, 0x2e, 0x61, 0x70,
|
0x6d, 0x65, 0x22, 0x32, 0x0a, 0x14, 0x52, 0x65, 0x73, 0x65, 0x72, 0x76, 0x65, 0x44, 0x6f, 0x6d,
|
||||||
0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x52, 0x65,
|
0x61, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x65, 0x6e,
|
||||||
0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x37, 0x5a, 0x35, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62,
|
0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x65, 0x6e,
|
||||||
0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x70, 0x73, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x6b, 0x69, 0x2f,
|
0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x22, 0x46, 0x0a, 0x1a, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65,
|
||||||
0x75, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c,
|
0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x52, 0x65, 0x71,
|
||||||
0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x62, 0x62,
|
0x75, 0x65, 0x73, 0x74, 0x12, 0x28, 0x0a, 0x07, 0x72, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x18,
|
||||||
0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x44, 0x4e, 0x53, 0x52,
|
||||||
|
0x65, 0x63, 0x6f, 0x72, 0x64, 0x52, 0x07, 0x72, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x22, 0x47,
|
||||||
|
0x0a, 0x1b, 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, 0x12, 0x28, 0x0a,
|
||||||
|
0x07, 0x72, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0e,
|
||||||
|
0x2e, 0x61, 0x70, 0x69, 0x2e, 0x44, 0x4e, 0x53, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x52, 0x07,
|
||||||
|
0x72, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x22, 0x96, 0x01, 0x0a, 0x09, 0x44, 0x4e, 0x53, 0x52,
|
||||||
|
0x65, 0x63, 0x6f, 0x72, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20,
|
||||||
|
0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x2d, 0x0a, 0x04, 0x74, 0x79, 0x70,
|
||||||
|
0x65, 0x18, 0x02, 0x20, 0x01, 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, 0x86, 0x03, 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, 0x37,
|
||||||
|
0x0a, 0x0d, 0x52, 0x65, 0x73, 0x65, 0x72, 0x76, 0x65, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12,
|
||||||
|
0x19, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x52, 0x65, 0x73, 0x65, 0x72, 0x76, 0x65, 0x44, 0x6f, 0x6d,
|
||||||
|
0x61, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0b, 0x2e, 0x61, 0x70, 0x69,
|
||||||
|
0x2e, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x30, 0x0a, 0x09, 0x47, 0x65, 0x74, 0x44, 0x6f,
|
||||||
|
0x6d, 0x61, 0x69, 0x6e, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72,
|
||||||
|
0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x0b, 0x2e, 0x61,
|
||||||
|
0x70, 0x69, 0x2e, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x34, 0x0a, 0x0d, 0x52, 0x65, 0x6c,
|
||||||
|
0x65, 0x61, 0x73, 0x65, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f,
|
||||||
|
0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70,
|
||||||
|
0x74, 0x79, 0x1a, 0x0b, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12,
|
||||||
|
0x58, 0x0a, 0x13, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x52,
|
||||||
|
0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x12, 0x1f, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x72, 0x65,
|
||||||
|
0x61, 0x74, 0x65, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73,
|
||||||
|
0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x72,
|
||||||
|
0x65, 0x61, 0x74, 0x65, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64,
|
||||||
|
0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x37, 0x5a, 0x35, 0x67, 0x69, 0x74,
|
||||||
|
0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x70, 0x73, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73,
|
||||||
|
0x6b, 0x69, 0x2f, 0x75, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72,
|
||||||
|
0x6e, 0x61, 0x6c, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f,
|
||||||
|
0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -345,33 +697,52 @@ func file_internal_machine_api_pb_cluster_proto_rawDescGZIP() []byte {
|
|||||||
return file_internal_machine_api_pb_cluster_proto_rawDescData
|
return file_internal_machine_api_pb_cluster_proto_rawDescData
|
||||||
}
|
}
|
||||||
|
|
||||||
var file_internal_machine_api_pb_cluster_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
|
var file_internal_machine_api_pb_cluster_proto_enumTypes = make([]protoimpl.EnumInfo, 2)
|
||||||
var file_internal_machine_api_pb_cluster_proto_msgTypes = make([]protoimpl.MessageInfo, 4)
|
var file_internal_machine_api_pb_cluster_proto_msgTypes = make([]protoimpl.MessageInfo, 9)
|
||||||
var file_internal_machine_api_pb_cluster_proto_goTypes = []any{
|
var file_internal_machine_api_pb_cluster_proto_goTypes = []any{
|
||||||
(MachineMember_MembershipState)(0), // 0: api.MachineMember.MembershipState
|
(MachineMember_MembershipState)(0), // 0: api.MachineMember.MembershipState
|
||||||
(*AddMachineRequest)(nil), // 1: api.AddMachineRequest
|
(DNSRecord_RecordType)(0), // 1: api.DNSRecord.RecordType
|
||||||
(*AddMachineResponse)(nil), // 2: api.AddMachineResponse
|
(*AddMachineRequest)(nil), // 2: api.AddMachineRequest
|
||||||
(*MachineMember)(nil), // 3: api.MachineMember
|
(*AddMachineResponse)(nil), // 3: api.AddMachineResponse
|
||||||
(*ListMachinesResponse)(nil), // 4: api.ListMachinesResponse
|
(*MachineMember)(nil), // 4: api.MachineMember
|
||||||
(*NetworkConfig)(nil), // 5: api.NetworkConfig
|
(*ListMachinesResponse)(nil), // 5: api.ListMachinesResponse
|
||||||
(*MachineInfo)(nil), // 6: api.MachineInfo
|
(*Domain)(nil), // 6: api.Domain
|
||||||
(*emptypb.Empty)(nil), // 7: google.protobuf.Empty
|
(*ReserveDomainRequest)(nil), // 7: api.ReserveDomainRequest
|
||||||
|
(*CreateDomainRecordsRequest)(nil), // 8: api.CreateDomainRecordsRequest
|
||||||
|
(*CreateDomainRecordsResponse)(nil), // 9: api.CreateDomainRecordsResponse
|
||||||
|
(*DNSRecord)(nil), // 10: api.DNSRecord
|
||||||
|
(*NetworkConfig)(nil), // 11: api.NetworkConfig
|
||||||
|
(*IP)(nil), // 12: api.IP
|
||||||
|
(*MachineInfo)(nil), // 13: api.MachineInfo
|
||||||
|
(*emptypb.Empty)(nil), // 14: google.protobuf.Empty
|
||||||
}
|
}
|
||||||
var file_internal_machine_api_pb_cluster_proto_depIdxs = []int32{
|
var file_internal_machine_api_pb_cluster_proto_depIdxs = []int32{
|
||||||
5, // 0: api.AddMachineRequest.network:type_name -> api.NetworkConfig
|
11, // 0: api.AddMachineRequest.network:type_name -> api.NetworkConfig
|
||||||
6, // 1: api.AddMachineResponse.machine:type_name -> api.MachineInfo
|
12, // 1: api.AddMachineRequest.public_ip:type_name -> api.IP
|
||||||
6, // 2: api.MachineMember.machine:type_name -> api.MachineInfo
|
13, // 2: api.AddMachineResponse.machine:type_name -> api.MachineInfo
|
||||||
0, // 3: api.MachineMember.state:type_name -> api.MachineMember.MembershipState
|
13, // 3: api.MachineMember.machine:type_name -> api.MachineInfo
|
||||||
3, // 4: api.ListMachinesResponse.machines:type_name -> api.MachineMember
|
0, // 4: api.MachineMember.state:type_name -> api.MachineMember.MembershipState
|
||||||
1, // 5: api.Cluster.AddMachine:input_type -> api.AddMachineRequest
|
4, // 5: api.ListMachinesResponse.machines:type_name -> api.MachineMember
|
||||||
7, // 6: api.Cluster.ListMachines:input_type -> google.protobuf.Empty
|
10, // 6: api.CreateDomainRecordsRequest.records:type_name -> api.DNSRecord
|
||||||
2, // 7: api.Cluster.AddMachine:output_type -> api.AddMachineResponse
|
10, // 7: api.CreateDomainRecordsResponse.records:type_name -> api.DNSRecord
|
||||||
4, // 8: api.Cluster.ListMachines:output_type -> api.ListMachinesResponse
|
1, // 8: api.DNSRecord.type:type_name -> api.DNSRecord.RecordType
|
||||||
7, // [7:9] is the sub-list for method output_type
|
2, // 9: api.Cluster.AddMachine:input_type -> api.AddMachineRequest
|
||||||
5, // [5:7] is the sub-list for method input_type
|
14, // 10: api.Cluster.ListMachines:input_type -> google.protobuf.Empty
|
||||||
5, // [5:5] is the sub-list for extension type_name
|
7, // 11: api.Cluster.ReserveDomain:input_type -> api.ReserveDomainRequest
|
||||||
5, // [5:5] is the sub-list for extension extendee
|
14, // 12: api.Cluster.GetDomain:input_type -> google.protobuf.Empty
|
||||||
0, // [0:5] is the sub-list for field type_name
|
14, // 13: api.Cluster.ReleaseDomain:input_type -> google.protobuf.Empty
|
||||||
|
8, // 14: api.Cluster.CreateDomainRecords:input_type -> api.CreateDomainRecordsRequest
|
||||||
|
3, // 15: api.Cluster.AddMachine:output_type -> api.AddMachineResponse
|
||||||
|
5, // 16: api.Cluster.ListMachines:output_type -> api.ListMachinesResponse
|
||||||
|
6, // 17: api.Cluster.ReserveDomain:output_type -> api.Domain
|
||||||
|
6, // 18: api.Cluster.GetDomain:output_type -> api.Domain
|
||||||
|
6, // 19: api.Cluster.ReleaseDomain:output_type -> api.Domain
|
||||||
|
9, // 20: api.Cluster.CreateDomainRecords:output_type -> api.CreateDomainRecordsResponse
|
||||||
|
15, // [15:21] is the sub-list for method output_type
|
||||||
|
9, // [9:15] is the sub-list for method input_type
|
||||||
|
9, // [9:9] is the sub-list for extension type_name
|
||||||
|
9, // [9:9] is the sub-list for extension extendee
|
||||||
|
0, // [0:9] is the sub-list for field type_name
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() { file_internal_machine_api_pb_cluster_proto_init() }
|
func init() { file_internal_machine_api_pb_cluster_proto_init() }
|
||||||
@@ -379,6 +750,7 @@ func file_internal_machine_api_pb_cluster_proto_init() {
|
|||||||
if File_internal_machine_api_pb_cluster_proto != nil {
|
if File_internal_machine_api_pb_cluster_proto != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
file_internal_machine_api_pb_common_proto_init()
|
||||||
file_internal_machine_api_pb_machine_proto_init()
|
file_internal_machine_api_pb_machine_proto_init()
|
||||||
if !protoimpl.UnsafeEnabled {
|
if !protoimpl.UnsafeEnabled {
|
||||||
file_internal_machine_api_pb_cluster_proto_msgTypes[0].Exporter = func(v any, i int) any {
|
file_internal_machine_api_pb_cluster_proto_msgTypes[0].Exporter = func(v any, i int) any {
|
||||||
@@ -429,14 +801,74 @@ func file_internal_machine_api_pb_cluster_proto_init() {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
file_internal_machine_api_pb_cluster_proto_msgTypes[4].Exporter = func(v any, i int) any {
|
||||||
|
switch v := v.(*Domain); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file_internal_machine_api_pb_cluster_proto_msgTypes[5].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[6].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[7].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[8].Exporter = func(v any, i int) any {
|
||||||
|
switch v := v.(*DNSRecord); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
type x struct{}
|
type x struct{}
|
||||||
out := protoimpl.TypeBuilder{
|
out := protoimpl.TypeBuilder{
|
||||||
File: protoimpl.DescBuilder{
|
File: protoimpl.DescBuilder{
|
||||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||||
RawDescriptor: file_internal_machine_api_pb_cluster_proto_rawDesc,
|
RawDescriptor: file_internal_machine_api_pb_cluster_proto_rawDesc,
|
||||||
NumEnums: 1,
|
NumEnums: 2,
|
||||||
NumMessages: 4,
|
NumMessages: 9,
|
||||||
NumExtensions: 0,
|
NumExtensions: 0,
|
||||||
NumServices: 1,
|
NumServices: 1,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -5,16 +5,23 @@ package api;
|
|||||||
option go_package = "github.com/psviderski/uncloud/internal/machine/api/pb";
|
option go_package = "github.com/psviderski/uncloud/internal/machine/api/pb";
|
||||||
|
|
||||||
import "google/protobuf/empty.proto";
|
import "google/protobuf/empty.proto";
|
||||||
|
import "internal/machine/api/pb/common.proto";
|
||||||
import "internal/machine/api/pb/machine.proto";
|
import "internal/machine/api/pb/machine.proto";
|
||||||
|
|
||||||
service Cluster {
|
service Cluster {
|
||||||
rpc AddMachine(AddMachineRequest) returns (AddMachineResponse);
|
rpc AddMachine(AddMachineRequest) returns (AddMachineResponse);
|
||||||
rpc ListMachines(google.protobuf.Empty) returns (ListMachinesResponse);
|
rpc ListMachines(google.protobuf.Empty) returns (ListMachinesResponse);
|
||||||
|
|
||||||
|
rpc ReserveDomain(ReserveDomainRequest) returns (Domain);
|
||||||
|
rpc GetDomain(google.protobuf.Empty) returns (Domain);
|
||||||
|
rpc ReleaseDomain(google.protobuf.Empty) returns (Domain);
|
||||||
|
rpc CreateDomainRecords(CreateDomainRecordsRequest) returns (CreateDomainRecordsResponse);
|
||||||
}
|
}
|
||||||
|
|
||||||
message AddMachineRequest {
|
message AddMachineRequest {
|
||||||
string name = 1;
|
string name = 1;
|
||||||
NetworkConfig network = 2;
|
NetworkConfig network = 2;
|
||||||
|
IP public_ip = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
message AddMachineResponse {
|
message AddMachineResponse {
|
||||||
@@ -41,3 +48,31 @@ message MachineMember {
|
|||||||
message ListMachinesResponse {
|
message ListMachinesResponse {
|
||||||
repeated MachineMember machines = 1;
|
repeated MachineMember machines = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message Domain {
|
||||||
|
string name = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ReserveDomainRequest {
|
||||||
|
string endpoint = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message CreateDomainRecordsRequest {
|
||||||
|
repeated DNSRecord records = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message CreateDomainRecordsResponse {
|
||||||
|
repeated DNSRecord records = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message DNSRecord {
|
||||||
|
string name = 1;
|
||||||
|
|
||||||
|
enum RecordType {
|
||||||
|
UNSPECIFIED = 0;
|
||||||
|
A = 1;
|
||||||
|
AAAA = 2;
|
||||||
|
}
|
||||||
|
RecordType type = 2;
|
||||||
|
repeated string values = 3;
|
||||||
|
}
|
||||||
|
|||||||
@@ -20,8 +20,12 @@ import (
|
|||||||
const _ = grpc.SupportPackageIsVersion9
|
const _ = grpc.SupportPackageIsVersion9
|
||||||
|
|
||||||
const (
|
const (
|
||||||
Cluster_AddMachine_FullMethodName = "/api.Cluster/AddMachine"
|
Cluster_AddMachine_FullMethodName = "/api.Cluster/AddMachine"
|
||||||
Cluster_ListMachines_FullMethodName = "/api.Cluster/ListMachines"
|
Cluster_ListMachines_FullMethodName = "/api.Cluster/ListMachines"
|
||||||
|
Cluster_ReserveDomain_FullMethodName = "/api.Cluster/ReserveDomain"
|
||||||
|
Cluster_GetDomain_FullMethodName = "/api.Cluster/GetDomain"
|
||||||
|
Cluster_ReleaseDomain_FullMethodName = "/api.Cluster/ReleaseDomain"
|
||||||
|
Cluster_CreateDomainRecords_FullMethodName = "/api.Cluster/CreateDomainRecords"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ClusterClient is the client API for Cluster service.
|
// ClusterClient is the client API for Cluster service.
|
||||||
@@ -30,6 +34,10 @@ const (
|
|||||||
type ClusterClient interface {
|
type ClusterClient interface {
|
||||||
AddMachine(ctx context.Context, in *AddMachineRequest, opts ...grpc.CallOption) (*AddMachineResponse, error)
|
AddMachine(ctx context.Context, in *AddMachineRequest, opts ...grpc.CallOption) (*AddMachineResponse, error)
|
||||||
ListMachines(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*ListMachinesResponse, error)
|
ListMachines(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*ListMachinesResponse, error)
|
||||||
|
ReserveDomain(ctx context.Context, in *ReserveDomainRequest, opts ...grpc.CallOption) (*Domain, error)
|
||||||
|
GetDomain(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*Domain, error)
|
||||||
|
ReleaseDomain(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*Domain, error)
|
||||||
|
CreateDomainRecords(ctx context.Context, in *CreateDomainRecordsRequest, opts ...grpc.CallOption) (*CreateDomainRecordsResponse, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type clusterClient struct {
|
type clusterClient struct {
|
||||||
@@ -60,12 +68,56 @@ func (c *clusterClient) ListMachines(ctx context.Context, in *emptypb.Empty, opt
|
|||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *clusterClient) ReserveDomain(ctx context.Context, in *ReserveDomainRequest, opts ...grpc.CallOption) (*Domain, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(Domain)
|
||||||
|
err := c.cc.Invoke(ctx, Cluster_ReserveDomain_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *clusterClient) GetDomain(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*Domain, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(Domain)
|
||||||
|
err := c.cc.Invoke(ctx, Cluster_GetDomain_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *clusterClient) ReleaseDomain(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*Domain, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(Domain)
|
||||||
|
err := c.cc.Invoke(ctx, Cluster_ReleaseDomain_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *clusterClient) CreateDomainRecords(ctx context.Context, in *CreateDomainRecordsRequest, opts ...grpc.CallOption) (*CreateDomainRecordsResponse, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(CreateDomainRecordsResponse)
|
||||||
|
err := c.cc.Invoke(ctx, Cluster_CreateDomainRecords_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
// ClusterServer is the server API for Cluster service.
|
// ClusterServer is the server API for Cluster service.
|
||||||
// All implementations must embed UnimplementedClusterServer
|
// All implementations must embed UnimplementedClusterServer
|
||||||
// for forward compatibility.
|
// for forward compatibility.
|
||||||
type ClusterServer interface {
|
type ClusterServer interface {
|
||||||
AddMachine(context.Context, *AddMachineRequest) (*AddMachineResponse, error)
|
AddMachine(context.Context, *AddMachineRequest) (*AddMachineResponse, error)
|
||||||
ListMachines(context.Context, *emptypb.Empty) (*ListMachinesResponse, error)
|
ListMachines(context.Context, *emptypb.Empty) (*ListMachinesResponse, error)
|
||||||
|
ReserveDomain(context.Context, *ReserveDomainRequest) (*Domain, error)
|
||||||
|
GetDomain(context.Context, *emptypb.Empty) (*Domain, error)
|
||||||
|
ReleaseDomain(context.Context, *emptypb.Empty) (*Domain, error)
|
||||||
|
CreateDomainRecords(context.Context, *CreateDomainRecordsRequest) (*CreateDomainRecordsResponse, error)
|
||||||
mustEmbedUnimplementedClusterServer()
|
mustEmbedUnimplementedClusterServer()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,6 +134,18 @@ 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) ReserveDomain(context.Context, *ReserveDomainRequest) (*Domain, error) {
|
||||||
|
return nil, status.Errorf(codes.Unimplemented, "method ReserveDomain not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedClusterServer) GetDomain(context.Context, *emptypb.Empty) (*Domain, error) {
|
||||||
|
return nil, status.Errorf(codes.Unimplemented, "method GetDomain not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedClusterServer) ReleaseDomain(context.Context, *emptypb.Empty) (*Domain, error) {
|
||||||
|
return nil, status.Errorf(codes.Unimplemented, "method ReleaseDomain not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedClusterServer) CreateDomainRecords(context.Context, *CreateDomainRecordsRequest) (*CreateDomainRecordsResponse, error) {
|
||||||
|
return nil, status.Errorf(codes.Unimplemented, "method CreateDomainRecords not implemented")
|
||||||
|
}
|
||||||
func (UnimplementedClusterServer) mustEmbedUnimplementedClusterServer() {}
|
func (UnimplementedClusterServer) mustEmbedUnimplementedClusterServer() {}
|
||||||
func (UnimplementedClusterServer) testEmbeddedByValue() {}
|
func (UnimplementedClusterServer) testEmbeddedByValue() {}
|
||||||
|
|
||||||
@@ -139,6 +203,78 @@ 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_ReserveDomain_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(ReserveDomainRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(ClusterServer).ReserveDomain(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: Cluster_ReserveDomain_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(ClusterServer).ReserveDomain(ctx, req.(*ReserveDomainRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _Cluster_GetDomain_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(emptypb.Empty)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(ClusterServer).GetDomain(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: Cluster_GetDomain_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(ClusterServer).GetDomain(ctx, req.(*emptypb.Empty))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _Cluster_ReleaseDomain_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(emptypb.Empty)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(ClusterServer).ReleaseDomain(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: Cluster_ReleaseDomain_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(ClusterServer).ReleaseDomain(ctx, req.(*emptypb.Empty))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _Cluster_CreateDomainRecords_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(CreateDomainRecordsRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(ClusterServer).CreateDomainRecords(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: Cluster_CreateDomainRecords_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(ClusterServer).CreateDomainRecords(ctx, req.(*CreateDomainRecordsRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
// Cluster_ServiceDesc is the grpc.ServiceDesc for Cluster service.
|
// Cluster_ServiceDesc is the grpc.ServiceDesc for Cluster service.
|
||||||
// It's only intended for direct use with grpc.RegisterService,
|
// It's only intended for direct use with grpc.RegisterService,
|
||||||
// and not to be introspected or modified (even as a copy)
|
// and not to be introspected or modified (even as a copy)
|
||||||
@@ -154,6 +290,22 @@ var Cluster_ServiceDesc = grpc.ServiceDesc{
|
|||||||
MethodName: "ListMachines",
|
MethodName: "ListMachines",
|
||||||
Handler: _Cluster_ListMachines_Handler,
|
Handler: _Cluster_ListMachines_Handler,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
MethodName: "ReserveDomain",
|
||||||
|
Handler: _Cluster_ReserveDomain_Handler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MethodName: "GetDomain",
|
||||||
|
Handler: _Cluster_GetDomain_Handler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MethodName: "ReleaseDomain",
|
||||||
|
Handler: _Cluster_ReleaseDomain_Handler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MethodName: "CreateDomainRecords",
|
||||||
|
Handler: _Cluster_CreateDomainRecords_Handler,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
Streams: []grpc.StreamDesc{},
|
Streams: []grpc.StreamDesc{},
|
||||||
Metadata: "internal/machine/api/pb/cluster.proto",
|
Metadata: "internal/machine/api/pb/cluster.proto",
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -15,22 +15,31 @@ service Docker {
|
|||||||
rpc ListContainers(ListContainersRequest) returns (ListContainersResponse);
|
rpc ListContainers(ListContainersRequest) returns (ListContainersResponse);
|
||||||
rpc RemoveContainer(RemoveContainerRequest) returns (google.protobuf.Empty);
|
rpc RemoveContainer(RemoveContainerRequest) returns (google.protobuf.Empty);
|
||||||
rpc PullImage(PullImageRequest) returns (stream JSONMessage);
|
rpc PullImage(PullImageRequest) returns (stream JSONMessage);
|
||||||
|
rpc InspectImage(InspectImageRequest) returns (InspectImageResponse);
|
||||||
|
// InspectRemoteImage returns the image metadata for an image in a remote registry using the machine's
|
||||||
|
// Docker auth credentials if necessary.
|
||||||
|
rpc InspectRemoteImage(InspectRemoteImageRequest) returns (InspectRemoteImageResponse);
|
||||||
|
|
||||||
|
rpc CreateServiceContainer(CreateServiceContainerRequest) returns (CreateContainerResponse);
|
||||||
|
rpc InspectServiceContainer(InspectContainerRequest) returns (ServiceContainer);
|
||||||
|
rpc ListServiceContainers(ListServiceContainersRequest) returns (ListServiceContainersResponse);
|
||||||
|
rpc RemoveServiceContainer(RemoveContainerRequest) returns (google.protobuf.Empty);
|
||||||
}
|
}
|
||||||
|
|
||||||
message CreateContainerRequest {
|
message CreateContainerRequest {
|
||||||
// JSON serialized container.Config.
|
// JSON serialised container.Config.
|
||||||
bytes config = 1;
|
bytes config = 1;
|
||||||
// JSON serialized container.HostConfig.
|
// JSON serialised container.HostConfig.
|
||||||
bytes host_config = 2;
|
bytes host_config = 2;
|
||||||
// JSON serialized network.NetworkingConfig.
|
// JSON serialised network.NetworkingConfig.
|
||||||
bytes network_config = 3;
|
bytes network_config = 3;
|
||||||
// JSON serialized ocispec.Platform.
|
// JSON serialised ocispec.Platform.
|
||||||
bytes platform = 4;
|
bytes platform = 4;
|
||||||
string name = 5;
|
string name = 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
message CreateContainerResponse {
|
message CreateContainerResponse {
|
||||||
// JSON serialized container.CreateResponse.
|
// JSON serialised container.CreateResponse.
|
||||||
bytes response = 1;
|
bytes response = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,51 +48,114 @@ message InspectContainerRequest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
message InspectContainerResponse {
|
message InspectContainerResponse {
|
||||||
// JSON serialized container.InspectResponse.
|
// JSON serialised container.InspectResponse.
|
||||||
bytes response = 1;
|
bytes response = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
message StartContainerRequest {
|
message StartContainerRequest {
|
||||||
string id = 1;
|
string id = 1;
|
||||||
// JSON serialized container.StartOptions.
|
// JSON serialised container.StartOptions.
|
||||||
bytes options = 2;
|
bytes options = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
message StopContainerRequest {
|
message StopContainerRequest {
|
||||||
string id = 1;
|
string id = 1;
|
||||||
// JSON serialized container.StopOptions.
|
// JSON serialised container.StopOptions.
|
||||||
bytes options = 2;
|
bytes options = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
message ListContainersRequest {
|
message ListContainersRequest {
|
||||||
// JSON serialized container.ListOptions.
|
// JSON serialised container.ListOptions.
|
||||||
bytes options = 1;
|
bytes options = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListContainersResponse structure allows broadcasting ListContainers requests to multiple machines.
|
|
||||||
message ListContainersResponse {
|
message ListContainersResponse {
|
||||||
|
// Must contain only one repeated messages field to allow broadcasting ListContainers requests to multiple machines.
|
||||||
repeated MachineContainers messages = 1;
|
repeated MachineContainers messages = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
message MachineContainers {
|
message MachineContainers {
|
||||||
Metadata metadata = 1;
|
Metadata metadata = 1;
|
||||||
// JSON serialized []container.ContainerJSON.
|
// JSON serialised []container.InspectResponse.
|
||||||
bytes containers = 2;
|
bytes containers = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
message RemoveContainerRequest {
|
message RemoveContainerRequest {
|
||||||
string id = 1;
|
string id = 1;
|
||||||
// JSON serialized container.RemoveOptions.
|
// JSON serialised container.RemoveOptions.
|
||||||
bytes options = 2;
|
bytes options = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
message PullImageRequest {
|
message PullImageRequest {
|
||||||
string image = 1;
|
string image = 1;
|
||||||
// JSON serialized image.PullOptions.
|
// JSON serialised image.PullOptions.
|
||||||
bytes options = 2;
|
bytes options = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
message JSONMessage {
|
message JSONMessage {
|
||||||
// JSON serialized jsonmessage.JSONMessage.
|
// JSON serialised jsonmessage.JSONMessage.
|
||||||
bytes message = 1;
|
bytes message = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message InspectImageRequest {
|
||||||
|
string id = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message InspectImageResponse {
|
||||||
|
// Must contain only one repeated messages field to allow broadcasting InspectImage requests to multiple machines.
|
||||||
|
repeated Image messages = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message Image {
|
||||||
|
Metadata metadata = 1;
|
||||||
|
// JSON serialised types.ImageInspect.
|
||||||
|
bytes image = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message InspectRemoteImageRequest {
|
||||||
|
string id = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message InspectRemoteImageResponse {
|
||||||
|
// Must contain only one repeated messages field to allow broadcasting InspectRemoteImage requests to multiple machines.
|
||||||
|
repeated RemoteImage messages = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message RemoteImage {
|
||||||
|
Metadata metadata = 1;
|
||||||
|
// Image reference in the canonical form with the digest.
|
||||||
|
string reference = 2;
|
||||||
|
// Raw JSON manifest from the registry.
|
||||||
|
bytes manifest = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message CreateServiceContainerRequest {
|
||||||
|
string service_id = 1;
|
||||||
|
// JSON serialised api.ServiceSpec.
|
||||||
|
bytes service_spec = 2;
|
||||||
|
string container_name = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ServiceContainer {
|
||||||
|
// JSON serialised container.InspectResponse.
|
||||||
|
bytes container = 1;
|
||||||
|
// JSON serialised api.ServiceSpec.
|
||||||
|
bytes service_spec = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ListServiceContainersRequest {
|
||||||
|
string service_id = 1;
|
||||||
|
// JSON serialised container.ListOptions.
|
||||||
|
bytes options = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ListServiceContainersResponse {
|
||||||
|
// Must contain only one repeated messages field to allow broadcasting ListServiceContainers requests
|
||||||
|
// to multiple machines.
|
||||||
|
repeated MachineServiceContainers messages = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message MachineServiceContainers {
|
||||||
|
Metadata metadata = 1;
|
||||||
|
repeated ServiceContainer containers = 2;
|
||||||
|
}
|
||||||
|
|||||||
@@ -20,13 +20,19 @@ import (
|
|||||||
const _ = grpc.SupportPackageIsVersion9
|
const _ = grpc.SupportPackageIsVersion9
|
||||||
|
|
||||||
const (
|
const (
|
||||||
Docker_CreateContainer_FullMethodName = "/api.Docker/CreateContainer"
|
Docker_CreateContainer_FullMethodName = "/api.Docker/CreateContainer"
|
||||||
Docker_InspectContainer_FullMethodName = "/api.Docker/InspectContainer"
|
Docker_InspectContainer_FullMethodName = "/api.Docker/InspectContainer"
|
||||||
Docker_StartContainer_FullMethodName = "/api.Docker/StartContainer"
|
Docker_StartContainer_FullMethodName = "/api.Docker/StartContainer"
|
||||||
Docker_StopContainer_FullMethodName = "/api.Docker/StopContainer"
|
Docker_StopContainer_FullMethodName = "/api.Docker/StopContainer"
|
||||||
Docker_ListContainers_FullMethodName = "/api.Docker/ListContainers"
|
Docker_ListContainers_FullMethodName = "/api.Docker/ListContainers"
|
||||||
Docker_RemoveContainer_FullMethodName = "/api.Docker/RemoveContainer"
|
Docker_RemoveContainer_FullMethodName = "/api.Docker/RemoveContainer"
|
||||||
Docker_PullImage_FullMethodName = "/api.Docker/PullImage"
|
Docker_PullImage_FullMethodName = "/api.Docker/PullImage"
|
||||||
|
Docker_InspectImage_FullMethodName = "/api.Docker/InspectImage"
|
||||||
|
Docker_InspectRemoteImage_FullMethodName = "/api.Docker/InspectRemoteImage"
|
||||||
|
Docker_CreateServiceContainer_FullMethodName = "/api.Docker/CreateServiceContainer"
|
||||||
|
Docker_InspectServiceContainer_FullMethodName = "/api.Docker/InspectServiceContainer"
|
||||||
|
Docker_ListServiceContainers_FullMethodName = "/api.Docker/ListServiceContainers"
|
||||||
|
Docker_RemoveServiceContainer_FullMethodName = "/api.Docker/RemoveServiceContainer"
|
||||||
)
|
)
|
||||||
|
|
||||||
// DockerClient is the client API for Docker service.
|
// DockerClient is the client API for Docker service.
|
||||||
@@ -40,6 +46,14 @@ type DockerClient interface {
|
|||||||
ListContainers(ctx context.Context, in *ListContainersRequest, opts ...grpc.CallOption) (*ListContainersResponse, error)
|
ListContainers(ctx context.Context, in *ListContainersRequest, opts ...grpc.CallOption) (*ListContainersResponse, error)
|
||||||
RemoveContainer(ctx context.Context, in *RemoveContainerRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
|
RemoveContainer(ctx context.Context, in *RemoveContainerRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
|
||||||
PullImage(ctx context.Context, in *PullImageRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[JSONMessage], error)
|
PullImage(ctx context.Context, in *PullImageRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[JSONMessage], error)
|
||||||
|
InspectImage(ctx context.Context, in *InspectImageRequest, opts ...grpc.CallOption) (*InspectImageResponse, error)
|
||||||
|
// InspectRemoteImage returns the image metadata for an image in a remote registry using the machine's
|
||||||
|
// Docker auth credentials if necessary.
|
||||||
|
InspectRemoteImage(ctx context.Context, in *InspectRemoteImageRequest, opts ...grpc.CallOption) (*InspectRemoteImageResponse, error)
|
||||||
|
CreateServiceContainer(ctx context.Context, in *CreateServiceContainerRequest, opts ...grpc.CallOption) (*CreateContainerResponse, error)
|
||||||
|
InspectServiceContainer(ctx context.Context, in *InspectContainerRequest, opts ...grpc.CallOption) (*ServiceContainer, error)
|
||||||
|
ListServiceContainers(ctx context.Context, in *ListServiceContainersRequest, opts ...grpc.CallOption) (*ListServiceContainersResponse, error)
|
||||||
|
RemoveServiceContainer(ctx context.Context, in *RemoveContainerRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type dockerClient struct {
|
type dockerClient struct {
|
||||||
@@ -129,6 +143,66 @@ func (c *dockerClient) PullImage(ctx context.Context, in *PullImageRequest, opts
|
|||||||
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
|
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
|
||||||
type Docker_PullImageClient = grpc.ServerStreamingClient[JSONMessage]
|
type Docker_PullImageClient = grpc.ServerStreamingClient[JSONMessage]
|
||||||
|
|
||||||
|
func (c *dockerClient) InspectImage(ctx context.Context, in *InspectImageRequest, opts ...grpc.CallOption) (*InspectImageResponse, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(InspectImageResponse)
|
||||||
|
err := c.cc.Invoke(ctx, Docker_InspectImage_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *dockerClient) InspectRemoteImage(ctx context.Context, in *InspectRemoteImageRequest, opts ...grpc.CallOption) (*InspectRemoteImageResponse, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(InspectRemoteImageResponse)
|
||||||
|
err := c.cc.Invoke(ctx, Docker_InspectRemoteImage_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *dockerClient) CreateServiceContainer(ctx context.Context, in *CreateServiceContainerRequest, opts ...grpc.CallOption) (*CreateContainerResponse, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(CreateContainerResponse)
|
||||||
|
err := c.cc.Invoke(ctx, Docker_CreateServiceContainer_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *dockerClient) InspectServiceContainer(ctx context.Context, in *InspectContainerRequest, opts ...grpc.CallOption) (*ServiceContainer, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(ServiceContainer)
|
||||||
|
err := c.cc.Invoke(ctx, Docker_InspectServiceContainer_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *dockerClient) ListServiceContainers(ctx context.Context, in *ListServiceContainersRequest, opts ...grpc.CallOption) (*ListServiceContainersResponse, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(ListServiceContainersResponse)
|
||||||
|
err := c.cc.Invoke(ctx, Docker_ListServiceContainers_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *dockerClient) RemoveServiceContainer(ctx context.Context, in *RemoveContainerRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(emptypb.Empty)
|
||||||
|
err := c.cc.Invoke(ctx, Docker_RemoveServiceContainer_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
// DockerServer is the server API for Docker service.
|
// DockerServer is the server API for Docker service.
|
||||||
// All implementations must embed UnimplementedDockerServer
|
// All implementations must embed UnimplementedDockerServer
|
||||||
// for forward compatibility.
|
// for forward compatibility.
|
||||||
@@ -140,6 +214,14 @@ type DockerServer interface {
|
|||||||
ListContainers(context.Context, *ListContainersRequest) (*ListContainersResponse, error)
|
ListContainers(context.Context, *ListContainersRequest) (*ListContainersResponse, error)
|
||||||
RemoveContainer(context.Context, *RemoveContainerRequest) (*emptypb.Empty, error)
|
RemoveContainer(context.Context, *RemoveContainerRequest) (*emptypb.Empty, error)
|
||||||
PullImage(*PullImageRequest, grpc.ServerStreamingServer[JSONMessage]) error
|
PullImage(*PullImageRequest, grpc.ServerStreamingServer[JSONMessage]) error
|
||||||
|
InspectImage(context.Context, *InspectImageRequest) (*InspectImageResponse, error)
|
||||||
|
// InspectRemoteImage returns the image metadata for an image in a remote registry using the machine's
|
||||||
|
// Docker auth credentials if necessary.
|
||||||
|
InspectRemoteImage(context.Context, *InspectRemoteImageRequest) (*InspectRemoteImageResponse, error)
|
||||||
|
CreateServiceContainer(context.Context, *CreateServiceContainerRequest) (*CreateContainerResponse, error)
|
||||||
|
InspectServiceContainer(context.Context, *InspectContainerRequest) (*ServiceContainer, error)
|
||||||
|
ListServiceContainers(context.Context, *ListServiceContainersRequest) (*ListServiceContainersResponse, error)
|
||||||
|
RemoveServiceContainer(context.Context, *RemoveContainerRequest) (*emptypb.Empty, error)
|
||||||
mustEmbedUnimplementedDockerServer()
|
mustEmbedUnimplementedDockerServer()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,6 +253,24 @@ func (UnimplementedDockerServer) RemoveContainer(context.Context, *RemoveContain
|
|||||||
func (UnimplementedDockerServer) PullImage(*PullImageRequest, grpc.ServerStreamingServer[JSONMessage]) error {
|
func (UnimplementedDockerServer) PullImage(*PullImageRequest, grpc.ServerStreamingServer[JSONMessage]) error {
|
||||||
return status.Errorf(codes.Unimplemented, "method PullImage not implemented")
|
return status.Errorf(codes.Unimplemented, "method PullImage not implemented")
|
||||||
}
|
}
|
||||||
|
func (UnimplementedDockerServer) InspectImage(context.Context, *InspectImageRequest) (*InspectImageResponse, error) {
|
||||||
|
return nil, status.Errorf(codes.Unimplemented, "method InspectImage not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedDockerServer) InspectRemoteImage(context.Context, *InspectRemoteImageRequest) (*InspectRemoteImageResponse, error) {
|
||||||
|
return nil, status.Errorf(codes.Unimplemented, "method InspectRemoteImage not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedDockerServer) CreateServiceContainer(context.Context, *CreateServiceContainerRequest) (*CreateContainerResponse, error) {
|
||||||
|
return nil, status.Errorf(codes.Unimplemented, "method CreateServiceContainer not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedDockerServer) InspectServiceContainer(context.Context, *InspectContainerRequest) (*ServiceContainer, error) {
|
||||||
|
return nil, status.Errorf(codes.Unimplemented, "method InspectServiceContainer not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedDockerServer) ListServiceContainers(context.Context, *ListServiceContainersRequest) (*ListServiceContainersResponse, error) {
|
||||||
|
return nil, status.Errorf(codes.Unimplemented, "method ListServiceContainers not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedDockerServer) RemoveServiceContainer(context.Context, *RemoveContainerRequest) (*emptypb.Empty, error) {
|
||||||
|
return nil, status.Errorf(codes.Unimplemented, "method RemoveServiceContainer not implemented")
|
||||||
|
}
|
||||||
func (UnimplementedDockerServer) mustEmbedUnimplementedDockerServer() {}
|
func (UnimplementedDockerServer) mustEmbedUnimplementedDockerServer() {}
|
||||||
func (UnimplementedDockerServer) testEmbeddedByValue() {}
|
func (UnimplementedDockerServer) testEmbeddedByValue() {}
|
||||||
|
|
||||||
@@ -311,6 +411,114 @@ func _Docker_PullImage_Handler(srv interface{}, stream grpc.ServerStream) error
|
|||||||
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
|
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
|
||||||
type Docker_PullImageServer = grpc.ServerStreamingServer[JSONMessage]
|
type Docker_PullImageServer = grpc.ServerStreamingServer[JSONMessage]
|
||||||
|
|
||||||
|
func _Docker_InspectImage_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(InspectImageRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(DockerServer).InspectImage(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: Docker_InspectImage_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(DockerServer).InspectImage(ctx, req.(*InspectImageRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _Docker_InspectRemoteImage_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(InspectRemoteImageRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(DockerServer).InspectRemoteImage(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: Docker_InspectRemoteImage_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(DockerServer).InspectRemoteImage(ctx, req.(*InspectRemoteImageRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _Docker_CreateServiceContainer_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(CreateServiceContainerRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(DockerServer).CreateServiceContainer(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: Docker_CreateServiceContainer_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(DockerServer).CreateServiceContainer(ctx, req.(*CreateServiceContainerRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _Docker_InspectServiceContainer_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(InspectContainerRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(DockerServer).InspectServiceContainer(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: Docker_InspectServiceContainer_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(DockerServer).InspectServiceContainer(ctx, req.(*InspectContainerRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _Docker_ListServiceContainers_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(ListServiceContainersRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(DockerServer).ListServiceContainers(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: Docker_ListServiceContainers_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(DockerServer).ListServiceContainers(ctx, req.(*ListServiceContainersRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _Docker_RemoveServiceContainer_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(RemoveContainerRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(DockerServer).RemoveServiceContainer(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: Docker_RemoveServiceContainer_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(DockerServer).RemoveServiceContainer(ctx, req.(*RemoveContainerRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
// Docker_ServiceDesc is the grpc.ServiceDesc for Docker service.
|
// Docker_ServiceDesc is the grpc.ServiceDesc for Docker service.
|
||||||
// It's only intended for direct use with grpc.RegisterService,
|
// It's only intended for direct use with grpc.RegisterService,
|
||||||
// and not to be introspected or modified (even as a copy)
|
// and not to be introspected or modified (even as a copy)
|
||||||
@@ -342,6 +550,30 @@ var Docker_ServiceDesc = grpc.ServiceDesc{
|
|||||||
MethodName: "RemoveContainer",
|
MethodName: "RemoveContainer",
|
||||||
Handler: _Docker_RemoveContainer_Handler,
|
Handler: _Docker_RemoveContainer_Handler,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
MethodName: "InspectImage",
|
||||||
|
Handler: _Docker_InspectImage_Handler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MethodName: "InspectRemoteImage",
|
||||||
|
Handler: _Docker_InspectRemoteImage_Handler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MethodName: "CreateServiceContainer",
|
||||||
|
Handler: _Docker_CreateServiceContainer_Handler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MethodName: "InspectServiceContainer",
|
||||||
|
Handler: _Docker_InspectServiceContainer_Handler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MethodName: "ListServiceContainers",
|
||||||
|
Handler: _Docker_ListServiceContainers_Handler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MethodName: "RemoveServiceContainer",
|
||||||
|
Handler: _Docker_RemoveServiceContainer_Handler,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
Streams: []grpc.StreamDesc{
|
Streams: []grpc.StreamDesc{
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -26,9 +26,10 @@ type MachineInfo struct {
|
|||||||
sizeCache protoimpl.SizeCache
|
sizeCache protoimpl.SizeCache
|
||||||
unknownFields protoimpl.UnknownFields
|
unknownFields protoimpl.UnknownFields
|
||||||
|
|
||||||
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
|
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
|
||||||
Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`
|
Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`
|
||||||
Network *NetworkConfig `protobuf:"bytes,3,opt,name=network,proto3" json:"network,omitempty"`
|
Network *NetworkConfig `protobuf:"bytes,3,opt,name=network,proto3" json:"network,omitempty"`
|
||||||
|
PublicIp *IP `protobuf:"bytes,4,opt,name=public_ip,json=publicIp,proto3" json:"public_ip,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *MachineInfo) Reset() {
|
func (x *MachineInfo) Reset() {
|
||||||
@@ -84,6 +85,13 @@ func (x *MachineInfo) GetNetwork() *NetworkConfig {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (x *MachineInfo) GetPublicIp() *IP {
|
||||||
|
if x != nil {
|
||||||
|
return x.PublicIp
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
type NetworkConfig struct {
|
type NetworkConfig struct {
|
||||||
state protoimpl.MessageState
|
state protoimpl.MessageState
|
||||||
sizeCache protoimpl.SizeCache
|
sizeCache protoimpl.SizeCache
|
||||||
@@ -92,7 +100,7 @@ type NetworkConfig struct {
|
|||||||
Subnet *IPPrefix `protobuf:"bytes,1,opt,name=subnet,proto3" json:"subnet,omitempty"`
|
Subnet *IPPrefix `protobuf:"bytes,1,opt,name=subnet,proto3" json:"subnet,omitempty"`
|
||||||
ManagementIp *IP `protobuf:"bytes,2,opt,name=management_ip,json=managementIp,proto3" json:"management_ip,omitempty"`
|
ManagementIp *IP `protobuf:"bytes,2,opt,name=management_ip,json=managementIp,proto3" json:"management_ip,omitempty"`
|
||||||
Endpoints []*IPPort `protobuf:"bytes,3,rep,name=endpoints,proto3" json:"endpoints,omitempty"`
|
Endpoints []*IPPort `protobuf:"bytes,3,rep,name=endpoints,proto3" json:"endpoints,omitempty"`
|
||||||
PublicKey []byte `protobuf:"bytes,4,opt,name=publicKey,proto3" json:"publicKey,omitempty"`
|
PublicKey []byte `protobuf:"bytes,4,opt,name=public_key,json=publicKey,proto3" json:"public_key,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *NetworkConfig) Reset() {
|
func (x *NetworkConfig) Reset() {
|
||||||
@@ -162,6 +170,11 @@ type InitClusterRequest struct {
|
|||||||
|
|
||||||
MachineName string `protobuf:"bytes,1,opt,name=machineName,proto3" json:"machineName,omitempty"`
|
MachineName string `protobuf:"bytes,1,opt,name=machineName,proto3" json:"machineName,omitempty"`
|
||||||
Network *IPPrefix `protobuf:"bytes,2,opt,name=network,proto3" json:"network,omitempty"`
|
Network *IPPrefix `protobuf:"bytes,2,opt,name=network,proto3" json:"network,omitempty"`
|
||||||
|
// Types that are assignable to PublicIpConfig:
|
||||||
|
//
|
||||||
|
// *InitClusterRequest_PublicIp
|
||||||
|
// *InitClusterRequest_PublicIpAuto
|
||||||
|
PublicIpConfig isInitClusterRequest_PublicIpConfig `protobuf_oneof:"public_ip_config"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *InitClusterRequest) Reset() {
|
func (x *InitClusterRequest) Reset() {
|
||||||
@@ -210,6 +223,43 @@ func (x *InitClusterRequest) GetNetwork() *IPPrefix {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *InitClusterRequest) GetPublicIpConfig() isInitClusterRequest_PublicIpConfig {
|
||||||
|
if m != nil {
|
||||||
|
return m.PublicIpConfig
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *InitClusterRequest) GetPublicIp() *IP {
|
||||||
|
if x, ok := x.GetPublicIpConfig().(*InitClusterRequest_PublicIp); ok {
|
||||||
|
return x.PublicIp
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *InitClusterRequest) GetPublicIpAuto() bool {
|
||||||
|
if x, ok := x.GetPublicIpConfig().(*InitClusterRequest_PublicIpAuto); ok {
|
||||||
|
return x.PublicIpAuto
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
type isInitClusterRequest_PublicIpConfig interface {
|
||||||
|
isInitClusterRequest_PublicIpConfig()
|
||||||
|
}
|
||||||
|
|
||||||
|
type InitClusterRequest_PublicIp struct {
|
||||||
|
PublicIp *IP `protobuf:"bytes,3,opt,name=public_ip,json=publicIp,proto3,oneof"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type InitClusterRequest_PublicIpAuto struct {
|
||||||
|
PublicIpAuto bool `protobuf:"varint,4,opt,name=public_ip_auto,json=publicIpAuto,proto3,oneof"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*InitClusterRequest_PublicIp) isInitClusterRequest_PublicIpConfig() {}
|
||||||
|
|
||||||
|
func (*InitClusterRequest_PublicIpAuto) isInitClusterRequest_PublicIpConfig() {}
|
||||||
|
|
||||||
type InitClusterResponse struct {
|
type InitClusterResponse struct {
|
||||||
state protoimpl.MessageState
|
state protoimpl.MessageState
|
||||||
sizeCache protoimpl.SizeCache
|
sizeCache protoimpl.SizeCache
|
||||||
@@ -590,87 +640,96 @@ var file_internal_machine_api_pb_machine_proto_rawDesc = []byte{
|
|||||||
0x70, 0x74, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x24, 0x69, 0x6e, 0x74, 0x65, 0x72,
|
0x70, 0x74, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x24, 0x69, 0x6e, 0x74, 0x65, 0x72,
|
||||||
0x6e, 0x61, 0x6c, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f,
|
0x6e, 0x61, 0x6c, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f,
|
||||||
0x70, 0x62, 0x2f, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22,
|
0x70, 0x62, 0x2f, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22,
|
||||||
0x5f, 0x0a, 0x0b, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x0e,
|
0x85, 0x01, 0x0a, 0x0b, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x12,
|
||||||
0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x12,
|
0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12,
|
||||||
0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61,
|
0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e,
|
||||||
0x6d, 0x65, 0x12, 0x2c, 0x0a, 0x07, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x18, 0x03, 0x20,
|
0x61, 0x6d, 0x65, 0x12, 0x2c, 0x0a, 0x07, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x18, 0x03,
|
||||||
0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72,
|
0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4e, 0x65, 0x74, 0x77, 0x6f,
|
||||||
0x6b, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x07, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b,
|
0x72, 0x6b, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x07, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72,
|
||||||
0x22, 0xad, 0x01, 0x0a, 0x0d, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x43, 0x6f, 0x6e, 0x66,
|
0x6b, 0x12, 0x24, 0x0a, 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x69, 0x70, 0x18, 0x04,
|
||||||
0x69, 0x67, 0x12, 0x25, 0x0a, 0x06, 0x73, 0x75, 0x62, 0x6e, 0x65, 0x74, 0x18, 0x01, 0x20, 0x01,
|
0x20, 0x01, 0x28, 0x0b, 0x32, 0x07, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x50, 0x52, 0x08, 0x70,
|
||||||
|
0x75, 0x62, 0x6c, 0x69, 0x63, 0x49, 0x70, 0x22, 0xae, 0x01, 0x0a, 0x0d, 0x4e, 0x65, 0x74, 0x77,
|
||||||
|
0x6f, 0x72, 0x6b, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x25, 0x0a, 0x06, 0x73, 0x75, 0x62,
|
||||||
|
0x6e, 0x65, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x61, 0x70, 0x69, 0x2e,
|
||||||
|
0x49, 0x50, 0x50, 0x72, 0x65, 0x66, 0x69, 0x78, 0x52, 0x06, 0x73, 0x75, 0x62, 0x6e, 0x65, 0x74,
|
||||||
|
0x12, 0x2c, 0x0a, 0x0d, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x5f, 0x69,
|
||||||
|
0x70, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x07, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x50,
|
||||||
|
0x52, 0x0c, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x49, 0x70, 0x12, 0x29,
|
||||||
|
0x0a, 0x09, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28,
|
||||||
|
0x0b, 0x32, 0x0b, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x50, 0x50, 0x6f, 0x72, 0x74, 0x52, 0x09,
|
||||||
|
0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x70, 0x75, 0x62,
|
||||||
|
0x6c, 0x69, 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x70,
|
||||||
|
0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x22, 0xc3, 0x01, 0x0a, 0x12, 0x49, 0x6e, 0x69,
|
||||||
|
0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12,
|
||||||
|
0x20, 0x0a, 0x0b, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x01,
|
||||||
|
0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x4e, 0x61, 0x6d,
|
||||||
|
0x65, 0x12, 0x27, 0x0a, 0x07, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x18, 0x02, 0x20, 0x01,
|
||||||
0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x50, 0x50, 0x72, 0x65, 0x66, 0x69,
|
0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x50, 0x50, 0x72, 0x65, 0x66, 0x69,
|
||||||
0x78, 0x52, 0x06, 0x73, 0x75, 0x62, 0x6e, 0x65, 0x74, 0x12, 0x2c, 0x0a, 0x0d, 0x6d, 0x61, 0x6e,
|
0x78, 0x52, 0x07, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x12, 0x26, 0x0a, 0x09, 0x70, 0x75,
|
||||||
0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x70, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b,
|
0x62, 0x6c, 0x69, 0x63, 0x5f, 0x69, 0x70, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x07, 0x2e,
|
||||||
0x32, 0x07, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x50, 0x52, 0x0c, 0x6d, 0x61, 0x6e, 0x61, 0x67,
|
0x61, 0x70, 0x69, 0x2e, 0x49, 0x50, 0x48, 0x00, 0x52, 0x08, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63,
|
||||||
0x65, 0x6d, 0x65, 0x6e, 0x74, 0x49, 0x70, 0x12, 0x29, 0x0a, 0x09, 0x65, 0x6e, 0x64, 0x70, 0x6f,
|
0x49, 0x70, 0x12, 0x26, 0x0a, 0x0e, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x69, 0x70, 0x5f,
|
||||||
0x69, 0x6e, 0x74, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0b, 0x2e, 0x61, 0x70, 0x69,
|
0x61, 0x75, 0x74, 0x6f, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x48, 0x00, 0x52, 0x0c, 0x70, 0x75,
|
||||||
0x2e, 0x49, 0x50, 0x50, 0x6f, 0x72, 0x74, 0x52, 0x09, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e,
|
0x62, 0x6c, 0x69, 0x63, 0x49, 0x70, 0x41, 0x75, 0x74, 0x6f, 0x42, 0x12, 0x0a, 0x10, 0x70, 0x75,
|
||||||
0x74, 0x73, 0x12, 0x1c, 0x0a, 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x18,
|
0x62, 0x6c, 0x69, 0x63, 0x5f, 0x69, 0x70, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0x41,
|
||||||
0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79,
|
0x0a, 0x13, 0x49, 0x6e, 0x69, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x73,
|
||||||
0x22, 0x5f, 0x0a, 0x12, 0x49, 0x6e, 0x69, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x52,
|
0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2a, 0x0a, 0x07, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65,
|
||||||
0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x20, 0x0a, 0x0b, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e,
|
0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d, 0x61, 0x63,
|
||||||
0x65, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x6d, 0x61, 0x63,
|
0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x07, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e,
|
||||||
0x68, 0x69, 0x6e, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x27, 0x0a, 0x07, 0x6e, 0x65, 0x74, 0x77,
|
0x65, 0x22, 0x79, 0x0a, 0x12, 0x4a, 0x6f, 0x69, 0x6e, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72,
|
||||||
0x6f, 0x72, 0x6b, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x61, 0x70, 0x69, 0x2e,
|
0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2a, 0x0a, 0x07, 0x6d, 0x61, 0x63, 0x68, 0x69,
|
||||||
0x49, 0x50, 0x50, 0x72, 0x65, 0x66, 0x69, 0x78, 0x52, 0x07, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72,
|
0x6e, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d,
|
||||||
0x6b, 0x22, 0x41, 0x0a, 0x13, 0x49, 0x6e, 0x69, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72,
|
0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x07, 0x6d, 0x61, 0x63, 0x68,
|
||||||
0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2a, 0x0a, 0x07, 0x6d, 0x61, 0x63, 0x68,
|
0x69, 0x6e, 0x65, 0x12, 0x37, 0x0a, 0x0e, 0x6f, 0x74, 0x68, 0x65, 0x72, 0x5f, 0x6d, 0x61, 0x63,
|
||||||
0x69, 0x6e, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x61, 0x70, 0x69, 0x2e,
|
0x68, 0x69, 0x6e, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x61, 0x70,
|
||||||
0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x07, 0x6d, 0x61, 0x63,
|
0x69, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x0d, 0x6f,
|
||||||
0x68, 0x69, 0x6e, 0x65, 0x22, 0x79, 0x0a, 0x12, 0x4a, 0x6f, 0x69, 0x6e, 0x43, 0x6c, 0x75, 0x73,
|
0x74, 0x68, 0x65, 0x72, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x22, 0x25, 0x0a, 0x0d,
|
||||||
0x74, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2a, 0x0a, 0x07, 0x6d, 0x61,
|
0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x14, 0x0a,
|
||||||
0x63, 0x68, 0x69, 0x6e, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x61, 0x70,
|
0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x6f,
|
||||||
0x69, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x07, 0x6d,
|
0x6b, 0x65, 0x6e, 0x22, 0xc3, 0x01, 0x0a, 0x07, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12,
|
||||||
0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x12, 0x37, 0x0a, 0x0e, 0x6f, 0x74, 0x68, 0x65, 0x72, 0x5f,
|
0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12,
|
||||||
0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x10,
|
0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e,
|
||||||
0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f,
|
0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28,
|
||||||
0x52, 0x0d, 0x6f, 0x74, 0x68, 0x65, 0x72, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x22,
|
0x09, 0x52, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x12, 0x36, 0x0a, 0x0a, 0x63, 0x6f, 0x6e, 0x74, 0x61,
|
||||||
0x25, 0x0a, 0x0d, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
|
0x69, 0x6e, 0x65, 0x72, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x61, 0x70,
|
||||||
0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52,
|
0x69, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69,
|
||||||
0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0xc3, 0x01, 0x0a, 0x07, 0x53, 0x65, 0x72, 0x76, 0x69,
|
0x6e, 0x65, 0x72, 0x52, 0x0a, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x1a,
|
||||||
0x63, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02,
|
0x48, 0x0a, 0x09, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x1d, 0x0a, 0x0a,
|
||||||
0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09,
|
0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
|
||||||
0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x18, 0x03,
|
0x52, 0x09, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x64, 0x12, 0x1c, 0x0a, 0x09, 0x63,
|
||||||
0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x12, 0x36, 0x0a, 0x0a, 0x63, 0x6f,
|
0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09,
|
||||||
0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16,
|
0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x22, 0x27, 0x0a, 0x15, 0x49, 0x6e, 0x73,
|
||||||
0x2e, 0x61, 0x70, 0x69, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x43, 0x6f, 0x6e,
|
0x70, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65,
|
||||||
0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x0a, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65,
|
0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02,
|
||||||
0x72, 0x73, 0x1a, 0x48, 0x0a, 0x09, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12,
|
0x69, 0x64, 0x22, 0x40, 0x0a, 0x16, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72,
|
||||||
0x1d, 0x0a, 0x0a, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20,
|
0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x26, 0x0a, 0x07,
|
||||||
0x01, 0x28, 0x09, 0x52, 0x09, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x64, 0x12, 0x1c,
|
0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0c, 0x2e,
|
||||||
0x0a, 0x09, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28,
|
0x61, 0x70, 0x69, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x07, 0x73, 0x65, 0x72,
|
||||||
0x0c, 0x52, 0x09, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x22, 0x27, 0x0a, 0x15,
|
0x76, 0x69, 0x63, 0x65, 0x32, 0xc0, 0x02, 0x0a, 0x07, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65,
|
||||||
0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65,
|
0x12, 0x40, 0x0a, 0x0b, 0x49, 0x6e, 0x69, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x12,
|
||||||
0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28,
|
0x17, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x69, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65,
|
||||||
0x09, 0x52, 0x02, 0x69, 0x64, 0x22, 0x40, 0x0a, 0x16, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74,
|
0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49,
|
||||||
0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12,
|
0x6e, 0x69, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
|
||||||
0x26, 0x0a, 0x07, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b,
|
0x73, 0x65, 0x12, 0x3e, 0x0a, 0x0b, 0x4a, 0x6f, 0x69, 0x6e, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65,
|
||||||
0x32, 0x0c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x07,
|
0x72, 0x12, 0x17, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4a, 0x6f, 0x69, 0x6e, 0x43, 0x6c, 0x75, 0x73,
|
||||||
0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x32, 0xc0, 0x02, 0x0a, 0x07, 0x4d, 0x61, 0x63, 0x68,
|
0x74, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f,
|
||||||
0x69, 0x6e, 0x65, 0x12, 0x40, 0x0a, 0x0b, 0x49, 0x6e, 0x69, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74,
|
0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70,
|
||||||
0x65, 0x72, 0x12, 0x17, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x69, 0x74, 0x43, 0x6c, 0x75,
|
0x74, 0x79, 0x12, 0x33, 0x0a, 0x05, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x16, 0x2e, 0x67, 0x6f,
|
||||||
0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x61, 0x70,
|
0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d,
|
||||||
0x69, 0x2e, 0x49, 0x6e, 0x69, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x73,
|
0x70, 0x74, 0x79, 0x1a, 0x12, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52,
|
||||||
0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3e, 0x0a, 0x0b, 0x4a, 0x6f, 0x69, 0x6e, 0x43, 0x6c, 0x75,
|
0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x33, 0x0a, 0x07, 0x49, 0x6e, 0x73, 0x70, 0x65,
|
||||||
0x73, 0x74, 0x65, 0x72, 0x12, 0x17, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4a, 0x6f, 0x69, 0x6e, 0x43,
|
0x63, 0x74, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74,
|
||||||
0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e,
|
0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x10, 0x2e, 0x61, 0x70, 0x69,
|
||||||
0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e,
|
0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x49, 0x0a, 0x0e,
|
||||||
0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x33, 0x0a, 0x05, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x16,
|
0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x1a,
|
||||||
0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66,
|
0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72, 0x76,
|
||||||
0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x12, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x54, 0x6f, 0x6b,
|
0x69, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x61, 0x70, 0x69,
|
||||||
0x65, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x33, 0x0a, 0x07, 0x49, 0x6e,
|
0x2e, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52,
|
||||||
0x73, 0x70, 0x65, 0x63, 0x74, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70,
|
0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x37, 0x5a, 0x35, 0x67, 0x69, 0x74, 0x68, 0x75,
|
||||||
0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x10, 0x2e,
|
0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x70, 0x73, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x6b, 0x69,
|
||||||
0x61, 0x70, 0x69, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x12,
|
0x2f, 0x75, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61,
|
||||||
0x49, 0x0a, 0x0e, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63,
|
0x6c, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x62,
|
||||||
0x65, 0x12, 0x1a, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53,
|
0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||||
0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e,
|
|
||||||
0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69,
|
|
||||||
0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x37, 0x5a, 0x35, 0x67, 0x69,
|
|
||||||
0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x70, 0x73, 0x76, 0x69, 0x64, 0x65, 0x72,
|
|
||||||
0x73, 0x6b, 0x69, 0x2f, 0x75, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2f, 0x69, 0x6e, 0x74, 0x65,
|
|
||||||
0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x2f, 0x61, 0x70, 0x69,
|
|
||||||
0x2f, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -697,37 +756,39 @@ var file_internal_machine_api_pb_machine_proto_goTypes = []any{
|
|||||||
(*InspectServiceRequest)(nil), // 7: api.InspectServiceRequest
|
(*InspectServiceRequest)(nil), // 7: api.InspectServiceRequest
|
||||||
(*InspectServiceResponse)(nil), // 8: api.InspectServiceResponse
|
(*InspectServiceResponse)(nil), // 8: api.InspectServiceResponse
|
||||||
(*Service_Container)(nil), // 9: api.Service.Container
|
(*Service_Container)(nil), // 9: api.Service.Container
|
||||||
(*IPPrefix)(nil), // 10: api.IPPrefix
|
(*IP)(nil), // 10: api.IP
|
||||||
(*IP)(nil), // 11: api.IP
|
(*IPPrefix)(nil), // 11: api.IPPrefix
|
||||||
(*IPPort)(nil), // 12: api.IPPort
|
(*IPPort)(nil), // 12: api.IPPort
|
||||||
(*emptypb.Empty)(nil), // 13: google.protobuf.Empty
|
(*emptypb.Empty)(nil), // 13: 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.NetworkConfig.subnet:type_name -> api.IPPrefix
|
10, // 1: api.MachineInfo.public_ip:type_name -> api.IP
|
||||||
11, // 2: api.NetworkConfig.management_ip:type_name -> api.IP
|
11, // 2: api.NetworkConfig.subnet:type_name -> api.IPPrefix
|
||||||
12, // 3: api.NetworkConfig.endpoints:type_name -> api.IPPort
|
10, // 3: api.NetworkConfig.management_ip:type_name -> api.IP
|
||||||
10, // 4: api.InitClusterRequest.network:type_name -> api.IPPrefix
|
12, // 4: api.NetworkConfig.endpoints:type_name -> api.IPPort
|
||||||
0, // 5: api.InitClusterResponse.machine:type_name -> api.MachineInfo
|
11, // 5: api.InitClusterRequest.network:type_name -> api.IPPrefix
|
||||||
0, // 6: api.JoinClusterRequest.machine:type_name -> api.MachineInfo
|
10, // 6: api.InitClusterRequest.public_ip:type_name -> api.IP
|
||||||
0, // 7: api.JoinClusterRequest.other_machines:type_name -> api.MachineInfo
|
0, // 7: api.InitClusterResponse.machine:type_name -> api.MachineInfo
|
||||||
9, // 8: api.Service.containers:type_name -> api.Service.Container
|
0, // 8: api.JoinClusterRequest.machine:type_name -> api.MachineInfo
|
||||||
6, // 9: api.InspectServiceResponse.service:type_name -> api.Service
|
0, // 9: api.JoinClusterRequest.other_machines:type_name -> api.MachineInfo
|
||||||
2, // 10: api.Machine.InitCluster:input_type -> api.InitClusterRequest
|
9, // 10: api.Service.containers:type_name -> api.Service.Container
|
||||||
4, // 11: api.Machine.JoinCluster:input_type -> api.JoinClusterRequest
|
6, // 11: api.InspectServiceResponse.service:type_name -> api.Service
|
||||||
13, // 12: api.Machine.Token:input_type -> google.protobuf.Empty
|
2, // 12: api.Machine.InitCluster:input_type -> api.InitClusterRequest
|
||||||
13, // 13: api.Machine.Inspect:input_type -> google.protobuf.Empty
|
4, // 13: api.Machine.JoinCluster:input_type -> api.JoinClusterRequest
|
||||||
7, // 14: api.Machine.InspectService:input_type -> api.InspectServiceRequest
|
13, // 14: api.Machine.Token:input_type -> google.protobuf.Empty
|
||||||
3, // 15: api.Machine.InitCluster:output_type -> api.InitClusterResponse
|
13, // 15: api.Machine.Inspect:input_type -> google.protobuf.Empty
|
||||||
13, // 16: api.Machine.JoinCluster:output_type -> google.protobuf.Empty
|
7, // 16: api.Machine.InspectService:input_type -> api.InspectServiceRequest
|
||||||
5, // 17: api.Machine.Token:output_type -> api.TokenResponse
|
3, // 17: api.Machine.InitCluster:output_type -> api.InitClusterResponse
|
||||||
0, // 18: api.Machine.Inspect:output_type -> api.MachineInfo
|
13, // 18: api.Machine.JoinCluster:output_type -> google.protobuf.Empty
|
||||||
8, // 19: api.Machine.InspectService:output_type -> api.InspectServiceResponse
|
5, // 19: api.Machine.Token:output_type -> api.TokenResponse
|
||||||
15, // [15:20] is the sub-list for method output_type
|
0, // 20: api.Machine.Inspect:output_type -> api.MachineInfo
|
||||||
10, // [10:15] is the sub-list for method input_type
|
8, // 21: api.Machine.InspectService:output_type -> api.InspectServiceResponse
|
||||||
10, // [10:10] is the sub-list for extension type_name
|
17, // [17:22] is the sub-list for method output_type
|
||||||
10, // [10:10] is the sub-list for extension extendee
|
12, // [12:17] is the sub-list for method input_type
|
||||||
0, // [0:10] is the sub-list for field type_name
|
12, // [12:12] is the sub-list for extension type_name
|
||||||
|
12, // [12:12] is the sub-list for extension extendee
|
||||||
|
0, // [0:12] is the sub-list for field type_name
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() { file_internal_machine_api_pb_machine_proto_init() }
|
func init() { file_internal_machine_api_pb_machine_proto_init() }
|
||||||
@@ -858,6 +919,10 @@ func file_internal_machine_api_pb_machine_proto_init() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
file_internal_machine_api_pb_machine_proto_msgTypes[2].OneofWrappers = []any{
|
||||||
|
(*InitClusterRequest_PublicIp)(nil),
|
||||||
|
(*InitClusterRequest_PublicIpAuto)(nil),
|
||||||
|
}
|
||||||
type x struct{}
|
type x struct{}
|
||||||
out := protoimpl.TypeBuilder{
|
out := protoimpl.TypeBuilder{
|
||||||
File: protoimpl.DescBuilder{
|
File: protoimpl.DescBuilder{
|
||||||
|
|||||||
@@ -19,18 +19,24 @@ message MachineInfo {
|
|||||||
string id = 1;
|
string id = 1;
|
||||||
string name = 2;
|
string name = 2;
|
||||||
NetworkConfig network = 3;
|
NetworkConfig network = 3;
|
||||||
|
IP public_ip = 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
message NetworkConfig {
|
message NetworkConfig {
|
||||||
IPPrefix subnet = 1;
|
IPPrefix subnet = 1;
|
||||||
IP management_ip = 2;
|
IP management_ip = 2;
|
||||||
repeated IPPort endpoints = 3;
|
repeated IPPort endpoints = 3;
|
||||||
bytes publicKey = 4;
|
bytes public_key = 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
message InitClusterRequest {
|
message InitClusterRequest {
|
||||||
string machineName = 1;
|
string machineName = 1;
|
||||||
IPPrefix network = 2;
|
IPPrefix network = 2;
|
||||||
|
|
||||||
|
oneof public_ip_config {
|
||||||
|
IP public_ip = 3;
|
||||||
|
bool public_ip_auto = 4;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
message InitClusterResponse {
|
message InitClusterResponse {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
"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"
|
||||||
"uncloud/internal/machine/api/pb"
|
"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
|
||||||
|
|||||||
@@ -0,0 +1,164 @@
|
|||||||
|
package caddyfile
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"maps"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"slices"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/caddyserver/caddy/v2"
|
||||||
|
"github.com/caddyserver/caddy/v2/caddyconfig"
|
||||||
|
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||||
|
"github.com/caddyserver/caddy/v2/modules/caddyhttp/reverseproxy"
|
||||||
|
"github.com/psviderski/uncloud/internal/machine/docker"
|
||||||
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GenerateConfig(containers []api.ServiceContainer, verifyResponse string) (*caddy.Config, error) {
|
||||||
|
// Maps hostnames to lists of upstreams (container IP:port pairs).
|
||||||
|
httpHostUpstreams := make(map[string][]string)
|
||||||
|
httpsHostUpstreams := make(map[string][]string)
|
||||||
|
for _, ctr := range containers {
|
||||||
|
logger := slog.With("container", ctr.ID)
|
||||||
|
network, ok := ctr.NetworkSettings.Networks[docker.NetworkName]
|
||||||
|
if !ok {
|
||||||
|
// Container is not connected to the uncloud Docker network (could be host network).
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if network.IPAddress == "" {
|
||||||
|
logger.Error("Container has no IPv4 address.")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
ports, err := ctr.ServicePorts()
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("Failed to parse service ports for container.", "err", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, port := range ports {
|
||||||
|
switch port.Protocol {
|
||||||
|
case api.ProtocolHTTP:
|
||||||
|
upstream := net.JoinHostPort(network.IPAddress, strconv.Itoa(int(port.ContainerPort)))
|
||||||
|
httpHostUpstreams[port.Hostname] = append(httpHostUpstreams[port.Hostname], upstream)
|
||||||
|
case api.ProtocolHTTPS:
|
||||||
|
upstream := net.JoinHostPort(network.IPAddress, strconv.Itoa(int(port.ContainerPort)))
|
||||||
|
httpsHostUpstreams[port.Hostname] = append(httpsHostUpstreams[port.Hostname], upstream)
|
||||||
|
default:
|
||||||
|
// TODO: implement L4 ingress routing for TCP and UDP.
|
||||||
|
logger.Error("Unsupported protocol for ingress port.", "port", port)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var warnings []caddyconfig.Warning
|
||||||
|
servers := make(map[string]*caddyhttp.Server)
|
||||||
|
servers["http"] = &caddyhttp.Server{
|
||||||
|
Listen: []string{fmt.Sprintf(":%d", caddyhttp.DefaultHTTPPort)},
|
||||||
|
Routes: append(
|
||||||
|
hostUpstreamsToRoutes(httpHostUpstreams, &warnings),
|
||||||
|
// Add a route to respond with a static verification response at the /.uncloud-verify path.
|
||||||
|
verificationRoute(verifyResponse, &warnings),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
servers["https"] = &caddyhttp.Server{
|
||||||
|
Listen: []string{fmt.Sprintf(":%d", caddyhttp.DefaultHTTPSPort)},
|
||||||
|
Routes: hostUpstreamsToRoutes(httpsHostUpstreams, &warnings),
|
||||||
|
}
|
||||||
|
|
||||||
|
httpApp := caddyhttp.App{
|
||||||
|
Servers: servers,
|
||||||
|
}
|
||||||
|
config := &caddy.Config{
|
||||||
|
AppsRaw: caddy.ModuleMap{
|
||||||
|
"http": caddyconfig.JSON(httpApp, &warnings),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
if len(warnings) > 0 {
|
||||||
|
// warnings only contains errors from JSON marshaling, which are highly unlikely with correct code.
|
||||||
|
for _, w := range warnings {
|
||||||
|
err = errors.Join(err, errors.New(w.Message))
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("marshal Caddy configuration: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return config, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// hostUpstreamsToRoutes converts a map of hostnames to upstreams to a list of Caddy routes.
|
||||||
|
func hostUpstreamsToRoutes(hostUpstreams map[string][]string, warnings *[]caddyconfig.Warning) []caddyhttp.Route {
|
||||||
|
// Sort hostnames for deterministic output.
|
||||||
|
hostnames := slices.Collect(maps.Keys(hostUpstreams))
|
||||||
|
slices.Sort(hostnames)
|
||||||
|
|
||||||
|
routes := make([]caddyhttp.Route, 0, len(hostUpstreams))
|
||||||
|
for _, hostname := range hostnames {
|
||||||
|
upstreams := hostUpstreams[hostname]
|
||||||
|
upstreamPool := make([]*reverseproxy.Upstream, len(upstreams))
|
||||||
|
for i, upstream := range upstreams {
|
||||||
|
upstreamPool[i] = &reverseproxy.Upstream{
|
||||||
|
Dial: upstream,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
handler := &reverseproxy.Handler{
|
||||||
|
Upstreams: upstreamPool,
|
||||||
|
}
|
||||||
|
|
||||||
|
routes = append(routes, caddyhttp.Route{
|
||||||
|
MatcherSetsRaw: caddyhttp.RawMatcherSets{
|
||||||
|
{
|
||||||
|
"host": caddyconfig.JSON(caddyhttp.MatchHost{hostname}, warnings),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
HandlersRaw: []json.RawMessage{
|
||||||
|
caddyconfig.JSONModuleObject(handler, "handler", "reverse_proxy", warnings),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return routes
|
||||||
|
}
|
||||||
|
|
||||||
|
// verificationRoute returns a Caddy route that responds with the given static response at the /.uncloud-verify path.
|
||||||
|
func verificationRoute(response string, warnings *[]caddyconfig.Warning) caddyhttp.Route {
|
||||||
|
// Return the following route:
|
||||||
|
// {
|
||||||
|
// "match": [
|
||||||
|
// {
|
||||||
|
// "path": [
|
||||||
|
// "/.uncloud-verify"
|
||||||
|
// ]
|
||||||
|
// }
|
||||||
|
// ],
|
||||||
|
// "handle": [
|
||||||
|
// {
|
||||||
|
// "handler": "static_response",
|
||||||
|
// "body": "<response>",
|
||||||
|
// "status_code": 200
|
||||||
|
// }
|
||||||
|
// ]
|
||||||
|
// }
|
||||||
|
|
||||||
|
staticResponse := caddyhttp.StaticResponse{
|
||||||
|
StatusCode: caddyhttp.WeakString(strconv.Itoa(http.StatusOK)),
|
||||||
|
Body: response,
|
||||||
|
}
|
||||||
|
|
||||||
|
return caddyhttp.Route{
|
||||||
|
MatcherSetsRaw: caddyhttp.RawMatcherSets{
|
||||||
|
{
|
||||||
|
"path": caddyconfig.JSON(caddyhttp.MatchPath{VerifyPath}, warnings),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
HandlersRaw: []json.RawMessage{
|
||||||
|
caddyconfig.JSONModuleObject(staticResponse, "handler", "static_response", warnings),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,362 @@
|
|||||||
|
package caddyfile
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/docker/docker/api/types"
|
||||||
|
"github.com/docker/docker/api/types/container"
|
||||||
|
"github.com/docker/docker/api/types/network"
|
||||||
|
"github.com/psviderski/uncloud/internal/machine/docker"
|
||||||
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGenerateConfig(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
containers []api.ServiceContainer
|
||||||
|
want string
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "empty containers",
|
||||||
|
containers: []api.ServiceContainer{},
|
||||||
|
want: `{
|
||||||
|
"servers": {
|
||||||
|
"http": {
|
||||||
|
"listen": [":80"],
|
||||||
|
"routes": [{
|
||||||
|
"match": [{"path": ["/.uncloud-verify"]}],
|
||||||
|
"handle": [{
|
||||||
|
"body": "verification-response-body",
|
||||||
|
"handler": "static_response",
|
||||||
|
"status_code": 200
|
||||||
|
}]
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
"https": {
|
||||||
|
"listen": [":443"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: "HTTP container",
|
||||||
|
containers: []api.ServiceContainer{
|
||||||
|
newContainer("10.210.0.2", "app.example.com:8080/http"),
|
||||||
|
},
|
||||||
|
want: `{
|
||||||
|
"servers": {
|
||||||
|
"http": {
|
||||||
|
"listen": [":80"],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"match": [{"host": ["app.example.com"]}],
|
||||||
|
"handle": [{
|
||||||
|
"handler": "reverse_proxy",
|
||||||
|
"upstreams": [{"dial": "10.210.0.2:8080"}]
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"match": [{"path": ["/.uncloud-verify"]}],
|
||||||
|
"handle": [{
|
||||||
|
"body": "verification-response-body",
|
||||||
|
"handler": "static_response",
|
||||||
|
"status_code": 200
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"https": {
|
||||||
|
"listen": [":443"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "load balancing multiple containers",
|
||||||
|
containers: []api.ServiceContainer{
|
||||||
|
newContainer("10.210.0.2", "app.example.com:8080/http"),
|
||||||
|
newContainer("10.210.0.3", "app.example.com:8080/http"),
|
||||||
|
},
|
||||||
|
want: `{
|
||||||
|
"servers": {
|
||||||
|
"http": {
|
||||||
|
"listen": [":80"],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"match": [{"host": ["app.example.com"]}],
|
||||||
|
"handle": [{
|
||||||
|
"handler": "reverse_proxy",
|
||||||
|
"upstreams": [
|
||||||
|
{"dial": "10.210.0.2:8080"},
|
||||||
|
{"dial": "10.210.0.3:8080"}
|
||||||
|
]
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"match": [{"path": ["/.uncloud-verify"]}],
|
||||||
|
"handle": [{
|
||||||
|
"body": "verification-response-body",
|
||||||
|
"handler": "static_response",
|
||||||
|
"status_code": 200
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"https": {
|
||||||
|
"listen": [":443"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "HTTPS container",
|
||||||
|
containers: []api.ServiceContainer{
|
||||||
|
newContainer("10.210.0.2", "secure.example.com:8000/https"),
|
||||||
|
},
|
||||||
|
want: `{
|
||||||
|
"servers": {
|
||||||
|
"http": {
|
||||||
|
"listen": [":80"],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"match": [{"path": ["/.uncloud-verify"]}],
|
||||||
|
"handle": [{
|
||||||
|
"body": "verification-response-body",
|
||||||
|
"handler": "static_response",
|
||||||
|
"status_code": 200
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"https": {
|
||||||
|
"listen": [":443"],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"match": [{"host": ["secure.example.com"]}],
|
||||||
|
"handle": [{
|
||||||
|
"handler": "reverse_proxy",
|
||||||
|
"upstreams": [{"dial": "10.210.0.2:8000"}]
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "mixed HTTP and HTTPS",
|
||||||
|
containers: []api.ServiceContainer{
|
||||||
|
newContainer("10.210.0.2",
|
||||||
|
"app.example.com:8080/http",
|
||||||
|
"web.example.com:8000/http"),
|
||||||
|
newContainer("10.210.0.3",
|
||||||
|
"app.example.com:8080/http",
|
||||||
|
"secure.example.com:8888/https"),
|
||||||
|
newContainer("10.210.0.4",
|
||||||
|
"web.example.com:8000/http",
|
||||||
|
"secure.example.com:8888/https"),
|
||||||
|
newContainer("10.210.0.5",
|
||||||
|
"app.example.com:8080/http",
|
||||||
|
"web.example.com:8000/http",
|
||||||
|
"secure.example.com:8888/https"),
|
||||||
|
},
|
||||||
|
want: `{
|
||||||
|
"servers": {
|
||||||
|
"http": {
|
||||||
|
"listen": [":80"],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"match": [{"host": ["app.example.com"]}],
|
||||||
|
"handle": [{
|
||||||
|
"handler": "reverse_proxy",
|
||||||
|
"upstreams": [
|
||||||
|
{"dial": "10.210.0.2:8080"},
|
||||||
|
{"dial": "10.210.0.3:8080"},
|
||||||
|
{"dial": "10.210.0.5:8080"}
|
||||||
|
]
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"match": [{"host": ["web.example.com"]}],
|
||||||
|
"handle": [{
|
||||||
|
"handler": "reverse_proxy",
|
||||||
|
"upstreams": [
|
||||||
|
{"dial": "10.210.0.2:8000"},
|
||||||
|
{"dial": "10.210.0.4:8000"},
|
||||||
|
{"dial": "10.210.0.5:8000"}
|
||||||
|
]
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"match": [{"path": ["/.uncloud-verify"]}],
|
||||||
|
"handle": [{
|
||||||
|
"body": "verification-response-body",
|
||||||
|
"handler": "static_response",
|
||||||
|
"status_code": 200
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"https": {
|
||||||
|
"listen": [":443"],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"match": [{"host": ["secure.example.com"]}],
|
||||||
|
"handle": [{
|
||||||
|
"handler": "reverse_proxy",
|
||||||
|
"upstreams": [
|
||||||
|
{"dial": "10.210.0.3:8888"},
|
||||||
|
{"dial": "10.210.0.4:8888"},
|
||||||
|
{"dial": "10.210.0.5:8888"}
|
||||||
|
]
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "container without uncloud network ignored",
|
||||||
|
containers: []api.ServiceContainer{
|
||||||
|
newContainerWithoutNetwork("ignored.example.com:8080/http"),
|
||||||
|
},
|
||||||
|
want: `{
|
||||||
|
"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,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "container with invalid port ignored",
|
||||||
|
containers: []api.ServiceContainer{
|
||||||
|
newContainer("10.210.0.2", "invalid-port"),
|
||||||
|
},
|
||||||
|
want: `{
|
||||||
|
"servers": {
|
||||||
|
"http": {
|
||||||
|
"listen": [":80"],
|
||||||
|
"routes": [{
|
||||||
|
"match": [{"path": ["/.uncloud-verify"]}],
|
||||||
|
"handle": [{
|
||||||
|
"body": "verification-response-body",
|
||||||
|
"handler": "static_response",
|
||||||
|
"status_code": 200
|
||||||
|
}]
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
"https": {
|
||||||
|
"listen": [":443"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "containers with unsupported protocols ignored",
|
||||||
|
containers: []api.ServiceContainer{
|
||||||
|
newContainer("10.210.0.2", "5000/tcp"),
|
||||||
|
newContainer("10.210.0.3", "5000/udp"),
|
||||||
|
newContainer("10.210.0.4", "80:8080/tcp@host"),
|
||||||
|
},
|
||||||
|
want: `{
|
||||||
|
"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,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
config, err := GenerateConfig(tt.containers, "verification-response-body")
|
||||||
|
|
||||||
|
if tt.wantErr {
|
||||||
|
assert.Error(t, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.Len(t, config.AppsRaw, 1, "Expected one http app")
|
||||||
|
require.Contains(t, config.AppsRaw, "http", "Expected http app")
|
||||||
|
|
||||||
|
assert.JSONEq(t, tt.want, string(config.AppsRaw["http"]), "Generated Caddy app config doesn't match")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newContainer(ip string, ports ...string) api.ServiceContainer {
|
||||||
|
portsLabel := strings.Join(ports, ",")
|
||||||
|
return api.ServiceContainer{Container: api.Container{ContainerJSON: types.ContainerJSON{
|
||||||
|
ContainerJSONBase: &types.ContainerJSONBase{},
|
||||||
|
NetworkSettings: &types.NetworkSettings{
|
||||||
|
Networks: map[string]*network.EndpointSettings{
|
||||||
|
docker.NetworkName: {
|
||||||
|
IPAddress: ip,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Config: &container.Config{
|
||||||
|
Labels: map[string]string{
|
||||||
|
api.LabelServicePorts: portsLabel,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newContainerWithoutNetwork(ports ...string) api.ServiceContainer {
|
||||||
|
portsLabel := strings.Join(ports, ",")
|
||||||
|
return api.ServiceContainer{Container: api.Container{ContainerJSON: types.ContainerJSON{
|
||||||
|
ContainerJSONBase: &types.ContainerJSONBase{},
|
||||||
|
NetworkSettings: &types.NetworkSettings{
|
||||||
|
Networks: map[string]*network.EndpointSettings{
|
||||||
|
"other-network": {
|
||||||
|
IPAddress: "172.17.0.2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Config: &container.Config{
|
||||||
|
Labels: map[string]string{
|
||||||
|
api.LabelServicePorts: portsLabel,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}}
|
||||||
|
}
|
||||||
@@ -3,34 +3,31 @@ package caddyfile
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/caddyserver/caddy/v2"
|
|
||||||
"github.com/caddyserver/caddy/v2/caddyconfig"
|
|
||||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
|
||||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp/reverseproxy"
|
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
|
||||||
"uncloud/internal/api"
|
"github.com/psviderski/uncloud/internal/fs"
|
||||||
"uncloud/internal/fs"
|
"github.com/psviderski/uncloud/internal/machine/store"
|
||||||
"uncloud/internal/machine/docker"
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
"uncloud/internal/machine/store"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const CaddyGroup = "uncloud"
|
const (
|
||||||
|
CaddyGroup = "uncloud"
|
||||||
|
VerifyPath = "/.uncloud-verify"
|
||||||
|
)
|
||||||
|
|
||||||
// Controller monitors container changes in the cluster store and generates a configuration file for Caddy reverse
|
// Controller monitors container changes in the cluster store and generates a configuration file for Caddy reverse
|
||||||
// proxy. The generated Caddyfile allows Caddy to route external traffic to service containers across the internal
|
// proxy. The generated Caddyfile allows Caddy to route external traffic to service containers across the internal
|
||||||
// network.
|
// network.
|
||||||
type Controller struct {
|
type Controller struct {
|
||||||
store *store.Store
|
store *store.Store
|
||||||
path string
|
path string
|
||||||
|
verifyResponse string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewController(store *store.Store, path string) (*Controller, error) {
|
func NewController(store *store.Store, path string, verifyResponse string) (*Controller, error) {
|
||||||
dir := filepath.Dir(path)
|
dir := filepath.Dir(path)
|
||||||
if err := os.MkdirAll(dir, 0750); err != nil {
|
if err := os.MkdirAll(dir, 0750); err != nil {
|
||||||
return nil, fmt.Errorf("create parent directory for Caddy configuration '%s': %w", dir, err)
|
return nil, fmt.Errorf("create parent directory for Caddy configuration '%s': %w", dir, err)
|
||||||
@@ -40,8 +37,9 @@ func NewController(store *store.Store, path string) (*Controller, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return &Controller{
|
return &Controller{
|
||||||
store: store,
|
store: store,
|
||||||
path: path,
|
path: path,
|
||||||
|
verifyResponse: verifyResponse,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,81 +90,23 @@ func (c *Controller) Run(ctx context.Context) error {
|
|||||||
// filterAvailableContainers filters out containers from this machine that are likely unavailable. The availability
|
// 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.
|
// 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.
|
// TODO: implement machine membership check using Corrossion Admin client.
|
||||||
func (c *Controller) filterAvailableContainers(containerRecords []store.ContainerRecord) ([]api.Container, error) {
|
func (c *Controller) filterAvailableContainers(
|
||||||
containers := make([]api.Container, len(containerRecords))
|
containerRecords []store.ContainerRecord,
|
||||||
|
) ([]api.ServiceContainer, error) {
|
||||||
|
containers := make([]api.ServiceContainer, len(containerRecords))
|
||||||
for i, cr := range containerRecords {
|
for i, cr := range containerRecords {
|
||||||
containers[i] = cr.Container
|
containers[i] = api.ServiceContainer{
|
||||||
|
Container: cr.Container,
|
||||||
|
// TODO: restore ServiceSpec from the container record once it's saved in the store.
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return containers, nil
|
return containers, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Controller) generateConfig(containers []api.Container) error {
|
func (c *Controller) generateConfig(containers []api.ServiceContainer) error {
|
||||||
// Maps hostnames to lists of upstreams (container IP:port pairs).
|
config, err := GenerateConfig(containers, c.verifyResponse)
|
||||||
httpHostUpstreams := make(map[string][]string)
|
if err != nil {
|
||||||
httpsHostUpstreams := make(map[string][]string)
|
return err
|
||||||
for _, ctr := range containers {
|
|
||||||
logger := slog.With("container", ctr.ID)
|
|
||||||
network, ok := ctr.NetworkSettings.Networks[docker.NetworkName]
|
|
||||||
if !ok {
|
|
||||||
// Container is not connected to the uncloud Docker network (could be host network).
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if network.IPAddress == "" {
|
|
||||||
logger.Error("Container has no IPv4 address.")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
ports, err := ctr.ServicePorts()
|
|
||||||
if err != nil {
|
|
||||||
logger.Error("Failed to parse service ports for container.", "err", err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, port := range ports {
|
|
||||||
switch port.Protocol {
|
|
||||||
case api.ProtocolHTTP:
|
|
||||||
upstream := net.JoinHostPort(network.IPAddress, strconv.Itoa(int(port.ContainerPort)))
|
|
||||||
httpHostUpstreams[port.Hostname] = append(httpHostUpstreams[port.Hostname], upstream)
|
|
||||||
case api.ProtocolHTTPS:
|
|
||||||
upstream := net.JoinHostPort(network.IPAddress, strconv.Itoa(int(port.ContainerPort)))
|
|
||||||
httpsHostUpstreams[port.Hostname] = append(httpHostUpstreams[port.Hostname], upstream)
|
|
||||||
default:
|
|
||||||
if port.Mode == api.PortModeIngress {
|
|
||||||
// TODO: implement L4 ingress routing for TCP and UDP.
|
|
||||||
logger.Error("Unsupported protocol for ingress port.", "port", port)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var warnings []caddyconfig.Warning
|
|
||||||
servers := make(map[string]*caddyhttp.Server)
|
|
||||||
servers["http"] = &caddyhttp.Server{
|
|
||||||
Listen: []string{fmt.Sprintf(":%d", caddyhttp.DefaultHTTPPort)},
|
|
||||||
Routes: hostUpstreamsToRoutes(httpHostUpstreams, &warnings),
|
|
||||||
}
|
|
||||||
servers["https"] = &caddyhttp.Server{
|
|
||||||
Listen: []string{fmt.Sprintf(":%d", caddyhttp.DefaultHTTPSPort)},
|
|
||||||
Routes: hostUpstreamsToRoutes(httpsHostUpstreams, &warnings),
|
|
||||||
}
|
|
||||||
|
|
||||||
httpApp := caddyhttp.App{
|
|
||||||
Servers: servers,
|
|
||||||
}
|
|
||||||
config := &caddy.Config{
|
|
||||||
AppsRaw: caddy.ModuleMap{
|
|
||||||
"http": caddyconfig.JSON(httpApp, &warnings),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
var err error
|
|
||||||
if len(warnings) > 0 {
|
|
||||||
// warnings only contains errors from JSON marshaling, which are highly unlikely with correct code.
|
|
||||||
for _, w := range warnings {
|
|
||||||
err = errors.Join(err, errors.New(w.Message))
|
|
||||||
}
|
|
||||||
return fmt.Errorf("marshal Caddy configuration: %w", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
configBytes, err := json.MarshalIndent(config, "", " ")
|
configBytes, err := json.MarshalIndent(config, "", " ")
|
||||||
@@ -183,31 +123,3 @@ func (c *Controller) generateConfig(containers []api.Container) error {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// hostUpstreamsToRoutes converts a map of hostnames to upstreams to a list of Caddy routes.
|
|
||||||
func hostUpstreamsToRoutes(hostUpstreams map[string][]string, warnings *[]caddyconfig.Warning) []caddyhttp.Route {
|
|
||||||
routes := make([]caddyhttp.Route, 0, len(hostUpstreams))
|
|
||||||
for hostname, upstreams := range hostUpstreams {
|
|
||||||
upstreamPool := make([]*reverseproxy.Upstream, len(upstreams))
|
|
||||||
for i, upstream := range upstreams {
|
|
||||||
upstreamPool[i] = &reverseproxy.Upstream{
|
|
||||||
Dial: upstream,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
handler := &reverseproxy.Handler{
|
|
||||||
Upstreams: upstreamPool,
|
|
||||||
}
|
|
||||||
|
|
||||||
routes = append(routes, caddyhttp.Route{
|
|
||||||
MatcherSetsRaw: caddyhttp.RawMatcherSets{
|
|
||||||
{
|
|
||||||
"host": caddyconfig.JSON(caddyhttp.MatchHost{hostname}, warnings),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
HandlersRaw: []json.RawMessage{
|
|
||||||
caddyconfig.JSONModuleObject(handler, "handler", "reverse_proxy", warnings),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return routes
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -11,11 +11,11 @@ import (
|
|||||||
"log/slog"
|
"log/slog"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"time"
|
"time"
|
||||||
"uncloud/internal/corrosion"
|
"github.com/psviderski/uncloud/internal/corrosion"
|
||||||
"uncloud/internal/machine/api/pb"
|
"github.com/psviderski/uncloud/internal/machine/api/pb"
|
||||||
"uncloud/internal/machine/network"
|
"github.com/psviderski/uncloud/internal/machine/network"
|
||||||
"uncloud/internal/machine/store"
|
"github.com/psviderski/uncloud/internal/machine/store"
|
||||||
"uncloud/internal/secret"
|
"github.com/psviderski/uncloud/internal/secret"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Cluster struct {
|
type Cluster struct {
|
||||||
@@ -110,6 +110,15 @@ func (c *Cluster) AddMachine(ctx context.Context, req *pb.AddMachineRequest) (*p
|
|||||||
if len(req.Network.Endpoints) == 0 {
|
if len(req.Network.Endpoints) == 0 {
|
||||||
return nil, status.Error(codes.InvalidArgument, "endpoints not set")
|
return nil, status.Error(codes.InvalidArgument, "endpoints not set")
|
||||||
}
|
}
|
||||||
|
if req.PublicIp != nil {
|
||||||
|
ip, err := req.PublicIp.ToAddr()
|
||||||
|
if err != nil {
|
||||||
|
return nil, status.Errorf(codes.InvalidArgument, "invalid public IP: %v", err)
|
||||||
|
}
|
||||||
|
if !ip.IsValid() {
|
||||||
|
return nil, status.Error(codes.InvalidArgument, "invalid public IP")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
machines, err := c.store.ListMachines(ctx)
|
machines, err := c.store.ListMachines(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -174,6 +183,7 @@ func (c *Cluster) AddMachine(ctx context.Context, req *pb.AddMachineRequest) (*p
|
|||||||
Endpoints: req.Network.Endpoints,
|
Endpoints: req.Network.Endpoints,
|
||||||
PublicKey: req.Network.PublicKey,
|
PublicKey: req.Network.PublicKey,
|
||||||
},
|
},
|
||||||
|
PublicIp: req.PublicIp,
|
||||||
}
|
}
|
||||||
// TODO: announce the new machine to the cluster members and achieve consensus.
|
// TODO: announce the new machine to the cluster members and achieve consensus.
|
||||||
// We should perhaps not proceed if this machine is in a minority partition.
|
// We should perhaps not proceed if this machine is in a minority partition.
|
||||||
@@ -232,24 +242,3 @@ func (c *Cluster) ListMachines(ctx context.Context, _ *emptypb.Empty) (*pb.ListM
|
|||||||
|
|
||||||
return &pb.ListMachinesResponse{Machines: members}, nil
|
return &pb.ListMachinesResponse{Machines: members}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
//func (c *Cluster) ListServices(ctx context.Context, _ *emptypb.Empty) (*pb.ListServicesResponse, error) {
|
|
||||||
// if err := c.checkInitialised(ctx); err != nil {
|
|
||||||
// return nil, err
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// return &pb.ListServicesResponse{
|
|
||||||
// Messages: []*pb.Services{
|
|
||||||
// {
|
|
||||||
// Services: []*pb.Service{
|
|
||||||
// {
|
|
||||||
// Name: "service1",
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// Name: "service2",
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// }, nil
|
|
||||||
//}
|
|
||||||
|
|||||||
@@ -0,0 +1,159 @@
|
|||||||
|
package cluster
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"github.com/psviderski/uncloud/internal/dns"
|
||||||
|
"github.com/psviderski/uncloud/internal/machine/api/pb"
|
||||||
|
"github.com/psviderski/uncloud/internal/machine/store"
|
||||||
|
"google.golang.org/grpc/codes"
|
||||||
|
"google.golang.org/grpc/status"
|
||||||
|
"google.golang.org/protobuf/types/known/emptypb"
|
||||||
|
)
|
||||||
|
|
||||||
|
// uncloudDNSKey is the key used to store the details of the reserved domain in the store.
|
||||||
|
const uncloudDNSKey = "uncloud_dns"
|
||||||
|
|
||||||
|
type uncloudDNSDomain struct {
|
||||||
|
// Endpoint is the API endpoint of the Uncloud DNS service where the domain is reserved.
|
||||||
|
Endpoint string
|
||||||
|
Name string
|
||||||
|
// TODO: encrypt the token in the store.
|
||||||
|
Token string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cluster) ReserveDomain(ctx context.Context, req *pb.ReserveDomainRequest) (*pb.Domain, error) {
|
||||||
|
if err := c.checkInitialised(ctx); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Endpoint == "" {
|
||||||
|
return nil, status.Error(codes.InvalidArgument, "API endpoint not set")
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := c.storedDomain(ctx); err == nil {
|
||||||
|
return nil, status.Errorf(codes.AlreadyExists, "domain already reserved")
|
||||||
|
} else {
|
||||||
|
if s := status.Convert(err); s.Code() != codes.NotFound {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dnsClient := dns.NewClient()
|
||||||
|
name, token, err := dnsClient.ReserveDomain(req.Endpoint)
|
||||||
|
if err != nil {
|
||||||
|
return nil, status.Errorf(codes.Internal, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
domain := uncloudDNSDomain{
|
||||||
|
Endpoint: req.Endpoint,
|
||||||
|
Name: name,
|
||||||
|
Token: token,
|
||||||
|
}
|
||||||
|
domainJSON, err := json.Marshal(domain)
|
||||||
|
if err != nil {
|
||||||
|
return nil, status.Errorf(codes.Internal, "marshal reserved domain for store: %v", err)
|
||||||
|
}
|
||||||
|
if err = c.store.Put(ctx, uncloudDNSKey, domainJSON); err != nil {
|
||||||
|
return nil, status.Errorf(codes.Internal, "store reserved domain: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &pb.Domain{Name: name}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cluster) GetDomain(ctx context.Context, _ *emptypb.Empty) (*pb.Domain, error) {
|
||||||
|
if err := c.checkInitialised(ctx); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
domain, err := c.storedDomain(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &pb.Domain{Name: domain.Name}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cluster) storedDomain(ctx context.Context) (uncloudDNSDomain, error) {
|
||||||
|
var domain uncloudDNSDomain
|
||||||
|
var domainJSON []byte
|
||||||
|
|
||||||
|
if err := c.store.Get(ctx, uncloudDNSKey, &domainJSON); err != nil {
|
||||||
|
if errors.Is(err, store.ErrKeyNotFound) {
|
||||||
|
return domain, status.Errorf(codes.NotFound, "domain not found")
|
||||||
|
}
|
||||||
|
return domain, status.Errorf(codes.Internal, "get domain from store: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.Unmarshal(domainJSON, &domain); err != nil {
|
||||||
|
return domain, status.Errorf(codes.Internal, "unmarshal domain: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return domain, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cluster) ReleaseDomain(ctx context.Context, _ *emptypb.Empty) (*pb.Domain, error) {
|
||||||
|
if err := c.checkInitialised(ctx); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
domain, err := c.storedDomain(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = c.store.Delete(ctx, uncloudDNSKey); err != nil {
|
||||||
|
return nil, status.Errorf(codes.Internal, "delete domain from store: %v", err)
|
||||||
|
}
|
||||||
|
// TODO: implement and call Uncloud DNS endpoint to release/delete the domain.
|
||||||
|
|
||||||
|
return &pb.Domain{Name: domain.Name}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cluster) CreateDomainRecords(
|
||||||
|
ctx context.Context, req *pb.CreateDomainRecordsRequest,
|
||||||
|
) (*pb.CreateDomainRecordsResponse, error) {
|
||||||
|
if err := c.checkInitialised(ctx); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
domain, err := c.storedDomain(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
dnsClient := dns.NewClient()
|
||||||
|
recordsReq := make([]dns.RecordRequest, len(req.Records))
|
||||||
|
for i, r := range req.Records {
|
||||||
|
recordsReq[i] = dns.RecordRequest{
|
||||||
|
Name: r.Name,
|
||||||
|
Type: dns.RecordType(r.Type.String()),
|
||||||
|
Values: r.Values,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
recordsResp, err := dnsClient.CreateRecords(domain.Endpoint, domain.Name, domain.Token, recordsReq)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := &pb.CreateDomainRecordsResponse{
|
||||||
|
Records: make([]*pb.DNSRecord, len(recordsResp)),
|
||||||
|
}
|
||||||
|
for i, r := range recordsResp {
|
||||||
|
resp.Records[i] = &pb.DNSRecord{
|
||||||
|
Name: r.FQDN,
|
||||||
|
Values: r.Values,
|
||||||
|
}
|
||||||
|
|
||||||
|
switch r.Type {
|
||||||
|
case dns.RecordTypeA:
|
||||||
|
resp.Records[i].Type = pb.DNSRecord_A
|
||||||
|
case dns.RecordTypeAAAA:
|
||||||
|
resp.Records[i].Type = pb.DNSRecord_AAAA
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@ package cluster
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"uncloud/internal/secret"
|
"github.com/psviderski/uncloud/internal/secret"
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewMachineID generates a new unique machine ID.
|
// NewMachineID generates a new unique machine ID.
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
"net/netip"
|
"net/netip"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"uncloud/internal/fs"
|
"github.com/psviderski/uncloud/internal/fs"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
package machine
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/jmoiron/sqlx"
|
||||||
|
_ "modernc.org/sqlite"
|
||||||
|
)
|
||||||
|
|
||||||
|
const DBFileName = "machine.db"
|
||||||
|
|
||||||
|
// NewDB creates a new connection to machine SQLite database and runs schema migrations if necessary.
|
||||||
|
func NewDB(path string) (*sqlx.DB, error) {
|
||||||
|
// Create the database file with 0600 permissions if it doesn't exist, or update permissions if exists.
|
||||||
|
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||||
|
file, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR, 0600)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("create SQLite database '%s': %w", path, err)
|
||||||
|
}
|
||||||
|
file.Close()
|
||||||
|
} else {
|
||||||
|
if err = os.Chmod(path, 0600); err != nil {
|
||||||
|
return nil, fmt.Errorf("update SQLite database permissions '%s': %w", path, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// - Write-Ahead Logging (WAL) mode for better read/write performance.
|
||||||
|
// - Busy timeout (5s) to make concurrent writes wait on each other instead of failing immediately.
|
||||||
|
conn := path + "?_pragma=journal_mode=WAL&_pragma=synchronous=NORMAL&_pragma=busy_timeout=5000&_time_format=sqlite"
|
||||||
|
db, err := sqlx.Connect("sqlite", conn)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("connect to SQLite database '%s': %w", conn, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
schema := `
|
||||||
|
CREATE TABLE IF NOT EXISTS containers (
|
||||||
|
id TEXT NOT NULL PRIMARY KEY,
|
||||||
|
service_id TEXT NOT NULL,
|
||||||
|
service_name TEXT AS (json_extract(service_spec, '$.Name')),
|
||||||
|
service_spec TEXT NOT NULL CHECK (json_valid(service_spec)),
|
||||||
|
-- 'subsecond' modifier is used to store timestamps with millisecond precision.
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT (datetime('subsecond')),
|
||||||
|
updated_at TIMESTAMP NOT NULL DEFAULT (datetime('subsecond'))
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_containers_service_id ON containers (service_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_containers_service_name ON containers (service_name);
|
||||||
|
`
|
||||||
|
|
||||||
|
if _, err = db.Exec(schema); err != nil {
|
||||||
|
return nil, fmt.Errorf("create schema: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return db, nil
|
||||||
|
}
|
||||||
@@ -3,18 +3,23 @@ package docker
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"github.com/distribution/reference"
|
||||||
"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/network"
|
"github.com/docker/docker/api/types/network"
|
||||||
"github.com/docker/docker/errdefs"
|
"github.com/docker/docker/errdefs"
|
||||||
"github.com/docker/docker/pkg/jsonmessage"
|
"github.com/docker/docker/pkg/jsonmessage"
|
||||||
|
regtypes "github.com/google/go-containerregistry/pkg/v1/types"
|
||||||
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/pkg/api"
|
||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
"google.golang.org/grpc/codes"
|
"google.golang.org/grpc/codes"
|
||||||
"google.golang.org/grpc/status"
|
"google.golang.org/grpc/status"
|
||||||
"io"
|
|
||||||
"uncloud/internal/machine/api/pb"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Client is a gRPC client for the Docker service that provides a similar interface to the Docker HTTP client.
|
// Client is a gRPC client for the Docker service that provides a similar interface to the Docker HTTP client.
|
||||||
@@ -72,10 +77,8 @@ func (c *Client) CreateContainer(
|
|||||||
Name: name,
|
Name: name,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if s, ok := status.FromError(err); ok {
|
if status.Convert(err).Code() == codes.NotFound {
|
||||||
if s.Code() == codes.NotFound {
|
return resp, errdefs.NotFound(err)
|
||||||
return resp, errdefs.NotFound(err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return resp, err
|
return resp, err
|
||||||
}
|
}
|
||||||
@@ -92,10 +95,8 @@ func (c *Client) InspectContainer(ctx context.Context, id string) (types.Contain
|
|||||||
|
|
||||||
grpcResp, err := c.grpcClient.InspectContainer(ctx, &pb.InspectContainerRequest{Id: id})
|
grpcResp, err := c.grpcClient.InspectContainer(ctx, &pb.InspectContainerRequest{Id: id})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if s, ok := status.FromError(err); ok {
|
if status.Convert(err).Code() == codes.NotFound {
|
||||||
if s.Code() == codes.NotFound {
|
return resp, errdefs.NotFound(err)
|
||||||
return resp, errdefs.NotFound(err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return resp, err
|
return resp, err
|
||||||
}
|
}
|
||||||
@@ -118,10 +119,8 @@ func (c *Client) StartContainer(ctx context.Context, id string, opts container.S
|
|||||||
Options: optsBytes,
|
Options: optsBytes,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if s, ok := status.FromError(err); ok {
|
if status.Convert(err).Code() == codes.NotFound {
|
||||||
if s.Code() == codes.NotFound {
|
return errdefs.NotFound(err)
|
||||||
return errdefs.NotFound(err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
@@ -139,10 +138,8 @@ func (c *Client) StopContainer(ctx context.Context, id string, opts container.St
|
|||||||
Options: optsBytes,
|
Options: optsBytes,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if s, ok := status.FromError(err); ok {
|
if status.Convert(err).Code() == codes.NotFound {
|
||||||
if s.Code() == codes.NotFound {
|
return errdefs.NotFound(err)
|
||||||
return errdefs.NotFound(err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
@@ -191,10 +188,8 @@ func (c *Client) RemoveContainer(ctx context.Context, id string, opts container.
|
|||||||
Options: optsBytes,
|
Options: optsBytes,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if s, ok := status.FromError(err); ok {
|
if status.Convert(err).Code() == codes.NotFound {
|
||||||
if s.Code() == codes.NotFound {
|
return errdefs.NotFound(err)
|
||||||
return errdefs.NotFound(err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
@@ -237,3 +232,228 @@ func (c *Client) PullImage(ctx context.Context, image string) (<-chan PullImageM
|
|||||||
|
|
||||||
return ch, nil
|
return ch, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// InspectImage returns the image information for the given image ID. The request may be sent to multiple machines.
|
||||||
|
func (c *Client) InspectImage(ctx context.Context, id string) ([]api.MachineImage, error) {
|
||||||
|
resp, err := c.grpcClient.InspectImage(ctx, &pb.InspectImageRequest{Id: id})
|
||||||
|
if err != nil {
|
||||||
|
// If the request was sent to only one machine, err is an actual error from the machine.
|
||||||
|
if status.Convert(err).Code() == codes.NotFound {
|
||||||
|
return nil, errdefs.NotFound(err)
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
notFoundCount := 0
|
||||||
|
for _, msg := range resp.Messages {
|
||||||
|
if msg.Metadata != nil && msg.Metadata.Status != nil && codes.Code(msg.Metadata.Status.Code) == codes.NotFound {
|
||||||
|
notFoundCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(resp.Messages) == notFoundCount {
|
||||||
|
return nil, errdefs.NotFound(fmt.Errorf("image not found: %s", id))
|
||||||
|
}
|
||||||
|
|
||||||
|
images := make([]api.MachineImage, len(resp.Messages))
|
||||||
|
for i, msg := range resp.Messages {
|
||||||
|
images[i].Metadata = msg.Metadata
|
||||||
|
if msg.Metadata != nil && msg.Metadata.Error != "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = json.Unmarshal(msg.Image, &images[i].Image); err != nil {
|
||||||
|
return nil, fmt.Errorf("unmarshal image: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return images, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// InspectRemoteImage returns the image metadata for an image in a remote registry using the machine's Docker auth
|
||||||
|
// credentials if necessary. If the response from a machine doesn't contain an error, the api.RemoteImage will either
|
||||||
|
// contain an IndexManifest or an ImageManifest.
|
||||||
|
func (c *Client) InspectRemoteImage(ctx context.Context, id string) ([]api.MachineRemoteImage, error) {
|
||||||
|
resp, err := c.grpcClient.InspectRemoteImage(ctx, &pb.InspectRemoteImageRequest{Id: id})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
images := make([]api.MachineRemoteImage, 0, len(resp.Messages))
|
||||||
|
var parseErr error
|
||||||
|
for _, msg := range resp.Messages {
|
||||||
|
mri, err := parseRemoteImageMessage(msg)
|
||||||
|
if err != nil {
|
||||||
|
parseErr = errors.Join(parseErr, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
images = append(images, mri)
|
||||||
|
}
|
||||||
|
|
||||||
|
return images, parseErr
|
||||||
|
}
|
||||||
|
|
||||||
|
type manifestMediaType struct {
|
||||||
|
MediaType regtypes.MediaType `json:"mediaType"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseRemoteImageMessage(msg *pb.RemoteImage) (api.MachineRemoteImage, error) {
|
||||||
|
mri := api.MachineRemoteImage{
|
||||||
|
Metadata: msg.Metadata,
|
||||||
|
}
|
||||||
|
if msg.Metadata != nil && msg.Metadata.Error != "" {
|
||||||
|
return mri, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ref, err := reference.ParseNormalizedNamed(msg.Reference)
|
||||||
|
if err != nil {
|
||||||
|
// The reference is the string representation of a valid canonical reference from the server which
|
||||||
|
// is expected to be parseable.
|
||||||
|
return mri, fmt.Errorf("parse reference: %w", err)
|
||||||
|
}
|
||||||
|
if canonicalRef, ok := ref.(reference.Canonical); ok {
|
||||||
|
mri.Image.Reference = canonicalRef
|
||||||
|
} else {
|
||||||
|
return mri, fmt.Errorf("unexpected non-canonical image reference: %s", msg.Reference)
|
||||||
|
}
|
||||||
|
|
||||||
|
manifest := manifestMediaType{}
|
||||||
|
if err = json.Unmarshal(msg.Manifest, &manifest); err != nil {
|
||||||
|
return mri, fmt.Errorf("unmarshal manifest: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case manifest.MediaType.IsIndex():
|
||||||
|
index := &ocispec.Index{}
|
||||||
|
if err = json.Unmarshal(msg.Manifest, index); err != nil {
|
||||||
|
return mri, fmt.Errorf("unmarshal index manifest: %w", err)
|
||||||
|
}
|
||||||
|
mri.Image.IndexManifest = index
|
||||||
|
case manifest.MediaType.IsImage():
|
||||||
|
image := &ocispec.Manifest{}
|
||||||
|
if err = json.Unmarshal(msg.Manifest, image); err != nil {
|
||||||
|
return mri, fmt.Errorf("unmarshal image manifest: %w", err)
|
||||||
|
}
|
||||||
|
mri.Image.ImageManifest = image
|
||||||
|
default:
|
||||||
|
return mri, fmt.Errorf("unexpected manifest media type: %s", manifest.MediaType)
|
||||||
|
}
|
||||||
|
|
||||||
|
return mri, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateServiceContainer creates a new container for the service with the given specifications.
|
||||||
|
func (c *Client) CreateServiceContainer(
|
||||||
|
ctx context.Context, serviceID string, spec api.ServiceSpec, containerName string,
|
||||||
|
) (container.CreateResponse, error) {
|
||||||
|
var resp container.CreateResponse
|
||||||
|
|
||||||
|
specBytes, err := json.Marshal(spec)
|
||||||
|
if err != nil {
|
||||||
|
return resp, fmt.Errorf("marshal service spec: %w", err)
|
||||||
|
}
|
||||||
|
grpcResp, err := c.grpcClient.CreateServiceContainer(ctx, &pb.CreateServiceContainerRequest{
|
||||||
|
ServiceId: serviceID,
|
||||||
|
ServiceSpec: specBytes,
|
||||||
|
ContainerName: containerName,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
if status.Convert(err).Code() == codes.NotFound {
|
||||||
|
return resp, errdefs.NotFound(err)
|
||||||
|
}
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = json.Unmarshal(grpcResp.Response, &resp); err != nil {
|
||||||
|
return resp, fmt.Errorf("unmarshal gRPC response: %w", err)
|
||||||
|
}
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// InspectServiceContainer returns the container information and service specification that was used to create the
|
||||||
|
// container with the given ID.
|
||||||
|
func (c *Client) InspectServiceContainer(ctx context.Context, id string) (api.ServiceContainer, error) {
|
||||||
|
var resp api.ServiceContainer
|
||||||
|
|
||||||
|
grpcResp, err := c.grpcClient.InspectServiceContainer(ctx, &pb.InspectContainerRequest{Id: id})
|
||||||
|
if err != nil {
|
||||||
|
if status.Convert(err).Code() == codes.NotFound {
|
||||||
|
return resp, errdefs.NotFound(err)
|
||||||
|
}
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = json.Unmarshal(grpcResp.Container, &resp.Container); err != nil {
|
||||||
|
return resp, fmt.Errorf("unmarshal container: %w", err)
|
||||||
|
}
|
||||||
|
if err = json.Unmarshal(grpcResp.ServiceSpec, &resp.ServiceSpec); err != nil {
|
||||||
|
return resp, fmt.Errorf("unmarshal service spec: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type MachineServiceContainers struct {
|
||||||
|
Metadata *pb.Metadata
|
||||||
|
Containers []api.ServiceContainer
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListServiceContainers returns all containers on requested machines that belong to the service with the given
|
||||||
|
// name or ID. If serviceNameOrID is empty, all service containers are returned.
|
||||||
|
func (c *Client) ListServiceContainers(
|
||||||
|
ctx context.Context, serviceNameOrID string, opts container.ListOptions,
|
||||||
|
) ([]MachineServiceContainers, error) {
|
||||||
|
optsBytes, err := json.Marshal(opts)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("marshal options: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := c.grpcClient.ListServiceContainers(ctx, &pb.ListServiceContainersRequest{
|
||||||
|
ServiceId: serviceNameOrID,
|
||||||
|
Options: optsBytes,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
machineContainers := make([]MachineServiceContainers, len(resp.Messages))
|
||||||
|
for i, msg := range resp.Messages {
|
||||||
|
machineContainers[i].Metadata = msg.Metadata
|
||||||
|
if msg.Metadata != nil && msg.Metadata.Error != "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
containers := make([]api.ServiceContainer, len(msg.Containers))
|
||||||
|
for j, sc := range msg.Containers {
|
||||||
|
if err = json.Unmarshal(sc.Container, &containers[j].Container); err != nil {
|
||||||
|
return nil, fmt.Errorf("unmarshal container: %w", err)
|
||||||
|
}
|
||||||
|
if err = json.Unmarshal(sc.ServiceSpec, &containers[j].ServiceSpec); err != nil {
|
||||||
|
return nil, fmt.Errorf("unmarshal service spec: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
machineContainers[i].Containers = containers
|
||||||
|
}
|
||||||
|
|
||||||
|
return machineContainers, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveServiceContainer stops (kills after grace period) and removes a service container with the given ID.
|
||||||
|
// A service container is a container that has been created with CreateServiceContainer.
|
||||||
|
func (c *Client) RemoveServiceContainer(ctx context.Context, id string, opts container.RemoveOptions) error {
|
||||||
|
optsBytes, err := json.Marshal(opts)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("marshal options: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = c.grpcClient.RemoveServiceContainer(ctx, &pb.RemoveContainerRequest{
|
||||||
|
Id: id,
|
||||||
|
Options: optsBytes,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
if status.Convert(err).Code() == codes.NotFound {
|
||||||
|
return errdefs.NotFound(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,10 +8,10 @@ import (
|
|||||||
"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/pkg/api"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"time"
|
"time"
|
||||||
"uncloud/internal/api"
|
|
||||||
"uncloud/internal/machine/store"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
"github.com/docker/docker/libnetwork/iptables"
|
"github.com/docker/docker/libnetwork/iptables"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"uncloud/internal/machine/network"
|
"github.com/psviderski/uncloud/internal/machine/network"
|
||||||
)
|
)
|
||||||
|
|
||||||
// EnsureUncloudNetwork creates the Docker bridge network NetworkName with the provided machine subnet
|
// EnsureUncloudNetwork creates the Docker bridge network NetworkName with the provided machine subnet
|
||||||
|
|||||||
@@ -2,32 +2,54 @@ package docker
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"database/sql"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log/slog"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/distribution/reference"
|
||||||
"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"
|
||||||
"github.com/docker/docker/api/types/image"
|
"github.com/docker/docker/api/types/image"
|
||||||
"github.com/docker/docker/api/types/network"
|
"github.com/docker/docker/api/types/network"
|
||||||
"github.com/docker/docker/client"
|
"github.com/docker/docker/client"
|
||||||
|
"github.com/docker/go-connections/nat"
|
||||||
|
"github.com/google/go-containerregistry/pkg/authn"
|
||||||
|
"github.com/google/go-containerregistry/pkg/name"
|
||||||
|
"github.com/google/go-containerregistry/pkg/v1/remote"
|
||||||
|
"github.com/jmoiron/sqlx"
|
||||||
|
"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/secret"
|
||||||
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
"google.golang.org/grpc/codes"
|
"google.golang.org/grpc/codes"
|
||||||
"google.golang.org/grpc/status"
|
"google.golang.org/grpc/status"
|
||||||
"google.golang.org/protobuf/types/known/emptypb"
|
"google.golang.org/protobuf/types/known/emptypb"
|
||||||
"io"
|
|
||||||
"uncloud/internal/machine/api/pb"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewServer creates a new Docker gRPC server with the provided Docker client.
|
// NewServer creates a new Docker gRPC server with the provided Docker client.
|
||||||
func NewServer(cli *client.Client) *Server {
|
func NewServer(cli *client.Client, db *sqlx.DB) *Server {
|
||||||
return &Server{client: cli}
|
return &Server{
|
||||||
|
client: cli,
|
||||||
|
db: db,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateContainer creates a new container based on the given configuration.
|
// CreateContainer creates a new container based on the given configuration.
|
||||||
@@ -149,13 +171,17 @@ func (s *Server) ListContainers(ctx context.Context, req *pb.ListContainersReque
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, status.Errorf(codes.Internal, err.Error())
|
return nil, status.Errorf(codes.Internal, err.Error())
|
||||||
}
|
}
|
||||||
containers := make([]types.ContainerJSON, len(containerSummaries))
|
containers := make([]types.ContainerJSON, 0, len(containerSummaries))
|
||||||
for i, cs := range containerSummaries {
|
for _, cs := range containerSummaries {
|
||||||
c, err := s.client.ContainerInspect(ctx, cs.ID)
|
c, err := s.client.ContainerInspect(ctx, cs.ID)
|
||||||
if err != nil {
|
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)
|
return nil, status.Errorf(codes.Internal, "inspect container %s: %v", cs.ID, err)
|
||||||
}
|
}
|
||||||
containers[i] = c
|
containers = append(containers, c)
|
||||||
}
|
}
|
||||||
|
|
||||||
containersBytes, err := json.Marshal(containers)
|
containersBytes, err := json.Marshal(containers)
|
||||||
@@ -239,3 +265,337 @@ func (s *Server) PullImage(req *pb.PullImageRequest, stream grpc.ServerStreaming
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// InspectImage returns the image information for the given image ID.
|
||||||
|
func (s *Server) InspectImage(ctx context.Context, req *pb.InspectImageRequest) (*pb.InspectImageResponse, error) {
|
||||||
|
resp, _, err := s.client.ImageInspectWithRaw(ctx, req.Id)
|
||||||
|
if err != nil {
|
||||||
|
if client.IsErrNotFound(err) {
|
||||||
|
return nil, status.Errorf(codes.NotFound, err.Error())
|
||||||
|
}
|
||||||
|
return nil, status.Errorf(codes.Internal, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
respBytes, err := json.Marshal(resp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, status.Errorf(codes.Internal, "marshal response: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &pb.InspectImageResponse{
|
||||||
|
Messages: []*pb.Image{
|
||||||
|
{
|
||||||
|
Image: respBytes,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// InspectRemoteImage returns the image metadata for an image in a remote registry using the machine's Docker auth
|
||||||
|
// credentials if necessary.
|
||||||
|
func (s *Server) InspectRemoteImage(
|
||||||
|
_ context.Context, req *pb.InspectRemoteImageRequest,
|
||||||
|
) (*pb.InspectRemoteImageResponse, error) {
|
||||||
|
ref, err := name.ParseReference(req.Id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, status.Errorf(codes.InvalidArgument, "parse image: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
desc, err := remote.Get(ref, remote.WithAuthFromKeychain(authn.DefaultKeychain))
|
||||||
|
if err != nil {
|
||||||
|
return nil, status.Errorf(codes.Internal, "fetch image manifest: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
namedRef, err := reference.ParseNormalizedNamed(ref.String())
|
||||||
|
if err != nil {
|
||||||
|
return nil, status.Errorf(codes.InvalidArgument, "parse image: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var canonicalRef reference.Canonical
|
||||||
|
if _, ok := namedRef.(reference.Canonical); ok {
|
||||||
|
canonicalRef = namedRef.(reference.Canonical)
|
||||||
|
} else {
|
||||||
|
if canonicalRef, err = reference.WithDigest(namedRef, digest.Digest(desc.Digest.String())); err != nil {
|
||||||
|
return nil, status.Errorf(codes.Internal, "add digest to image: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &pb.InspectRemoteImageResponse{
|
||||||
|
Messages: []*pb.RemoteImage{
|
||||||
|
{
|
||||||
|
Reference: reference.FamiliarString(canonicalRef),
|
||||||
|
Manifest: desc.Manifest,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateServiceContainer creates a new container for the service with the given specifications.
|
||||||
|
func (s *Server) CreateServiceContainer(
|
||||||
|
ctx context.Context, req *pb.CreateServiceContainerRequest,
|
||||||
|
) (*pb.CreateContainerResponse, error) {
|
||||||
|
if !api.ValidateServiceID(req.ServiceId) {
|
||||||
|
return nil, status.Errorf(codes.InvalidArgument, "invalid service ID: '%s'", req.ServiceId)
|
||||||
|
}
|
||||||
|
|
||||||
|
var spec api.ServiceSpec
|
||||||
|
if err := json.Unmarshal(req.ServiceSpec, &spec); err != nil {
|
||||||
|
return nil, status.Errorf(codes.InvalidArgument, "unmarshal service spec: %v", err)
|
||||||
|
}
|
||||||
|
spec = spec.SetDefaults()
|
||||||
|
if err := spec.Validate(); err != nil {
|
||||||
|
return nil, status.Errorf(codes.InvalidArgument, "invalid service spec: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
containerName := req.ContainerName
|
||||||
|
if containerName == "" {
|
||||||
|
suffix, err := secret.RandomAlphaNumeric(4)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("generate random suffix: %w", err)
|
||||||
|
}
|
||||||
|
containerName = fmt.Sprintf("%s-%s", spec.Name, suffix)
|
||||||
|
}
|
||||||
|
|
||||||
|
config := &container.Config{
|
||||||
|
Cmd: spec.Container.Command,
|
||||||
|
Entrypoint: spec.Container.Entrypoint,
|
||||||
|
Hostname: containerName,
|
||||||
|
Image: spec.Container.Image,
|
||||||
|
Labels: map[string]string{
|
||||||
|
api.LabelServiceID: req.ServiceId,
|
||||||
|
api.LabelServiceName: spec.Name,
|
||||||
|
api.LabelServiceMode: spec.Mode,
|
||||||
|
api.LabelManaged: "",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if spec.Mode == "" {
|
||||||
|
config.Labels[api.LabelServiceMode] = api.ServiceModeReplicated
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: do not set the ports as container labels once migrated to retrieve them from the spec in DB.
|
||||||
|
var err error
|
||||||
|
if len(spec.Ports) > 0 {
|
||||||
|
encodedPorts := make([]string, len(spec.Ports))
|
||||||
|
for i, p := range spec.Ports {
|
||||||
|
encodedPorts[i], err = p.String()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("encode service port spec: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
config.Labels[api.LabelServicePorts] = strings.Join(encodedPorts, ",")
|
||||||
|
}
|
||||||
|
|
||||||
|
portBindings := make(nat.PortMap)
|
||||||
|
for _, p := range spec.Ports {
|
||||||
|
if p.Mode != api.PortModeHost {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
port := nat.Port(fmt.Sprintf("%d/%s", p.ContainerPort, p.Protocol))
|
||||||
|
portBindings[port] = []nat.PortBinding{
|
||||||
|
{
|
||||||
|
HostPort: strconv.Itoa(int(p.PublishedPort)),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if p.HostIP.IsValid() {
|
||||||
|
portBindings[port][0].HostIP = p.HostIP.String()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
hostConfig := &container.HostConfig{
|
||||||
|
Binds: spec.Container.Volumes,
|
||||||
|
Init: spec.Container.Init,
|
||||||
|
PortBindings: portBindings,
|
||||||
|
// Always restart service containers if they exit or a machine restarts.
|
||||||
|
// For one-off containers and batch jobs we plan to use a different service type/mode.
|
||||||
|
RestartPolicy: container.RestartPolicy{
|
||||||
|
Name: container.RestartPolicyAlways,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
networkConfig := &network.NetworkingConfig{
|
||||||
|
EndpointsConfig: map[string]*network.EndpointSettings{
|
||||||
|
NetworkName: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := s.client.ContainerCreate(ctx, config, hostConfig, networkConfig, nil, containerName)
|
||||||
|
if err != nil {
|
||||||
|
if client.IsErrNotFound(err) {
|
||||||
|
return nil, status.Error(codes.NotFound, err.Error())
|
||||||
|
}
|
||||||
|
return nil, status.Error(codes.Internal, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
respBytes, err := json.Marshal(resp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, status.Errorf(codes.Internal, "marshal response: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store the container spec in the database or remove the container with its anonymous volumes if storing fails.
|
||||||
|
removeContainer := func() {
|
||||||
|
_ = s.client.ContainerRemove(ctx, resp.ID, container.RemoveOptions{RemoveVolumes: true})
|
||||||
|
}
|
||||||
|
|
||||||
|
specBytes, err := json.Marshal(spec)
|
||||||
|
if err != nil {
|
||||||
|
removeContainer()
|
||||||
|
return nil, status.Errorf(codes.Internal, "marshal service spec: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err = s.db.ExecContext(ctx,
|
||||||
|
`INSERT INTO containers (id, service_id, service_spec) VALUES ($1, $2, $3)`,
|
||||||
|
resp.ID, req.ServiceId, string(specBytes)); err != nil {
|
||||||
|
removeContainer()
|
||||||
|
return nil, status.Errorf(codes.Internal, "store container in database: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &pb.CreateContainerResponse{Response: respBytes}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// InspectServiceContainer returns the container information and service specification that was used to create the
|
||||||
|
// container with the given ID.
|
||||||
|
func (s *Server) InspectServiceContainer(
|
||||||
|
ctx context.Context, req *pb.InspectContainerRequest,
|
||||||
|
) (*pb.ServiceContainer, error) {
|
||||||
|
ctr, err := s.client.ContainerInspect(ctx, req.Id)
|
||||||
|
if err != nil {
|
||||||
|
if client.IsErrNotFound(err) {
|
||||||
|
return nil, status.Errorf(codes.NotFound, err.Error())
|
||||||
|
}
|
||||||
|
return nil, status.Errorf(codes.Internal, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
ctrBytes, err := json.Marshal(ctr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, status.Errorf(codes.Internal, "marshal response: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
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{
|
||||||
|
Container: ctrBytes,
|
||||||
|
ServiceSpec: specBytes,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListServiceContainers returns all containers that belong to the service with the given name or ID.
|
||||||
|
// If req.ServiceId is empty, all service containers are returned.
|
||||||
|
func (s *Server) ListServiceContainers(
|
||||||
|
ctx context.Context, req *pb.ListServiceContainersRequest,
|
||||||
|
) (*pb.ListServiceContainersResponse, error) {
|
||||||
|
var opts container.ListOptions
|
||||||
|
if len(req.Options) > 0 {
|
||||||
|
if err := json.Unmarshal(req.Options, &opts); err != nil {
|
||||||
|
return nil, status.Errorf(codes.InvalidArgument, "unmarshal options: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle filters separately because they implement custom JSON unmarshalling.
|
||||||
|
var raw map[string]json.RawMessage
|
||||||
|
if err := json.Unmarshal(req.Options, &raw); err != nil {
|
||||||
|
return nil, status.Errorf(codes.InvalidArgument, "unmarshal options to raw map: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if filtersBytes, ok := raw["Filters"]; ok {
|
||||||
|
args, err := filters.FromJSON(string(filtersBytes))
|
||||||
|
if err != nil {
|
||||||
|
return nil, status.Errorf(codes.InvalidArgument, "unmarshal filters: %v", err)
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
if err != nil {
|
||||||
|
return nil, status.Error(codes.Internal, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
containers := make([]*pb.ServiceContainer, 0, len(containerSummaries))
|
||||||
|
for _, cs := range containerSummaries {
|
||||||
|
if req.ServiceId != "" &&
|
||||||
|
cs.Labels[api.LabelServiceID] != req.ServiceId && cs.Labels[api.LabelServiceName] != req.ServiceId {
|
||||||
|
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 {
|
||||||
|
return nil, status.Errorf(codes.Internal, "marshal container: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
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.
|
||||||
|
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{
|
||||||
|
Container: ctrBytes,
|
||||||
|
ServiceSpec: specBytes,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return &pb.ListServiceContainersResponse{
|
||||||
|
Messages: []*pb.MachineServiceContainers{
|
||||||
|
{
|
||||||
|
Containers: containers,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveServiceContainer stops (kills after grace period) and removes a service container with the given ID.
|
||||||
|
// The difference between this method and RemoveContainer is that it also removes the container from the machine
|
||||||
|
// database.
|
||||||
|
func (s *Server) RemoveServiceContainer(ctx context.Context, req *pb.RemoveContainerRequest) (*emptypb.Empty, error) {
|
||||||
|
ctrID := req.Id
|
||||||
|
// If the ID is not a full Docker ID, inspect the container to get its full ID.
|
||||||
|
if !fullDockerIDRegex.MatchString(req.Id) {
|
||||||
|
ctr, err := s.client.ContainerInspect(ctx, req.Id)
|
||||||
|
if err != nil {
|
||||||
|
if client.IsErrNotFound(err) {
|
||||||
|
return nil, status.Error(codes.NotFound, err.Error())
|
||||||
|
}
|
||||||
|
return nil, status.Error(codes.Internal, err.Error())
|
||||||
|
}
|
||||||
|
ctrID = ctr.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := s.RemoveContainer(ctx, req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err = s.db.ExecContext(ctx, `DELETE FROM containers WHERE id = $1`, ctrID); err != nil {
|
||||||
|
slog.Error("Failed to remove container from machine database.", "err", err, "id", ctrID)
|
||||||
|
// Do not return an error because the container has already been removed from the Docker daemon.
|
||||||
|
// The orphaned db record will be ignored and eventually cleaned up by the garbage collector.
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|||||||
+52
-34
@@ -5,32 +5,35 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/docker/docker/client"
|
|
||||||
"github.com/docker/go-connections/sockets"
|
|
||||||
"github.com/siderolabs/grpc-proxy/proxy"
|
|
||||||
"golang.org/x/sync/errgroup"
|
|
||||||
"google.golang.org/grpc"
|
|
||||||
"google.golang.org/grpc/codes"
|
|
||||||
"google.golang.org/grpc/status"
|
|
||||||
"google.golang.org/protobuf/types/known/emptypb"
|
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net"
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"os"
|
"os"
|
||||||
"os/user"
|
"os/user"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
"uncloud/internal/corrosion"
|
|
||||||
"uncloud/internal/docker"
|
"github.com/docker/docker/client"
|
||||||
"uncloud/internal/fs"
|
"github.com/docker/go-connections/sockets"
|
||||||
"uncloud/internal/machine/api/pb"
|
"github.com/psviderski/uncloud/internal/corrosion"
|
||||||
apiproxy "uncloud/internal/machine/api/proxy"
|
"github.com/psviderski/uncloud/internal/docker"
|
||||||
"uncloud/internal/machine/caddyfile"
|
"github.com/psviderski/uncloud/internal/fs"
|
||||||
"uncloud/internal/machine/cluster"
|
"github.com/psviderski/uncloud/internal/machine/api/pb"
|
||||||
"uncloud/internal/machine/corroservice"
|
apiproxy "github.com/psviderski/uncloud/internal/machine/api/proxy"
|
||||||
machinedocker "uncloud/internal/machine/docker"
|
"github.com/psviderski/uncloud/internal/machine/caddyfile"
|
||||||
"uncloud/internal/machine/network"
|
"github.com/psviderski/uncloud/internal/machine/cluster"
|
||||||
"uncloud/internal/machine/store"
|
"github.com/psviderski/uncloud/internal/machine/corroservice"
|
||||||
|
machinedocker "github.com/psviderski/uncloud/internal/machine/docker"
|
||||||
|
"github.com/psviderski/uncloud/internal/machine/network"
|
||||||
|
"github.com/psviderski/uncloud/internal/machine/store"
|
||||||
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
|
"github.com/siderolabs/grpc-proxy/proxy"
|
||||||
|
"golang.org/x/sync/errgroup"
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
"google.golang.org/grpc/codes"
|
||||||
|
"google.golang.org/grpc/status"
|
||||||
|
"google.golang.org/protobuf/types/known/emptypb"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -56,9 +59,9 @@ type Config struct {
|
|||||||
// DockerClient manages system and user containers using the local Docker daemon.
|
// DockerClient manages system and user containers using the local Docker daemon.
|
||||||
DockerClient *client.Client
|
DockerClient *client.Client
|
||||||
|
|
||||||
// CaddyfilePath specifies where the machine generates the Caddy reverse proxy configuration file for routing
|
// CaddyConfigPath specifies where the machine generates the Caddy reverse proxy configuration file for routing
|
||||||
// external traffic to service containers across the internal network. Default is DataDir/caddy/Caddyfile.
|
// external traffic to service containers across the internal network. Default is DataDir/caddy/caddy.json.
|
||||||
CaddyfilePath string
|
CaddyConfigPath string
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetDefaults returns a new Config with default values set where not provided.
|
// SetDefaults returns a new Config with default values set where not provided.
|
||||||
@@ -121,8 +124,8 @@ func (c *Config) SetDefaults() (*Config, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if cfg.CaddyfilePath == "" {
|
if cfg.CaddyConfigPath == "" {
|
||||||
cfg.CaddyfilePath = filepath.Join(cfg.DataDir, "caddy", "caddy.json")
|
cfg.CaddyConfigPath = filepath.Join(cfg.DataDir, "caddy", "caddy.json")
|
||||||
}
|
}
|
||||||
|
|
||||||
return &cfg, nil
|
return &cfg, nil
|
||||||
@@ -208,7 +211,13 @@ func NewMachine(config *Config) (*Machine, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("create Docker client: %w", err)
|
return nil, fmt.Errorf("create Docker client: %w", err)
|
||||||
}
|
}
|
||||||
dockerServer := machinedocker.NewServer(dockerCli)
|
|
||||||
|
dbFilePath := filepath.Join(config.DataDir, DBFileName)
|
||||||
|
db, err := NewDB(dbFilePath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("init machine database: %w", err)
|
||||||
|
}
|
||||||
|
dockerServer := machinedocker.NewServer(dockerCli, db)
|
||||||
|
|
||||||
// Init a local gRPC proxy server that proxies requests to the local or remote machine API servers.
|
// Init a local gRPC proxy server that proxies requests to the local or remote machine API servers.
|
||||||
proxyDirector := apiproxy.NewDirector(config.MachineSockPath, APIPort)
|
proxyDirector := apiproxy.NewDirector(config.MachineSockPath, APIPort)
|
||||||
@@ -357,7 +366,9 @@ func (m *Machine) Run(ctx context.Context) error {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
caddyfileCtrl, err := caddyfile.NewController(m.store, m.config.CaddyfilePath)
|
// Create a new Caddyfile controller for managing the Caddy reverse proxy configuration.
|
||||||
|
// It will also serve the current machine ID at /.uncloud-verify to verify Caddy reachability.
|
||||||
|
caddyfileCtrl, err := caddyfile.NewController(m.store, m.config.CaddyConfigPath, m.state.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("create Caddyfile controller: %w", err)
|
return fmt.Errorf("create Caddyfile controller: %w", err)
|
||||||
}
|
}
|
||||||
@@ -536,9 +547,9 @@ func (m *Machine) InitCluster(ctx context.Context, req *pb.InitClusterRequest) (
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, status.Errorf(codes.Internal, "list routable IPs: %v", err)
|
return nil, status.Errorf(codes.Internal, "list routable IPs: %v", err)
|
||||||
}
|
}
|
||||||
publicIP, err := network.GetPublicIP()
|
publicIP, pubIPErr := network.GetPublicIP()
|
||||||
// Ignore the error if failed to get the public IP using API services.
|
// Ignore the error if failed to get the public IP using API services.
|
||||||
if err == nil {
|
if pubIPErr == nil && !slices.Contains(ips, publicIP) {
|
||||||
ips = append(ips, publicIP)
|
ips = append(ips, publicIP)
|
||||||
}
|
}
|
||||||
endpoints := make([]*pb.IPPort, len(ips))
|
endpoints := make([]*pb.IPPort, len(ips))
|
||||||
@@ -556,6 +567,12 @@ func (m *Machine) InitCluster(ctx context.Context, req *pb.InitClusterRequest) (
|
|||||||
PublicKey: m.state.Network.PublicKey,
|
PublicKey: m.state.Network.PublicKey,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
if req.GetPublicIp() != nil {
|
||||||
|
addReq.PublicIp = req.GetPublicIp()
|
||||||
|
} else if req.GetPublicIpAuto() && pubIPErr == nil {
|
||||||
|
addReq.PublicIp = pb.NewIP(publicIP)
|
||||||
|
}
|
||||||
|
|
||||||
addResp, err := m.cluster.AddMachine(ctx, addReq)
|
addResp, err := m.cluster.AddMachine(ctx, addReq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, status.Errorf(codes.Internal, "add machine to cluster: %v", err)
|
return nil, status.Errorf(codes.Internal, "add machine to cluster: %v", err)
|
||||||
@@ -674,7 +691,7 @@ func (m *Machine) Token(_ context.Context, _ *emptypb.Empty) (*pb.TokenResponse,
|
|||||||
}
|
}
|
||||||
publicIP, err := network.GetPublicIP()
|
publicIP, err := network.GetPublicIP()
|
||||||
// Ignore the error if failed to get the public IP using API services.
|
// Ignore the error if failed to get the public IP using API services.
|
||||||
if err == nil {
|
if err == nil && !slices.Contains(ips, publicIP) {
|
||||||
ips = append(ips, publicIP)
|
ips = append(ips, publicIP)
|
||||||
}
|
}
|
||||||
endpoints := make([]netip.AddrPort, len(ips))
|
endpoints := make([]netip.AddrPort, len(ips))
|
||||||
@@ -682,7 +699,7 @@ func (m *Machine) Token(_ context.Context, _ *emptypb.Empty) (*pb.TokenResponse,
|
|||||||
endpoints[i] = netip.AddrPortFrom(ip, network.WireGuardPort)
|
endpoints[i] = netip.AddrPortFrom(ip, network.WireGuardPort)
|
||||||
}
|
}
|
||||||
|
|
||||||
token := NewToken(m.state.Network.PublicKey, endpoints)
|
token := NewToken(m.state.Network.PublicKey, publicIP, endpoints)
|
||||||
tokenStr, err := token.String()
|
tokenStr, err := token.String()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, status.Error(codes.Internal, err.Error())
|
return nil, status.Error(codes.Internal, err.Error())
|
||||||
@@ -703,7 +720,7 @@ func (m *Machine) Inspect(_ context.Context, _ *emptypb.Empty) (*pb.MachineInfo,
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// InspectService returns detailed information about a service and its containers.
|
// InspectService returns detailed information about a service and its containers stored in the cluster store.
|
||||||
func (m *Machine) InspectService(
|
func (m *Machine) InspectService(
|
||||||
ctx context.Context, req *pb.InspectServiceRequest,
|
ctx context.Context, req *pb.InspectServiceRequest,
|
||||||
) (*pb.InspectServiceResponse, error) {
|
) (*pb.InspectServiceResponse, error) {
|
||||||
@@ -735,10 +752,11 @@ func (m *Machine) InspectService(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ctr := api.ServiceContainer{Container: records[0].Container}
|
||||||
svc := &pb.Service{
|
svc := &pb.Service{
|
||||||
Id: records[0].Container.ServiceID(),
|
Id: ctr.ServiceID(),
|
||||||
Name: records[0].Container.ServiceName(),
|
Name: ctr.ServiceName(),
|
||||||
Mode: records[0].Container.ServiceMode(),
|
Mode: ctr.ServiceMode(),
|
||||||
Containers: containers,
|
Containers: containers,
|
||||||
}
|
}
|
||||||
return &pb.InspectServiceResponse{Service: svc}, nil
|
return &pb.InspectServiceResponse{Service: svc}, nil
|
||||||
|
|||||||
@@ -14,12 +14,12 @@ import (
|
|||||||
"slices"
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
"uncloud/internal/machine/api/pb"
|
"github.com/psviderski/uncloud/internal/machine/api/pb"
|
||||||
"uncloud/internal/machine/caddyfile"
|
"github.com/psviderski/uncloud/internal/machine/caddyfile"
|
||||||
"uncloud/internal/machine/corroservice"
|
"github.com/psviderski/uncloud/internal/machine/corroservice"
|
||||||
"uncloud/internal/machine/docker"
|
"github.com/psviderski/uncloud/internal/machine/docker"
|
||||||
"uncloud/internal/machine/network"
|
"github.com/psviderski/uncloud/internal/machine/network"
|
||||||
"uncloud/internal/machine/store"
|
"github.com/psviderski/uncloud/internal/machine/store"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|||||||
@@ -2,10 +2,10 @@ package network
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/psviderski/uncloud/internal/secret"
|
||||||
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
||||||
"net"
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"uncloud/internal/secret"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ package network
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/psviderski/uncloud/internal/secret"
|
||||||
"net"
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"uncloud/internal/secret"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// MachineIP returns the IP address of the machine which is the first address in the subnet.
|
// MachineIP returns the IP address of the machine which is the first address in the subnet.
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"time"
|
"time"
|
||||||
"uncloud/internal/secret"
|
"github.com/psviderski/uncloud/internal/secret"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"time"
|
"time"
|
||||||
"uncloud/internal/secret"
|
"github.com/psviderski/uncloud/internal/secret"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import (
|
|||||||
"slices"
|
"slices"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
"uncloud/internal/secret"
|
"github.com/psviderski/uncloud/internal/secret"
|
||||||
)
|
)
|
||||||
|
|
||||||
type WireGuardNetwork struct {
|
type WireGuardNetwork struct {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sync"
|
"sync"
|
||||||
"uncloud/internal/machine/network"
|
"github.com/psviderski/uncloud/internal/machine/network"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|||||||
@@ -5,10 +5,10 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
sq "github.com/Masterminds/squirrel"
|
sq "github.com/Masterminds/squirrel"
|
||||||
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
"uncloud/internal/api"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"google.golang.org/protobuf/encoding/protojson"
|
"google.golang.org/protobuf/encoding/protojson"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"uncloud/internal/corrosion"
|
"github.com/psviderski/uncloud/internal/corrosion"
|
||||||
"uncloud/internal/machine/api/pb"
|
"github.com/psviderski/uncloud/internal/machine/api/pb"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -49,6 +49,11 @@ func (s *Store) Put(ctx context.Context, key string, value any) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Store) Delete(ctx context.Context, key string) error {
|
||||||
|
_, err := s.corro.ExecContext(ctx, "DELETE FROM cluster WHERE key = ?", key)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Store) CreateMachine(ctx context.Context, m *pb.MachineInfo) error {
|
func (s *Store) CreateMachine(ctx context.Context, m *pb.MachineInfo) error {
|
||||||
mJSON, err := protojson.Marshal(m)
|
mJSON, err := protojson.Marshal(m)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -74,10 +79,13 @@ func (s *Store) ListMachines(ctx context.Context) ([]*pb.MachineInfo, error) {
|
|||||||
if err = rows.Scan(&mJSON); err != nil {
|
if err = rows.Scan(&mJSON); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protojsonParser := protojson.UnmarshalOptions{DiscardUnknown: true}
|
||||||
var m pb.MachineInfo
|
var m pb.MachineInfo
|
||||||
if err = protojson.Unmarshal([]byte(mJSON), &m); err != nil {
|
if err = protojsonParser.Unmarshal([]byte(mJSON), &m); err != nil {
|
||||||
return nil, fmt.Errorf("unmarshal machine info: %w", err)
|
return nil, fmt.Errorf("unmarshal machine info: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = m.Network.Validate(); err != nil {
|
if err = m.Network.Validate(); err != nil {
|
||||||
slog.Error("Invalid network configuration for machine in store", "id", m.Id, "err", err)
|
slog.Error("Invalid network configuration for machine in store", "id", m.Id, "err", err)
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"strings"
|
"strings"
|
||||||
"uncloud/internal/secret"
|
"github.com/psviderski/uncloud/internal/secret"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -16,13 +16,15 @@ const (
|
|||||||
// Token represents the machine's token for joining a cluster.
|
// Token represents the machine's token for joining a cluster.
|
||||||
type Token struct {
|
type Token struct {
|
||||||
PublicKey secret.Secret
|
PublicKey secret.Secret
|
||||||
|
PublicIP netip.Addr
|
||||||
Endpoints []netip.AddrPort
|
Endpoints []netip.AddrPort
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewToken creates a new machine token with the given public key and endpoints.
|
// NewToken creates a new machine token with the given public key and endpoints.
|
||||||
func NewToken(publicKey secret.Secret, endpoints []netip.AddrPort) Token {
|
func NewToken(publicKey secret.Secret, publicIP netip.Addr, endpoints []netip.AddrPort) Token {
|
||||||
return Token{
|
return Token{
|
||||||
PublicKey: publicKey,
|
PublicKey: publicKey,
|
||||||
|
PublicIP: publicIP,
|
||||||
Endpoints: endpoints,
|
Endpoints: endpoints,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,9 +12,9 @@ import (
|
|||||||
"google.golang.org/protobuf/types/known/emptypb"
|
"google.golang.org/protobuf/types/known/emptypb"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"time"
|
"time"
|
||||||
"uncloud/internal/machine"
|
"github.com/psviderski/uncloud/internal/machine"
|
||||||
"uncloud/internal/machine/api/pb"
|
"github.com/psviderski/uncloud/internal/machine/api/pb"
|
||||||
"uncloud/internal/machine/cluster"
|
"github.com/psviderski/uncloud/internal/machine/cluster"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -89,7 +89,7 @@ func (p *Provisioner) initCluster(ctx context.Context, machines []Machine) error
|
|||||||
// Init a new cluster on the first machine.
|
// Init a new cluster on the first machine.
|
||||||
initMachine := machines[0]
|
initMachine := machines[0]
|
||||||
|
|
||||||
if err := p.WaitMachineReady(ctx, initMachine, 5*time.Second); err != nil {
|
if err := p.WaitMachineReady(ctx, initMachine, 30*time.Second); err != nil {
|
||||||
return fmt.Errorf("wait for machine %q to be ready: %w", initMachine.Name, err)
|
return fmt.Errorf("wait for machine %q to be ready: %w", initMachine.Name, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ package ucind
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"uncloud/internal/cli/config"
|
"github.com/psviderski/uncloud/internal/cli/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ConfigUpdater struct {
|
type ConfigUpdater struct {
|
||||||
|
|||||||
@@ -9,14 +9,14 @@ import (
|
|||||||
"github.com/docker/docker/api/types/image"
|
"github.com/docker/docker/api/types/image"
|
||||||
dockerclient "github.com/docker/docker/client"
|
dockerclient "github.com/docker/docker/client"
|
||||||
"github.com/docker/go-connections/nat"
|
"github.com/docker/go-connections/nat"
|
||||||
|
"github.com/psviderski/uncloud/internal/secret"
|
||||||
|
"github.com/psviderski/uncloud/pkg/client"
|
||||||
|
"github.com/psviderski/uncloud/pkg/client/connector"
|
||||||
"google.golang.org/protobuf/types/known/emptypb"
|
"google.golang.org/protobuf/types/known/emptypb"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"time"
|
"time"
|
||||||
"uncloud/internal/cli/client"
|
|
||||||
"uncloud/internal/cli/client/connector"
|
|
||||||
"uncloud/internal/secret"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"slices"
|
||||||
|
|
||||||
|
"github.com/docker/docker/api/types/container"
|
||||||
|
"github.com/psviderski/uncloud/internal/machine/api/pb"
|
||||||
|
"google.golang.org/grpc/metadata"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Client interface {
|
||||||
|
ContainerClient
|
||||||
|
DNSClient
|
||||||
|
ImageClient
|
||||||
|
MachineClient
|
||||||
|
ServiceClient
|
||||||
|
}
|
||||||
|
|
||||||
|
type ContainerClient interface {
|
||||||
|
CreateContainer(
|
||||||
|
ctx context.Context, serviceID string, spec ServiceSpec, machineID string,
|
||||||
|
) (container.CreateResponse, error)
|
||||||
|
InspectContainer(ctx context.Context, serviceNameOrID, containerNameOrID string) (MachineServiceContainer, error)
|
||||||
|
RemoveContainer(ctx context.Context, serviceNameOrID, containerNameOrID string, opts container.RemoveOptions) error
|
||||||
|
StartContainer(ctx context.Context, serviceNameOrID, containerNameOrID string) error
|
||||||
|
StopContainer(ctx context.Context, serviceNameOrID, containerNameOrID string, opts container.StopOptions) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type DNSClient interface {
|
||||||
|
GetDomain(ctx context.Context) (string, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ImageClient interface {
|
||||||
|
InspectImage(ctx context.Context, id string) ([]MachineImage, error)
|
||||||
|
InspectRemoteImage(ctx context.Context, id string) ([]MachineRemoteImage, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type MachineClient interface {
|
||||||
|
InspectMachine(ctx context.Context, id string) (*pb.MachineMember, error)
|
||||||
|
ListMachines(ctx context.Context) ([]*pb.MachineMember, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ServiceClient interface {
|
||||||
|
InspectService(ctx context.Context, id string) (Service, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProxyMachinesContext returns a new context that proxies gRPC requests to the specified machines.
|
||||||
|
// If namesOrIDs is nil, all machines are included.
|
||||||
|
func ProxyMachinesContext(ctx context.Context, cli MachineClient, namesOrIDs []string) (context.Context, error) {
|
||||||
|
machines, err := cli.ListMachines(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("list machines: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
md := metadata.New(nil)
|
||||||
|
for _, m := range machines {
|
||||||
|
if namesOrIDs == nil ||
|
||||||
|
slices.Contains(namesOrIDs, m.Machine.Name) || slices.Contains(namesOrIDs, m.Machine.Id) {
|
||||||
|
machineIP, _ := m.Machine.Network.ManagementIp.ToAddr()
|
||||||
|
md.Append("machines", machineIP.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return metadata.NewOutgoingContext(ctx, md), nil
|
||||||
|
}
|
||||||
@@ -1,11 +1,13 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/docker/docker/api/types"
|
|
||||||
"github.com/docker/go-units"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/docker/docker/api/types"
|
||||||
|
"github.com/docker/go-units"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -20,69 +22,6 @@ type Container struct {
|
|||||||
types.ContainerJSON
|
types.ContainerJSON
|
||||||
}
|
}
|
||||||
|
|
||||||
// NameWithoutSlash returns the container name without the leading slash.
|
|
||||||
func (c *Container) NameWithoutSlash() string {
|
|
||||||
return c.Name[1:]
|
|
||||||
}
|
|
||||||
|
|
||||||
// ServiceID returns the ID of the service this container belongs to.
|
|
||||||
func (c *Container) ServiceID() string {
|
|
||||||
return c.Config.Labels[LabelServiceID]
|
|
||||||
}
|
|
||||||
|
|
||||||
// ServiceName returns the name of the service this container belongs to.
|
|
||||||
func (c *Container) ServiceName() string {
|
|
||||||
return c.Config.Labels[LabelServiceName]
|
|
||||||
}
|
|
||||||
|
|
||||||
// ServiceMode returns the replication mode of the service this container belongs to.
|
|
||||||
func (c *Container) ServiceMode() string {
|
|
||||||
return c.Config.Labels[LabelServiceMode]
|
|
||||||
}
|
|
||||||
|
|
||||||
// ServicePorts returns the ports this container publishes as part of its service.
|
|
||||||
func (c *Container) ServicePorts() ([]PortSpec, error) {
|
|
||||||
encoded, ok := c.Config.Labels[LabelServicePorts]
|
|
||||||
if !ok {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
if strings.TrimSpace(encoded) == "" {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
publishPorts := strings.Split(encoded, ",")
|
|
||||||
ports := make([]PortSpec, len(publishPorts))
|
|
||||||
for i, p := range publishPorts {
|
|
||||||
port, err := ParsePortSpec(strings.TrimSpace(p))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
ports[i] = port
|
|
||||||
}
|
|
||||||
|
|
||||||
return ports, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ServiceSpec constructs a service spec from the container's configuration.
|
|
||||||
func (c *Container) ServiceSpec() (ServiceSpec, error) {
|
|
||||||
ports, err := c.ServicePorts()
|
|
||||||
if err != nil {
|
|
||||||
return ServiceSpec{}, fmt.Errorf("get service ports: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return ServiceSpec{
|
|
||||||
Container: ContainerSpec{
|
|
||||||
Command: c.Config.Cmd,
|
|
||||||
Image: c.Config.Image,
|
|
||||||
Init: c.HostConfig.Init,
|
|
||||||
Volumes: c.HostConfig.Binds,
|
|
||||||
},
|
|
||||||
Mode: c.ServiceMode(),
|
|
||||||
Name: c.ServiceName(),
|
|
||||||
Ports: ports,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Healthy determines if the container is running and healthy.
|
// Healthy determines if the container is running and healthy.
|
||||||
// A running container with no health check configured is considered healthy.
|
// A running container with no health check configured is considered healthy.
|
||||||
func (c *Container) Healthy() bool {
|
func (c *Container) Healthy() bool {
|
||||||
@@ -151,8 +90,71 @@ func (c *Container) HumanState() (string, error) {
|
|||||||
c.State.ExitCode, units.HumanDuration(time.Now().UTC().Sub(finishedAt))), nil
|
c.State.ExitCode, units.HumanDuration(time.Now().UTC().Sub(finishedAt))), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Container) UnmarshalJSON(data []byte) error {
|
||||||
|
// A temporary type that's identical to Container but doesn't have the UnmarshalJSON method.
|
||||||
|
type ContainerAlias Container
|
||||||
|
|
||||||
|
var temp ContainerAlias
|
||||||
|
if err := json.Unmarshal(data, &temp); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
*c = Container(temp)
|
||||||
|
c.Name = strings.TrimPrefix(c.Name, "/")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type ServiceContainer struct {
|
||||||
|
Container
|
||||||
|
ServiceSpec ServiceSpec
|
||||||
|
}
|
||||||
|
|
||||||
|
type MachineContainer struct {
|
||||||
|
MachineID string
|
||||||
|
Container Container
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServiceID returns the ID of the service this container belongs to.
|
||||||
|
func (c *ServiceContainer) ServiceID() string {
|
||||||
|
return c.Config.Labels[LabelServiceID]
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServiceName returns the name of the service this container belongs to.
|
||||||
|
func (c *ServiceContainer) ServiceName() string {
|
||||||
|
return c.Config.Labels[LabelServiceName]
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServiceMode returns the replication mode of the service this container belongs to.
|
||||||
|
func (c *ServiceContainer) ServiceMode() string {
|
||||||
|
return c.Config.Labels[LabelServiceMode]
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServicePorts returns the ports this container publishes as part of its service.
|
||||||
|
func (c *ServiceContainer) ServicePorts() ([]PortSpec, error) {
|
||||||
|
encoded, ok := c.Config.Labels[LabelServicePorts]
|
||||||
|
if !ok {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(encoded) == "" {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
publishPorts := strings.Split(encoded, ",")
|
||||||
|
ports := make([]PortSpec, len(publishPorts))
|
||||||
|
for i, p := range publishPorts {
|
||||||
|
port, err := ParsePortSpec(strings.TrimSpace(p))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
ports[i] = port
|
||||||
|
}
|
||||||
|
|
||||||
|
return ports, nil
|
||||||
|
}
|
||||||
|
|
||||||
// ConflictingServicePorts returns a list of service ports that conflict with the given ports.
|
// ConflictingServicePorts returns a list of service ports that conflict with the given ports.
|
||||||
func (c *Container) ConflictingServicePorts(ports []PortSpec) ([]PortSpec, error) {
|
func (c *ServiceContainer) ConflictingServicePorts(ports []PortSpec) ([]PortSpec, error) {
|
||||||
svcPorts, err := c.ServicePorts()
|
svcPorts, err := c.ServicePorts()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("get service ports: %w", err)
|
return nil, fmt.Errorf("get service ports: %w", err)
|
||||||
@@ -1,60 +1,15 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"net/netip"
|
||||||
|
"testing"
|
||||||
|
|
||||||
"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/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"net/netip"
|
|
||||||
"reflect"
|
|
||||||
"testing"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestContainer_ServiceSpec(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
init := true
|
|
||||||
ctr := &Container{ContainerJSON: types.ContainerJSON{
|
|
||||||
ContainerJSONBase: &types.ContainerJSONBase{
|
|
||||||
HostConfig: &container.HostConfig{
|
|
||||||
Binds: []string{"/host/path:/container/path"},
|
|
||||||
Init: &init,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Config: &container.Config{
|
|
||||||
Cmd: []string{"/app/server"},
|
|
||||||
Image: "app:latest",
|
|
||||||
Labels: map[string]string{
|
|
||||||
LabelServiceID: "test-service-id",
|
|
||||||
LabelServiceName: "test-service-name",
|
|
||||||
LabelServicePorts: "app.example.com:8000/https",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
|
|
||||||
expectedSpec := ServiceSpec{
|
|
||||||
Container: ContainerSpec{
|
|
||||||
Command: []string{"/app/server"},
|
|
||||||
Image: "app:latest",
|
|
||||||
Init: &init,
|
|
||||||
Volumes: []string{"/host/path:/container/path"},
|
|
||||||
},
|
|
||||||
Name: "test-service-name",
|
|
||||||
Ports: []PortSpec{
|
|
||||||
{
|
|
||||||
Hostname: "app.example.com",
|
|
||||||
ContainerPort: 8000,
|
|
||||||
Protocol: ProtocolHTTPS,
|
|
||||||
Mode: PortModeIngress,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
spec, err := ctr.ServiceSpec()
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.True(t, reflect.DeepEqual(spec, expectedSpec))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestContainer_Healthy(t *testing.T) {
|
func TestContainer_Healthy(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
@@ -325,13 +280,13 @@ func TestContainer_ConflictingServicePorts(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) {
|
||||||
ctr := &Container{ContainerJSON: types.ContainerJSON{
|
ctr := &ServiceContainer{Container: Container{ContainerJSON: types.ContainerJSON{
|
||||||
Config: &container.Config{
|
Config: &container.Config{
|
||||||
Labels: map[string]string{
|
Labels: map[string]string{
|
||||||
LabelServicePorts: tt.containerPorts,
|
LabelServicePorts: tt.containerPorts,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}}
|
}}}
|
||||||
|
|
||||||
got, err := ctr.ConflictingServicePorts(tt.checkPorts)
|
got, err := ctr.ConflictingServicePorts(tt.checkPorts)
|
||||||
if tt.wantErr {
|
if tt.wantErr {
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
var ErrNotFound = errors.New("not found")
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/distribution/reference"
|
||||||
|
"github.com/docker/docker/api/types"
|
||||||
|
v1 "github.com/opencontainers/image-spec/specs-go/v1"
|
||||||
|
"github.com/psviderski/uncloud/internal/machine/api/pb"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MachineImage struct {
|
||||||
|
Metadata *pb.Metadata
|
||||||
|
Image types.ImageInspect
|
||||||
|
}
|
||||||
|
|
||||||
|
// MachineRemoteImage represents an image in a remote registry fetched by a particular machine.
|
||||||
|
type MachineRemoteImage struct {
|
||||||
|
Metadata *pb.Metadata
|
||||||
|
Image RemoteImage
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoteImage represents an image in a remote registry. The canonical reference includes the image digest.
|
||||||
|
// Either IndexManifest or ImageManifest must be set depending on whether the reference points to an index
|
||||||
|
// (multi-platform image) or a single-platform image.
|
||||||
|
type RemoteImage struct {
|
||||||
|
Reference reference.Canonical
|
||||||
|
IndexManifest *v1.Index
|
||||||
|
ImageManifest *v1.Manifest
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ package api
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
@@ -62,9 +63,6 @@ func (p *PortSpec) Validate() error {
|
|||||||
return fmt.Errorf("invalid hostname '%s': %w", p.Hostname, err)
|
return fmt.Errorf("invalid hostname '%s': %w", p.Hostname, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if p.Hostname == "" && (p.Protocol == ProtocolHTTP || p.Protocol == ProtocolHTTPS) {
|
|
||||||
return fmt.Errorf("hostname is required with '%s' or '%s' protocols", ProtocolHTTP, ProtocolHTTPS)
|
|
||||||
}
|
|
||||||
case PortModeHost:
|
case PortModeHost:
|
||||||
if p.PublishedPort == 0 {
|
if p.PublishedPort == 0 {
|
||||||
return fmt.Errorf("published port is required in %s mode", PortModeHost)
|
return fmt.Errorf("published port is required in %s mode", PortModeHost)
|
||||||
@@ -216,9 +214,7 @@ func ParsePortSpec(port string) (PortSpec, error) {
|
|||||||
return spec, fmt.Errorf("invalid host IP '%s': %w", parts[0], err)
|
return spec, fmt.Errorf("invalid host IP '%s': %w", parts[0], err)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if parts[0] == "" {
|
// Hostname may be empty.
|
||||||
return spec, fmt.Errorf("hostname must not be empty")
|
|
||||||
}
|
|
||||||
spec.Hostname = parts[0]
|
spec.Hostname = parts[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -266,3 +262,31 @@ func validateHostname(hostname string) error {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PortsEqual returns true if the two port sets are equal. The order of the ports is not important.
|
||||||
|
func PortsEqual(a, b []PortSpec) bool {
|
||||||
|
if len(a) != len(b) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
aSerialised := make([]string, len(a))
|
||||||
|
bSerialised := make([]string, len(b))
|
||||||
|
|
||||||
|
for i := range a {
|
||||||
|
aSerialised[i], err = a[i].String()
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
bSerialised[i], err = b[i].String()
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
slices.Sort(aSerialised)
|
||||||
|
slices.Sort(bSerialised)
|
||||||
|
|
||||||
|
return slices.Equal(aSerialised, bSerialised)
|
||||||
|
}
|
||||||
@@ -50,6 +50,22 @@ func TestPortSpec_Validate(t *testing.T) {
|
|||||||
Mode: PortModeIngress,
|
Mode: PortModeIngress,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "ingress mode without hostname http",
|
||||||
|
spec: PortSpec{
|
||||||
|
ContainerPort: 8080,
|
||||||
|
Protocol: ProtocolHTTP,
|
||||||
|
Mode: PortModeIngress,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ingress mode without hostname https",
|
||||||
|
spec: PortSpec{
|
||||||
|
ContainerPort: 8080,
|
||||||
|
Protocol: ProtocolHTTPS,
|
||||||
|
Mode: PortModeIngress,
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "ingress mode with hostname and http",
|
name: "ingress mode with hostname and http",
|
||||||
spec: PortSpec{
|
spec: PortSpec{
|
||||||
@@ -164,24 +180,6 @@ func TestPortSpec_Validate(t *testing.T) {
|
|||||||
},
|
},
|
||||||
wantErr: "invalid hostname 'app': must be a valid domain name containing at least one dot",
|
wantErr: "invalid hostname 'app': must be a valid domain name containing at least one dot",
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: "missing hostname with http",
|
|
||||||
spec: PortSpec{
|
|
||||||
ContainerPort: 8080,
|
|
||||||
Protocol: ProtocolHTTP,
|
|
||||||
Mode: PortModeIngress,
|
|
||||||
},
|
|
||||||
wantErr: "hostname is required with 'http' or 'https' protocols",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "missing hostname with https",
|
|
||||||
spec: PortSpec{
|
|
||||||
ContainerPort: 8080,
|
|
||||||
Protocol: ProtocolHTTPS,
|
|
||||||
Mode: PortModeIngress,
|
|
||||||
},
|
|
||||||
wantErr: "hostname is required with 'http' or 'https' protocols",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: "host IP in ingress mode",
|
name: "host IP in ingress mode",
|
||||||
spec: PortSpec{
|
spec: PortSpec{
|
||||||
@@ -468,6 +466,15 @@ func TestParsePortSpec(t *testing.T) {
|
|||||||
Mode: PortModeIngress,
|
Mode: PortModeIngress,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "container port http without hostname",
|
||||||
|
port: "8080/http",
|
||||||
|
expected: PortSpec{
|
||||||
|
ContainerPort: 8080,
|
||||||
|
Protocol: ProtocolHTTP,
|
||||||
|
Mode: PortModeIngress,
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "hostname and container port http",
|
name: "hostname and container port http",
|
||||||
port: "app.example.com:8080/http",
|
port: "app.example.com:8080/http",
|
||||||
@@ -602,21 +609,6 @@ func TestParsePortSpec(t *testing.T) {
|
|||||||
port: "app.example.com:invalid:8080",
|
port: "app.example.com:invalid:8080",
|
||||||
wantErr: "invalid published port",
|
wantErr: "invalid published port",
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: "missing hostname with http",
|
|
||||||
port: "8080/http",
|
|
||||||
wantErr: "hostname is required",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "missing hostname with http",
|
|
||||||
port: "8080/https",
|
|
||||||
wantErr: "hostname is required",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "missing hostname with published https port",
|
|
||||||
port: "8000:8080/https",
|
|
||||||
wantErr: "hostname is required",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: "invalid hostname",
|
name: "invalid hostname",
|
||||||
port: "app:8080/http",
|
port: "app:8080/http",
|
||||||
@@ -0,0 +1,252 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"maps"
|
||||||
|
"reflect"
|
||||||
|
"regexp"
|
||||||
|
"slices"
|
||||||
|
|
||||||
|
"github.com/distribution/reference"
|
||||||
|
"github.com/psviderski/uncloud/internal/machine/api/pb"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
ServiceModeReplicated = "replicated"
|
||||||
|
ServiceModeGlobal = "global"
|
||||||
|
|
||||||
|
// PullPolicyAlways means the image is always pulled from the registry.
|
||||||
|
PullPolicyAlways = "always"
|
||||||
|
// PullPolicyMissing means the image is pulled from the registry only if it's not available on the machine where
|
||||||
|
// a container is started. This is the default pull policy.
|
||||||
|
// TODO: make each machine aware of the images on other machines and it possible to pull from them.
|
||||||
|
// Pull from the registry only if the image is missing on all machines.
|
||||||
|
PullPolicyMissing = "missing"
|
||||||
|
// PullPolicyNever means the image is never pulled from the registry. A service with this pull policy can only be
|
||||||
|
// deployed to machines where the image is already available.
|
||||||
|
// TODO: see the TODO above for PullPolicyMissing. Pull from other machines in the cluster if available.
|
||||||
|
PullPolicyNever = "never"
|
||||||
|
)
|
||||||
|
|
||||||
|
var serviceIDRegexp = regexp.MustCompile("^[0-9a-f]{32}$")
|
||||||
|
|
||||||
|
func ValidateServiceID(id string) bool {
|
||||||
|
return serviceIDRegexp.MatchString(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServiceSpec defines the desired state of a service.
|
||||||
|
// ATTENTION: after changing this struct, verify if deploy.EvalContainerSpecChange needs to be updated.
|
||||||
|
type ServiceSpec struct {
|
||||||
|
Container ContainerSpec
|
||||||
|
// Mode is the replication mode of the service. Default is ServiceModeReplicated if empty.
|
||||||
|
Mode string
|
||||||
|
Name string
|
||||||
|
// Ports defines what service ports to publish to make the service accessible outside the cluster.
|
||||||
|
Ports []PortSpec
|
||||||
|
// Replicas is the number of containers to run for the service. Only valid for a replicated service.
|
||||||
|
Replicas uint `json:",omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ServiceSpec) SetDefaults() ServiceSpec {
|
||||||
|
spec := s.Clone()
|
||||||
|
|
||||||
|
if spec.Mode == "" {
|
||||||
|
spec.Mode = ServiceModeReplicated
|
||||||
|
}
|
||||||
|
// Ensure the replicated service has at least one replica.
|
||||||
|
if spec.Mode == ServiceModeReplicated && spec.Replicas == 0 {
|
||||||
|
spec.Replicas = 1
|
||||||
|
}
|
||||||
|
spec.Container = spec.Container.SetDefaults()
|
||||||
|
|
||||||
|
return spec
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ServiceSpec) Validate() error {
|
||||||
|
if err := s.Container.Validate(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch s.Mode {
|
||||||
|
case "", ServiceModeGlobal, ServiceModeReplicated:
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("invalid mode: %q", s.Mode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: validate the service name is a valid DNS label.
|
||||||
|
|
||||||
|
for _, p := range s.Ports {
|
||||||
|
if (p.Mode == "" || p.Mode == PortModeIngress) &&
|
||||||
|
p.Protocol != ProtocolHTTP && p.Protocol != ProtocolHTTPS {
|
||||||
|
return fmt.Errorf("unsupported protocol for ingress port %d: %s", p.ContainerPort, p.Protocol)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: validate there is no conflict between ports.
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ServiceSpec) Clone() ServiceSpec {
|
||||||
|
spec := *s
|
||||||
|
|
||||||
|
if s.Ports != nil {
|
||||||
|
spec.Ports = make([]PortSpec, len(s.Ports))
|
||||||
|
copy(spec.Ports, s.Ports)
|
||||||
|
}
|
||||||
|
spec.Container = s.Container.Clone()
|
||||||
|
|
||||||
|
return spec
|
||||||
|
}
|
||||||
|
|
||||||
|
// ContainerSpec defines the desired state of a container in a service.
|
||||||
|
// ATTENTION: after changing this struct, verify if deploy.EvalContainerSpecChange needs to be updated.
|
||||||
|
type ContainerSpec struct {
|
||||||
|
// Command overrides the default CMD of the image to be executed when running a container.
|
||||||
|
Command []string
|
||||||
|
// Entrypoint overrides the default ENTRYPOINT of the image.
|
||||||
|
Entrypoint []string
|
||||||
|
Image string
|
||||||
|
// Run a custom init inside the container. If nil, use the daemon's configured settings.
|
||||||
|
Init *bool
|
||||||
|
// PullPolicy determines when to pull the image from the registry or use the image already available in the cluster.
|
||||||
|
// Default is PullPolicyMissing if empty.
|
||||||
|
PullPolicy string
|
||||||
|
// List of volumes to bind mount into the container.
|
||||||
|
Volumes []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetDefaults returns a copy of the container spec with default values set.
|
||||||
|
func (s *ContainerSpec) SetDefaults() ContainerSpec {
|
||||||
|
spec := s.Clone()
|
||||||
|
if spec.PullPolicy == "" {
|
||||||
|
spec.PullPolicy = PullPolicyMissing
|
||||||
|
}
|
||||||
|
|
||||||
|
return spec
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ContainerSpec) Validate() error {
|
||||||
|
if _, err := reference.ParseDockerRef(s.Image); err != nil {
|
||||||
|
return fmt.Errorf("invalid image: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ContainerSpec) Equals(spec ContainerSpec) bool {
|
||||||
|
orig := s.SetDefaults()
|
||||||
|
spec = spec.SetDefaults()
|
||||||
|
|
||||||
|
slices.Sort(orig.Volumes)
|
||||||
|
slices.Sort(spec.Volumes)
|
||||||
|
|
||||||
|
return reflect.DeepEqual(orig, spec)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ContainerSpec) Clone() ContainerSpec {
|
||||||
|
spec := *s
|
||||||
|
|
||||||
|
if s.Command != nil {
|
||||||
|
spec.Command = make([]string, len(s.Command))
|
||||||
|
copy(spec.Command, s.Command)
|
||||||
|
}
|
||||||
|
if s.Entrypoint != nil {
|
||||||
|
spec.Entrypoint = make([]string, len(s.Entrypoint))
|
||||||
|
copy(spec.Entrypoint, s.Entrypoint)
|
||||||
|
}
|
||||||
|
if s.Volumes != nil {
|
||||||
|
spec.Volumes = make([]string, len(s.Volumes))
|
||||||
|
copy(spec.Volumes, s.Volumes)
|
||||||
|
}
|
||||||
|
|
||||||
|
return spec
|
||||||
|
}
|
||||||
|
|
||||||
|
type Service struct {
|
||||||
|
ID string
|
||||||
|
Name string
|
||||||
|
Mode string
|
||||||
|
Containers []MachineServiceContainer
|
||||||
|
}
|
||||||
|
|
||||||
|
type MachineServiceContainer struct {
|
||||||
|
MachineID string
|
||||||
|
Container ServiceContainer
|
||||||
|
}
|
||||||
|
|
||||||
|
// Endpoints returns the exposed HTTP and HTTPS endpoints of the service.
|
||||||
|
func (s *Service) Endpoints() []string {
|
||||||
|
endpoints := make(map[string]struct{})
|
||||||
|
|
||||||
|
// Container specs may differ between containers in the same service, e.g. during a rolling update,
|
||||||
|
// so we need to collect all unique endpoints.
|
||||||
|
for _, ctr := range s.Containers {
|
||||||
|
ports, err := ctr.Container.ServicePorts()
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, port := range ports {
|
||||||
|
protocol := ""
|
||||||
|
switch port.Protocol {
|
||||||
|
case ProtocolHTTP:
|
||||||
|
protocol = "http"
|
||||||
|
case ProtocolHTTPS:
|
||||||
|
protocol = "https"
|
||||||
|
default:
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if port.Hostname == "" {
|
||||||
|
// There shouldn't be http(s) ports without a hostname but just in case ignore them.
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
endpoint := fmt.Sprintf("%s://%s", protocol, port.Hostname)
|
||||||
|
if port.PublishedPort != 0 {
|
||||||
|
// For non-standard ports (80/443), include the port in the URL.
|
||||||
|
if !(port.Protocol == ProtocolHTTP && port.PublishedPort == 80) &&
|
||||||
|
!(port.Protocol == ProtocolHTTPS && port.PublishedPort == 443) {
|
||||||
|
endpoint += fmt.Sprintf(":%d", port.PublishedPort)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
endpoint += fmt.Sprintf(" → :%d", port.ContainerPort)
|
||||||
|
endpoints[endpoint] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return slices.Sorted(maps.Keys(endpoints))
|
||||||
|
}
|
||||||
|
|
||||||
|
func ServiceFromProto(s *pb.Service) (Service, error) {
|
||||||
|
var err error
|
||||||
|
containers := make([]MachineServiceContainer, len(s.Containers))
|
||||||
|
for i, sc := range s.Containers {
|
||||||
|
containers[i], err = machineContainerFromProto(sc)
|
||||||
|
if err != nil {
|
||||||
|
return Service{}, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Service{
|
||||||
|
ID: s.Id,
|
||||||
|
Name: s.Name,
|
||||||
|
Mode: s.Mode,
|
||||||
|
Containers: containers,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func machineContainerFromProto(sc *pb.Service_Container) (MachineServiceContainer, error) {
|
||||||
|
var c Container
|
||||||
|
if err := json.Unmarshal(sc.Container, &c); err != nil {
|
||||||
|
return MachineServiceContainer{}, fmt.Errorf("unmarshal container: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return MachineServiceContainer{
|
||||||
|
MachineID: sc.MachineId,
|
||||||
|
Container: ServiceContainer{Container: c},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/Masterminds/semver"
|
||||||
|
"github.com/distribution/reference"
|
||||||
|
"github.com/google/go-containerregistry/pkg/name"
|
||||||
|
"github.com/google/go-containerregistry/pkg/v1/remote"
|
||||||
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
|
"github.com/psviderski/uncloud/pkg/client/deploy"
|
||||||
|
"regexp"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
CaddyServiceName = "caddy"
|
||||||
|
// CaddyImage is the official Caddy Docker image on Docker Hub: https://hub.docker.com/_/caddy
|
||||||
|
CaddyImage = "caddy"
|
||||||
|
)
|
||||||
|
|
||||||
|
var caddyImageTagRegex = regexp.MustCompile(`^2\.\d+\.\d+$`)
|
||||||
|
|
||||||
|
// NewCaddyDeployment creates a new deployment for a Caddy reverse proxy service.
|
||||||
|
// The service is deployed in global mode to all machines in the cluster. If the image is not provided, the latest
|
||||||
|
// version of the official Caddy Docker image is used.
|
||||||
|
func (cli *Client) NewCaddyDeployment(image string, filter deploy.MachineFilter) (*deploy.Deployment, error) {
|
||||||
|
latest, err := LatestCaddyImage()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("look up latest Caddy image: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if image == "" {
|
||||||
|
image = reference.FamiliarString(latest)
|
||||||
|
}
|
||||||
|
|
||||||
|
spec := api.ServiceSpec{
|
||||||
|
Container: api.ContainerSpec{
|
||||||
|
Command: []string{"caddy", "run", "-c", "/config/caddy.json", "--watch"},
|
||||||
|
Image: image,
|
||||||
|
Volumes: []string{"/var/lib/uncloud/caddy:/config"},
|
||||||
|
},
|
||||||
|
Mode: api.ServiceModeGlobal,
|
||||||
|
Name: CaddyServiceName,
|
||||||
|
Ports: []api.PortSpec{
|
||||||
|
{
|
||||||
|
PublishedPort: 80,
|
||||||
|
ContainerPort: 80,
|
||||||
|
Protocol: api.ProtocolTCP,
|
||||||
|
Mode: api.PortModeHost,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
PublishedPort: 443,
|
||||||
|
ContainerPort: 443,
|
||||||
|
Protocol: api.ProtocolTCP,
|
||||||
|
Mode: api.PortModeHost,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return cli.NewDeployment(spec, &deploy.RollingStrategy{MachineFilter: filter}), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// LatestCaddyImage returns the latest image of the official Caddy Docker image on Docker Hub.
|
||||||
|
// The latest image is determined by the latest version tag 2.x.x.
|
||||||
|
func LatestCaddyImage() (reference.NamedTagged, error) {
|
||||||
|
repo, err := name.NewRepository(CaddyImage)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("parse image: %w", err)
|
||||||
|
}
|
||||||
|
tags, err := remote.List(repo)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("list image tags: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default to the 'latest' tag but try to find the latest version tag 2.x.x.
|
||||||
|
latestTag := "latest"
|
||||||
|
var latestVersion *semver.Version
|
||||||
|
for _, t := range tags {
|
||||||
|
if !caddyImageTagRegex.MatchString(t) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
v, err := semver.NewVersion(t)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if latestVersion == nil || v.GreaterThan(latestVersion) {
|
||||||
|
latestVersion = v
|
||||||
|
latestTag = t
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
image, err := reference.ParseDockerRef(CaddyImage)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("parse image: %w", err)
|
||||||
|
}
|
||||||
|
imageWithTag, err := reference.WithTag(image, latestTag)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("set image tag: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return imageWithTag, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/distribution/reference"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestLatestCaddyImage(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
image, err := LatestCaddyImage()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Regexp(t, `^caddy:2\.\d+\.\d+$`, reference.FamiliarString(image))
|
||||||
|
}
|
||||||
@@ -4,16 +4,16 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
"github.com/docker/cli/cli/streams"
|
"github.com/docker/cli/cli/streams"
|
||||||
|
"github.com/psviderski/uncloud/internal/machine/api/pb"
|
||||||
|
"github.com/psviderski/uncloud/internal/machine/docker"
|
||||||
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
"google.golang.org/grpc/metadata"
|
"google.golang.org/grpc/metadata"
|
||||||
"os"
|
|
||||||
"uncloud/internal/machine/api/pb"
|
|
||||||
"uncloud/internal/machine/docker"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var ErrNotFound = errors.New("not found")
|
|
||||||
|
|
||||||
// Client is a client for the machine API.
|
// Client is a client for the machine API.
|
||||||
type Client struct {
|
type Client struct {
|
||||||
connector Connector
|
connector Connector
|
||||||
@@ -26,6 +26,8 @@ type Client struct {
|
|||||||
Docker *docker.Client
|
Docker *docker.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var _ api.Client = (*Client)(nil)
|
||||||
|
|
||||||
// Connector is an interface for establishing a connection to the machine API.
|
// Connector is an interface for establishing a connection to the machine API.
|
||||||
type Connector interface {
|
type Connector interface {
|
||||||
Connect(ctx context.Context) (*grpc.ClientConn, error)
|
Connect(ctx context.Context) (*grpc.ClientConn, error)
|
||||||
@@ -2,8 +2,9 @@ package client
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"github.com/psviderski/uncloud/internal/machine/api/pb"
|
||||||
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
"google.golang.org/protobuf/types/known/emptypb"
|
"google.golang.org/protobuf/types/known/emptypb"
|
||||||
"uncloud/internal/machine/api/pb"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func (cli *Client) InspectMachine(ctx context.Context, id string) (*pb.MachineMember, error) {
|
func (cli *Client) InspectMachine(ctx context.Context, id string) (*pb.MachineMember, error) {
|
||||||
@@ -18,7 +19,7 @@ func (cli *Client) InspectMachine(ctx context.Context, id string) (*pb.MachineMe
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, ErrNotFound
|
return nil, api.ErrNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cli *Client) ListMachines(ctx context.Context) ([]*pb.MachineMember, error) {
|
func (cli *Client) ListMachines(ctx context.Context) ([]*pb.MachineMember, error) {
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
package compose
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"github.com/compose-spec/compose-go/v2/graph"
|
||||||
|
"github.com/compose-spec/compose-go/v2/types"
|
||||||
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
|
"github.com/psviderski/uncloud/pkg/client/deploy"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Client interface {
|
||||||
|
api.DNSClient
|
||||||
|
deploy.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
type Deployment struct {
|
||||||
|
Client Client
|
||||||
|
Project *types.Project
|
||||||
|
SpecResolver *deploy.ServiceSpecResolver
|
||||||
|
plan *deploy.SequenceOperation
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDeployment(ctx context.Context, cli Client, project *types.Project) (*Deployment, error) {
|
||||||
|
domain, err := cli.GetDomain(ctx)
|
||||||
|
if err != nil && !errors.Is(err, api.ErrNotFound) {
|
||||||
|
return nil, fmt.Errorf("get cluster domain: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resolver := &deploy.ServiceSpecResolver{
|
||||||
|
// If the domain is not found (not reserved), an empty domain is used for the resolver.
|
||||||
|
ClusterDomain: domain,
|
||||||
|
// TODO: provide an image resolver.
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Deployment{
|
||||||
|
Client: cli,
|
||||||
|
Project: project,
|
||||||
|
SpecResolver: resolver,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Deployment) Plan(ctx context.Context) (deploy.SequenceOperation, error) {
|
||||||
|
if d.plan != nil {
|
||||||
|
return *d.plan, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
plan := deploy.SequenceOperation{}
|
||||||
|
err := graph.InDependencyOrder(ctx, d.Project,
|
||||||
|
func(ctx context.Context, name string, _ types.ServiceConfig) error {
|
||||||
|
spec, err := d.ServiceSpec(name)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("convert compose service '%s' to service spec: %w", name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: properly handle depends_on conditions in the service deployment plan as the first operation.
|
||||||
|
deployment := deploy.NewDeployment(d.Client, spec, nil)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("create deployment for service '%s': %w", name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
servicePlan, err := deployment.Plan(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("create deployment plan for service '%s': %w", name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip no-op (up-to-date) service plans.
|
||||||
|
if len(servicePlan.Operations) > 0 {
|
||||||
|
plan.Operations = append(plan.Operations, &servicePlan)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
d.plan = &plan
|
||||||
|
}
|
||||||
|
|
||||||
|
return plan, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServiceSpec returns the service specification for the given compose service that is ready for deployment.
|
||||||
|
func (d *Deployment) ServiceSpec(name string) (api.ServiceSpec, error) {
|
||||||
|
service, err := d.Project.GetService(name)
|
||||||
|
if err != nil {
|
||||||
|
return api.ServiceSpec{}, fmt.Errorf("get config for compose service '%s': %w", name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
spec, err := ServiceSpecFromCompose(name, service)
|
||||||
|
if err != nil {
|
||||||
|
return spec, fmt.Errorf("convert compose service '%s' to service spec: %w", name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: resolve the image to a digest and supported platforms using an image resolver that broadcasts requests
|
||||||
|
// to all machines in the cluster. If service.PullPolicy is "missing":
|
||||||
|
// - Broadcast request if any machine contains a particular image and resolve it to image@digest.
|
||||||
|
// - If not found, broadcast request to resolve an image using a registry, and resolve it to image@digest.
|
||||||
|
// TODO: configure placement filter based on the supported platforms of the image.
|
||||||
|
|
||||||
|
// TODO: maybe instantiate ImageResolver here based on PullPolicy of each service?
|
||||||
|
|
||||||
|
return spec, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Deployment) Run(ctx context.Context) error {
|
||||||
|
plan, err := d.Plan(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("create plan: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return plan.Execute(ctx, d.Client)
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package compose
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/compose-spec/compose-go/v2/types"
|
||||||
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
const PortsExtensionKey = "x-ports"
|
||||||
|
|
||||||
|
type PortsSource []string
|
||||||
|
|
||||||
|
// TransformServicesPortsExtension transforms the ports extension of all services in the project by replacing a string
|
||||||
|
// representation of each port with a parsed PortSpec.
|
||||||
|
func transformServicesPortsExtension(project *types.Project) (*types.Project, error) {
|
||||||
|
return project.WithServicesTransform(func(name string, service types.ServiceConfig) (types.ServiceConfig, error) {
|
||||||
|
ports, ok := service.Extensions[PortsExtensionKey].(PortsSource)
|
||||||
|
if !ok {
|
||||||
|
return service, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
specs, err := transformPortsExtension(ports)
|
||||||
|
if err != nil {
|
||||||
|
return service, err
|
||||||
|
}
|
||||||
|
|
||||||
|
service.Extensions[PortsExtensionKey] = specs
|
||||||
|
return service, nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func transformPortsExtension(ports PortsSource) ([]api.PortSpec, error) {
|
||||||
|
var specs []api.PortSpec
|
||||||
|
for _, port := range ports {
|
||||||
|
spec, err := api.ParsePortSpec(port)
|
||||||
|
if err != nil {
|
||||||
|
return specs, fmt.Errorf("parse port %q: %w", port, err)
|
||||||
|
}
|
||||||
|
specs = append(specs, spec)
|
||||||
|
}
|
||||||
|
|
||||||
|
return specs, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package compose
|
||||||
|
|
||||||
|
// TODO: make compose, cli, and api packages public.
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
composecli "github.com/compose-spec/compose-go/v2/cli"
|
||||||
|
"github.com/compose-spec/compose-go/v2/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
func LoadProject(ctx context.Context, paths []string) (*types.Project, error) {
|
||||||
|
options, err := composecli.NewProjectOptions(
|
||||||
|
paths,
|
||||||
|
// First apply os.Environment, always wins.
|
||||||
|
composecli.WithOsEnv,
|
||||||
|
// Read dot env file to populate project environment.
|
||||||
|
composecli.WithDotEnv,
|
||||||
|
// Get compose file path set by COMPOSE_FILE.
|
||||||
|
composecli.WithConfigFileEnv,
|
||||||
|
// If none was selected, get default compose-ports-long.yaml file from current dir or parent folders.
|
||||||
|
composecli.WithDefaultConfigPath,
|
||||||
|
composecli.WithExtension(PortsExtensionKey, PortsSource{}),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("create compose parser options: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
project, err := options.LoadProject(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if project, err = transformServicesPortsExtension(project); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return project, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
package compose
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/compose-spec/compose-go/v2/types"
|
||||||
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ServiceSpecFromCompose(name string, service types.ServiceConfig) (api.ServiceSpec, error) {
|
||||||
|
pullPolicy := ""
|
||||||
|
switch service.PullPolicy {
|
||||||
|
case types.PullPolicyAlways:
|
||||||
|
pullPolicy = api.PullPolicyAlways
|
||||||
|
case "", types.PullPolicyMissing, types.PullPolicyIfNotPresent:
|
||||||
|
pullPolicy = api.PullPolicyMissing
|
||||||
|
case types.PullPolicyNever:
|
||||||
|
pullPolicy = api.PullPolicyNever
|
||||||
|
default:
|
||||||
|
return api.ServiceSpec{}, fmt.Errorf("unsupported pull policy: '%s'", service.PullPolicy)
|
||||||
|
}
|
||||||
|
|
||||||
|
spec := api.ServiceSpec{
|
||||||
|
Container: api.ContainerSpec{
|
||||||
|
Command: service.Command,
|
||||||
|
Image: service.Image,
|
||||||
|
Init: service.Init,
|
||||||
|
PullPolicy: pullPolicy,
|
||||||
|
// TODO: env
|
||||||
|
// TODO: volumes
|
||||||
|
},
|
||||||
|
Name: name,
|
||||||
|
}
|
||||||
|
|
||||||
|
if ports, ok := service.Extensions[PortsExtensionKey].([]api.PortSpec); ok {
|
||||||
|
spec.Ports = ports
|
||||||
|
}
|
||||||
|
|
||||||
|
if service.Deploy != nil {
|
||||||
|
switch service.Deploy.Mode {
|
||||||
|
case "global":
|
||||||
|
spec.Mode = api.ServiceModeGlobal
|
||||||
|
case "", "replicated":
|
||||||
|
spec.Mode = api.ServiceModeReplicated
|
||||||
|
if service.Deploy.Replicas != nil {
|
||||||
|
spec.Replicas = uint(*service.Deploy.Replicas)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return spec, fmt.Errorf("unsupported deploy mode: %s", service.Deploy.Mode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return spec, nil
|
||||||
|
}
|
||||||
@@ -3,13 +3,13 @@ package connector
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/psviderski/uncloud/internal/machine"
|
||||||
|
"github.com/psviderski/uncloud/internal/sshexec"
|
||||||
"golang.org/x/crypto/ssh"
|
"golang.org/x/crypto/ssh"
|
||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
"google.golang.org/grpc/credentials/insecure"
|
"google.golang.org/grpc/credentials/insecure"
|
||||||
"net"
|
"net"
|
||||||
"strings"
|
"strings"
|
||||||
"uncloud/internal/machine"
|
|
||||||
"uncloud/internal/sshexec"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type SSHConnectorConfig struct {
|
type SSHConnectorConfig struct {
|
||||||
@@ -3,16 +3,16 @@ package connector
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/psviderski/uncloud/internal/cli/config"
|
||||||
|
machine2 "github.com/psviderski/uncloud/internal/machine"
|
||||||
|
"github.com/psviderski/uncloud/internal/machine/network"
|
||||||
|
"github.com/psviderski/uncloud/internal/machine/network/tunnel"
|
||||||
|
"github.com/psviderski/uncloud/pkg/client"
|
||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
"google.golang.org/grpc/credentials/insecure"
|
"google.golang.org/grpc/credentials/insecure"
|
||||||
"net"
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"strconv"
|
"strconv"
|
||||||
"uncloud/internal/cli/client"
|
|
||||||
"uncloud/internal/cli/config"
|
|
||||||
machine2 "uncloud/internal/machine"
|
|
||||||
"uncloud/internal/machine/network"
|
|
||||||
"uncloud/internal/machine/network/tunnel"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// WireGuardConnector establishes a connection to the cluster API through a WireGuard tunnel
|
// WireGuardConnector establishes a connection to the cluster API through a WireGuard tunnel
|
||||||
@@ -4,17 +4,15 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/docker/compose/v2/pkg/progress"
|
"github.com/docker/compose/v2/pkg/progress"
|
||||||
"github.com/docker/docker/api/types/container"
|
"github.com/docker/docker/api/types/container"
|
||||||
"github.com/docker/docker/api/types/network"
|
|
||||||
dockerclient "github.com/docker/docker/client"
|
dockerclient "github.com/docker/docker/client"
|
||||||
"github.com/docker/docker/pkg/jsonmessage"
|
"github.com/docker/docker/pkg/jsonmessage"
|
||||||
"github.com/docker/go-connections/nat"
|
"github.com/psviderski/uncloud/internal/secret"
|
||||||
"strconv"
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
"strings"
|
"google.golang.org/grpc/status"
|
||||||
"uncloud/internal/api"
|
|
||||||
machinedocker "uncloud/internal/machine/docker"
|
|
||||||
"uncloud/internal/secret"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// CreateContainer creates a new container for the given service on the specified machine.
|
// CreateContainer creates a new container for the given service on the specified machine.
|
||||||
@@ -23,9 +21,11 @@ func (cli *Client) CreateContainer(
|
|||||||
) (container.CreateResponse, error) {
|
) (container.CreateResponse, error) {
|
||||||
var resp container.CreateResponse
|
var resp container.CreateResponse
|
||||||
|
|
||||||
if serviceID == "" {
|
spec = spec.SetDefaults()
|
||||||
return resp, errors.New("service ID is required")
|
if err := spec.Validate(); err != nil {
|
||||||
|
return resp, fmt.Errorf("invalid service spec: %w", err)
|
||||||
}
|
}
|
||||||
|
// TODO: validate spec.Name is consistent with serviceID if this is not the first container in the service.
|
||||||
|
|
||||||
machine, err := cli.InspectMachine(ctx, machineID)
|
machine, err := cli.InspectMachine(ctx, machineID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -38,75 +38,39 @@ func (cli *Client) CreateContainer(
|
|||||||
}
|
}
|
||||||
containerName := fmt.Sprintf("%s-%s", spec.Name, suffix)
|
containerName := fmt.Sprintf("%s-%s", spec.Name, suffix)
|
||||||
|
|
||||||
config := &container.Config{
|
|
||||||
Cmd: spec.Container.Command,
|
|
||||||
Image: spec.Container.Image,
|
|
||||||
Labels: map[string]string{
|
|
||||||
api.LabelServiceID: serviceID,
|
|
||||||
api.LabelServiceName: spec.Name,
|
|
||||||
api.LabelManaged: "",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
if spec.Mode != "" {
|
|
||||||
config.Labels[api.LabelServiceMode] = spec.Mode
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(spec.Ports) > 0 {
|
|
||||||
encodedPorts := make([]string, len(spec.Ports))
|
|
||||||
for i, p := range spec.Ports {
|
|
||||||
encodedPorts[i], err = p.String()
|
|
||||||
if err != nil {
|
|
||||||
return resp, fmt.Errorf("encode service port spec: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
config.Labels[api.LabelServicePorts] = strings.Join(encodedPorts, ",")
|
|
||||||
}
|
|
||||||
|
|
||||||
portBindings := make(nat.PortMap)
|
|
||||||
for _, p := range spec.Ports {
|
|
||||||
if p.Mode != api.PortModeHost {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
port := nat.Port(fmt.Sprintf("%d/%s", p.ContainerPort, p.Protocol))
|
|
||||||
portBindings[port] = []nat.PortBinding{
|
|
||||||
{
|
|
||||||
HostPort: strconv.Itoa(int(p.PublishedPort)),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
if p.HostIP.IsValid() {
|
|
||||||
portBindings[port][0].HostIP = p.HostIP.String()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
hostConfig := &container.HostConfig{
|
|
||||||
Binds: spec.Container.Volumes,
|
|
||||||
Init: spec.Container.Init,
|
|
||||||
PortBindings: portBindings,
|
|
||||||
}
|
|
||||||
netConfig := &network.NetworkingConfig{
|
|
||||||
EndpointsConfig: map[string]*network.EndpointSettings{
|
|
||||||
machinedocker.NetworkName: {},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// Proxy Docker gRPC requests to the selected machine.
|
// Proxy Docker gRPC requests to the selected machine.
|
||||||
ctx = proxyToMachine(ctx, machine.Machine)
|
ctx = proxyToMachine(ctx, machine.Machine)
|
||||||
|
|
||||||
pw := progress.ContextWriter(ctx)
|
pw := progress.ContextWriter(ctx)
|
||||||
eventID := fmt.Sprintf("Container %s on %s", containerName, machine.Machine.Name)
|
eventID := fmt.Sprintf("Container %s on %s", containerName, machine.Machine.Name)
|
||||||
|
|
||||||
pw.Event(progress.CreatingEvent(eventID))
|
pw.Event(progress.CreatingEvent(eventID))
|
||||||
resp, err = cli.Docker.CreateContainer(ctx, config, hostConfig, netConfig, nil, containerName)
|
|
||||||
|
if spec.Container.PullPolicy == api.PullPolicyAlways {
|
||||||
|
if err = cli.pullImageWithProgress(ctx, spec.Container.Image, machine.Machine.Name, eventID); err != nil {
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err = cli.Docker.CreateServiceContainer(ctx, serviceID, spec, containerName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
switch spec.Container.PullPolicy {
|
||||||
|
case api.PullPolicyAlways, api.PullPolicyNever:
|
||||||
|
return resp, err
|
||||||
|
case api.PullPolicyMissing:
|
||||||
|
default:
|
||||||
|
return resp, fmt.Errorf("unsupported pull policy: '%s'", spec.Container.PullPolicy)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Not found error is expected if the image is missing.
|
||||||
if !dockerclient.IsErrNotFound(err) {
|
if !dockerclient.IsErrNotFound(err) {
|
||||||
return resp, err
|
return resp, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pull the missing image and create the container again.
|
// Pull the missing image and create the container again.
|
||||||
if err = cli.pullImageWithProgress(ctx, config.Image, machine.Machine.Name, eventID); err != nil {
|
if err = cli.pullImageWithProgress(ctx, spec.Container.Image, machine.Machine.Name, eventID); err != nil {
|
||||||
return resp, err
|
return resp, err
|
||||||
}
|
}
|
||||||
if resp, err = cli.Docker.CreateContainer(ctx, config, hostConfig, netConfig, nil, containerName); err != nil {
|
if resp, err = cli.Docker.CreateServiceContainer(ctx, serviceID, spec, containerName); err != nil {
|
||||||
return resp, err
|
return resp, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -127,14 +91,15 @@ func (cli *Client) pullImageWithProgress(ctx context.Context, image, machineName
|
|||||||
|
|
||||||
pullCh, err := cli.Docker.PullImage(ctx, image)
|
pullCh, err := cli.Docker.PullImage(ctx, image)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
statusErr := status.Convert(err)
|
||||||
pw.Event(progress.Event{
|
pw.Event(progress.Event{
|
||||||
ID: eventID,
|
ID: eventID,
|
||||||
ParentID: parentEventID,
|
ParentID: parentEventID,
|
||||||
Text: "Error",
|
Text: "Error",
|
||||||
Status: progress.Error,
|
Status: progress.Error,
|
||||||
StatusText: errors.Unwrap(err).Error(),
|
StatusText: statusErr.Message(),
|
||||||
})
|
})
|
||||||
return fmt.Errorf("pull image: %w", err)
|
return fmt.Errorf("pull image: %w", errors.New(statusErr.Message()))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait for pull to complete by reading all progress messages and converting them to events.
|
// Wait for pull to complete by reading all progress messages and converting them to events.
|
||||||
@@ -147,14 +112,15 @@ func (cli *Client) pullImageWithProgress(ctx context.Context, image, machineName
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
statusErr := status.Convert(err)
|
||||||
pw.Event(progress.Event{
|
pw.Event(progress.Event{
|
||||||
ID: eventID,
|
ID: eventID,
|
||||||
ParentID: parentEventID,
|
ParentID: parentEventID,
|
||||||
Text: "Error",
|
Text: "Error",
|
||||||
Status: progress.Error,
|
Status: progress.Error,
|
||||||
StatusText: errors.Unwrap(err).Error(),
|
StatusText: statusErr.Message(),
|
||||||
})
|
})
|
||||||
return fmt.Errorf("pull image: %w", err)
|
return fmt.Errorf("pull image: %w", errors.New(statusErr.Message()))
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: add like in compose: --quiet-pull Pull without printing progress information
|
// TODO: add like in compose: --quiet-pull Pull without printing progress information
|
||||||
@@ -224,29 +190,26 @@ func toPullProgressEvent(jm jsonmessage.JSONMessage) *progress.Event {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// InspectContainer returns the information about the specified container within the service.
|
// InspectContainer returns the information about the specified container within the service.
|
||||||
func (cli *Client) InspectContainer(ctx context.Context, serviceID, containerID string) (api.MachineContainer, error) {
|
func (cli *Client) InspectContainer(
|
||||||
var ctr api.MachineContainer
|
ctx context.Context, serviceNameOrID, containerNameOrID string,
|
||||||
|
) (api.MachineServiceContainer, error) {
|
||||||
svc, err := cli.InspectService(ctx, serviceID)
|
svc, err := cli.InspectService(ctx, serviceNameOrID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ctr, fmt.Errorf("inspect service: %w", err)
|
return api.MachineServiceContainer{}, fmt.Errorf("inspect service: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, c := range svc.Containers {
|
for _, c := range svc.Containers {
|
||||||
if c.Container.ID == containerID || c.Container.NameWithoutSlash() == containerID {
|
if c.Container.ID == containerNameOrID || c.Container.Name == containerNameOrID {
|
||||||
ctr = c
|
return c, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ctr.MachineID == "" {
|
|
||||||
return ctr, ErrNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
return ctr, nil
|
return api.MachineServiceContainer{}, api.ErrNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
// StartContainer starts the specified container within the service.
|
// StartContainer starts the specified container within the service.
|
||||||
func (cli *Client) StartContainer(ctx context.Context, serviceID, containerID string) error {
|
func (cli *Client) StartContainer(ctx context.Context, serviceNameOrID, containerNameOrID string) error {
|
||||||
ctr, err := cli.InspectContainer(ctx, serviceID, containerID)
|
ctr, err := cli.InspectContainer(ctx, serviceNameOrID, containerNameOrID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -258,7 +221,7 @@ func (cli *Client) StartContainer(ctx context.Context, serviceID, containerID st
|
|||||||
ctx = proxyToMachine(ctx, machine.Machine)
|
ctx = proxyToMachine(ctx, machine.Machine)
|
||||||
|
|
||||||
pw := progress.ContextWriter(ctx)
|
pw := progress.ContextWriter(ctx)
|
||||||
eventID := fmt.Sprintf("Container %s on %s", ctr.Container.NameWithoutSlash(), machine.Machine.Name)
|
eventID := fmt.Sprintf("Container %s on %s", ctr.Container.Name, machine.Machine.Name)
|
||||||
|
|
||||||
pw.Event(progress.StartingEvent(eventID))
|
pw.Event(progress.StartingEvent(eventID))
|
||||||
if err = cli.Docker.StartContainer(ctx, ctr.Container.ID, container.StartOptions{}); err != nil {
|
if err = cli.Docker.StartContainer(ctx, ctr.Container.ID, container.StartOptions{}); err != nil {
|
||||||
@@ -271,9 +234,9 @@ func (cli *Client) StartContainer(ctx context.Context, serviceID, containerID st
|
|||||||
|
|
||||||
// StopContainer stops the specified container within the service.
|
// StopContainer stops the specified container within the service.
|
||||||
func (cli *Client) StopContainer(
|
func (cli *Client) StopContainer(
|
||||||
ctx context.Context, serviceID, containerID string, opts container.StopOptions,
|
ctx context.Context, serviceNameOrID, containerNameOrID string, opts container.StopOptions,
|
||||||
) error {
|
) error {
|
||||||
ctr, err := cli.InspectContainer(ctx, serviceID, containerID)
|
ctr, err := cli.InspectContainer(ctx, serviceNameOrID, containerNameOrID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -285,7 +248,7 @@ func (cli *Client) StopContainer(
|
|||||||
ctx = proxyToMachine(ctx, machine.Machine)
|
ctx = proxyToMachine(ctx, machine.Machine)
|
||||||
|
|
||||||
pw := progress.ContextWriter(ctx)
|
pw := progress.ContextWriter(ctx)
|
||||||
eventID := fmt.Sprintf("Container %s on %s", ctr.Container.NameWithoutSlash(), machine.Machine.Name)
|
eventID := fmt.Sprintf("Container %s on %s", ctr.Container.Name, machine.Machine.Name)
|
||||||
|
|
||||||
pw.Event(progress.StoppingEvent(eventID))
|
pw.Event(progress.StoppingEvent(eventID))
|
||||||
if err = cli.Docker.StopContainer(ctx, ctr.Container.ID, opts); err != nil {
|
if err = cli.Docker.StopContainer(ctx, ctr.Container.ID, opts); err != nil {
|
||||||
@@ -298,9 +261,9 @@ func (cli *Client) StopContainer(
|
|||||||
|
|
||||||
// RemoveContainer removes the specified container within the service.
|
// RemoveContainer removes the specified container within the service.
|
||||||
func (cli *Client) RemoveContainer(
|
func (cli *Client) RemoveContainer(
|
||||||
ctx context.Context, serviceID, containerID string, opts container.RemoveOptions,
|
ctx context.Context, serviceNameOrID, containerNameOrID string, opts container.RemoveOptions,
|
||||||
) error {
|
) error {
|
||||||
ctr, err := cli.InspectContainer(ctx, serviceID, containerID)
|
ctr, err := cli.InspectContainer(ctx, serviceNameOrID, containerNameOrID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -312,10 +275,10 @@ func (cli *Client) RemoveContainer(
|
|||||||
ctx = proxyToMachine(ctx, machine.Machine)
|
ctx = proxyToMachine(ctx, machine.Machine)
|
||||||
|
|
||||||
pw := progress.ContextWriter(ctx)
|
pw := progress.ContextWriter(ctx)
|
||||||
eventID := fmt.Sprintf("Container %s on %s", ctr.Container.NameWithoutSlash(), machine.Machine.Name)
|
eventID := fmt.Sprintf("Container %s on %s", ctr.Container.Name, machine.Machine.Name)
|
||||||
|
|
||||||
pw.Event(progress.RemovingEvent(eventID))
|
pw.Event(progress.RemovingEvent(eventID))
|
||||||
if err = cli.Docker.RemoveContainer(ctx, ctr.Container.ID, opts); err != nil {
|
if err = cli.Docker.RemoveServiceContainer(ctx, ctr.Container.ID, opts); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
pw.Event(progress.RemovedEvent(eventID))
|
pw.Event(progress.RemovedEvent(eventID))
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
|
"github.com/psviderski/uncloud/pkg/client/deploy"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewDeployment creates a new deployment for the given service specification.
|
||||||
|
// If strategy is nil, a default deploy.RollingStrategy will be used.
|
||||||
|
func (cli *Client) NewDeployment(spec api.ServiceSpec, strategy deploy.Strategy) *deploy.Deployment {
|
||||||
|
return deploy.NewDeployment(cli, spec, strategy)
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package deploy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ContainerSpecStatus string
|
||||||
|
|
||||||
|
const ContainerUpToDate ContainerSpecStatus = "up-to-date"
|
||||||
|
const ContainerNeedsUpdate ContainerSpecStatus = "needs-update"
|
||||||
|
const ContainerNeedsRecreate ContainerSpecStatus = "needs-recreate"
|
||||||
|
|
||||||
|
func EvalContainerSpecChange(current api.ServiceSpec, new api.ServiceSpec) ContainerSpecStatus {
|
||||||
|
current = current.SetDefaults()
|
||||||
|
new = new.SetDefaults()
|
||||||
|
|
||||||
|
// Pull policy doesn't affect the container configuration.
|
||||||
|
new.Container.PullPolicy = current.Container.PullPolicy
|
||||||
|
if !current.Container.Equals(new.Container) {
|
||||||
|
return ContainerNeedsRecreate
|
||||||
|
}
|
||||||
|
|
||||||
|
if current.Mode != new.Mode {
|
||||||
|
return ContainerNeedsRecreate
|
||||||
|
}
|
||||||
|
if current.Name != new.Name {
|
||||||
|
return ContainerNeedsRecreate
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: compare mutable properties such as memory or CPU limits when they are implemented.
|
||||||
|
|
||||||
|
// TODO: change ports check to ContainerNeedsUpdate when ingress ports are stored only the machine DB instead
|
||||||
|
// of as labels ans synced to the cluster store. Host ports changes should be handled as ContainerNeedsRecreate.
|
||||||
|
if !api.PortsEqual(current.Ports, new.Ports) {
|
||||||
|
return ContainerNeedsRecreate
|
||||||
|
}
|
||||||
|
|
||||||
|
return ContainerUpToDate
|
||||||
|
}
|
||||||
@@ -1,56 +1,46 @@
|
|||||||
package client
|
package deploy
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/distribution/reference"
|
"github.com/psviderski/uncloud/internal/machine/api/pb"
|
||||||
"strings"
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
"uncloud/internal/api"
|
|
||||||
"uncloud/internal/secret"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type Client interface {
|
||||||
|
api.ContainerClient
|
||||||
|
api.DNSClient
|
||||||
|
api.ImageClient
|
||||||
|
api.MachineClient
|
||||||
|
api.ServiceClient
|
||||||
|
}
|
||||||
|
|
||||||
// Deployment manages the process of creating or updating a service to match a desired state.
|
// Deployment manages the process of creating or updating a service to match a desired state.
|
||||||
// It coordinates the validation, planning, and execution of deployment operations.
|
// It coordinates the validation, planning, and execution of deployment operations.
|
||||||
type Deployment struct {
|
type Deployment struct {
|
||||||
Service *api.Service
|
Service *api.Service
|
||||||
Spec api.ServiceSpec
|
Spec api.ServiceSpec
|
||||||
Strategy Strategy
|
Strategy Strategy
|
||||||
cli *Client
|
cli Client
|
||||||
plan *Plan
|
plan *Plan
|
||||||
}
|
}
|
||||||
|
|
||||||
type Plan struct {
|
type Plan struct {
|
||||||
ServiceID string
|
ServiceID string
|
||||||
Operation
|
ServiceName string
|
||||||
|
SequenceOperation
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MachineFilter determines which machines participate in a deployment operation by returning true for
|
||||||
|
// machines that should be included.
|
||||||
|
type MachineFilter func(m *pb.MachineInfo) bool
|
||||||
|
|
||||||
|
var ErrNoMatchingMachines = errors.New("no machines match the filter")
|
||||||
|
|
||||||
// NewDeployment creates a new deployment for the given service specification.
|
// NewDeployment creates a new deployment for the given service specification.
|
||||||
// If strategy is nil, a default RollingStrategy will be used.
|
// If strategy is nil, a default RollingStrategy will be used.
|
||||||
func (cli *Client) NewDeployment(spec api.ServiceSpec, strategy Strategy) (*Deployment, error) {
|
func NewDeployment(cli Client, spec api.ServiceSpec, strategy Strategy) *Deployment {
|
||||||
if err := spec.Validate(); err != nil {
|
|
||||||
return nil, fmt.Errorf("invalid service spec: %w", err)
|
|
||||||
}
|
|
||||||
if spec.Name == "" {
|
|
||||||
// Generate a random service name from the image when not provided.
|
|
||||||
img, err := reference.ParseDockerRef(spec.Container.Image)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("invalid image: %w", err)
|
|
||||||
}
|
|
||||||
// Get the image name without the repository and tag/digest parts.
|
|
||||||
imageName := reference.FamiliarName(img)
|
|
||||||
// Get the last part of the image name (path), e.g. "nginx" from "bitnami/nginx".
|
|
||||||
if i := strings.LastIndex(imageName, "/"); i != -1 {
|
|
||||||
imageName = imageName[i+1:]
|
|
||||||
}
|
|
||||||
// Append a random suffix to the image name to generate an optimistically unique service name.
|
|
||||||
suffix, err := secret.RandomAlphaNumeric(4)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("generate random suffix: %w", err)
|
|
||||||
}
|
|
||||||
spec.Name = fmt.Sprintf("%s-%s", imageName, suffix)
|
|
||||||
}
|
|
||||||
|
|
||||||
if strategy == nil {
|
if strategy == nil {
|
||||||
strategy = &RollingStrategy{}
|
strategy = &RollingStrategy{}
|
||||||
}
|
}
|
||||||
@@ -59,7 +49,7 @@ func (cli *Client) NewDeployment(spec api.ServiceSpec, strategy Strategy) (*Depl
|
|||||||
Spec: spec,
|
Spec: spec,
|
||||||
Strategy: strategy,
|
Strategy: strategy,
|
||||||
cli: cli,
|
cli: cli,
|
||||||
}, nil
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Plan returns a plan of operations to reconcile the service to the desired state.
|
// Plan returns a plan of operations to reconcile the service to the desired state.
|
||||||
@@ -69,14 +59,34 @@ func (d *Deployment) Plan(ctx context.Context) (Plan, error) {
|
|||||||
return *d.plan, nil
|
return *d.plan, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate the new spec before planning.
|
// Validate the user-provided spec before resolving it.
|
||||||
if err := d.Validate(ctx); err != nil {
|
if err := d.Validate(ctx); err != nil {
|
||||||
return Plan{}, fmt.Errorf("invalid deployment: %w", err)
|
return Plan{}, fmt.Errorf("invalid deployment: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
plan, err := d.Strategy.Plan(ctx, d.cli, d.Service, d.Spec)
|
clusterDomain, err := d.cli.GetDomain(ctx)
|
||||||
|
if err != nil && !errors.Is(err, api.ErrNotFound) {
|
||||||
|
return Plan{}, fmt.Errorf("get cluster domain: %w", err)
|
||||||
|
}
|
||||||
|
// TODO: enable image resolver when it's ready to pin digests and look up existing images.
|
||||||
|
//imageResolver := &ImageDigestResolver{
|
||||||
|
// Ctx: ctx,
|
||||||
|
// Client: d.cli,
|
||||||
|
//}
|
||||||
|
specResolver := &ServiceSpecResolver{
|
||||||
|
// If the domain is not found (not reserved), an empty domain is used for the resolver.
|
||||||
|
ClusterDomain: clusterDomain,
|
||||||
|
//ImageResolver: imageResolver,
|
||||||
|
}
|
||||||
|
|
||||||
|
resolvedSpec, err := specResolver.Resolve(d.Spec)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Plan{}, fmt.Errorf("create plan using %T: %w", d.Strategy, err)
|
return Plan{}, fmt.Errorf("resolve service spec: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
plan, err := d.Strategy.Plan(ctx, d.cli, d.Service, resolvedSpec)
|
||||||
|
if err != nil {
|
||||||
|
return Plan{}, fmt.Errorf("create plan using %s strategy: %w", d.Strategy.Type(), err)
|
||||||
}
|
}
|
||||||
d.plan = &plan
|
d.plan = &plan
|
||||||
|
|
||||||
@@ -88,15 +98,12 @@ func (d *Deployment) Validate(ctx context.Context) error {
|
|||||||
if err := d.Spec.Validate(); err != nil {
|
if err := d.Spec.Validate(); err != nil {
|
||||||
return fmt.Errorf("invalid service spec: %w", err)
|
return fmt.Errorf("invalid service spec: %w", err)
|
||||||
}
|
}
|
||||||
if d.Spec.Name == "" {
|
|
||||||
return errors.New("service name is required")
|
|
||||||
}
|
|
||||||
|
|
||||||
if d.Service == nil {
|
if d.Service == nil {
|
||||||
svc, err := d.cli.InspectService(ctx, d.Spec.Name)
|
svc, err := d.cli.InspectService(ctx, d.Spec.Name)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
d.Service = &svc
|
d.Service = &svc
|
||||||
} else if !errors.Is(err, ErrNotFound) {
|
} else if !errors.Is(err, api.ErrNotFound) {
|
||||||
return fmt.Errorf("inspect service: %w", err)
|
return fmt.Errorf("inspect service: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -108,7 +115,14 @@ func (d *Deployment) Validate(ctx context.Context) error {
|
|||||||
if d.Service.Name != d.Spec.Name {
|
if d.Service.Name != d.Spec.Name {
|
||||||
return errors.New("service name cannot be changed")
|
return errors.New("service name cannot be changed")
|
||||||
}
|
}
|
||||||
if d.Service.Mode != d.Spec.Mode {
|
|
||||||
|
// Resolve the default mode if not specified.
|
||||||
|
mode := d.Spec.Mode
|
||||||
|
if mode == "" {
|
||||||
|
mode = api.ServiceModeReplicated
|
||||||
|
}
|
||||||
|
|
||||||
|
if mode != d.Service.Mode {
|
||||||
return errors.New("service mode cannot be changed")
|
return errors.New("service mode cannot be changed")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,11 +133,11 @@ func (d *Deployment) Validate(ctx context.Context) error {
|
|||||||
// It will create a new plan if one hasn't been created yet. The deployment will either create a new service or update
|
// It will create a new plan if one hasn't been created yet. The deployment will either create a new service or update
|
||||||
// the existing one to match the desired specification.
|
// the existing one to match the desired specification.
|
||||||
// TODO: forbid to run the same deployment more than once.
|
// TODO: forbid to run the same deployment more than once.
|
||||||
func (d *Deployment) Run(ctx context.Context) (string, error) {
|
func (d *Deployment) Run(ctx context.Context) (Plan, error) {
|
||||||
plan, err := d.Plan(ctx)
|
plan, err := d.Plan(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("plan: %w", err)
|
return plan, fmt.Errorf("create plan: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return plan.ServiceID, plan.Execute(ctx, d.cli)
|
return plan, plan.Execute(ctx, d.cli)
|
||||||
}
|
}
|
||||||
@@ -1,20 +1,31 @@
|
|||||||
package client
|
package deploy
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/docker/docker/api/types/container"
|
"github.com/docker/docker/api/types/container"
|
||||||
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
"strings"
|
"strings"
|
||||||
"uncloud/internal/api"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Operation represents a single atomic operation in a deployment process.
|
// Operation represents a single atomic operation in a deployment process.
|
||||||
// Operations can be composed to form complex deployment strategies.
|
// Operations can be composed to form complex deployment strategies.
|
||||||
type Operation interface {
|
type Operation interface {
|
||||||
Execute(ctx context.Context, cli *Client) error
|
// Execute performs the operation using the provided client.
|
||||||
|
// TODO: Encapsulate the client in the operation as otherwise it gives an impression that different clients
|
||||||
|
// can be provided. But in reality, the operation is tightly coupled with the client that was used to create it.
|
||||||
|
Execute(ctx context.Context, cli Client) error
|
||||||
|
// Format returns a human-readable representation of the operation.
|
||||||
|
Format(resolver NameResolver) string
|
||||||
String() string
|
String() string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NameResolver resolves machine and container IDs to their names.
|
||||||
|
type NameResolver interface {
|
||||||
|
MachineName(machineID string) string
|
||||||
|
ContainerName(containerID string) string
|
||||||
|
}
|
||||||
|
|
||||||
// RunContainerOperation creates and starts a new container on a specific machine.
|
// RunContainerOperation creates and starts a new container on a specific machine.
|
||||||
type RunContainerOperation struct {
|
type RunContainerOperation struct {
|
||||||
ServiceID string
|
ServiceID string
|
||||||
@@ -22,7 +33,7 @@ type RunContainerOperation struct {
|
|||||||
MachineID string
|
MachineID string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *RunContainerOperation) Execute(ctx context.Context, cli *Client) error {
|
func (o *RunContainerOperation) Execute(ctx context.Context, cli Client) error {
|
||||||
resp, err := cli.CreateContainer(ctx, o.ServiceID, o.Spec, o.MachineID)
|
resp, err := cli.CreateContainer(ctx, o.ServiceID, o.Spec, o.MachineID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("create container: %w", err)
|
return fmt.Errorf("create container: %w", err)
|
||||||
@@ -36,8 +47,14 @@ func (o *RunContainerOperation) Execute(ctx context.Context, cli *Client) error
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (o *RunContainerOperation) Format(resolver NameResolver) string {
|
||||||
|
machineName := resolver.MachineName(o.MachineID)
|
||||||
|
return fmt.Sprintf("%s: Run container [image=%s]", machineName, o.Spec.Container.Image)
|
||||||
|
}
|
||||||
|
|
||||||
func (o *RunContainerOperation) String() string {
|
func (o *RunContainerOperation) String() string {
|
||||||
return fmt.Sprintf("RunContainerOperation[%s, %s, %s]", o.ServiceID, o.Spec.Name, o.MachineID)
|
return fmt.Sprintf("RunContainerOperation[service_id=%s, image=%s, machine_id=%s]",
|
||||||
|
o.ServiceID, o.Spec.Container.Image, o.MachineID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// StopContainerOperation stops a container on a specific machine.
|
// StopContainerOperation stops a container on a specific machine.
|
||||||
@@ -47,15 +64,21 @@ type StopContainerOperation struct {
|
|||||||
MachineID string
|
MachineID string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *StopContainerOperation) Execute(ctx context.Context, cli *Client) error {
|
func (o *StopContainerOperation) Execute(ctx context.Context, cli Client) error {
|
||||||
if err := cli.StopContainer(ctx, o.ServiceID, o.ContainerID, container.StopOptions{}); err != nil {
|
if err := cli.StopContainer(ctx, o.ServiceID, o.ContainerID, container.StopOptions{}); err != nil {
|
||||||
return fmt.Errorf("stop container: %w", err)
|
return fmt.Errorf("stop container: %w", err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (o *StopContainerOperation) Format(resolver NameResolver) string {
|
||||||
|
machineName := resolver.MachineName(o.MachineID)
|
||||||
|
return fmt.Sprintf("%s: Stop container [name=%s]", machineName, resolver.ContainerName(o.ContainerID))
|
||||||
|
}
|
||||||
|
|
||||||
func (o *StopContainerOperation) String() string {
|
func (o *StopContainerOperation) String() string {
|
||||||
return fmt.Sprintf("StopContainerOperation[%s, %s, %s]", o.ServiceID, o.ContainerID, o.MachineID)
|
return fmt.Sprintf("StopContainerOperation[service_id=%s, container_id=%s, machine_id=%s]",
|
||||||
|
o.ServiceID, o.ContainerID, o.MachineID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// RemoveContainerOperation stops and removes a container from a specific machine.
|
// RemoveContainerOperation stops and removes a container from a specific machine.
|
||||||
@@ -65,7 +88,7 @@ type RemoveContainerOperation struct {
|
|||||||
MachineID string
|
MachineID string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *RemoveContainerOperation) Execute(ctx context.Context, cli *Client) error {
|
func (o *RemoveContainerOperation) Execute(ctx context.Context, cli Client) error {
|
||||||
if err := cli.StopContainer(ctx, o.ServiceID, o.ContainerID, container.StopOptions{}); err != nil {
|
if err := cli.StopContainer(ctx, o.ServiceID, o.ContainerID, container.StopOptions{}); err != nil {
|
||||||
return fmt.Errorf("stop container: %w", err)
|
return fmt.Errorf("stop container: %w", err)
|
||||||
}
|
}
|
||||||
@@ -76,8 +99,14 @@ func (o *RemoveContainerOperation) Execute(ctx context.Context, cli *Client) err
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (o *RemoveContainerOperation) Format(resolver NameResolver) string {
|
||||||
|
machineName := resolver.MachineName(o.MachineID)
|
||||||
|
return fmt.Sprintf("%s: Remove container [name=%s]", machineName, resolver.ContainerName(o.ContainerID))
|
||||||
|
}
|
||||||
|
|
||||||
func (o *RemoveContainerOperation) String() string {
|
func (o *RemoveContainerOperation) String() string {
|
||||||
return fmt.Sprintf("RemoveContainerOperation[%s, %s, %s]", o.ServiceID, o.ContainerID, o.MachineID)
|
return fmt.Sprintf("RemoveContainerOperation[service_id=%s, container_id=%s, machine_id=%s]",
|
||||||
|
o.ServiceID, o.ContainerID, o.MachineID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SequenceOperation is a composite operation that executes a sequence of operations in order.
|
// SequenceOperation is a composite operation that executes a sequence of operations in order.
|
||||||
@@ -85,7 +114,7 @@ type SequenceOperation struct {
|
|||||||
Operations []Operation
|
Operations []Operation
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *SequenceOperation) Execute(ctx context.Context, cli *Client) error {
|
func (o *SequenceOperation) Execute(ctx context.Context, cli Client) error {
|
||||||
for _, op := range o.Operations {
|
for _, op := range o.Operations {
|
||||||
if err := op.Execute(ctx, cli); err != nil {
|
if err := op.Execute(ctx, cli); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -94,6 +123,15 @@ func (o *SequenceOperation) Execute(ctx context.Context, cli *Client) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (o *SequenceOperation) Format(resolver NameResolver) string {
|
||||||
|
ops := make([]string, len(o.Operations))
|
||||||
|
for i, op := range o.Operations {
|
||||||
|
ops[i] = "- " + op.Format(resolver)
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Join(ops, "\n")
|
||||||
|
}
|
||||||
|
|
||||||
func (o *SequenceOperation) String() string {
|
func (o *SequenceOperation) String() string {
|
||||||
ops := make([]string, len(o.Operations))
|
ops := make([]string, len(o.Operations))
|
||||||
for i, op := range o.Operations {
|
for i, op := range o.Operations {
|
||||||
@@ -0,0 +1,318 @@
|
|||||||
|
package deploy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"github.com/distribution/reference"
|
||||||
|
"github.com/docker/docker/api/types"
|
||||||
|
"github.com/opencontainers/go-digest"
|
||||||
|
"github.com/psviderski/uncloud/internal/secret"
|
||||||
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
|
"google.golang.org/grpc/codes"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ServiceSpecResolver transforms user-provided service specs into deployment-ready form.
|
||||||
|
type ServiceSpecResolver struct {
|
||||||
|
ClusterDomain string
|
||||||
|
ImageResolver *ImageDigestResolver
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve transforms a service spec into its fully resolved form ready for deployment.
|
||||||
|
func (r *ServiceSpecResolver) Resolve(spec api.ServiceSpec) (api.ServiceSpec, error) {
|
||||||
|
if err := spec.Validate(); err != nil {
|
||||||
|
return spec, fmt.Errorf("invalid service spec: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
spec = spec.Clone()
|
||||||
|
|
||||||
|
steps := []func(*api.ServiceSpec) error{
|
||||||
|
r.applyDefaults,
|
||||||
|
r.resolveServiceName,
|
||||||
|
r.expandIngressPorts,
|
||||||
|
r.resolveImageDigest,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, step := range steps {
|
||||||
|
if err := step(&spec); err != nil {
|
||||||
|
return spec, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return spec, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ServiceSpecResolver) applyDefaults(spec *api.ServiceSpec) error {
|
||||||
|
if spec.Mode == "" {
|
||||||
|
spec.Mode = api.ServiceModeReplicated
|
||||||
|
}
|
||||||
|
// Ensure the replicated service has at least one replica.
|
||||||
|
if spec.Mode == api.ServiceModeReplicated && spec.Replicas == 0 {
|
||||||
|
spec.Replicas = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if spec.Container.PullPolicy == "" {
|
||||||
|
spec.Container.PullPolicy = api.PullPolicyMissing
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// resolveServiceName generates a service name from the image when not provided.
|
||||||
|
func (r *ServiceSpecResolver) resolveServiceName(spec *api.ServiceSpec) error {
|
||||||
|
if spec.Name != "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate a random service name from the image when not provided.
|
||||||
|
img, err := reference.ParseDockerRef(spec.Container.Image)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid image: %w", err)
|
||||||
|
}
|
||||||
|
// Get the image name without the repository and tag/digest parts.
|
||||||
|
imageName := reference.FamiliarName(img)
|
||||||
|
// Get the last part of the image name (path), e.g. "nginx" from "bitnami/nginx".
|
||||||
|
if i := strings.LastIndex(imageName, "/"); i != -1 {
|
||||||
|
imageName = imageName[i+1:]
|
||||||
|
}
|
||||||
|
// Append a random suffix to the image name to generate an optimistically unique service name.
|
||||||
|
suffix, err := secret.RandomAlphaNumeric(4)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("generate random suffix: %w", err)
|
||||||
|
}
|
||||||
|
spec.Name = fmt.Sprintf("%s-%s", imageName, suffix)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// expandIngressPorts processes HTTP(S) ingress ports in a service spec by:
|
||||||
|
// 1. Setting a default hostname (service-name.cluster-domain) for ports without a hostname.
|
||||||
|
// 2. Duplicating a port with a cluster domain hostname for ports with external domains.
|
||||||
|
// This ensures every ingress port is accessible via the cluster domain, while preserving any custom domains specified
|
||||||
|
// by the user.
|
||||||
|
func (r *ServiceSpecResolver) expandIngressPorts(spec *api.ServiceSpec) error {
|
||||||
|
for i, port := range spec.Ports {
|
||||||
|
if port.Protocol != api.ProtocolHTTP && port.Protocol != api.ProtocolHTTPS {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if port.Hostname == "" {
|
||||||
|
if r.ClusterDomain == "" {
|
||||||
|
return fmt.Errorf("cluster domain must be reserved to generate hostname for ingress port: %d/%s",
|
||||||
|
port.ContainerPort, port.Protocol)
|
||||||
|
}
|
||||||
|
// Assign the default hostname (service-name.cluster-domain).
|
||||||
|
spec.Ports[i].Hostname = fmt.Sprintf("%s.%s", spec.Name, r.ClusterDomain)
|
||||||
|
} else {
|
||||||
|
if r.ClusterDomain == "" {
|
||||||
|
// When no cluster domain is reserved, use only the provided hostname.
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasSuffix(port.Hostname, "."+r.ClusterDomain) {
|
||||||
|
// If the hostname is already a cluster subdomain, use as is.
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// For external domains, duplicate the port with a service-name.cluster-domain hostname so the service
|
||||||
|
// can be accessed via both hostnames.
|
||||||
|
newPort := port
|
||||||
|
newPort.Hostname = fmt.Sprintf("%s.%s", spec.Name, r.ClusterDomain)
|
||||||
|
spec.Ports = append(spec.Ports, newPort)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ServiceSpecResolver) resolveImageDigest(spec *api.ServiceSpec) error {
|
||||||
|
if r.ImageResolver == nil {
|
||||||
|
// Skip digest resolution when no resolver is provided.
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
image, err := r.ImageResolver.Resolve(spec.Container.Image, spec.Container.PullPolicy)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("resolve image digest: %w", err)
|
||||||
|
}
|
||||||
|
spec.Container.Image = image
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type ImageResolverClient interface {
|
||||||
|
api.ImageClient
|
||||||
|
api.MachineClient
|
||||||
|
}
|
||||||
|
|
||||||
|
type ImageDigestResolver struct {
|
||||||
|
Ctx context.Context
|
||||||
|
Client ImageResolverClient
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve resolves the image to the image with the digest according to the pull policy:
|
||||||
|
// - always: Fetch the latest digest for the image tag in the registry.
|
||||||
|
// - missing: Find the latest image matching the tag on any machine and use its digest, if it exists.
|
||||||
|
// When there is no matching image on any machine, it behaves like 'always'.
|
||||||
|
// - never: !Not implemented! Similar to 'missing' but when there is no matching image on any machine,
|
||||||
|
// it returns an error.
|
||||||
|
//
|
||||||
|
// If the image is already pinned to a digest, it is returned as is.
|
||||||
|
func (r *ImageDigestResolver) Resolve(image, policy string) (string, error) {
|
||||||
|
if r.Ctx == nil {
|
||||||
|
r.Ctx = context.Background()
|
||||||
|
}
|
||||||
|
|
||||||
|
ref, err := reference.ParseNormalizedNamed(image)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("parse image: %w", err)
|
||||||
|
}
|
||||||
|
if _, ok := ref.(reference.Canonical); ok {
|
||||||
|
// The image is already pinned to a digest.
|
||||||
|
return image, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
switch policy {
|
||||||
|
case api.PullPolicyAlways:
|
||||||
|
return r.resolveAlways(image)
|
||||||
|
case api.PullPolicyMissing:
|
||||||
|
return r.resolveMissing(image)
|
||||||
|
case api.PullPolicyNever:
|
||||||
|
return "", fmt.Errorf("pull policy '%s' is not supported yet", policy)
|
||||||
|
}
|
||||||
|
return image, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// resolveAlways resolves the image to the image with the digest by querying the registry from all machines.
|
||||||
|
func (r *ImageDigestResolver) resolveAlways(image string) (string, error) {
|
||||||
|
// TODO: broadcast to a subset of machines in large clusters to avoid being rate-limited by the registry.
|
||||||
|
ctx, err := api.ProxyMachinesContext(r.Ctx, r.Client, nil)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("create request context to broadcast to all machines: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
remoteImages, err := r.Client.InspectRemoteImage(ctx, image)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("inspect image '%s' in registry from all machines: %w", image, err)
|
||||||
|
}
|
||||||
|
if len(remoteImages) == 0 {
|
||||||
|
return "", fmt.Errorf("inspect image '%s' in registry from all machines: unexpected empty response", image)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, ri := range remoteImages {
|
||||||
|
if ri.Metadata != nil && ri.Metadata.Error != "" {
|
||||||
|
// Save the last error to return it if all machines fail to inspect the image.
|
||||||
|
err = fmt.Errorf("inspect image '%s' in registry on machine '%s': %s",
|
||||||
|
image, ri.Metadata.Machine, ri.Metadata.Error)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
return reference.FamiliarString(ri.Image.Reference), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ImageDigestResolver) resolveMissing(image string) (string, error) {
|
||||||
|
ctx, err := api.ProxyMachinesContext(r.Ctx, r.Client, nil)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("create request context to broadcast to all machines: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
machineImages, err := r.Client.InspectImage(ctx, image)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, api.ErrNotFound) {
|
||||||
|
// If the image is missing on all machines, the 'missing' policy is equivalent to 'always'.
|
||||||
|
return r.resolveAlways(image)
|
||||||
|
}
|
||||||
|
return "", fmt.Errorf("inspect image '%s' on all machines: %w", image, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var availableImages []types.ImageInspect
|
||||||
|
for _, mi := range machineImages {
|
||||||
|
// Metadata can be nil if the request was proxied to only one machine.
|
||||||
|
if mi.Metadata != nil && mi.Metadata.Error != "" {
|
||||||
|
if codes.Code(mi.Metadata.Status.Code) != codes.NotFound {
|
||||||
|
fmt.Printf("WARNING: failed to inspect image '%s' on machine '%s': %s\n",
|
||||||
|
image, mi.Metadata.Machine, mi.Metadata.Error)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
availableImages = append(availableImages, mi.Image)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(availableImages) == 0 {
|
||||||
|
// If the image is missing on all machines, the 'missing' policy is equivalent to 'always'.
|
||||||
|
return r.resolveAlways(image)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the latest image with a RepoDigest.
|
||||||
|
var latestDigest digest.Digest
|
||||||
|
var latestCreated time.Time
|
||||||
|
for _, img := range availableImages {
|
||||||
|
if len(img.RepoDigests) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: handle multiple RepoDigests. This could happen for example if the same image was pulled twice using
|
||||||
|
// both its index (multi-arch) digest and manifest (platform-specific) digest:
|
||||||
|
// {
|
||||||
|
// "Id": "sha256:6fee7566e4273ee6078f08e167e36434b35f72152232a5e6f1446288817dabe5",
|
||||||
|
// "RepoTags": [
|
||||||
|
// "traefik/whoami:latest"
|
||||||
|
// ],
|
||||||
|
// "RepoDigests": [
|
||||||
|
// "traefik/whoami@sha256:200689790a0a0ea48ca45992e0450bc26ccab5307375b41c84dfc4f2475937ab",
|
||||||
|
// "traefik/whoami@sha256:4f90b33ddca9c4d4f06527070d6e503b16d71016edea036842be2a84e60c91cb"
|
||||||
|
// ],
|
||||||
|
// ...
|
||||||
|
// }
|
||||||
|
// Should the registry be queried to find out which digest to use?
|
||||||
|
repoDigest := img.RepoDigests[0]
|
||||||
|
created, err := time.Parse(time.RFC3339Nano, img.Created)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if created.After(latestCreated) {
|
||||||
|
ref, err := reference.ParseNormalizedNamed(repoDigest)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if c, ok := ref.(reference.Canonical); ok {
|
||||||
|
latestDigest = c.Digest()
|
||||||
|
latestCreated = created
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if latestDigest != "" {
|
||||||
|
return imageWithDigest(image, latestDigest)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't pin the digest if no RepoDigests were found. This means the available images were not pulled from
|
||||||
|
// a registry but built locally or loaded from an archive. In this case, the available images (could be multiple
|
||||||
|
// for different platforms) should be copied to other machines to be able to run service containers on them.
|
||||||
|
return image, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// imageWithDigest adds a digest to an image string if it doesn't already contain one.
|
||||||
|
func imageWithDigest(image string, dig digest.Digest) (string, error) {
|
||||||
|
ref, err := reference.ParseNormalizedNamed(image)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("parse image: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := ref.(reference.Canonical); !ok {
|
||||||
|
// Preserves the original tag if present.
|
||||||
|
img, err := reference.WithDigest(ref, dig)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("add digest to image: %w", err)
|
||||||
|
}
|
||||||
|
return reference.FamiliarString(img), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return image, nil
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user