mirror of
https://github.com/psviderski/uncloud.git
synced 2026-08-26 19:13:34 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
137abde808 | ||
|
|
342969b7f4 | ||
|
|
d9e350c4dd | ||
|
|
3f4f263c05 | ||
|
|
0760ba3b18 | ||
|
|
0896bd27fb | ||
|
|
a16b2d1c74 | ||
|
|
52117954a8 | ||
|
|
435a794fa5 | ||
|
|
9642863d7b | ||
|
|
6bb9bdeb5a | ||
|
|
30f1e135f1 | ||
|
|
58ecc17ac1 | ||
|
|
4c239ed79a | ||
|
|
968dd50705 | ||
|
|
d7f437107d | ||
|
|
72548fbd1b | ||
|
|
a5f6ec4a68 | ||
|
|
4cc68e3133 | ||
|
|
2bacebe900 | ||
|
|
09bea1d12d | ||
|
|
5e9b883ad5 | ||
|
|
1046396edc | ||
|
|
35804458b0 | ||
|
|
6bb64ac5dc | ||
|
|
e5f68947f9 | ||
|
|
289d964b2a | ||
|
|
d0fd9db258 | ||
|
|
0ea997e826 | ||
|
|
2d84059d7f | ||
|
|
ecec42ea60 | ||
|
|
af4629b679 | ||
|
|
f6cf5cc250 | ||
|
|
f37bfd01fe | ||
|
|
d9897e5d1e | ||
|
|
bcb67a4073 | ||
|
|
bcb5a2a173 | ||
|
|
c3b88077ac | ||
|
|
98e1bdc9b5 | ||
|
|
e13408d79f | ||
|
|
8610322682 | ||
|
|
0be42f1f59 | ||
|
|
dcbfcbd0fb | ||
|
|
d0e46186ed | ||
|
|
625a8baed3 | ||
|
|
98db5ed440 | ||
|
|
c4398f9f06 | ||
|
|
a53fa2c1d8 | ||
|
|
5e2b2ac836 | ||
|
|
43f28284f1 | ||
|
|
b17fd7531f | ||
|
|
9859be50a6 | ||
|
|
f209851986 | ||
|
|
4da51636fd | ||
|
|
76a9f8e8b6 | ||
|
|
a477db6b16 | ||
|
|
04fd1b1425 | ||
|
|
c0bf1f2930 | ||
|
|
0bf759333a | ||
|
|
b482d836aa | ||
|
|
3d2d9b78e8 | ||
|
|
99219dc376 | ||
|
|
408703360c | ||
|
|
ac48b8bee6 | ||
|
|
e3a310397b | ||
|
|
b2f7c6fb33 | ||
|
|
653f47f507 | ||
|
|
5b66cec627 | ||
|
|
0d9a2b6e07 | ||
|
|
fb68303825 | ||
|
|
cfe5b018f3 | ||
|
|
b451f2cf7c | ||
|
|
7d25bbafdf | ||
|
|
58e1c2eea3 | ||
|
|
6c791840bd | ||
|
|
3af97306e1 | ||
|
|
d645958631 | ||
|
|
48bc94828e | ||
|
|
a43f4bf5df | ||
|
|
22f705d6c9 | ||
|
|
efcb9ee947 | ||
|
|
a347b32b65 | ||
|
|
7e865b61c2 | ||
|
|
b959f5d9f3 | ||
|
|
df64a00813 | ||
|
|
07dc5c373d | ||
|
|
a6e8616b36 | ||
|
|
7041244728 |
@@ -32,3 +32,6 @@ dist/
|
|||||||
|
|
||||||
# Web
|
# Web
|
||||||
node_modules/
|
node_modules/
|
||||||
|
|
||||||
|
# VS Code
|
||||||
|
.vscode
|
||||||
|
|||||||
+22
-3
@@ -40,7 +40,9 @@ archives:
|
|||||||
- id: uncloud
|
- id: uncloud
|
||||||
builds:
|
builds:
|
||||||
- uncloud
|
- uncloud
|
||||||
format: gz
|
formats:
|
||||||
|
# gz is not compatible with homebrew releases, so we use tar.gz
|
||||||
|
- tar.gz
|
||||||
name_template: >-
|
name_template: >-
|
||||||
{{ .Binary }}_
|
{{ .Binary }}_
|
||||||
{{- if eq .Os "darwin"}}macos
|
{{- if eq .Os "darwin"}}macos
|
||||||
@@ -49,14 +51,16 @@ archives:
|
|||||||
# use zip for windows archives
|
# use zip for windows archives
|
||||||
format_overrides:
|
format_overrides:
|
||||||
- goos: windows
|
- goos: windows
|
||||||
format: zip
|
formats:
|
||||||
|
- zip
|
||||||
files:
|
files:
|
||||||
- none*
|
- none*
|
||||||
|
|
||||||
- id: uncloudd
|
- id: uncloudd
|
||||||
builds:
|
builds:
|
||||||
- uncloudd
|
- uncloudd
|
||||||
format: gz
|
formats:
|
||||||
|
- tar.gz
|
||||||
name_template: "{{ .Binary }}_{{ .Os }}_{{ .Arch }}"
|
name_template: "{{ .Binary }}_{{ .Os }}_{{ .Arch }}"
|
||||||
files:
|
files:
|
||||||
- none*
|
- none*
|
||||||
@@ -70,3 +74,18 @@ changelog:
|
|||||||
exclude:
|
exclude:
|
||||||
- "^docs:"
|
- "^docs:"
|
||||||
- "^test:"
|
- "^test:"
|
||||||
|
|
||||||
|
brews:
|
||||||
|
- name: uncloud
|
||||||
|
description: "Uncloud CLI"
|
||||||
|
homepage: "https://uncloud.run"
|
||||||
|
ids:
|
||||||
|
- uncloud
|
||||||
|
install: |
|
||||||
|
bin.install "uncloud"
|
||||||
|
bin.install_symlink "uncloud" => "uc"
|
||||||
|
skip_upload: false
|
||||||
|
repository:
|
||||||
|
owner: psviderski
|
||||||
|
name: homebrew-tap
|
||||||
|
token: "{{ .Env.GITHUB_TOKEN_HOMEBREW_TAP }}"
|
||||||
|
|||||||
@@ -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,6 +20,10 @@ 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"
|
||||||
|
|||||||
@@ -1,16 +1,76 @@
|
|||||||
<div align="center">
|
<div align="center">
|
||||||
<img src="./website/images/logo.svg" height="100" alt="Uncloud logo"/>
|
<img src="./website/images/logo.svg" height="100" alt="Uncloud logo"/>
|
||||||
<h1>Uncloud</h2>
|
<h1>Uncloud</h1>
|
||||||
A lightweight Docker clustering tool for running web apps on your own servers — from cloud VMs to bare metal.<br>
|
<p><strong>Docker simplicity. Multi-machine power.</strong></p>
|
||||||
Replace Heroku and Render, no Kubernetes required.
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
## Quick start
|
Uncloud is a lightweight clustering and container orchestration tool that lets you deploy and manage web apps across
|
||||||
|
cloud VMs and bare metal with minimized cluster management overhead. It creates a secure WireGuard mesh network between
|
||||||
|
your Docker hosts and provides automatic service discovery, load balancing, ingress with HTTPS, and simple CLI commands
|
||||||
|
to manage your apps.
|
||||||
|
|
||||||
|
Unlike traditional orchestrators, there's no central control plane and quorum to maintain. Each machine maintains a
|
||||||
|
synchronized copy of the cluster state through peer-to-peer communication, keeping cluster operations functional even if
|
||||||
|
some machines go offline.
|
||||||
|
|
||||||
|
Uncloud aims to be the solution for developers who want the flexibility of self-hosted infrastructure without the
|
||||||
|
operational complexity of Kubernetes.
|
||||||
|
|
||||||
|
## ✨ Features
|
||||||
|
|
||||||
|
* **Deploy anywhere**: Combine cloud VMs, dedicated servers, and bare metal into a unified computing environment —
|
||||||
|
regardless of location or provider.
|
||||||
|
* **Zero-config private network**: Automatic WireGuard mesh with peer discovery and NAT traversal. Containers get unique
|
||||||
|
IPs for direct cross-machine communication.
|
||||||
|
* **No control plane**: Fully decentralized design eliminates single points of failure and reduces operational overhead.
|
||||||
|
* **Imperative over declarative**: Favoring imperative operations over state reconciliation simplifies both the mental
|
||||||
|
model and troubleshooting.
|
||||||
|
* **Service discovery**: Built-in DNS server resolves service names to container IPs.
|
||||||
|
* **Automatic HTTPS**: Built-in Caddy reverse proxy handles TLS certificate provisioning and renewal using Let's
|
||||||
|
Encrypt.
|
||||||
|
* **Docker-like CLI**: Familiar commands for managing both infrastructure and applications.
|
||||||
|
* **Remote management**: Control your entire infrastructure through SSH access to any single machine in the cluster.
|
||||||
|
|
||||||
|
Coming soon:
|
||||||
|
|
||||||
|
* Project isolation through environments/namespaces
|
||||||
|
* Infrastructure as Code using Docker Compose format
|
||||||
|
* Persistent volumes and secrets management
|
||||||
|
* Monitoring and log aggregation
|
||||||
|
* Database deployment and management
|
||||||
|
* Curated application catalog
|
||||||
|
|
||||||
|
Here is a diagram of an Uncloud multi-provider cluster of 3 machines:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 💫 Why Uncloud?
|
||||||
|
|
||||||
|
Modern cloud platforms like Heroku and Render offer amazing developer experiences but at a premium price. Traditional
|
||||||
|
container orchestrators like Kubernetes provide power and flexibility but require significant operational expertise. I
|
||||||
|
believe there's a sweet spot in between — a pragmatic solution for the majority of us who aren't running at Google
|
||||||
|
scale. You should be able to:
|
||||||
|
|
||||||
|
* **Own your infrastructure and data**: Whether driven by costs, compliance, or flexibility, run applications on any
|
||||||
|
combination of cloud VMs and personal hardware while maintaining the cloud-like experience you love.
|
||||||
|
* **Stay simple**: Don't worry about control planes, highly-available clusters, or complex YAML configurations for
|
||||||
|
common use cases.
|
||||||
|
* **Build with proven primitives**: Get production-grade networking, deployment primitives, service discovery, load
|
||||||
|
balancing, and ingress with HTTPS out of the box without becoming a distributed systems expert.
|
||||||
|
* **Support sustainable computing** 🌿: Minimize system overhead to maximize resources available for your applications.
|
||||||
|
|
||||||
|
Uncloud's goal is to make deployment and management of containerized applications feel as seamless as using a cloud
|
||||||
|
platform, whether you're running on a $5 VPS, a spare Mac mini, or a rack of bare metal servers.
|
||||||
|
|
||||||
|
## 🚀 Quick start
|
||||||
|
|
||||||
1. Install Uncloud CLI:
|
1. Install Uncloud CLI:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl -fsSL https://raw.githubusercontent.com/psviderski/uncloud/main/scripts/install_cli.sh | sh
|
brew install psviderski/tap/uncloud
|
||||||
|
|
||||||
|
# or using curl (macOS/Linux)
|
||||||
|
curl -fsS https://get.uncloud.run/install.sh | sh
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Initialize your first machine:
|
2. Initialize your first machine:
|
||||||
@@ -19,26 +79,107 @@ curl -fsSL https://raw.githubusercontent.com/psviderski/uncloud/main/scripts/ins
|
|||||||
uc machine init root@your-server-ip
|
uc machine init root@your-server-ip
|
||||||
```
|
```
|
||||||
|
|
||||||
3. Create a DNS A record pointing `app.example.com` to your server's IP address, then deploy your app with
|
3. Create a DNS A record in your DNS provider (Cloudflare, Namecheap, etc.) that points `app.example.com` to your
|
||||||
automatic HTTPS:
|
server's IP address. Allow a few minutes for DNS propagation.
|
||||||
|
4. Deploy your app from a Docker image:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
uc run --name my-app -p app.example.com:8000/https registry/app
|
uc run -p app.example.com:8000/https my-app-image
|
||||||
```
|
```
|
||||||
|
|
||||||
That's it! Your app is now running and accessible at https://app.example.com ✨
|
That's it! Your app is now running and accessible at https://app.example.com ✨
|
||||||
|
|
||||||
## Project status
|
5. Clean up when you're done:
|
||||||
|
|
||||||
Uncloud is an open source project I'm ([@psviderski](https://github.com/psviderski)) actively developing. I'd love to
|
```bash
|
||||||
share this journey with you. [Subscribe](https://uncloud.run/) to follow the progress, get early insights into new
|
uc ls
|
||||||
features, and be the first to know when it's ready for production use.
|
# Copy the service ID from the output and remove it:
|
||||||
|
uc rm my-app-id
|
||||||
|
```
|
||||||
|
|
||||||
I'd also love your input:
|
## ⚙️ How it works
|
||||||
|
|
||||||
- 🐛 Found a bug? [Open an issue](https://github.com/psviderski/uncloud/issues)
|
Check out the [design document](docs/design.md) to understand Uncloud's design philosophy and goals. Here, let's peek
|
||||||
- 💡 Have ideas? [Join the discussion](https://github.com/psviderski/uncloud/discussions)
|
under the hood to see what happens when you run certain commands.
|
||||||
|
|
||||||
## Motivation
|
**When you initialize a new cluster on a machine:**
|
||||||
|
|
||||||
TBD
|
```bash
|
||||||
|
uc machine init root@your-server-ip
|
||||||
|
```
|
||||||
|
|
||||||
|
1. The CLI SSHs into the machine and installs Docker, the `uncloudd` machine daemon and
|
||||||
|
[corrosion](https://github.com/superfly/corrosion) service, managed by systemd.
|
||||||
|
2. Generates a unique WireGuard key pair, allocates a dedicated subnet `10.210.0.0/24` for the machine and its
|
||||||
|
containers, and configures `uncloudd` accordingly. All subsequent communication happens with `uncloudd`
|
||||||
|
through its gRPC API over SSH.
|
||||||
|
3. Configures and starts `corrosion`, a CRDT-based distributed SQLite database to share cluster state between machines.
|
||||||
|
4. Creates a Docker bridge network connected to the WireGuard interface.
|
||||||
|
5. This machine becomes an entry point for the newly created cluster which is stored in the cluster config under
|
||||||
|
`~/.config/uncloud` on your local machine.
|
||||||
|
|
||||||
|
**When you add another machine:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uc machine add ubuntu@second-server-ip
|
||||||
|
```
|
||||||
|
|
||||||
|
1. The second machine gets provisioned just like the first. A non-root SSH user will need `sudo` access.
|
||||||
|
2. Allocates a new subnet `10.210.1.0/24` for the second machine and its containers.
|
||||||
|
3. Registers the second machine in the cluster state and exchanges WireGuard keys with the first machine.
|
||||||
|
4. Both machines establish a WireGuard tunnel between each other, allowing Docker containers connected to the bridge
|
||||||
|
network to communicate directly across machines.
|
||||||
|
5. Configures and starts `corrosion` on the second machine to sync the cluster state.
|
||||||
|
6. The second machine is added as an alternative entry point in the cluster config.
|
||||||
|
7. If one of the machines goes offline, the other machine can still serve cluster operations.
|
||||||
|
|
||||||
|
If one more machine is added, the process repeats with a new subnet. The new machine needs to establish a WireGuard
|
||||||
|
connection with only one of the existing machines. Other machines will learn about it through the shared cluster state
|
||||||
|
and automatically establish a WireGuard tunnel with it.
|
||||||
|
|
||||||
|
**When you run a service:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uc run -p app.example.com:8000/https my-app-image
|
||||||
|
```
|
||||||
|
|
||||||
|
1. CLI picks a machine to run your container.
|
||||||
|
2. `uncloudd` that the CLI communicates with uses [`grpc-proxy`](https://github.com/siderolabs/grpc-proxy) to forward
|
||||||
|
the request to the target machine to launch a container there.
|
||||||
|
3. `uncloudd` on the target machine starts the Docker container in the bridge network and stores its info in the
|
||||||
|
cluster's distributed state.
|
||||||
|
4. The container gets a cluster-unique IP address from the bridge network (in the `10.210.X.2-254` range) and becomes
|
||||||
|
accessible from other machines in the cluster.
|
||||||
|
5. Caddy reverse proxy which runs in [`global`](https://github.com/compose-spec/compose-spec/blob/main/deploy.md#mode)
|
||||||
|
mode on each machine watches the cluster state for new services and updates its configuration to route traffic to the
|
||||||
|
new container.
|
||||||
|
|
||||||
|
Look ma, no control plane or master nodes to maintain! Just a simple overlay network and eventually consistent state
|
||||||
|
sync that lets machines work together. Want to check on things or make changes? Connect to any machine either implicitly
|
||||||
|
using the CLI or directly over SSH. They all have the complete cluster state and can control everything. It's like each
|
||||||
|
machine is a full backup of your control plane.
|
||||||
|
|
||||||
|
## 🏗 Project status
|
||||||
|
|
||||||
|
Uncloud is currently in active development and is **not ready for production use**. Features may change significantly
|
||||||
|
and there may be breaking changes between releases.
|
||||||
|
|
||||||
|
I'd love your input! Here's how you can contribute:
|
||||||
|
|
||||||
|
* 🐛 Found a bug? [Open an issue](https://github.com/psviderski/uncloud/issues)
|
||||||
|
* 💡 Have ideas? [Join the discussion](https://github.com/psviderski/uncloud/discussions)
|
||||||
|
|
||||||
|
## 📫 Stay updated
|
||||||
|
|
||||||
|
* [Subscribe](https://uncloud.run/#subscribe) to my newsletter to follow the progress, get early insights into new
|
||||||
|
features, and be the first to know when it's ready for production use.
|
||||||
|
* Watch this repository for releases.
|
||||||
|
* Follow [@psviderski](https://github.com/psviderski) on GitHub.
|
||||||
|
|
||||||
|
## ❤️ Contributors
|
||||||
|
|
||||||
|
Thank you [@cedws](https://github.com/cedws) for being the first contributor to Uncloud! 🎉
|
||||||
|
|
||||||
|
<a href="https://github.com/psviderski/uncloud/graphs/contributors">
|
||||||
|
<img src="https://contrib.rocks/image?repo=psviderski/uncloud" />
|
||||||
|
</a>
|
||||||
|
|||||||
@@ -0,0 +1,247 @@
|
|||||||
|
package caddy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"github.com/charmbracelet/huh"
|
||||||
|
"github.com/docker/compose/v2/pkg/progress"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"maps"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
"uncloud/internal/cli"
|
||||||
|
"uncloud/internal/cli/client"
|
||||||
|
"uncloud/internal/machine/api/pb"
|
||||||
|
)
|
||||||
|
|
||||||
|
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 deploy(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 deploy(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, client.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 client.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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize a machine and container name resolver to properly format the plan output.
|
||||||
|
machines, err := clusterClient.ListMachines(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("list machines: %w", err)
|
||||||
|
}
|
||||||
|
machineNames := make(map[string]string, len(machines))
|
||||||
|
for _, m := range machines {
|
||||||
|
machineNames[m.Machine.Id] = m.Machine.Name
|
||||||
|
}
|
||||||
|
containerNames := make(map[string]string, len(svc.Containers))
|
||||||
|
for _, c := range svc.Containers {
|
||||||
|
containerNames[c.Container.ID] = c.Container.NameWithoutSlash()
|
||||||
|
}
|
||||||
|
resolver := client.NewNameResolver(machineNames, containerNames)
|
||||||
|
|
||||||
|
if opts.image == "" {
|
||||||
|
fmt.Printf("Target image: %s (latest stable)\n", d.Spec.Container.Image)
|
||||||
|
}
|
||||||
|
|
||||||
|
plan, err := d.Plan(ctx)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, client.ErrNoMatchingMachines) {
|
||||||
|
return fmt.Errorf("no machines found matching: %s", opts.machine)
|
||||||
|
}
|
||||||
|
return fmt.Errorf("plan caddy deployment: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(plan.SequenceOperation.Operations) == 0 {
|
||||||
|
if opts.machine != "" {
|
||||||
|
fmt.Printf("%s service is up to date on selected machines.\n", client.CaddyServiceName)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("%s service is up to date.\n", client.CaddyServiceName)
|
||||||
|
}
|
||||||
|
} 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.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fmt.Println()
|
||||||
|
|
||||||
|
fmt.Println("Deployment plan:")
|
||||||
|
fmt.Println(plan.Format(resolver))
|
||||||
|
fmt.Println()
|
||||||
|
|
||||||
|
confirmed, err := 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()
|
||||||
|
if _, err = clusterClient.GetDomain(ctx); err != nil {
|
||||||
|
if errors.Is(err, client.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 {
|
||||||
|
records, err = clusterClient.CreateIngressRecords(ctx, client.CaddyServiceName)
|
||||||
|
return err
|
||||||
|
}, uncli.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 confirm() (bool, error) {
|
||||||
|
var confirmed bool
|
||||||
|
form := huh.NewForm(
|
||||||
|
huh.NewGroup(
|
||||||
|
huh.NewConfirm().
|
||||||
|
Title(
|
||||||
|
"Do you want to continue?",
|
||||||
|
).
|
||||||
|
Affirmative("Yes!").
|
||||||
|
Negative("No").
|
||||||
|
Value(&confirmed),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
if err := form.Run(); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return confirmed, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func machineFilter(machines []string) client.MachineFilter {
|
||||||
|
if len(machines) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return func(m *pb.MachineInfo) bool {
|
||||||
|
return slices.Contains(machines, m.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,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,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"
|
||||||
|
"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,62 @@
|
|||||||
|
package dns
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"google.golang.org/grpc/codes"
|
||||||
|
"google.golang.org/grpc/status"
|
||||||
|
"uncloud/internal/cli"
|
||||||
|
"uncloud/internal/machine/api/pb"
|
||||||
|
)
|
||||||
|
|
||||||
|
const DefaultUncloudDNSAPIEndpoint = "https://dns.uncloud.run/v1"
|
||||||
|
|
||||||
|
type reserveOptions struct {
|
||||||
|
endpoint string
|
||||||
|
cluster string
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
client, err := uncli.ConnectCluster(ctx, opts.cluster)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("connect to cluster: %w", err)
|
||||||
|
}
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
domain, err := client.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)
|
||||||
|
fmt.Println("Redeploy the Caddy service ('uc caddy deploy') to configure DNS records for the domain " +
|
||||||
|
"to route traffic to the services in the cluster.")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -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/spf13/cobra"
|
||||||
|
"uncloud/internal/cli"
|
||||||
|
"uncloud/internal/cli/client"
|
||||||
|
)
|
||||||
|
|
||||||
|
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, client.ErrNotFound) {
|
||||||
|
return errors.New("no domain reserved")
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println(domain)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
+124
-4
@@ -1,16 +1,29 @@
|
|||||||
package machine
|
package machine
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/cenkalti/backoff/v4"
|
||||||
|
"github.com/docker/compose/v2/pkg/progress"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
"google.golang.org/grpc/codes"
|
||||||
|
"google.golang.org/grpc/status"
|
||||||
|
"google.golang.org/protobuf/types/known/emptypb"
|
||||||
|
"net/netip"
|
||||||
|
"time"
|
||||||
"uncloud/internal/cli"
|
"uncloud/internal/cli"
|
||||||
|
"uncloud/internal/cli/client"
|
||||||
"uncloud/internal/cli/config"
|
"uncloud/internal/cli/config"
|
||||||
|
"uncloud/internal/machine/api/pb"
|
||||||
)
|
)
|
||||||
|
|
||||||
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 +46,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 +69,101 @@ 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...")
|
||||||
|
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, client.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)
|
||||||
|
}
|
||||||
|
|
||||||
|
return 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))
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|||||||
+70
-10
@@ -1,7 +1,9 @@
|
|||||||
package machine
|
package machine
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/docker/compose/v2/pkg/progress"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"uncloud/internal/cli"
|
"uncloud/internal/cli"
|
||||||
@@ -10,10 +12,12 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type initOptions struct {
|
type initOptions struct {
|
||||||
name string
|
name string
|
||||||
network string
|
network string
|
||||||
sshKey string
|
noCaddy bool
|
||||||
cluster string
|
publicIP string
|
||||||
|
sshKey string
|
||||||
|
cluster string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewInitCommand() *cobra.Command {
|
func NewInitCommand() *cobra.Command {
|
||||||
@@ -39,19 +43,27 @@ 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().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().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_*)",
|
||||||
@@ -63,3 +75,51 @@ 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 {
|
||||||
|
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...")
|
||||||
|
|
||||||
|
d, err := client.NewCaddyDeployment("", nil)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("create caddy deployment: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return 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))
|
||||||
|
}
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import (
|
|||||||
"text/tabwriter"
|
"text/tabwriter"
|
||||||
"uncloud/internal/cli"
|
"uncloud/internal/cli"
|
||||||
"uncloud/internal/machine/network"
|
"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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import (
|
|||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
"uncloud/cmd/uncloud/caddy"
|
||||||
|
"uncloud/cmd/uncloud/dns"
|
||||||
"uncloud/cmd/uncloud/machine"
|
"uncloud/cmd/uncloud/machine"
|
||||||
"uncloud/cmd/uncloud/service"
|
"uncloud/cmd/uncloud/service"
|
||||||
"uncloud/internal/cli"
|
"uncloud/internal/cli"
|
||||||
@@ -41,6 +43,8 @@ func main() {
|
|||||||
_ = cmd.MarkPersistentFlagFilename("uncloud-config", "toml")
|
_ = cmd.MarkPersistentFlagFilename("uncloud-config", "toml")
|
||||||
|
|
||||||
cmd.AddCommand(
|
cmd.AddCommand(
|
||||||
|
caddy.NewRootCommand(),
|
||||||
|
dns.NewRootCommand(),
|
||||||
machine.NewRootCommand(),
|
machine.NewRootCommand(),
|
||||||
service.NewRootCommand(),
|
service.NewRootCommand(),
|
||||||
service.NewInspectCommand(),
|
service.NewInspectCommand(),
|
||||||
|
|||||||
@@ -70,21 +70,28 @@ func inspect(ctx context.Context, uncli *cli.CLI, opts inspectOptions) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, ctr := range svc.Containers {
|
for _, ctr := range svc.Containers {
|
||||||
createdAt := time.Unix(ctr.Container.Created, 0)
|
createdAt, err := time.Parse(time.RFC3339Nano, ctr.Container.Created)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("parse created time: %w", err)
|
||||||
|
}
|
||||||
created := units.HumanDuration(time.Now().UTC().Sub(createdAt)) + " ago"
|
created := units.HumanDuration(time.Now().UTC().Sub(createdAt)) + " ago"
|
||||||
|
|
||||||
machine := machinesNamesByID[ctr.MachineID]
|
machine := machinesNamesByID[ctr.MachineID]
|
||||||
if machine == "" {
|
if machine == "" {
|
||||||
machine = ctr.MachineID
|
machine = ctr.MachineID
|
||||||
}
|
}
|
||||||
|
state, err := ctr.Container.HumanState()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("get human state: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
_, err = fmt.Fprintf(
|
_, err = fmt.Fprintf(
|
||||||
tw,
|
tw,
|
||||||
"%s\t%s\t%s\t%s\t%s\n",
|
"%s\t%s\t%s\t%s\t%s\n",
|
||||||
stringid.TruncateID(ctr.Container.ID),
|
stringid.TruncateID(ctr.Container.ID),
|
||||||
ctr.Container.Image,
|
ctr.Container.Config.Image,
|
||||||
created,
|
created,
|
||||||
ctr.Container.Status,
|
state,
|
||||||
machine,
|
machine,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
"text/tabwriter"
|
"text/tabwriter"
|
||||||
"uncloud/internal/cli"
|
"uncloud/internal/cli"
|
||||||
)
|
)
|
||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,10 @@ package service
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/docker/cli/cli/streams"
|
||||||
|
"github.com/docker/compose/v2/pkg/progress"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
"os"
|
||||||
"uncloud/internal/cli"
|
"uncloud/internal/cli"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -40,10 +43,12 @@ func rm(ctx context.Context, uncli *cli.CLI, opts rmOptions) error {
|
|||||||
defer client.Close()
|
defer client.Close()
|
||||||
|
|
||||||
for _, s := range opts.services {
|
for _, s := range opts.services {
|
||||||
if err = client.RemoveService(ctx, s); err != nil {
|
err = progress.RunWithTitle(ctx, func(ctx context.Context) error {
|
||||||
return fmt.Errorf("remove service %q: %w", s, err)
|
if err = client.RemoveService(ctx, s); err != nil {
|
||||||
}
|
return fmt.Errorf("remove service '%s': %w", s, err)
|
||||||
fmt.Printf("Service %q removed.\n", s)
|
}
|
||||||
|
return nil
|
||||||
|
}, streams.NewOut(os.Stdout), "Removing service "+s)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ func NewRootCommand() *cobra.Command {
|
|||||||
Short: "Manage services in an Uncloud cluster.",
|
Short: "Manage services in an Uncloud cluster.",
|
||||||
}
|
}
|
||||||
cmd.AddCommand(
|
cmd.AddCommand(
|
||||||
|
NewInspectCommand(),
|
||||||
NewListCommand(),
|
NewListCommand(),
|
||||||
NewRmCommand(),
|
NewRmCommand(),
|
||||||
NewRunCommand(),
|
NewRunCommand(),
|
||||||
|
|||||||
+61
-18
@@ -4,18 +4,23 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
"uncloud/internal/api"
|
"uncloud/internal/api"
|
||||||
"uncloud/internal/cli"
|
"uncloud/internal/cli"
|
||||||
|
"uncloud/internal/cli/client"
|
||||||
|
"uncloud/internal/machine/api/pb"
|
||||||
)
|
)
|
||||||
|
|
||||||
type runOptions struct {
|
type runOptions struct {
|
||||||
command []string
|
command []string
|
||||||
image string
|
image string
|
||||||
machine string
|
machines []string
|
||||||
mode string
|
mode string
|
||||||
name string
|
name string
|
||||||
publish []string
|
publish []string
|
||||||
volumes []string
|
replicas uint
|
||||||
|
volumes []string
|
||||||
|
|
||||||
cluster string
|
cluster string
|
||||||
}
|
}
|
||||||
@@ -39,25 +44,27 @@ func NewRunCommand() *cobra.Command {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: implement placement constraints and translate --machine to a constraint.
|
|
||||||
//cmd.Flags().StringVarP(
|
|
||||||
// &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 %q (a specified number of containers across "+
|
||||||
"the machines) or %q (one container on every machine).",
|
"the machines) or %q (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().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,11 +79,34 @@ 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: %q", opts.mode)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var machineFilter client.MachineFilter
|
||||||
|
if len(opts.machines) > 0 {
|
||||||
|
var machines []string
|
||||||
|
for _, value := range opts.machines {
|
||||||
|
if value == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
mlist := strings.Split(value, ",")
|
||||||
|
for _, m := range mlist {
|
||||||
|
if m = strings.TrimSpace(m); m != "" {
|
||||||
|
machines = append(machines, m)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(machines) > 0 {
|
||||||
|
machineFilter = func(m *pb.MachineInfo) bool {
|
||||||
|
return slices.Contains(machines, m.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ports := make([]api.PortSpec, len(opts.publish))
|
ports := make([]api.PortSpec, len(opts.publish))
|
||||||
for i, publishPort := range opts.publish {
|
for i, publishPort := range opts.publish {
|
||||||
port, err := api.ParsePortSpec(publishPort)
|
port, err := api.ParsePortSpec(publishPort)
|
||||||
@@ -93,9 +123,10 @@ func run(ctx context.Context, uncli *cli.CLI, opts runOptions) error {
|
|||||||
Image: opts.image,
|
Image: opts.image,
|
||||||
Volumes: opts.volumes,
|
Volumes: opts.volumes,
|
||||||
},
|
},
|
||||||
Mode: opts.mode,
|
Mode: opts.mode,
|
||||||
Name: opts.name,
|
Name: opts.name,
|
||||||
Ports: ports,
|
Ports: ports,
|
||||||
|
Replicas: opts.replicas,
|
||||||
}
|
}
|
||||||
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)
|
||||||
@@ -107,9 +138,21 @@ func run(ctx context.Context, uncli *cli.CLI, opts runOptions) error {
|
|||||||
}
|
}
|
||||||
defer client.Close()
|
defer client.Close()
|
||||||
|
|
||||||
if _, err = client.RunService(ctx, spec); err != nil {
|
resp, err := client.RunService(ctx, spec, machineFilter)
|
||||||
|
if err != nil {
|
||||||
return fmt.Errorf("run service: %w", err)
|
return fmt.Errorf("run service: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
svc, err := client.InspectService(ctx, resp.ID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("inspect service: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Printf("%s endpoints:\n", svc.Name)
|
||||||
|
for _, endpoint := range svc.Endpoints() {
|
||||||
|
fmt.Printf(" • %s\n", endpoint)
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
module uncloud
|
module 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
|
||||||
@@ -13,11 +14,12 @@ require (
|
|||||||
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf
|
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf
|
||||||
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
|
||||||
@@ -37,15 +39,15 @@ 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.29.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
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
@@ -57,6 +59,7 @@ require (
|
|||||||
github.com/Masterminds/semver/v3 v3.2.1 // indirect
|
github.com/Masterminds/semver/v3 v3.2.1 // indirect
|
||||||
github.com/Masterminds/sprig/v3 v3.2.3 // indirect
|
github.com/Masterminds/sprig/v3 v3.2.3 // indirect
|
||||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||||
|
github.com/OneOfOne/xxhash v1.2.8 // indirect
|
||||||
github.com/antlr4-go/antlr/v4 v4.13.0 // indirect
|
github.com/antlr4-go/antlr/v4 v4.13.0 // indirect
|
||||||
github.com/armon/circbuf v0.0.0-20190214190532-5111143e8da2 // indirect
|
github.com/armon/circbuf v0.0.0-20190214190532-5111143e8da2 // indirect
|
||||||
github.com/armon/go-metrics v0.4.1 // indirect
|
github.com/armon/go-metrics v0.4.1 // indirect
|
||||||
@@ -79,6 +82,7 @@ require (
|
|||||||
github.com/charmbracelet/x/exp/strings v0.0.0-20240919170804-a4978c8e603a // indirect
|
github.com/charmbracelet/x/exp/strings v0.0.0-20240919170804-a4978c8e603a // indirect
|
||||||
github.com/charmbracelet/x/term v0.2.0 // indirect
|
github.com/charmbracelet/x/term v0.2.0 // indirect
|
||||||
github.com/chzyer/readline v1.5.1 // indirect
|
github.com/chzyer/readline v1.5.1 // indirect
|
||||||
|
github.com/cloudflare/cfssl v1.6.4 // indirect
|
||||||
github.com/compose-spec/compose-go/v2 v2.4.5 // 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
|
||||||
@@ -87,9 +91,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
|
||||||
@@ -131,7 +136,7 @@ require (
|
|||||||
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
|
||||||
@@ -192,6 +197,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
|
||||||
@@ -261,22 +267,24 @@ 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/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
|
||||||
@@ -287,13 +295,13 @@ 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
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ
|
|||||||
github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
|
github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
|
||||||
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
|
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
|
||||||
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
|
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
|
||||||
|
github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww=
|
||||||
|
github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
|
||||||
github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
|
github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
|
||||||
github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
|
github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
|
||||||
github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0=
|
github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0=
|
||||||
@@ -45,8 +47,9 @@ github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERo
|
|||||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||||
github.com/Microsoft/hcsshim v0.12.5 h1:bpTInLlDy/nDRWFVcefDZZ1+U8tS+rz3MxjKgu9boo0=
|
github.com/Microsoft/hcsshim v0.12.5 h1:bpTInLlDy/nDRWFVcefDZZ1+U8tS+rz3MxjKgu9boo0=
|
||||||
github.com/Microsoft/hcsshim v0.12.5/go.mod h1:tIUGego4G1EN5Hb6KC90aDYiUI2dqLSTTOCjVNpOgZ8=
|
github.com/Microsoft/hcsshim v0.12.5/go.mod h1:tIUGego4G1EN5Hb6KC90aDYiUI2dqLSTTOCjVNpOgZ8=
|
||||||
github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE=
|
|
||||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||||
|
github.com/OneOfOne/xxhash v1.2.8 h1:31czK/TI9sNkxIKfaUfGlU47BAxQ0ztGgd9vPyqimf8=
|
||||||
|
github.com/OneOfOne/xxhash v1.2.8/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q=
|
||||||
github.com/Shopify/logrus-bugsnag v0.0.0-20170309145241-6dbc35f2c30d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ=
|
github.com/Shopify/logrus-bugsnag v0.0.0-20170309145241-6dbc35f2c30d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ=
|
||||||
github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d h1:UrqY+r/OJnIp5u0s1SbQ8dVfLCZJsnvazdBP5hS4iRs=
|
github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d h1:UrqY+r/OJnIp5u0s1SbQ8dVfLCZJsnvazdBP5hS4iRs=
|
||||||
github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ=
|
github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ=
|
||||||
@@ -166,8 +169,9 @@ github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38
|
|||||||
github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag=
|
github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag=
|
||||||
github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I=
|
github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I=
|
||||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||||
github.com/cloudflare/cfssl v0.0.0-20180223231731-4e2dcbde5004 h1:lkAMpLVBDaj17e85keuznYcH5rqI438v41pKcBl4ZxQ=
|
|
||||||
github.com/cloudflare/cfssl v0.0.0-20180223231731-4e2dcbde5004/go.mod h1:yMWuSON2oQp+43nFtAV/uvKQIFpSPerB57DCt9t8sSA=
|
github.com/cloudflare/cfssl v0.0.0-20180223231731-4e2dcbde5004/go.mod h1:yMWuSON2oQp+43nFtAV/uvKQIFpSPerB57DCt9t8sSA=
|
||||||
|
github.com/cloudflare/cfssl v1.6.4 h1:NMOvfrEjFfC63K3SGXgAnFdsgkmiq4kATme5BfcqrO8=
|
||||||
|
github.com/cloudflare/cfssl v1.6.4/go.mod h1:8b3CQMxfWPAeom3zBnGJ6sd+G1NkL5TXqmDXacb+1J0=
|
||||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||||
github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I=
|
github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I=
|
||||||
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
|
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
|
||||||
@@ -197,9 +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=
|
||||||
@@ -216,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=
|
||||||
@@ -251,8 +255,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=
|
||||||
@@ -393,6 +397,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=
|
||||||
@@ -425,8 +431,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=
|
||||||
@@ -591,6 +597,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.3.3/go.mod h1:2BljVx/86SuTyjE+aPYlHCTNvZrnJXghYGpNiXLBMCQ=
|
||||||
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=
|
||||||
@@ -727,6 +735,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=
|
||||||
@@ -920,8 +929,8 @@ github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJ
|
|||||||
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=
|
||||||
@@ -1048,8 +1057,8 @@ github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljT
|
|||||||
github.com/urfave/cli v1.22.10/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
|
github.com/urfave/cli v1.22.10/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
|
||||||
github.com/urfave/cli v1.22.16 h1:MH0k6uJxdwdeWQTwhSO42Pwr4YLrNLwBtg1MRgTqPdQ=
|
github.com/urfave/cli v1.22.16 h1:MH0k6uJxdwdeWQTwhSO42Pwr4YLrNLwBtg1MRgTqPdQ=
|
||||||
github.com/urfave/cli v1.22.16/go.mod h1:EeJR6BKodywf4zciqrdw6hpCPk68JO9z5LazXZMn5Po=
|
github.com/urfave/cli v1.22.16/go.mod h1:EeJR6BKodywf4zciqrdw6hpCPk68JO9z5LazXZMn5Po=
|
||||||
github.com/vbatts/tar-split v0.11.5 h1:3bHCTIheBm1qFTcgh9oPu+nNBtX+XJIupG/vacinCts=
|
github.com/vbatts/tar-split v0.11.6 h1:4SjTW5+PU11n6fZenf2IPoV8/tz3AaYHMWjf23envGs=
|
||||||
github.com/vbatts/tar-split v0.11.5/go.mod h1:yZbwRsSeGjusneWgA781EKej9HF8vme8okylkAeNKLk=
|
github.com/vbatts/tar-split v0.11.6/go.mod h1:dqKNtesIOr2j2Qv3W/cHjnvk9I8+G7oAkFDFN6TCBEI=
|
||||||
github.com/vishvananda/netlink v1.3.0 h1:X7l42GfcV4S6E4vHTsw48qbrV+9PVojNfIhZcwQdrZk=
|
github.com/vishvananda/netlink v1.3.0 h1:X7l42GfcV4S6E4vHTsw48qbrV+9PVojNfIhZcwQdrZk=
|
||||||
github.com/vishvananda/netlink v1.3.0/go.mod h1:i6NetklAujEcC6fK0JPjT8qSwWyO0HLn4UKG+hGqeJs=
|
github.com/vishvananda/netlink v1.3.0/go.mod h1:i6NetklAujEcC6fK0JPjT8qSwWyO0HLn4UKG+hGqeJs=
|
||||||
github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8=
|
github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8=
|
||||||
@@ -1058,6 +1067,8 @@ github.com/warpfork/go-testmark v0.12.1 h1:rMgCpJfwy1sJ50x0M0NgyphxYYPMOODIJHhsX
|
|||||||
github.com/warpfork/go-testmark v0.12.1/go.mod h1:kHwy7wfvGSPh1rQJYKayD4AbtNaeyZdcGi9tNJTaa5Y=
|
github.com/warpfork/go-testmark v0.12.1/go.mod h1:kHwy7wfvGSPh1rQJYKayD4AbtNaeyZdcGi9tNJTaa5Y=
|
||||||
github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0 h1:GDDkbFiaK8jsSDJfjId/PEGEShv6ugrt4kYsC5UIDaQ=
|
github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0 h1:GDDkbFiaK8jsSDJfjId/PEGEShv6ugrt4kYsC5UIDaQ=
|
||||||
github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0/go.mod h1:x6AKhvSSexNrVSrViXSHUEbICjmGXhtgABaHIySUSGw=
|
github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0/go.mod h1:x6AKhvSSexNrVSrViXSHUEbICjmGXhtgABaHIySUSGw=
|
||||||
|
github.com/weppos/publicsuffix-go v0.15.1-0.20210511084619-b1f36a2d6c0b h1:FsyNrX12e5BkplJq7wKOLk0+C6LZ+KGXvuEcKUYm5ss=
|
||||||
|
github.com/weppos/publicsuffix-go v0.15.1-0.20210511084619-b1f36a2d6c0b/go.mod h1:HYux0V0Zi04bHNwOHy4cXJVz/TQjYonnF6aoYhj+3QE=
|
||||||
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
||||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
@@ -1070,37 +1081,43 @@ github.com/zeebo/blake3 v0.2.4/go.mod h1:7eeQ6d2iXWRGF6npfaxl2CU+xy2Fjo2gxeyZGCR
|
|||||||
github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo=
|
github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo=
|
||||||
github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4=
|
github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4=
|
||||||
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
|
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
|
||||||
|
github.com/zmap/zcrypto v0.0.0-20210511125630-18f1e0152cfc h1:zkGwegkOW709y0oiAraH/3D8njopUR/pARHv4tZZ6pw=
|
||||||
|
github.com/zmap/zcrypto v0.0.0-20210511125630-18f1e0152cfc/go.mod h1:FM4U1E3NzlNMRnSUTU3P1UdukWhYGifqEsjk9fn7BCk=
|
||||||
|
github.com/zmap/zlint/v3 v3.1.0 h1:WjVytZo79m/L1+/Mlphl09WBob6YTGljN5IGWZFpAv0=
|
||||||
|
github.com/zmap/zlint/v3 v3.1.0/go.mod h1:L7t8s3sEKkb0A2BxGy1IWrxt1ZATa1R4QfJZaQOD3zU=
|
||||||
go.etcd.io/bbolt v1.3.11 h1:yGEzV1wPz2yVCLsD8ZAiGHhHVlczyC9d1rP43/VCRJ0=
|
go.etcd.io/bbolt v1.3.11 h1:yGEzV1wPz2yVCLsD8ZAiGHhHVlczyC9d1rP43/VCRJ0=
|
||||||
go.etcd.io/bbolt v1.3.11/go.mod h1:dksAq7YMXoljX0xu6VF5DMZGbhYYoLUalEiSySYAS4I=
|
go.etcd.io/bbolt v1.3.11/go.mod h1:dksAq7YMXoljX0xu6VF5DMZGbhYYoLUalEiSySYAS4I=
|
||||||
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
|
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
|
||||||
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
|
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
|
||||||
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
|
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
|
||||||
|
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||||
|
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 h1:4Pp6oUg3+e/6M4C0A/3kJ2VYa++dsWVTtGgLVj5xtHg=
|
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 h1:4Pp6oUg3+e/6M4C0A/3kJ2VYa++dsWVTtGgLVj5xtHg=
|
||||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0/go.mod h1:Mjt1i1INqiaoZOMGR1RIUJN+i3ChKoFRqzrRQhlkbs0=
|
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0/go.mod h1:Mjt1i1INqiaoZOMGR1RIUJN+i3ChKoFRqzrRQhlkbs0=
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.46.1 h1:gbhw/u49SS3gkPWiYweQNJGm/uJN5GkI/FrosxSHT7A=
|
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.46.1 h1:gbhw/u49SS3gkPWiYweQNJGm/uJN5GkI/FrosxSHT7A=
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.46.1/go.mod h1:GnOaBaFQ2we3b9AGWJpsBa7v1S5RlQzlC3O7dRMxZhM=
|
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.46.1/go.mod h1:GnOaBaFQ2we3b9AGWJpsBa7v1S5RlQzlC3O7dRMxZhM=
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.52.0 h1:9l89oX4ba9kHbBol3Xin3leYJ+252h0zszDtBwyKe2A=
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 h1:yd02MEjBdJkG3uabWP9apV+OuWRIXGDuJEUJbOHmCFU=
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.52.0/go.mod h1:XLZfZboOJWHNKUv7eH0inh0E9VV6eWDFB/9yJyTLPp0=
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0/go.mod h1:umTcuxiv1n/s/S6/c2AT/g2CQ7u5C59sHDNmfSwgz7Q=
|
||||||
go.opentelemetry.io/otel v1.32.0 h1:WnBN+Xjcteh0zdk01SVqV55d/m62NJLJdIyb4y/WO5U=
|
go.opentelemetry.io/otel v1.33.0 h1:/FerN9bax5LoK51X/sI0SVYrjSE0/yUL7DpxW4K3FWw=
|
||||||
go.opentelemetry.io/otel v1.32.0/go.mod h1:00DCVSB0RQcnzlwyTfqtxSm+DRr9hpYrHjNGiBHVQIg=
|
go.opentelemetry.io/otel v1.33.0/go.mod h1:SUUkR6csvUQl+yjReHu5uM3EtVV7MBm5FHKRlNx4I8I=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.44.0 h1:jd0+5t/YynESZqsSyPz+7PAFdEop0dlN0+PkyHYo8oI=
|
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.44.0 h1:jd0+5t/YynESZqsSyPz+7PAFdEop0dlN0+PkyHYo8oI=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.44.0/go.mod h1:U707O40ee1FpQGyhvqnzmCJm1Wh6OX6GGBVn0E6Uyyk=
|
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.44.0/go.mod h1:U707O40ee1FpQGyhvqnzmCJm1Wh6OX6GGBVn0E6Uyyk=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.27.0 h1:R9DE4kQ4k+YtfLI2ULwX82VtNQ2J8yZmA7ZIF/D+7Mc=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0 h1:Vh5HayB/0HHfOQA7Ctx69E/Y/DcQSMPpKANYVMQ7fBA=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.27.0/go.mod h1:OQFyQVrDlbe+R7xrEyDr/2Wr67Ol0hRUgsfA+V5A95s=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0/go.mod h1:cpgtDBaqD/6ok/UG0jT15/uKjAY8mRA53diogHBg3UI=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0 h1:qFffATk0X+HD+f1Z8lswGiOQYKHRlzfmdJm0wEaVrFA=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0 h1:qFffATk0X+HD+f1Z8lswGiOQYKHRlzfmdJm0wEaVrFA=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0/go.mod h1:MOiCmryaYtc+V0Ei+Tx9o5S1ZjA7kzLucuVuyzBZloQ=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0/go.mod h1:MOiCmryaYtc+V0Ei+Tx9o5S1ZjA7kzLucuVuyzBZloQ=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.27.0 h1:QY7/0NeRPKlzusf40ZE4t1VlMKbqSNT7cJRYzWuja0s=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.27.0 h1:QY7/0NeRPKlzusf40ZE4t1VlMKbqSNT7cJRYzWuja0s=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.27.0/go.mod h1:HVkSiDhTM9BoUJU8qE6j2eSWLLXvi1USXjyd2BXT8PY=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.27.0/go.mod h1:HVkSiDhTM9BoUJU8qE6j2eSWLLXvi1USXjyd2BXT8PY=
|
||||||
go.opentelemetry.io/otel/metric v1.32.0 h1:xV2umtmNcThh2/a/aCP+h64Xx5wsj8qqnkYZktzNa0M=
|
go.opentelemetry.io/otel/metric v1.33.0 h1:r+JOocAyeRVXD8lZpjdQjzMadVZp2M4WmQ+5WtEnklQ=
|
||||||
go.opentelemetry.io/otel/metric v1.32.0/go.mod h1:jH7CIbbK6SH2V2wE16W05BHCtIDzauciCRLoc/SyMv8=
|
go.opentelemetry.io/otel/metric v1.33.0/go.mod h1:L9+Fyctbp6HFTddIxClbQkjtubW6O9QS3Ann/M82u6M=
|
||||||
go.opentelemetry.io/otel/sdk v1.32.0 h1:RNxepc9vK59A8XsgZQouW8ue8Gkb4jpWtJm9ge5lEG4=
|
go.opentelemetry.io/otel/sdk v1.33.0 h1:iax7M131HuAm9QkZotNHEfstof92xM+N8sr3uHXc2IM=
|
||||||
go.opentelemetry.io/otel/sdk v1.32.0/go.mod h1:LqgegDBjKMmb2GC6/PrTnteJG39I8/vJCAP9LlJXEjU=
|
go.opentelemetry.io/otel/sdk v1.33.0/go.mod h1:A1Q5oi7/9XaMlIWzPSxLRWOI8nG3FnzHJNbiENQuihM=
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.32.0 h1:rZvFnvmvawYb0alrYkjraqJq0Z4ZUJAiyYCU9snn1CU=
|
go.opentelemetry.io/otel/sdk/metric v1.32.0 h1:rZvFnvmvawYb0alrYkjraqJq0Z4ZUJAiyYCU9snn1CU=
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.32.0/go.mod h1:PWeZlq0zt9YkYAp3gjKZ0eicRYvOh1Gd+X99x6GHpCQ=
|
go.opentelemetry.io/otel/sdk/metric v1.32.0/go.mod h1:PWeZlq0zt9YkYAp3gjKZ0eicRYvOh1Gd+X99x6GHpCQ=
|
||||||
go.opentelemetry.io/otel/trace v1.32.0 h1:WIC9mYrXf8TmY/EXuULKc8hR17vE+Hjv2cssQDe03fM=
|
go.opentelemetry.io/otel/trace v1.33.0 h1:cCJuF7LRjUFso9LPnEAHJDB2pqzp+hbO8eu1qqW2d/s=
|
||||||
go.opentelemetry.io/otel/trace v1.32.0/go.mod h1:+i4rkvCraA+tG6AzwloGaCtkx53Fa+L+V8e9a7YvhT8=
|
go.opentelemetry.io/otel/trace v1.33.0/go.mod h1:uIcdVUZMpTAmz0tI1z04GoVSezK37CbGV4fr1f2nBck=
|
||||||
go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0=
|
go.opentelemetry.io/proto/otlp v1.4.0 h1:TA9WRvW6zMwP+Ssb6fLoUIuirti1gGbP28GcKG1jgeg=
|
||||||
go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8=
|
go.opentelemetry.io/proto/otlp v1.4.0/go.mod h1:PPBWZIP98o2ElSqI35IHfu7hIhSwvc5N38Jw8pXuGFY=
|
||||||
go.step.sm/cli-utils v0.9.0 h1:55jYcsQbnArNqepZyAwcato6Zy2MoZDRkWW+jF+aPfQ=
|
go.step.sm/cli-utils v0.9.0 h1:55jYcsQbnArNqepZyAwcato6Zy2MoZDRkWW+jF+aPfQ=
|
||||||
go.step.sm/cli-utils v0.9.0/go.mod h1:Y/CRoWl1FVR9j+7PnAewufAwKmBOTzR6l9+7EYGAnp8=
|
go.step.sm/cli-utils v0.9.0/go.mod h1:Y/CRoWl1FVR9j+7PnAewufAwKmBOTzR6l9+7EYGAnp8=
|
||||||
go.step.sm/crypto v0.45.0 h1:Z0WYAaaOYrJmKP9sJkPW+6wy3pgN3Ija8ek/D4serjc=
|
go.step.sm/crypto v0.45.0 h1:Z0WYAaaOYrJmKP9sJkPW+6wy3pgN3Ija8ek/D4serjc=
|
||||||
@@ -1161,8 +1178,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=
|
||||||
@@ -1202,8 +1219,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=
|
||||||
@@ -1266,8 +1283,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.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
|
||||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
@@ -1275,8 +1292,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=
|
||||||
@@ -1310,8 +1327,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=
|
||||||
@@ -1334,10 +1351,10 @@ google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98
|
|||||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||||
google.golang.org/genproto v0.0.0-20240401170217-c3f982113cda h1:wu/KJm9KJwpfHWhkkZGohVC6KRrc1oJNr4jwtQMOQXw=
|
google.golang.org/genproto v0.0.0-20240401170217-c3f982113cda h1:wu/KJm9KJwpfHWhkkZGohVC6KRrc1oJNr4jwtQMOQXw=
|
||||||
google.golang.org/genproto v0.0.0-20240401170217-c3f982113cda/go.mod h1:g2LLCvCeCSir/JJSWosk19BR4NVxGqHUC6rxIRsd7Aw=
|
google.golang.org/genproto v0.0.0-20240401170217-c3f982113cda/go.mod h1:g2LLCvCeCSir/JJSWosk19BR4NVxGqHUC6rxIRsd7Aw=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 h1:hjSy6tcFQZ171igDaN5QHOw2n6vx40juYbC/x67CEhc=
|
google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 h1:CkkIfIt50+lT6NHAVoRYEyAvQGFM7xEwXUUywFvEb3Q=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:qpvKtACPCQhAdu3PyQgV4l3LMXZEtft7y8QcarRsp9I=
|
google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576/go.mod h1:1R3kvZ1dtP3+4p4d3G8uJ8rFk/fWlScl38vanWACI08=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20241007155032-5fefd90f89a9 h1:QCqS/PdaHTSWGvupk2F/ehwHtGc0/GYkT+3GAcR1CCc=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576 h1:8ZmaLZE4XWrtU3MyClkYqqtl6Oegr3235h7jxsDyqCY=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20241007155032-5fefd90f89a9/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU=
|
||||||
google.golang.org/grpc v1.0.5/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
|
google.golang.org/grpc v1.0.5/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
|
||||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||||
@@ -1345,8 +1362,8 @@ google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyac
|
|||||||
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||||
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
|
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
|
||||||
google.golang.org/grpc v1.68.0 h1:aHQeeJbo8zAkAa3pRzrVjZlbz6uSfeOXlJNQM0RAbz0=
|
google.golang.org/grpc v1.68.1 h1:oI5oTa11+ng8r8XMMN7jAOmWfPZWbYpCFaMUTACxkM0=
|
||||||
google.golang.org/grpc v1.68.0/go.mod h1:fmSPC5AsjSBCK54MyHRx48kpOti1/jRfOlwEWywNjWA=
|
google.golang.org/grpc v1.68.1/go.mod h1:+q1XYFJjShcqn0QZHvCyeR4CXPA+llXIeUIfIe00waw=
|
||||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||||
@@ -1356,8 +1373,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=
|
||||||
|
|||||||
+126
-27
@@ -1,9 +1,11 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"github.com/docker/docker/api/types"
|
"github.com/docker/docker/api/types"
|
||||||
"regexp"
|
"github.com/docker/go-units"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -15,30 +17,39 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Container struct {
|
type Container struct {
|
||||||
types.Container
|
types.ContainerJSON
|
||||||
|
}
|
||||||
|
|
||||||
|
// NameWithoutSlash returns the container name without the leading slash.
|
||||||
|
// TODO: modify Name in original ContainerJSON structure when inspecting a Docker container and get rid of this method.
|
||||||
|
func (c *Container) NameWithoutSlash() string {
|
||||||
|
return c.Name[1:]
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServiceID returns the ID of the service this container belongs to.
|
// ServiceID returns the ID of the service this container belongs to.
|
||||||
func (c *Container) ServiceID() string {
|
func (c *Container) ServiceID() string {
|
||||||
return c.Labels[LabelServiceID]
|
return c.Config.Labels[LabelServiceID]
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServiceName returns the name of the service this container belongs to.
|
// ServiceName returns the name of the service this container belongs to.
|
||||||
func (c *Container) ServiceName() string {
|
func (c *Container) ServiceName() string {
|
||||||
return c.Labels[LabelServiceName]
|
return c.Config.Labels[LabelServiceName]
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServiceMode returns the replication mode of the service this container belongs to.
|
// ServiceMode returns the replication mode of the service this container belongs to.
|
||||||
func (c *Container) ServiceMode() string {
|
func (c *Container) ServiceMode() string {
|
||||||
return c.Labels[LabelServiceMode]
|
return c.Config.Labels[LabelServiceMode]
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServicePorts returns the ports this container publishes as part of its service.
|
// ServicePorts returns the ports this container publishes as part of its service.
|
||||||
func (c *Container) ServicePorts() ([]PortSpec, error) {
|
func (c *Container) ServicePorts() ([]PortSpec, error) {
|
||||||
encoded, ok := c.Labels[LabelServicePorts]
|
encoded, ok := c.Config.Labels[LabelServicePorts]
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
if strings.TrimSpace(encoded) == "" {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
publishPorts := strings.Split(encoded, ",")
|
publishPorts := strings.Split(encoded, ",")
|
||||||
ports := make([]PortSpec, len(publishPorts))
|
ports := make([]PortSpec, len(publishPorts))
|
||||||
@@ -53,34 +64,122 @@ func (c *Container) ServicePorts() ([]PortSpec, error) {
|
|||||||
return ports, nil
|
return ports, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// runningStatusRegex matches the status string of a running container.
|
// ServiceSpec constructs a service spec from the container's configuration.
|
||||||
// - "Up 3 minutes (healthy)" -> groups: ["Up 3 minutes (healthy)", "healthy"]
|
func (c *Container) ServiceSpec() (ServiceSpec, error) {
|
||||||
// - "Up 5 seconds" -> groups: ["Up 5 seconds", ""]
|
ports, err := c.ServicePorts()
|
||||||
// - "Up 2 hours (unhealthy)" -> groups: ["Up 2 hours (unhealthy)", "unhealthy"]
|
if err != nil {
|
||||||
// - "Up 1 minute (health: starting)" -> groups: ["Up 1 minute (health: starting)", "health: starting"]
|
return ServiceSpec{}, fmt.Errorf("get service ports: %w", err)
|
||||||
// - "Restarting (0) 5 seconds ago" -> no match
|
}
|
||||||
// See https://github.com/moby/moby/blob/c130ce1f5d1e38b98a97044a39557de43bc0d58f/container/state.go#L77-L90
|
|
||||||
// for more details on how the status string for a running container is formatted.
|
|
||||||
var runningStatusRegex = regexp.MustCompile(`^Up [^(]+(?:\(([^)]+)\))?$`)
|
|
||||||
|
|
||||||
// Healthy determines if the container is running and healthy based on its status string.
|
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.
|
||||||
// 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 {
|
||||||
if c.State != "running" {
|
if !c.State.Running || c.State.Paused || c.State.Restarting {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
matches := runningStatusRegex.FindStringSubmatch(c.Status)
|
// If there's no health status (no health check configured), container is considered healthy.
|
||||||
// Not "Up" or invalid format.
|
if c.State.Health == nil {
|
||||||
if matches == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// If there's no health status (no health check configured so no parentheses), container is considered healthy.
|
|
||||||
if matches[1] == "" {
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the health status in parentheses is "healthy", the container is considered healthy.
|
return c.State.Health.Status == types.Healthy
|
||||||
return matches[1] == types.Healthy
|
}
|
||||||
|
|
||||||
|
// HumanState returns a human-readable description of the container's state. Based on the Docker implementation:
|
||||||
|
// https://github.com/moby/moby/blob/b343d235a0a1f30c8f05b1d651238e72158dc25d/container/state.go#L79-L113
|
||||||
|
func (c *Container) HumanState() (string, error) {
|
||||||
|
startedAt, err := time.Parse(time.RFC3339Nano, c.State.StartedAt)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("parse started time: %w", err)
|
||||||
|
}
|
||||||
|
finishedAt, err := time.Parse(time.RFC3339Nano, c.State.FinishedAt)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("parse finished time: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.State.Running {
|
||||||
|
if c.State.Paused {
|
||||||
|
return fmt.Sprintf("Up %s (Paused)", units.HumanDuration(time.Now().UTC().Sub(startedAt))), nil
|
||||||
|
}
|
||||||
|
if c.State.Restarting {
|
||||||
|
return fmt.Sprintf("Restarting (%d) %s ago",
|
||||||
|
c.State.ExitCode, units.HumanDuration(time.Now().UTC().Sub(finishedAt))), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.State.Health != nil {
|
||||||
|
status := c.State.Health.Status
|
||||||
|
if status == types.Starting {
|
||||||
|
status = "health: " + status
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("Up %s (%s)", units.HumanDuration(time.Now().UTC().Sub(startedAt)), status), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("Up %s", units.HumanDuration(time.Now().UTC().Sub(startedAt))), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.State.Status == "removing" {
|
||||||
|
return "Removal In Progress", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.State.Dead {
|
||||||
|
return "Dead", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if startedAt.IsZero() {
|
||||||
|
return "Created", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if finishedAt.IsZero() {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("Exited (%d) %s ago",
|
||||||
|
c.State.ExitCode, units.HumanDuration(time.Now().UTC().Sub(finishedAt))), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConflictingServicePorts returns a list of service ports that conflict with the given ports.
|
||||||
|
func (c *Container) ConflictingServicePorts(ports []PortSpec) ([]PortSpec, error) {
|
||||||
|
svcPorts, err := c.ServicePorts()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("get service ports: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var conflicting []PortSpec
|
||||||
|
for _, p := range ports {
|
||||||
|
if p.Mode != PortModeHost {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Two host ports conflict if they have the same published port number and protocol, and either:
|
||||||
|
// * At least one host IP is not set (meaning it uses all interfaces)
|
||||||
|
// * Both host IPs are identical
|
||||||
|
for _, svcPort := range svcPorts {
|
||||||
|
if svcPort.Mode != PortModeHost ||
|
||||||
|
svcPort.PublishedPort != p.PublishedPort ||
|
||||||
|
svcPort.Protocol != p.Protocol {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if !svcPort.HostIP.IsValid() || !p.HostIP.IsValid() || svcPort.HostIP.Compare(p.HostIP) == 0 {
|
||||||
|
conflicting = append(conflicting, p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return conflicting, nil
|
||||||
}
|
}
|
||||||
|
|||||||
+294
-40
@@ -2,91 +2,345 @@ package api
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/docker/docker/api/types"
|
"github.com/docker/docker/api/types"
|
||||||
|
"github.com/docker/docker/api/types/container"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"net/netip"
|
||||||
|
"reflect"
|
||||||
"testing"
|
"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()
|
||||||
|
|
||||||
t.Run("exited", func(t *testing.T) {
|
t.Run("exited", func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
c := &Container{Container: types.Container{
|
c := &Container{ContainerJSON: types.ContainerJSON{
|
||||||
State: "exited",
|
ContainerJSONBase: &types.ContainerJSONBase{
|
||||||
Status: "Exited (0) 2 minutes ago",
|
State: &types.ContainerState{
|
||||||
|
Running: false,
|
||||||
|
Dead: false,
|
||||||
|
ExitCode: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
assert.False(t, c.Healthy())
|
assert.False(t, c.Healthy())
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("running with no health check", func(t *testing.T) {
|
t.Run("running with no health check", func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
c := &Container{Container: types.Container{
|
c := &Container{ContainerJSON: types.ContainerJSON{
|
||||||
State: "running",
|
ContainerJSONBase: &types.ContainerJSONBase{
|
||||||
Status: "Up 5 minutes",
|
State: &types.ContainerState{
|
||||||
|
Running: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
assert.True(t, c.Healthy())
|
assert.True(t, c.Healthy())
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("running and healthy", func(t *testing.T) {
|
t.Run("running and healthy", func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
c := &Container{Container: types.Container{
|
c := &Container{ContainerJSON: types.ContainerJSON{
|
||||||
State: "running",
|
ContainerJSONBase: &types.ContainerJSONBase{
|
||||||
Status: "Up 3 minutes (healthy)",
|
State: &types.ContainerState{
|
||||||
|
Running: true,
|
||||||
|
Health: &types.Health{
|
||||||
|
Status: types.Healthy,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
assert.True(t, c.Healthy())
|
assert.True(t, c.Healthy())
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("running but unhealthy", func(t *testing.T) {
|
t.Run("running but unhealthy", func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
c := &Container{Container: types.Container{
|
c := &Container{ContainerJSON: types.ContainerJSON{
|
||||||
State: "running",
|
ContainerJSONBase: &types.ContainerJSONBase{
|
||||||
Status: "Up 2 hours (unhealthy)",
|
State: &types.ContainerState{
|
||||||
|
Running: true,
|
||||||
|
Health: &types.Health{
|
||||||
|
Status: types.Unhealthy,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
assert.False(t, c.Healthy())
|
assert.False(t, c.Healthy())
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("running with health starting", func(t *testing.T) {
|
t.Run("running with health starting", func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
c := &Container{Container: types.Container{
|
c := &Container{ContainerJSON: types.ContainerJSON{
|
||||||
State: "running",
|
ContainerJSONBase: &types.ContainerJSONBase{
|
||||||
Status: "Up 1 minute (health: starting)",
|
State: &types.ContainerState{
|
||||||
|
Running: true,
|
||||||
|
Health: &types.Health{
|
||||||
|
Status: "starting",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
assert.False(t, c.Healthy())
|
assert.False(t, c.Healthy())
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("invalid up format no time", func(t *testing.T) {
|
t.Run("dead", func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
c := &Container{Container: types.Container{
|
c := &Container{ContainerJSON: types.ContainerJSON{
|
||||||
State: "running",
|
ContainerJSONBase: &types.ContainerJSONBase{
|
||||||
Status: "Up",
|
State: &types.ContainerState{
|
||||||
}}
|
Dead: true,
|
||||||
assert.False(t, c.Healthy())
|
Running: false,
|
||||||
})
|
},
|
||||||
|
},
|
||||||
t.Run("invalid up format empty parentheses", func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
c := &Container{Container: types.Container{
|
|
||||||
State: "running",
|
|
||||||
Status: "Up 5 minutes ()",
|
|
||||||
}}
|
|
||||||
assert.False(t, c.Healthy())
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("malformed status", func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
c := &Container{Container: types.Container{
|
|
||||||
State: "running",
|
|
||||||
Status: "Invalid status",
|
|
||||||
}}
|
}}
|
||||||
assert.False(t, c.Healthy())
|
assert.False(t, c.Healthy())
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("restarting", func(t *testing.T) {
|
t.Run("restarting", func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
c := &Container{Container: types.Container{
|
c := &Container{ContainerJSON: types.ContainerJSON{
|
||||||
State: "running",
|
ContainerJSONBase: &types.ContainerJSONBase{
|
||||||
Status: "Restarting (0) 5 seconds ago",
|
State: &types.ContainerState{
|
||||||
|
Restarting: true,
|
||||||
|
Running: true,
|
||||||
|
ExitCode: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
assert.False(t, c.Healthy())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("paused", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
c := &Container{ContainerJSON: types.ContainerJSON{
|
||||||
|
ContainerJSONBase: &types.ContainerJSONBase{
|
||||||
|
State: &types.ContainerState{
|
||||||
|
Paused: true,
|
||||||
|
Running: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
assert.False(t, c.Healthy())
|
assert.False(t, c.Healthy())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestContainer_ConflictingServicePorts(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
containerPorts string
|
||||||
|
checkPorts []PortSpec
|
||||||
|
want []PortSpec
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "no conflicts when container has no ports",
|
||||||
|
containerPorts: "",
|
||||||
|
checkPorts: []PortSpec{
|
||||||
|
{Mode: PortModeHost, PublishedPort: 8080, ContainerPort: 80, Protocol: ProtocolTCP},
|
||||||
|
},
|
||||||
|
want: nil,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "host mode ports with same published port and protocol conflict",
|
||||||
|
containerPorts: "8080:80/tcp@host",
|
||||||
|
checkPorts: []PortSpec{
|
||||||
|
{Mode: PortModeHost, PublishedPort: 8080, ContainerPort: 80, Protocol: ProtocolTCP},
|
||||||
|
},
|
||||||
|
want: []PortSpec{
|
||||||
|
{Mode: PortModeHost, PublishedPort: 8080, ContainerPort: 80, Protocol: ProtocolTCP},
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "host mode ports with same port but different protocols don't conflict",
|
||||||
|
containerPorts: "8080:80/tcp@host",
|
||||||
|
checkPorts: []PortSpec{
|
||||||
|
{Mode: PortModeHost, PublishedPort: 8080, ContainerPort: 80, Protocol: ProtocolUDP},
|
||||||
|
},
|
||||||
|
want: nil,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "multiple protocols on same port don't conflict",
|
||||||
|
containerPorts: "8080:80/tcp@host,8080:80/udp@host",
|
||||||
|
checkPorts: []PortSpec{
|
||||||
|
{Mode: PortModeHost, PublishedPort: 8080, ContainerPort: 80, Protocol: ProtocolUDP},
|
||||||
|
},
|
||||||
|
want: []PortSpec{
|
||||||
|
{Mode: PortModeHost, PublishedPort: 8080, ContainerPort: 80, Protocol: ProtocolUDP},
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "host mode ports with different published ports don't conflict",
|
||||||
|
containerPorts: "8080:80/tcp@host",
|
||||||
|
checkPorts: []PortSpec{
|
||||||
|
{Mode: PortModeHost, PublishedPort: 8081, ContainerPort: 80, Protocol: ProtocolTCP},
|
||||||
|
},
|
||||||
|
want: nil,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "host mode ports with same published port but different host IPs don't conflict",
|
||||||
|
containerPorts: "127.0.0.1:8080:80/tcp@host",
|
||||||
|
checkPorts: []PortSpec{
|
||||||
|
{
|
||||||
|
Mode: PortModeHost,
|
||||||
|
HostIP: netip.MustParseAddr("127.0.0.2"),
|
||||||
|
PublishedPort: 8080,
|
||||||
|
ContainerPort: 80,
|
||||||
|
Protocol: ProtocolTCP,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: nil,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "host mode ports with same published port, protocol, and host IP conflict",
|
||||||
|
containerPorts: "127.0.0.1:8080:80/tcp@host",
|
||||||
|
checkPorts: []PortSpec{
|
||||||
|
{
|
||||||
|
Mode: PortModeHost,
|
||||||
|
HostIP: netip.MustParseAddr("127.0.0.1"),
|
||||||
|
PublishedPort: 8080,
|
||||||
|
ContainerPort: 80,
|
||||||
|
Protocol: ProtocolTCP,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: []PortSpec{
|
||||||
|
{
|
||||||
|
Mode: PortModeHost,
|
||||||
|
HostIP: netip.MustParseAddr("127.0.0.1"),
|
||||||
|
PublishedPort: 8080,
|
||||||
|
ContainerPort: 80,
|
||||||
|
Protocol: ProtocolTCP,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "host mode port with no host IP conflicts with specific host IP on same port and protocol",
|
||||||
|
containerPorts: "8080:80/tcp@host",
|
||||||
|
checkPorts: []PortSpec{
|
||||||
|
{
|
||||||
|
Mode: PortModeHost,
|
||||||
|
HostIP: netip.MustParseAddr("127.0.0.1"),
|
||||||
|
PublishedPort: 8080,
|
||||||
|
ContainerPort: 80,
|
||||||
|
Protocol: ProtocolTCP,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: []PortSpec{
|
||||||
|
{
|
||||||
|
Mode: PortModeHost,
|
||||||
|
HostIP: netip.MustParseAddr("127.0.0.1"),
|
||||||
|
PublishedPort: 8080,
|
||||||
|
ContainerPort: 80,
|
||||||
|
Protocol: ProtocolTCP,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "host mode port with no host IP doesn't conflict with different protocol",
|
||||||
|
containerPorts: "8080:80/tcp@host",
|
||||||
|
checkPorts: []PortSpec{
|
||||||
|
{
|
||||||
|
Mode: PortModeHost,
|
||||||
|
HostIP: netip.MustParseAddr("127.0.0.1"),
|
||||||
|
PublishedPort: 8080,
|
||||||
|
ContainerPort: 80,
|
||||||
|
Protocol: ProtocolUDP,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: nil,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ingress mode ports don't conflict with host mode ports",
|
||||||
|
containerPorts: "8080:80/tcp",
|
||||||
|
checkPorts: []PortSpec{
|
||||||
|
{Mode: PortModeHost, PublishedPort: 8080, ContainerPort: 80, Protocol: ProtocolTCP},
|
||||||
|
},
|
||||||
|
want: nil,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "container with invalid port spec returns error",
|
||||||
|
containerPorts: "invalid:port:spec",
|
||||||
|
checkPorts: []PortSpec{
|
||||||
|
{Mode: PortModeHost, PublishedPort: 8080, ContainerPort: 80, Protocol: ProtocolTCP},
|
||||||
|
},
|
||||||
|
want: nil,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
ctr := &Container{ContainerJSON: types.ContainerJSON{
|
||||||
|
Config: &container.Config{
|
||||||
|
Labels: map[string]string{
|
||||||
|
LabelServicePorts: tt.containerPorts,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
|
||||||
|
got, err := ctr.ConflictingServicePorts(tt.checkPorts)
|
||||||
|
if tt.wantErr {
|
||||||
|
require.Error(t, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, tt.want, got)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -62,9 +62,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 +213,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]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+25
-33
@@ -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",
|
||||||
|
|||||||
+69
-2
@@ -4,6 +4,10 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/distribution/reference"
|
"github.com/distribution/reference"
|
||||||
|
"maps"
|
||||||
|
"reflect"
|
||||||
|
"regexp"
|
||||||
|
"slices"
|
||||||
"uncloud/internal/machine/api/pb"
|
"uncloud/internal/machine/api/pb"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -19,6 +23,8 @@ type ServiceSpec struct {
|
|||||||
Name string
|
Name string
|
||||||
// Ports defines what service ports to publish to make the service accessible outside the cluster.
|
// Ports defines what service ports to publish to make the service accessible outside the cluster.
|
||||||
Ports []PortSpec
|
Ports []PortSpec
|
||||||
|
// Replicas is the number of containers to run for the service. Only valid for a replicated service.
|
||||||
|
Replicas uint
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ServiceSpec) Validate() error {
|
func (s *ServiceSpec) Validate() error {
|
||||||
@@ -33,10 +39,27 @@ func (s *ServiceSpec) Validate() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO: validate there is no conflict between ports.
|
// TODO: validate there is no conflict between ports.
|
||||||
|
// TODO: return error if there are non-HTTP/HTTPS ingress ports that we don't support yet.
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Equals returns true if the service spec is equal to the given spec ignoring the number of replicas.
|
||||||
|
func (s *ServiceSpec) Equals(spec ServiceSpec) bool {
|
||||||
|
// TODO: ignore order of ports.
|
||||||
|
sCopy := *s
|
||||||
|
// Ignore the number of replicas when comparing.
|
||||||
|
sCopy.Replicas = 0
|
||||||
|
spec.Replicas = 0
|
||||||
|
return reflect.DeepEqual(*s, spec)
|
||||||
|
}
|
||||||
|
|
||||||
|
var serviceIDRegexp = regexp.MustCompile("^[0-9a-f]{32}$")
|
||||||
|
|
||||||
|
func ValidateServiceID(id string) bool {
|
||||||
|
return serviceIDRegexp.MatchString(id)
|
||||||
|
}
|
||||||
|
|
||||||
type ContainerSpec struct {
|
type ContainerSpec struct {
|
||||||
Command []string
|
Command []string
|
||||||
Image string
|
Image string
|
||||||
@@ -47,8 +70,7 @@ type ContainerSpec struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *ContainerSpec) Validate() error {
|
func (s *ContainerSpec) Validate() error {
|
||||||
_, err := reference.ParseDockerRef(s.Image)
|
if _, err := reference.ParseDockerRef(s.Image); err != nil {
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("invalid image: %w", err)
|
return fmt.Errorf("invalid image: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,6 +89,51 @@ type MachineContainer struct {
|
|||||||
Container Container
|
Container Container
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Endpoints returns the exposed HTTP and HTTPS endpoints of the service.
|
||||||
|
func (s *Service) Endpoints() []string {
|
||||||
|
endpoints := make(map[string]struct{})
|
||||||
|
|
||||||
|
// Container specs may differ between containers in the same service, e.g. during a rolling update,
|
||||||
|
// so we need to collect all unique endpoints.
|
||||||
|
for _, ctr := range s.Containers {
|
||||||
|
ports, err := ctr.Container.ServicePorts()
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, port := range ports {
|
||||||
|
protocol := ""
|
||||||
|
switch port.Protocol {
|
||||||
|
case ProtocolHTTP:
|
||||||
|
protocol = "http"
|
||||||
|
case ProtocolHTTPS:
|
||||||
|
protocol = "https"
|
||||||
|
default:
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if port.Hostname == "" {
|
||||||
|
// There shouldn't be http(s) ports without a hostname but just in case ignore them.
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
endpoint := fmt.Sprintf("%s://%s", protocol, port.Hostname)
|
||||||
|
if port.PublishedPort != 0 {
|
||||||
|
// For non-standard ports (80/443), include the port in the URL.
|
||||||
|
if !(port.Protocol == ProtocolHTTP && port.PublishedPort == 80) &&
|
||||||
|
!(port.Protocol == ProtocolHTTPS && port.PublishedPort == 443) {
|
||||||
|
endpoint += fmt.Sprintf(":%d", port.PublishedPort)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
endpoint += fmt.Sprintf(" → :%d", port.ContainerPort)
|
||||||
|
endpoints[endpoint] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return slices.Sorted(maps.Keys(endpoints))
|
||||||
|
}
|
||||||
|
|
||||||
func ServiceFromProto(s *pb.Service) (Service, error) {
|
func ServiceFromProto(s *pb.Service) (Service, error) {
|
||||||
var err error
|
var err error
|
||||||
containers := make([]MachineContainer, len(s.Containers))
|
containers := make([]MachineContainer, len(s.Containers))
|
||||||
|
|||||||
+96
-39
@@ -4,15 +4,19 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/charmbracelet/huh"
|
"github.com/docker/cli/cli/streams"
|
||||||
"google.golang.org/protobuf/types/known/emptypb"
|
|
||||||
"net/netip"
|
"net/netip"
|
||||||
|
"os"
|
||||||
"uncloud/internal/cli/client"
|
"uncloud/internal/cli/client"
|
||||||
"uncloud/internal/cli/client/connector"
|
"uncloud/internal/cli/client/connector"
|
||||||
"uncloud/internal/cli/config"
|
"uncloud/internal/cli/config"
|
||||||
|
"uncloud/internal/fs"
|
||||||
"uncloud/internal/machine"
|
"uncloud/internal/machine"
|
||||||
"uncloud/internal/machine/api/pb"
|
"uncloud/internal/machine/api/pb"
|
||||||
"uncloud/internal/sshexec"
|
"uncloud/internal/sshexec"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/huh"
|
||||||
|
"google.golang.org/protobuf/types/known/emptypb"
|
||||||
)
|
)
|
||||||
|
|
||||||
const defaultClusterName = "default"
|
const defaultClusterName = "default"
|
||||||
@@ -89,10 +93,14 @@ func (cli *CLI) ConnectCluster(ctx context.Context, clusterName string) (*client
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("parse SSH connection %q: %w", conn.SSH, err)
|
return nil, fmt.Errorf("parse SSH connection %q: %w", conn.SSH, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
keyPath := fs.ExpandHomeDir(conn.SSHKeyFile)
|
||||||
|
|
||||||
sshConfig := &connector.SSHConnectorConfig{
|
sshConfig := &connector.SSHConnectorConfig{
|
||||||
User: user,
|
User: user,
|
||||||
Host: host,
|
Host: host,
|
||||||
Port: port,
|
Port: port,
|
||||||
|
KeyPath: keyPath,
|
||||||
}
|
}
|
||||||
return client.New(ctx, connector.NewSSHConnector(sshConfig))
|
return client.New(ctx, connector.NewSSHConnector(sshConfig))
|
||||||
} else if conn.TCP.IsValid() {
|
} else if conn.TCP.IsValid() {
|
||||||
@@ -101,40 +109,57 @@ func (cli *CLI) ConnectCluster(ctx context.Context, clusterName string) (*client
|
|||||||
return nil, errors.New("no valid connection configuration found for the cluster")
|
return nil, errors.New("no valid connection configuration found for the cluster")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,65 +167,82 @@ 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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save the machine's SSH connection details in the cluster config.
|
// Save the machine's SSH connection details in the cluster config.
|
||||||
connCfg := config.MachineConnection{
|
connCfg := config.MachineConnection{
|
||||||
SSH: config.NewSSHDestination(remoteMachine.User, remoteMachine.Host, remoteMachine.Port),
|
SSH: config.NewSSHDestination(remoteMachine.User, remoteMachine.Host, remoteMachine.Port),
|
||||||
|
SSHKeyFile: remoteMachine.KeyPath,
|
||||||
}
|
}
|
||||||
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.
|
||||||
@@ -215,15 +257,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 {
|
||||||
@@ -238,24 +289,25 @@ 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)
|
||||||
|
|
||||||
// Save the machine's SSH connection details in the cluster config.
|
// Save the machine's SSH connection details in the cluster config.
|
||||||
connCfg := config.MachineConnection{
|
connCfg := config.MachineConnection{
|
||||||
SSH: config.NewSSHDestination(remoteMachine.User, remoteMachine.Host, remoteMachine.Port),
|
SSH: config.NewSSHDestination(remoteMachine.User, remoteMachine.Host, remoteMachine.Port),
|
||||||
|
SSHKeyFile: remoteMachine.KeyPath,
|
||||||
}
|
}
|
||||||
if clusterName == "" {
|
if clusterName == "" {
|
||||||
clusterName = cli.config.CurrentCluster
|
clusterName = cli.config.CurrentCluster
|
||||||
}
|
}
|
||||||
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
|
||||||
@@ -319,3 +371,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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
"regexp"
|
||||||
|
"uncloud/internal/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
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 MachineFilter) (*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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: set restart policy to always. https://github.com/psviderski/uncloud/issues/26
|
||||||
|
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, &RollingStrategy{MachineFilter: filter})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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,69 @@
|
|||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"testing"
|
||||||
|
"uncloud/internal/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestClient_NewCaddyDeployment(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
cli := &Client{}
|
||||||
|
|
||||||
|
t.Run("latest image from Docker Hub", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
deploy, err := cli.NewCaddyDeployment("", nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, "caddy", deploy.Spec.Name)
|
||||||
|
assert.Equal(t, api.ServiceModeGlobal, deploy.Spec.Mode)
|
||||||
|
assert.Regexp(t, `^caddy:2\.\d+\.\d+$`, deploy.Spec.Container.Image)
|
||||||
|
expectedPorts := []api.PortSpec{
|
||||||
|
{
|
||||||
|
PublishedPort: 80,
|
||||||
|
ContainerPort: 80,
|
||||||
|
Protocol: api.ProtocolTCP,
|
||||||
|
Mode: api.PortModeHost,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
PublishedPort: 443,
|
||||||
|
ContainerPort: 443,
|
||||||
|
Protocol: api.ProtocolTCP,
|
||||||
|
Mode: api.PortModeHost,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
assert.Equal(t, expectedPorts, deploy.Spec.Ports)
|
||||||
|
// TODO:
|
||||||
|
//assert.Equal(t, alwaysPullImage, deploy.Spec.Container.PullPolicy)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("custom image", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
image := "my-caddy:1.2.3"
|
||||||
|
deploy, err := cli.NewCaddyDeployment(image, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, "caddy", deploy.Spec.Name)
|
||||||
|
assert.Equal(t, api.ServiceModeGlobal, deploy.Spec.Mode)
|
||||||
|
assert.Equal(t, image, deploy.Spec.Container.Image)
|
||||||
|
expectedPorts := []api.PortSpec{
|
||||||
|
{
|
||||||
|
PublishedPort: 80,
|
||||||
|
ContainerPort: 80,
|
||||||
|
Protocol: api.ProtocolTCP,
|
||||||
|
Mode: api.PortModeHost,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
PublishedPort: 443,
|
||||||
|
ContainerPort: 443,
|
||||||
|
Protocol: api.ProtocolTCP,
|
||||||
|
Mode: api.PortModeHost,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
assert.Equal(t, expectedPorts, deploy.Spec.Ports)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"github.com/docker/cli/cli/streams"
|
"github.com/docker/cli/cli/streams"
|
||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
|
"google.golang.org/grpc/metadata"
|
||||||
"os"
|
"os"
|
||||||
"uncloud/internal/machine/api/pb"
|
"uncloud/internal/machine/api/pb"
|
||||||
"uncloud/internal/machine/docker"
|
"uncloud/internal/machine/docker"
|
||||||
@@ -20,12 +21,11 @@ type Client struct {
|
|||||||
|
|
||||||
pb.MachineClient
|
pb.MachineClient
|
||||||
pb.ClusterClient
|
pb.ClusterClient
|
||||||
*DockerClient
|
// Docker is a namespaced client for the Docker service to distinguish Uncloud-specific service container operations
|
||||||
|
// from generic Docker operations.
|
||||||
|
Docker *docker.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
// DockerClient is a type alias for the Docker client to embed it in Client with a more specific name.
|
|
||||||
type DockerClient = docker.Client
|
|
||||||
|
|
||||||
// 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)
|
||||||
@@ -46,7 +46,7 @@ func New(ctx context.Context, connector Connector) (*Client, error) {
|
|||||||
|
|
||||||
c.MachineClient = pb.NewMachineClient(c.conn)
|
c.MachineClient = pb.NewMachineClient(c.conn)
|
||||||
c.ClusterClient = pb.NewClusterClient(c.conn)
|
c.ClusterClient = pb.NewClusterClient(c.conn)
|
||||||
c.DockerClient = docker.NewClient(c.conn)
|
c.Docker = docker.NewClient(c.conn)
|
||||||
return c, nil
|
return c, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,3 +58,10 @@ func (cli *Client) Close() error {
|
|||||||
func (cli *Client) progressOut() *streams.Out {
|
func (cli *Client) progressOut() *streams.Out {
|
||||||
return streams.NewOut(os.Stdout)
|
return streams.NewOut(os.Stdout)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// proxyToMachine returns a new context that proxies gRPC requests to the specified machine.
|
||||||
|
func proxyToMachine(ctx context.Context, machine *pb.MachineInfo) context.Context {
|
||||||
|
machineIP, _ := machine.Network.ManagementIp.ToAddr()
|
||||||
|
md := metadata.Pairs("machines", machineIP.String())
|
||||||
|
return metadata.NewOutgoingContext(ctx, md)
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,6 +6,21 @@ import (
|
|||||||
"uncloud/internal/machine/api/pb"
|
"uncloud/internal/machine/api/pb"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func (cli *Client) InspectMachine(ctx context.Context, id string) (*pb.MachineMember, error) {
|
||||||
|
machines, err := cli.ListMachines(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, m := range machines {
|
||||||
|
if m.Machine.Id == id || m.Machine.Name == id {
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, ErrNotFound
|
||||||
|
}
|
||||||
|
|
||||||
func (cli *Client) ListMachines(ctx context.Context) ([]*pb.MachineMember, error) {
|
func (cli *Client) ListMachines(ctx context.Context) ([]*pb.MachineMember, error) {
|
||||||
resp, err := cli.ClusterClient.ListMachines(ctx, &emptypb.Empty{})
|
resp, err := cli.ClusterClient.ListMachines(ctx, &emptypb.Empty{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -0,0 +1,335 @@
|
|||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"github.com/docker/compose/v2/pkg/progress"
|
||||||
|
"github.com/docker/docker/api/types/container"
|
||||||
|
"github.com/docker/docker/api/types/network"
|
||||||
|
dockerclient "github.com/docker/docker/client"
|
||||||
|
"github.com/docker/docker/pkg/jsonmessage"
|
||||||
|
"github.com/docker/go-connections/nat"
|
||||||
|
"google.golang.org/grpc/status"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"uncloud/internal/api"
|
||||||
|
machinedocker "uncloud/internal/machine/docker"
|
||||||
|
"uncloud/internal/secret"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CreateContainer creates a new container for the given service on the specified machine.
|
||||||
|
func (cli *Client) CreateContainer(
|
||||||
|
ctx context.Context, serviceID string, spec api.ServiceSpec, machineID string,
|
||||||
|
) (container.CreateResponse, error) {
|
||||||
|
var resp container.CreateResponse
|
||||||
|
|
||||||
|
if !api.ValidateServiceID(serviceID) {
|
||||||
|
return resp, fmt.Errorf("invalid service ID: '%s'", serviceID)
|
||||||
|
}
|
||||||
|
// TODO: validate spec.Name is consistent with serviceID if this is not the first container in the service.
|
||||||
|
|
||||||
|
machine, err := cli.InspectMachine(ctx, machineID)
|
||||||
|
if err != nil {
|
||||||
|
return resp, fmt.Errorf("inspect machine '%s': %w", machineID, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
suffix, err := secret.RandomAlphaNumeric(4)
|
||||||
|
if err != nil {
|
||||||
|
return resp, fmt.Errorf("generate random suffix: %w", err)
|
||||||
|
}
|
||||||
|
containerName := fmt.Sprintf("%s-%s", spec.Name, suffix)
|
||||||
|
|
||||||
|
config := &container.Config{
|
||||||
|
Cmd: spec.Container.Command,
|
||||||
|
Hostname: containerName,
|
||||||
|
Image: spec.Container.Image,
|
||||||
|
Labels: map[string]string{
|
||||||
|
api.LabelServiceID: serviceID,
|
||||||
|
api.LabelServiceName: spec.Name,
|
||||||
|
api.LabelServiceMode: spec.Mode,
|
||||||
|
api.LabelManaged: "",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if spec.Mode == "" {
|
||||||
|
config.Labels[api.LabelServiceMode] = api.ServiceModeReplicated
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
// 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,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
netConfig := &network.NetworkingConfig{
|
||||||
|
EndpointsConfig: map[string]*network.EndpointSettings{
|
||||||
|
machinedocker.NetworkName: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Proxy Docker gRPC requests to the selected machine.
|
||||||
|
ctx = proxyToMachine(ctx, machine.Machine)
|
||||||
|
|
||||||
|
pw := progress.ContextWriter(ctx)
|
||||||
|
eventID := fmt.Sprintf("Container %s on %s", containerName, machine.Machine.Name)
|
||||||
|
|
||||||
|
pw.Event(progress.CreatingEvent(eventID))
|
||||||
|
resp, err = cli.Docker.CreateContainer(ctx, config, hostConfig, netConfig, nil, containerName)
|
||||||
|
if err != nil {
|
||||||
|
if !dockerclient.IsErrNotFound(err) {
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pull the missing image and create the container again.
|
||||||
|
if err = cli.pullImageWithProgress(ctx, config.Image, machine.Machine.Name, eventID); err != nil {
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
if resp, err = cli.Docker.CreateContainer(ctx, config, hostConfig, netConfig, nil, containerName); err != nil {
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pw.Event(progress.CreatedEvent(eventID))
|
||||||
|
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cli *Client) pullImageWithProgress(ctx context.Context, image, machineName, parentEventID string) error {
|
||||||
|
pw := progress.ContextWriter(ctx)
|
||||||
|
eventID := fmt.Sprintf("Image %s on %s", image, machineName)
|
||||||
|
pw.Event(progress.Event{
|
||||||
|
ID: eventID,
|
||||||
|
ParentID: parentEventID,
|
||||||
|
Status: progress.Working,
|
||||||
|
StatusText: "Pulling",
|
||||||
|
})
|
||||||
|
|
||||||
|
pullCh, err := cli.Docker.PullImage(ctx, image)
|
||||||
|
if err != nil {
|
||||||
|
statusErr := status.Convert(err)
|
||||||
|
pw.Event(progress.Event{
|
||||||
|
ID: eventID,
|
||||||
|
ParentID: parentEventID,
|
||||||
|
Text: "Error",
|
||||||
|
Status: progress.Error,
|
||||||
|
StatusText: statusErr.Message(),
|
||||||
|
})
|
||||||
|
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.
|
||||||
|
for msg := range pullCh {
|
||||||
|
if msg.Err != nil {
|
||||||
|
err = msg.Err
|
||||||
|
} else {
|
||||||
|
if msg.Message.Error != nil {
|
||||||
|
err = errors.New(msg.Message.Error.Message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
statusErr := status.Convert(err)
|
||||||
|
pw.Event(progress.Event{
|
||||||
|
ID: eventID,
|
||||||
|
ParentID: parentEventID,
|
||||||
|
Text: "Error",
|
||||||
|
Status: progress.Error,
|
||||||
|
StatusText: statusErr.Message(),
|
||||||
|
})
|
||||||
|
return fmt.Errorf("pull image: %w", errors.New(statusErr.Message()))
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: add like in compose: --quiet-pull Pull without printing progress information
|
||||||
|
e := toPullProgressEvent(msg.Message)
|
||||||
|
if e != nil {
|
||||||
|
e.ID = fmt.Sprintf("%s on %s", e.ID, machineName)
|
||||||
|
e.ParentID = eventID
|
||||||
|
// Grand children events are not printed by the tty progress writer but they are still required
|
||||||
|
// to calculate the progress line of their parent.
|
||||||
|
pw.Event(*e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pw.Event(progress.Event{
|
||||||
|
ID: eventID,
|
||||||
|
ParentID: parentEventID,
|
||||||
|
Status: progress.Done,
|
||||||
|
StatusText: "Pulled",
|
||||||
|
})
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// toPullProgressEvent converts a JSON progress message from the Docker API to a progress event.
|
||||||
|
// It's based on toPullProgressEvent from Docker Compose.
|
||||||
|
func toPullProgressEvent(jm jsonmessage.JSONMessage) *progress.Event {
|
||||||
|
if jm.ID == "" || jm.Progress == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
total int64
|
||||||
|
percent int
|
||||||
|
current int64
|
||||||
|
)
|
||||||
|
text := jm.Progress.String()
|
||||||
|
stat := progress.Working
|
||||||
|
|
||||||
|
switch jm.Status {
|
||||||
|
case "Preparing", "Waiting", "Pulling fs layer":
|
||||||
|
percent = 0
|
||||||
|
case "Downloading", "Extracting", "Verifying Checksum":
|
||||||
|
current = jm.Progress.Current
|
||||||
|
total = jm.Progress.Total
|
||||||
|
if jm.Progress.Total > 0 {
|
||||||
|
percent = int(jm.Progress.Current * 100 / jm.Progress.Total)
|
||||||
|
}
|
||||||
|
case "Download complete", "Already exists", "Pull complete":
|
||||||
|
stat = progress.Done
|
||||||
|
percent = 100
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.Contains(jm.Status, "Image is up to date") ||
|
||||||
|
strings.Contains(jm.Status, "Downloaded newer image") {
|
||||||
|
stat = progress.Done
|
||||||
|
percent = 100
|
||||||
|
}
|
||||||
|
|
||||||
|
return &progress.Event{
|
||||||
|
ID: jm.ID,
|
||||||
|
Current: current,
|
||||||
|
Total: total,
|
||||||
|
Percent: percent,
|
||||||
|
Text: jm.Status,
|
||||||
|
Status: stat,
|
||||||
|
StatusText: text,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// InspectContainer returns the information about the specified container within the service.
|
||||||
|
func (cli *Client) InspectContainer(ctx context.Context, serviceID, containerID string) (api.MachineContainer, error) {
|
||||||
|
var ctr api.MachineContainer
|
||||||
|
|
||||||
|
svc, err := cli.InspectService(ctx, serviceID)
|
||||||
|
if err != nil {
|
||||||
|
return ctr, fmt.Errorf("inspect service: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, c := range svc.Containers {
|
||||||
|
if c.Container.ID == containerID || c.Container.NameWithoutSlash() == containerID {
|
||||||
|
ctr = c
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ctr.MachineID == "" {
|
||||||
|
return ctr, ErrNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctr, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// StartContainer starts the specified container within the service.
|
||||||
|
func (cli *Client) StartContainer(ctx context.Context, serviceID, containerID string) error {
|
||||||
|
ctr, err := cli.InspectContainer(ctx, serviceID, containerID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
machine, err := cli.InspectMachine(ctx, ctr.MachineID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("inspect machine '%s': %w", ctr.MachineID, err)
|
||||||
|
}
|
||||||
|
ctx = proxyToMachine(ctx, machine.Machine)
|
||||||
|
|
||||||
|
pw := progress.ContextWriter(ctx)
|
||||||
|
eventID := fmt.Sprintf("Container %s on %s", ctr.Container.NameWithoutSlash(), machine.Machine.Name)
|
||||||
|
|
||||||
|
pw.Event(progress.StartingEvent(eventID))
|
||||||
|
if err = cli.Docker.StartContainer(ctx, ctr.Container.ID, container.StartOptions{}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
pw.Event(progress.StartedEvent(eventID))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// StopContainer stops the specified container within the service.
|
||||||
|
func (cli *Client) StopContainer(
|
||||||
|
ctx context.Context, serviceID, containerID string, opts container.StopOptions,
|
||||||
|
) error {
|
||||||
|
ctr, err := cli.InspectContainer(ctx, serviceID, containerID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
machine, err := cli.InspectMachine(ctx, ctr.MachineID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("inspect machine '%s': %w", ctr.MachineID, err)
|
||||||
|
}
|
||||||
|
ctx = proxyToMachine(ctx, machine.Machine)
|
||||||
|
|
||||||
|
pw := progress.ContextWriter(ctx)
|
||||||
|
eventID := fmt.Sprintf("Container %s on %s", ctr.Container.NameWithoutSlash(), machine.Machine.Name)
|
||||||
|
|
||||||
|
pw.Event(progress.StoppingEvent(eventID))
|
||||||
|
if err = cli.Docker.StopContainer(ctx, ctr.Container.ID, opts); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
pw.Event(progress.StoppedEvent(eventID))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveContainer removes the specified container within the service.
|
||||||
|
func (cli *Client) RemoveContainer(
|
||||||
|
ctx context.Context, serviceID, containerID string, opts container.RemoveOptions,
|
||||||
|
) error {
|
||||||
|
ctr, err := cli.InspectContainer(ctx, serviceID, containerID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
machine, err := cli.InspectMachine(ctx, ctr.MachineID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("inspect machine '%s': %w", ctr.MachineID, err)
|
||||||
|
}
|
||||||
|
ctx = proxyToMachine(ctx, machine.Machine)
|
||||||
|
|
||||||
|
pw := progress.ContextWriter(ctx)
|
||||||
|
eventID := fmt.Sprintf("Container %s on %s", ctr.Container.NameWithoutSlash(), machine.Machine.Name)
|
||||||
|
|
||||||
|
pw.Event(progress.RemovingEvent(eventID))
|
||||||
|
if err = cli.Docker.RemoveContainer(ctx, ctr.Container.ID, opts); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
pw.Event(progress.RemovedEvent(eventID))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"uncloud/internal/api"
|
||||||
|
"uncloud/internal/machine/api/pb"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
type Deployment struct {
|
||||||
|
Service *api.Service
|
||||||
|
Spec api.ServiceSpec
|
||||||
|
Strategy Strategy
|
||||||
|
cli *Client
|
||||||
|
plan *Plan
|
||||||
|
}
|
||||||
|
|
||||||
|
type Plan struct {
|
||||||
|
ServiceID 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.
|
||||||
|
// If strategy is nil, a default RollingStrategy will be used.
|
||||||
|
func (cli *Client) NewDeployment(spec api.ServiceSpec, strategy Strategy) (*Deployment, error) {
|
||||||
|
if strategy == nil {
|
||||||
|
strategy = &RollingStrategy{}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Deployment{
|
||||||
|
Spec: spec,
|
||||||
|
Strategy: strategy,
|
||||||
|
cli: cli,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Plan returns a plan of operations to reconcile the service to the desired state.
|
||||||
|
// If a plan has already been created, the same plan will be returned.
|
||||||
|
func (d *Deployment) Plan(ctx context.Context) (Plan, error) {
|
||||||
|
if d.plan != nil {
|
||||||
|
return *d.plan, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate the new spec before planning.
|
||||||
|
if err := d.Validate(ctx); err != nil {
|
||||||
|
return Plan{}, fmt.Errorf("invalid deployment: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
plan, err := d.Strategy.Plan(ctx, d.cli, d.Service, d.Spec)
|
||||||
|
if err != nil {
|
||||||
|
return Plan{}, fmt.Errorf("create plan using %s strategy: %w", d.Strategy.Type(), err)
|
||||||
|
}
|
||||||
|
d.plan = &plan
|
||||||
|
|
||||||
|
return plan, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate checks if the deployment specification is valid.
|
||||||
|
func (d *Deployment) Validate(ctx context.Context) error {
|
||||||
|
if err := d.Spec.Validate(); err != nil {
|
||||||
|
return fmt.Errorf("invalid service spec: %w", err)
|
||||||
|
}
|
||||||
|
if d.Spec.Name == "" {
|
||||||
|
return errors.New("service name is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.Service == nil {
|
||||||
|
svc, err := d.cli.InspectService(ctx, d.Spec.Name)
|
||||||
|
if err == nil {
|
||||||
|
d.Service = &svc
|
||||||
|
} else if !errors.Is(err, ErrNotFound) {
|
||||||
|
return fmt.Errorf("inspect service: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// d.Service is nil if the service doesn't exist yet (first deployment).
|
||||||
|
if d.Service == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.Service.Name != d.Spec.Name {
|
||||||
|
return errors.New("service name cannot be changed")
|
||||||
|
}
|
||||||
|
if d.Service.Mode != d.Spec.Mode {
|
||||||
|
return errors.New("service mode cannot be changed")
|
||||||
|
}
|
||||||
|
if d.Spec.Mode == api.ServiceModeReplicated && d.Spec.Replicas < 1 {
|
||||||
|
return errors.New("number of replicas must be at least 1")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run executes the deployment plan and returns the ID of the created or updated service.
|
||||||
|
// 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.
|
||||||
|
// TODO: forbid to run the same deployment more than once.
|
||||||
|
func (d *Deployment) Run(ctx context.Context) (string, error) {
|
||||||
|
plan, err := d.Plan(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("create plan: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return plan.ServiceID, plan.Execute(ctx, d.cli)
|
||||||
|
}
|
||||||
@@ -0,0 +1,161 @@
|
|||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"github.com/docker/compose/v2/pkg/progress"
|
||||||
|
"google.golang.org/grpc/codes"
|
||||||
|
"google.golang.org/grpc/status"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
"uncloud/internal/machine/api/pb"
|
||||||
|
"uncloud/internal/machine/caddyfile"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetDomain returns the cluster domain name or ErrNotFound if it hasn't been reserved yet.
|
||||||
|
func (cli *Client) GetDomain(ctx context.Context) (string, error) {
|
||||||
|
domain, err := cli.ClusterClient.GetDomain(ctx, nil)
|
||||||
|
if err != nil {
|
||||||
|
if status.Convert(err).Code() == codes.NotFound {
|
||||||
|
return "", ErrNotFound
|
||||||
|
}
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return domain.Name, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var ErrNoReachableMachines = errors.New("no internet-reachable machines running service containers")
|
||||||
|
|
||||||
|
// CreateIngressRecords verifies which machines running the specified service (typically Caddy) are reachable from
|
||||||
|
// the internet, then creates DNS records for the cluster domain pointing to those machines. It tests each machine
|
||||||
|
// by sending HTTP requests to their public IPs. Only machines that respond correctly with their machine ID are included
|
||||||
|
// in the resulting DNS configuration. Returns the created DNS records or an error.
|
||||||
|
func (cli *Client) CreateIngressRecords(ctx context.Context, serviceID string) ([]*pb.DNSRecord, error) {
|
||||||
|
svc, err := cli.InspectService(ctx, serviceID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("inspect service '%s': %w", serviceID, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
machineIDs := make(map[string]struct{}, len(svc.Containers))
|
||||||
|
for _, mc := range svc.Containers {
|
||||||
|
machineIDs[mc.MachineID] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
reachableMachines := make(chan *pb.MachineInfo)
|
||||||
|
|
||||||
|
for id := range machineIDs {
|
||||||
|
m, err := cli.InspectMachine(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("inspect machine '%s': %w", id, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.Machine.PublicIp == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
|
||||||
|
// Verify that the Caddy container is reachable on the machine by its public IP.
|
||||||
|
publicIP, _ := m.Machine.PublicIp.ToAddr()
|
||||||
|
|
||||||
|
pw := progress.ContextWriter(ctx)
|
||||||
|
eventID := fmt.Sprintf("Machine %s (%s)", m.Machine.Name, publicIP)
|
||||||
|
pw.Event(progress.NewEvent(eventID, progress.Working, "Querying"))
|
||||||
|
|
||||||
|
verifyURL := fmt.Sprintf("http://%s%s", publicIP, caddyfile.VerifyPath)
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, verifyURL, nil)
|
||||||
|
if err != nil {
|
||||||
|
pw.Event(progress.NewEvent(eventID, progress.Error, err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
client := &http.Client{Timeout: 5 * time.Second}
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
e := unreachable(eventID)
|
||||||
|
e.Text = fmt.Sprintf("Failed to send HTTP request: %v", err)
|
||||||
|
pw.Event(e)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
e := unreachable(eventID)
|
||||||
|
e.Text = fmt.Sprintf("Unexpected HTTP response status code: %d", resp.StatusCode)
|
||||||
|
pw.Event(e)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
e := unreachable(eventID)
|
||||||
|
e.Text = fmt.Sprintf("Failed to read HTTP response body: %v", err)
|
||||||
|
pw.Event(e)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check the response body is the machine ID to ensure the correct Caddy container is responding.
|
||||||
|
if string(body) == m.Machine.Id {
|
||||||
|
pw.Event(progress.NewEvent(eventID, progress.Done, "Reachable"))
|
||||||
|
reachableMachines <- m.Machine
|
||||||
|
} else {
|
||||||
|
bodyStr := string(body)
|
||||||
|
if len(bodyStr) > 50 {
|
||||||
|
bodyStr = bodyStr[:50] + "..."
|
||||||
|
}
|
||||||
|
|
||||||
|
e := unreachable(eventID)
|
||||||
|
e.Text = fmt.Sprintf("Unexpected HTTP response body: %s", bodyStr)
|
||||||
|
pw.Event(e)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
wg.Wait()
|
||||||
|
close(reachableMachines)
|
||||||
|
}()
|
||||||
|
|
||||||
|
var ingressIPs []string
|
||||||
|
for m := range reachableMachines {
|
||||||
|
ip, _ := m.PublicIp.ToAddr()
|
||||||
|
ingressIPs = append(ingressIPs, ip.String())
|
||||||
|
}
|
||||||
|
if len(ingressIPs) == 0 {
|
||||||
|
return nil, ErrNoReachableMachines
|
||||||
|
}
|
||||||
|
|
||||||
|
req := &pb.CreateDomainRecordsRequest{
|
||||||
|
Records: []*pb.DNSRecord{
|
||||||
|
{
|
||||||
|
Name: "*",
|
||||||
|
Type: pb.DNSRecord_A,
|
||||||
|
Values: ingressIPs,
|
||||||
|
},
|
||||||
|
// TODO: Add AAAA record with routable IPv6 addresses of machines running Caddy containers.
|
||||||
|
},
|
||||||
|
}
|
||||||
|
resp, err := cli.CreateDomainRecords(ctx, req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("create cluster domain records in Uncloud DNS: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp.Records, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// unreachable creates a new Unreachable error event.
|
||||||
|
func unreachable(id string) progress.Event {
|
||||||
|
return progress.NewEvent(
|
||||||
|
id,
|
||||||
|
progress.Error,
|
||||||
|
"Unreachable (probably behind NAT or firewall)",
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,166 @@
|
|||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"github.com/docker/docker/api/types/container"
|
||||||
|
"strings"
|
||||||
|
"uncloud/internal/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Operation represents a single atomic operation in a deployment process.
|
||||||
|
// Operations can be composed to form complex deployment strategies.
|
||||||
|
type Operation interface {
|
||||||
|
Execute(ctx context.Context, cli *Client) error
|
||||||
|
// Format returns a human-readable representation of the operation.
|
||||||
|
Format(resolver NameResolver) 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.
|
||||||
|
type RunContainerOperation struct {
|
||||||
|
ServiceID string
|
||||||
|
Spec api.ServiceSpec
|
||||||
|
MachineID string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *RunContainerOperation) Execute(ctx context.Context, cli *Client) error {
|
||||||
|
resp, err := cli.CreateContainer(ctx, o.ServiceID, o.Spec, o.MachineID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("create container: %w", err)
|
||||||
|
}
|
||||||
|
if err = cli.StartContainer(ctx, o.ServiceID, resp.ID); err != nil {
|
||||||
|
return fmt.Errorf("start container: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: wait for the container to become healthy
|
||||||
|
|
||||||
|
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 {
|
||||||
|
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.
|
||||||
|
type StopContainerOperation struct {
|
||||||
|
ServiceID string
|
||||||
|
ContainerID string
|
||||||
|
MachineID string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *StopContainerOperation) Execute(ctx context.Context, cli *Client) error {
|
||||||
|
if err := cli.StopContainer(ctx, o.ServiceID, o.ContainerID, container.StopOptions{}); err != nil {
|
||||||
|
return fmt.Errorf("stop container: %w", err)
|
||||||
|
}
|
||||||
|
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 {
|
||||||
|
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.
|
||||||
|
type RemoveContainerOperation struct {
|
||||||
|
ServiceID string
|
||||||
|
ContainerID string
|
||||||
|
MachineID string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *RemoveContainerOperation) Execute(ctx context.Context, cli *Client) error {
|
||||||
|
if err := cli.StopContainer(ctx, o.ServiceID, o.ContainerID, container.StopOptions{}); err != nil {
|
||||||
|
return fmt.Errorf("stop container: %w", err)
|
||||||
|
}
|
||||||
|
if err := cli.RemoveContainer(ctx, o.ServiceID, o.ContainerID, container.RemoveOptions{}); err != nil {
|
||||||
|
return fmt.Errorf("remove container: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
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.
|
||||||
|
type SequenceOperation struct {
|
||||||
|
Operations []Operation
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *SequenceOperation) Execute(ctx context.Context, cli *Client) error {
|
||||||
|
for _, op := range o.Operations {
|
||||||
|
if err := op.Execute(ctx, cli); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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 {
|
||||||
|
ops := make([]string, len(o.Operations))
|
||||||
|
for i, op := range o.Operations {
|
||||||
|
ops[i] = op.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("SequenceOperation[%s]", strings.Join(ops, ", "))
|
||||||
|
}
|
||||||
|
|
||||||
|
// MapNameResolver resolves machine and container IDs to their names using a static map.
|
||||||
|
type MapNameResolver struct {
|
||||||
|
machines map[string]string
|
||||||
|
containers map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewNameResolver(machines, containers map[string]string) *MapNameResolver {
|
||||||
|
return &MapNameResolver{
|
||||||
|
machines: machines,
|
||||||
|
containers: containers,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *MapNameResolver) MachineName(machineID string) string {
|
||||||
|
if name, ok := r.machines[machineID]; ok {
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
return machineID
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *MapNameResolver) ContainerName(containerID string) string {
|
||||||
|
if name, ok := r.containers[containerID]; ok {
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
return containerID
|
||||||
|
}
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/distribution/reference"
|
||||||
|
"strings"
|
||||||
|
"uncloud/internal/api"
|
||||||
|
"uncloud/internal/secret"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ServiceSpecResolver transforms user-provided service specs into deployment-ready form.
|
||||||
|
type ServiceSpecResolver struct {
|
||||||
|
ClusterDomain string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewServiceSpecResolver(clusterDomain string) *ServiceSpecResolver {
|
||||||
|
return &ServiceSpecResolver{ClusterDomain: clusterDomain}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve transforms a service spec into its fully resolved form ready for deployment.
|
||||||
|
func (r *ServiceSpecResolver) Resolve(spec *api.ServiceSpec) error {
|
||||||
|
if err := spec.Validate(); err != nil {
|
||||||
|
return fmt.Errorf("invalid service spec: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
steps := []func(*api.ServiceSpec) error{
|
||||||
|
r.applyDefaults,
|
||||||
|
r.resolveServiceName,
|
||||||
|
r.expandIngressPorts,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, step := range steps {
|
||||||
|
if err := step(spec); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ServiceSpecResolver) applyDefaults(spec *api.ServiceSpec) error {
|
||||||
|
if spec.Mode == "" {
|
||||||
|
spec.Mode = api.ServiceModeReplicated
|
||||||
|
}
|
||||||
|
// Ensure the replicated service has at least one replica.
|
||||||
|
if spec.Mode == api.ServiceModeReplicated && spec.Replicas == 0 {
|
||||||
|
spec.Replicas = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ServiceSpecResolver) resolveServiceName(spec *api.ServiceSpec) error {
|
||||||
|
if spec.Name != "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate a random service name from the image when not provided.
|
||||||
|
img, err := reference.ParseDockerRef(spec.Container.Image)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid image: %w", err)
|
||||||
|
}
|
||||||
|
// Get the image name without the repository and tag/digest parts.
|
||||||
|
imageName := reference.FamiliarName(img)
|
||||||
|
// Get the last part of the image name (path), e.g. "nginx" from "bitnami/nginx".
|
||||||
|
if i := strings.LastIndex(imageName, "/"); i != -1 {
|
||||||
|
imageName = imageName[i+1:]
|
||||||
|
}
|
||||||
|
// Append a random suffix to the image name to generate an optimistically unique service name.
|
||||||
|
suffix, err := secret.RandomAlphaNumeric(4)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("generate random suffix: %w", err)
|
||||||
|
}
|
||||||
|
spec.Name = fmt.Sprintf("%s-%s", imageName, suffix)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// expandIngressPorts processes HTTP(S) ingress ports in a service spec by:
|
||||||
|
// 1. Setting a default hostname (service-name.cluster-domain) for ports without a hostname.
|
||||||
|
// 2. Duplicating a port with a cluster domain hostname for ports with external domains.
|
||||||
|
// This ensures every ingress port is accessible via the cluster domain, while preserving any custom domains specified
|
||||||
|
// by the user.
|
||||||
|
func (r *ServiceSpecResolver) expandIngressPorts(spec *api.ServiceSpec) error {
|
||||||
|
for i, port := range spec.Ports {
|
||||||
|
if port.Protocol != api.ProtocolHTTP && port.Protocol != api.ProtocolHTTPS {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if port.Hostname == "" {
|
||||||
|
if r.ClusterDomain == "" {
|
||||||
|
return fmt.Errorf("cluster domain must be reserved to generate hostname for ingress port: %d/%s",
|
||||||
|
port.ContainerPort, port.Protocol)
|
||||||
|
}
|
||||||
|
// Assign the default hostname (service-name.cluster-domain).
|
||||||
|
spec.Ports[i].Hostname = fmt.Sprintf("%s.%s", spec.Name, r.ClusterDomain)
|
||||||
|
} else {
|
||||||
|
if r.ClusterDomain == "" {
|
||||||
|
// When no cluster domain is reserved, use only the provided hostname.
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasSuffix(port.Hostname, "."+r.ClusterDomain) {
|
||||||
|
// If the hostname is already a cluster subdomain, use as is.
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// For external domains, duplicate the port with a service-name.cluster-domain hostname so the service
|
||||||
|
// can be accessed via both hostnames.
|
||||||
|
newPort := port
|
||||||
|
newPort.Hostname = fmt.Sprintf("%s.%s", spec.Name, r.ClusterDomain)
|
||||||
|
spec.Ports = append(spec.Ports, newPort)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
+47
-382
@@ -4,65 +4,49 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/distribution/reference"
|
|
||||||
"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/filters"
|
"github.com/docker/docker/api/types/filters"
|
||||||
"github.com/docker/docker/api/types/network"
|
|
||||||
dockerclient "github.com/docker/docker/client"
|
|
||||||
"github.com/docker/docker/pkg/jsonmessage"
|
|
||||||
"github.com/docker/go-connections/nat"
|
|
||||||
"google.golang.org/grpc/codes"
|
"google.golang.org/grpc/codes"
|
||||||
"google.golang.org/grpc/metadata"
|
"google.golang.org/grpc/metadata"
|
||||||
"google.golang.org/grpc/status"
|
"google.golang.org/grpc/status"
|
||||||
"slices"
|
"slices"
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
"sync"
|
||||||
"uncloud/internal/api"
|
"uncloud/internal/api"
|
||||||
"uncloud/internal/machine/api/pb"
|
"uncloud/internal/machine/api/pb"
|
||||||
machinedocker "uncloud/internal/machine/docker"
|
|
||||||
"uncloud/internal/secret"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func (cli *Client) PrepareDeploymentSpec(ctx context.Context, spec api.ServiceSpec) (api.ServiceSpec, error) {
|
||||||
|
domain, err := cli.GetDomain(ctx)
|
||||||
|
if err != nil && !errors.Is(err, ErrNotFound) {
|
||||||
|
return spec, fmt.Errorf("get domain: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the domain is not found (not reserved), an empty domain is used for the resolver.
|
||||||
|
resolver := NewServiceSpecResolver(domain)
|
||||||
|
|
||||||
|
if err = resolver.Resolve(&spec); err != nil {
|
||||||
|
return spec, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return spec, nil
|
||||||
|
}
|
||||||
|
|
||||||
type RunServiceResponse struct {
|
type RunServiceResponse struct {
|
||||||
ID string
|
ID string
|
||||||
Name string
|
Name string
|
||||||
Containers []MachineContainerID
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type MachineContainerID struct {
|
func (cli *Client) RunService(
|
||||||
MachineID string
|
ctx context.Context, spec api.ServiceSpec, filter MachineFilter,
|
||||||
ContainerID string
|
) (RunServiceResponse, error) {
|
||||||
}
|
|
||||||
|
|
||||||
func (cli *Client) RunService(ctx context.Context, spec api.ServiceSpec) (RunServiceResponse, error) {
|
|
||||||
var resp RunServiceResponse
|
var resp RunServiceResponse
|
||||||
|
|
||||||
if err := spec.Validate(); err != nil {
|
if err := spec.Validate(); err != nil {
|
||||||
return resp, fmt.Errorf("invalid service spec: %w", err)
|
return resp, fmt.Errorf("invalid service spec: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
img, err := reference.ParseDockerRef(spec.Container.Image)
|
if spec.Name != "" {
|
||||||
if err != nil {
|
|
||||||
return resp, fmt.Errorf("invalid image: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if spec.Name == "" {
|
|
||||||
// Generate a random service name from the image if not specified.
|
|
||||||
// Get the image name without the repository and tag/digest parts.
|
|
||||||
imageName := reference.FamiliarName(img)
|
|
||||||
// Get the last part of the image name (path), e.g. "nginx" from "bitnami/nginx".
|
|
||||||
if i := strings.LastIndex(imageName, "/"); i != -1 {
|
|
||||||
imageName = imageName[i+1:]
|
|
||||||
}
|
|
||||||
// Append a random suffix to the image name to generate an optimistically unique service name.
|
|
||||||
suffix, err := secret.RandomAlphaNumeric(4)
|
|
||||||
if err != nil {
|
|
||||||
return resp, fmt.Errorf("generate random suffix: %w", err)
|
|
||||||
}
|
|
||||||
spec.Name = fmt.Sprintf("%s-%s", imageName, suffix)
|
|
||||||
} else {
|
|
||||||
// Optimistically check if a service with the specified name already exists.
|
// Optimistically check if a service with the specified name already exists.
|
||||||
_, err := cli.InspectService(ctx, spec.Name)
|
_, err := cli.InspectService(ctx, spec.Name)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
@@ -73,346 +57,30 @@ func (cli *Client) RunService(ctx context.Context, spec api.ServiceSpec) (RunSer
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
serviceID, err := secret.NewID()
|
var err error
|
||||||
if err != nil {
|
if spec, err = cli.PrepareDeploymentSpec(ctx, spec); err != nil {
|
||||||
return resp, fmt.Errorf("generate service ID: %w", err)
|
return resp, fmt.Errorf("prepare service spec ready for deployment: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = progress.RunWithTitle(ctx, func(ctx context.Context) error {
|
err = progress.RunWithTitle(ctx, func(ctx context.Context) error {
|
||||||
switch spec.Mode {
|
deploy, err := cli.NewDeployment(spec, &RollingStrategy{MachineFilter: filter})
|
||||||
case "", api.ServiceModeReplicated:
|
|
||||||
resp, err = cli.runReplicatedService(ctx, serviceID, spec)
|
|
||||||
case api.ServiceModeGlobal:
|
|
||||||
resp, err = cli.runGlobalService(ctx, serviceID, spec)
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("invalid mode: %q", spec.Mode)
|
|
||||||
}
|
|
||||||
|
|
||||||
return err
|
|
||||||
}, cli.progressOut(), "Running service "+spec.Name)
|
|
||||||
|
|
||||||
return resp, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cli *Client) runReplicatedService(ctx context.Context, id string, spec api.ServiceSpec) (RunServiceResponse, error) {
|
|
||||||
resp := RunServiceResponse{
|
|
||||||
ID: id,
|
|
||||||
Name: spec.Name,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find a machine to run a service replica on.
|
|
||||||
machines, err := cli.ListMachines(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return resp, fmt.Errorf("list machines: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: support selecting a particular machine by ID or name through the user options.
|
|
||||||
//var machine *pb.MachineMember
|
|
||||||
//if opts.Machine != "" {
|
|
||||||
// // Check if the machine ID or name exists if it's explicitly specified.
|
|
||||||
// for _, m := range machines {
|
|
||||||
// if m.Machine.Name == opts.Machine || m.Machine.Id == opts.Machine {
|
|
||||||
// machine = m
|
|
||||||
// break
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// if machine == nil {
|
|
||||||
// return resp, fmt.Errorf("machine %q not found", opts.Machine)
|
|
||||||
// }
|
|
||||||
//}
|
|
||||||
|
|
||||||
m := firstAvailableMachine(machines)
|
|
||||||
if m == nil {
|
|
||||||
return resp, errors.New("no available machine to run the service")
|
|
||||||
}
|
|
||||||
|
|
||||||
runResp, err := cli.runContainer(ctx, id, spec, m.Machine)
|
|
||||||
if err != nil {
|
|
||||||
return resp, fmt.Errorf("run container: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
resp.Containers = append(resp.Containers, MachineContainerID{
|
|
||||||
MachineID: m.Machine.Id,
|
|
||||||
ContainerID: runResp.ID,
|
|
||||||
})
|
|
||||||
|
|
||||||
return resp, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func firstAvailableMachine(machines []*pb.MachineMember) *pb.MachineMember {
|
|
||||||
// Find the first UP machine.
|
|
||||||
for _, m := range machines {
|
|
||||||
if m.State == pb.MachineMember_UP {
|
|
||||||
return m
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// There is no UP machine, try to find the first SUSPECT machine.
|
|
||||||
for _, m := range machines {
|
|
||||||
if m.State == pb.MachineMember_SUSPECT {
|
|
||||||
return m
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cli *Client) runGlobalService(ctx context.Context, id string, spec api.ServiceSpec) (RunServiceResponse, error) {
|
|
||||||
resp := RunServiceResponse{
|
|
||||||
ID: id,
|
|
||||||
Name: spec.Name,
|
|
||||||
}
|
|
||||||
|
|
||||||
machines, err := cli.ListMachines(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return resp, fmt.Errorf("list machines: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
wg := sync.WaitGroup{}
|
|
||||||
errCh := make(chan error)
|
|
||||||
mu := sync.Mutex{}
|
|
||||||
|
|
||||||
// Run a service container on each available machine.
|
|
||||||
for _, m := range machines {
|
|
||||||
if m.State != pb.MachineMember_UP && m.State != pb.MachineMember_SUSPECT {
|
|
||||||
// TODO: return failed machines in the response.
|
|
||||||
fmt.Printf("WARNING: failed to run a service container on machine '%s' which is Down.\n", m.Machine.Name)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
wg.Add(1)
|
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
|
|
||||||
runResp, err := cli.runContainer(ctx, id, spec, m.Machine)
|
|
||||||
if err != nil {
|
|
||||||
errCh <- fmt.Errorf("run container on machine '%s': %w", m.Machine.Name, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
mu.Lock()
|
|
||||||
resp.Containers = append(resp.Containers, MachineContainerID{
|
|
||||||
MachineID: m.Machine.Id,
|
|
||||||
ContainerID: runResp.ID,
|
|
||||||
})
|
|
||||||
mu.Unlock()
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
wg.Wait()
|
|
||||||
close(errCh)
|
|
||||||
}()
|
|
||||||
|
|
||||||
err = nil
|
|
||||||
for e := range errCh {
|
|
||||||
err = errors.Join(err, e)
|
|
||||||
}
|
|
||||||
|
|
||||||
return resp, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cli *Client) runContainer(
|
|
||||||
ctx context.Context, serviceID string, spec api.ServiceSpec, machine *pb.MachineInfo,
|
|
||||||
) (container.CreateResponse, error) {
|
|
||||||
var resp container.CreateResponse
|
|
||||||
|
|
||||||
// Proxy Docker gRPC requests to the selected machine.
|
|
||||||
machineIP, _ := machine.Network.ManagementIp.ToAddr()
|
|
||||||
md := metadata.Pairs("machines", machineIP.String())
|
|
||||||
ctx = metadata.NewOutgoingContext(ctx, md)
|
|
||||||
|
|
||||||
suffix, err := secret.RandomAlphaNumeric(4)
|
|
||||||
if err != nil {
|
|
||||||
return resp, fmt.Errorf("generate random suffix: %w", err)
|
|
||||||
}
|
|
||||||
containerName := fmt.Sprintf("%s-%s", spec.Name, suffix)
|
|
||||||
|
|
||||||
config := &container.Config{
|
|
||||||
Cmd: spec.Container.Command,
|
|
||||||
Image: spec.Container.Image,
|
|
||||||
Labels: map[string]string{
|
|
||||||
api.LabelServiceID: serviceID,
|
|
||||||
api.LabelServiceName: spec.Name,
|
|
||||||
api.LabelManaged: "",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
if spec.Mode == api.ServiceModeGlobal {
|
|
||||||
config.Labels[api.LabelServiceMode] = api.ServiceModeGlobal
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(spec.Ports) > 0 {
|
|
||||||
encodedPorts := make([]string, len(spec.Ports))
|
|
||||||
for i, p := range spec.Ports {
|
|
||||||
encodedPorts[i], err = p.String()
|
|
||||||
if err != nil {
|
|
||||||
return resp, fmt.Errorf("encode service port spec: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
config.Labels[api.LabelServicePorts] = strings.Join(encodedPorts, ",")
|
|
||||||
}
|
|
||||||
|
|
||||||
portBindings := make(nat.PortMap)
|
|
||||||
for _, p := range spec.Ports {
|
|
||||||
if p.Mode != api.PortModeHost {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
port := nat.Port(fmt.Sprintf("%d/%s", p.ContainerPort, p.Protocol))
|
|
||||||
portBindings[port] = []nat.PortBinding{
|
|
||||||
{
|
|
||||||
HostPort: strconv.Itoa(int(p.PublishedPort)),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
if p.HostIP.IsValid() {
|
|
||||||
portBindings[port][0].HostIP = p.HostIP.String()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
hostConfig := &container.HostConfig{
|
|
||||||
Binds: spec.Container.Volumes,
|
|
||||||
Init: spec.Container.Init,
|
|
||||||
PortBindings: portBindings,
|
|
||||||
}
|
|
||||||
netConfig := &network.NetworkingConfig{
|
|
||||||
EndpointsConfig: map[string]*network.EndpointSettings{
|
|
||||||
machinedocker.NetworkName: {},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
pw := progress.ContextWriter(ctx)
|
|
||||||
eventID := fmt.Sprintf("Container %s on %s", containerName, machine.Name)
|
|
||||||
|
|
||||||
pw.Event(progress.CreatingEvent(eventID))
|
|
||||||
resp, err = cli.CreateContainer(ctx, config, hostConfig, netConfig, nil, containerName)
|
|
||||||
if err != nil {
|
|
||||||
if !dockerclient.IsErrNotFound(err) {
|
|
||||||
return resp, fmt.Errorf("create container: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pull the missing image and create the container again.
|
|
||||||
if err = cli.pullImageWithProgress(ctx, config.Image, machine.Name, eventID); err != nil {
|
|
||||||
return resp, err
|
|
||||||
}
|
|
||||||
if resp, err = cli.CreateContainer(ctx, config, hostConfig, netConfig, nil, containerName); err != nil {
|
|
||||||
return resp, fmt.Errorf("create container: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pw.Event(progress.CreatedEvent(eventID))
|
|
||||||
|
|
||||||
pw.Event(progress.StartingEvent(eventID))
|
|
||||||
if err = cli.StartContainer(ctx, resp.ID, container.StartOptions{}); err != nil {
|
|
||||||
return resp, fmt.Errorf("start container: %w", err)
|
|
||||||
}
|
|
||||||
pw.Event(progress.StartedEvent(eventID))
|
|
||||||
|
|
||||||
return resp, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cli *Client) pullImageWithProgress(ctx context.Context, image, machineName, parentEventID string) error {
|
|
||||||
pw := progress.ContextWriter(ctx)
|
|
||||||
eventID := fmt.Sprintf("Image %s on %s", image, machineName)
|
|
||||||
pw.Event(progress.Event{
|
|
||||||
ID: eventID,
|
|
||||||
ParentID: parentEventID,
|
|
||||||
Status: progress.Working,
|
|
||||||
StatusText: "Pulling",
|
|
||||||
})
|
|
||||||
|
|
||||||
pullCh, err := cli.PullImage(ctx, image)
|
|
||||||
if err != nil {
|
|
||||||
pw.Event(progress.Event{
|
|
||||||
ID: eventID,
|
|
||||||
ParentID: parentEventID,
|
|
||||||
Text: "Error",
|
|
||||||
Status: progress.Error,
|
|
||||||
StatusText: errors.Unwrap(err).Error(),
|
|
||||||
})
|
|
||||||
return fmt.Errorf("pull image: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait for pull to complete by reading all progress messages and converting them to events.
|
|
||||||
for msg := range pullCh {
|
|
||||||
if msg.Err != nil {
|
|
||||||
err = msg.Err
|
|
||||||
} else {
|
|
||||||
if msg.Message.Error != nil {
|
|
||||||
err = errors.New(msg.Message.Error.Message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
pw.Event(progress.Event{
|
return fmt.Errorf("create deployment: %w", err)
|
||||||
ID: eventID,
|
|
||||||
ParentID: parentEventID,
|
|
||||||
Text: "Error",
|
|
||||||
Status: progress.Error,
|
|
||||||
StatusText: errors.Unwrap(err).Error(),
|
|
||||||
})
|
|
||||||
return fmt.Errorf("pull image: %w", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: add like in compose: --quiet-pull Pull without printing progress information
|
serviceID, err := deploy.Run(ctx)
|
||||||
e := toPullProgressEvent(msg.Message)
|
if err != nil {
|
||||||
if e != nil {
|
return err
|
||||||
e.ID = fmt.Sprintf("%s on %s", e.ID, machineName)
|
|
||||||
e.ParentID = eventID
|
|
||||||
// Grand children events are not printed by the tty progress writer but they are still required
|
|
||||||
// to calculate the progress line of their parent.
|
|
||||||
pw.Event(*e)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
pw.Event(progress.Event{
|
|
||||||
ID: eventID,
|
|
||||||
ParentID: parentEventID,
|
|
||||||
Status: progress.Done,
|
|
||||||
StatusText: "Pulled",
|
|
||||||
})
|
|
||||||
|
|
||||||
return nil
|
resp.ID = serviceID
|
||||||
}
|
// TODO: get the service name from the plan when it's available.
|
||||||
|
resp.Name = spec.Name
|
||||||
|
|
||||||
// toPullProgressEvent converts a JSON progress message from the Docker API to a progress event.
|
|
||||||
// It's based on toPullProgressEvent from Docker Compose.
|
|
||||||
func toPullProgressEvent(jm jsonmessage.JSONMessage) *progress.Event {
|
|
||||||
if jm.ID == "" || jm.Progress == nil {
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}, cli.progressOut(), fmt.Sprintf("Running service %s (%s mode)", spec.Name, spec.Mode))
|
||||||
|
|
||||||
var (
|
return resp, err
|
||||||
total int64
|
|
||||||
percent int
|
|
||||||
current int64
|
|
||||||
)
|
|
||||||
text := jm.Progress.String()
|
|
||||||
stat := progress.Working
|
|
||||||
|
|
||||||
switch jm.Status {
|
|
||||||
case "Preparing", "Waiting", "Pulling fs layer":
|
|
||||||
percent = 0
|
|
||||||
case "Downloading", "Extracting", "Verifying Checksum":
|
|
||||||
current = jm.Progress.Current
|
|
||||||
total = jm.Progress.Total
|
|
||||||
if jm.Progress.Total > 0 {
|
|
||||||
percent = int(jm.Progress.Current * 100 / jm.Progress.Total)
|
|
||||||
}
|
|
||||||
case "Download complete", "Already exists", "Pull complete":
|
|
||||||
stat = progress.Done
|
|
||||||
percent = 100
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.Contains(jm.Status, "Image is up to date") ||
|
|
||||||
strings.Contains(jm.Status, "Downloaded newer image") {
|
|
||||||
stat = progress.Done
|
|
||||||
percent = 100
|
|
||||||
}
|
|
||||||
|
|
||||||
return &progress.Event{
|
|
||||||
ID: jm.ID,
|
|
||||||
Current: current,
|
|
||||||
Total: total,
|
|
||||||
Percent: percent,
|
|
||||||
Text: jm.Status,
|
|
||||||
Status: stat,
|
|
||||||
StatusText: text,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// InspectService returns detailed information about a service and its containers.
|
// InspectService returns detailed information about a service and its containers.
|
||||||
@@ -447,7 +115,7 @@ func (cli *Client) InspectService(ctx context.Context, id string) (api.Service,
|
|||||||
filters.Arg("label", api.LabelManaged),
|
filters.Arg("label", api.LabelManaged),
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
machineContainers, err := cli.ListContainers(listCtx, opts)
|
machineContainers, err := cli.Docker.ListContainers(listCtx, opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return svc, fmt.Errorf("list containers: %w", err)
|
return svc, fmt.Errorf("list containers: %w", err)
|
||||||
}
|
}
|
||||||
@@ -483,7 +151,7 @@ func (cli *Client) InspectService(ctx context.Context, id string) (api.Service,
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, c := range mc.Containers {
|
for _, c := range mc.Containers {
|
||||||
ctr := api.Container{Container: c}
|
ctr := api.Container{ContainerJSON: c}
|
||||||
if ctr.ServiceID() == id || ctr.ServiceName() == id {
|
if ctr.ServiceID() == id || ctr.ServiceName() == id {
|
||||||
containers = append(containers, api.MachineContainer{
|
containers = append(containers, api.MachineContainer{
|
||||||
MachineID: machineID,
|
MachineID: machineID,
|
||||||
@@ -513,7 +181,7 @@ func (cli *Client) InspectService(ctx context.Context, id string) (api.Service,
|
|||||||
serviceID := containers[0].Container.ServiceID()
|
serviceID := containers[0].Container.ServiceID()
|
||||||
for _, mc := range containers[1:] {
|
for _, mc := range containers[1:] {
|
||||||
if mc.Container.ServiceID() != serviceID {
|
if mc.Container.ServiceID() != serviceID {
|
||||||
return svc, fmt.Errorf("multiple services found with name: %s", id)
|
return svc, fmt.Errorf("multiple services found with name '%s', use the service ID instead", id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -582,18 +250,15 @@ func (cli *Client) RemoveService(ctx context.Context, id string) error {
|
|||||||
go func() {
|
go func() {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
|
|
||||||
machineIP, ok := machineManagementIPByID[mc.MachineID]
|
err := cli.StopContainer(ctx, svc.ID, mc.Container.ID, container.StopOptions{})
|
||||||
if !ok {
|
if err != nil {
|
||||||
errCh <- fmt.Errorf("machine not found by ID: %s", mc.MachineID)
|
errCh <- fmt.Errorf("stop container '%s': %w", mc.Container.ID, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
removeCtx := metadata.NewOutgoingContext(ctx, metadata.Pairs("machines", machineIP))
|
|
||||||
// TODO: gracefully stop the container before removing it without force.
|
err = cli.RemoveContainer(ctx, svc.ID, mc.Container.ID, container.RemoveOptions{})
|
||||||
err := cli.RemoveContainer(removeCtx, mc.Container.ID, container.RemoveOptions{Force: true})
|
if err != nil && !errors.Is(err, ErrNotFound) {
|
||||||
if err != nil {
|
errCh <- fmt.Errorf("remove container '%s': %w", mc.Container.ID, err)
|
||||||
if !dockerclient.IsErrNotFound(err) {
|
|
||||||
errCh <- fmt.Errorf("remove container '%s': %w", mc.Container.ID, err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
@@ -636,7 +301,7 @@ func (cli *Client) ListServices(ctx context.Context) ([]api.Service, error) {
|
|||||||
filters.Arg("label", api.LabelManaged),
|
filters.Arg("label", api.LabelManaged),
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
machineContainers, err := cli.ListContainers(listCtx, opts)
|
machineContainers, err := cli.Docker.ListContainers(listCtx, opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("list containers: %w", err)
|
return nil, fmt.Errorf("list containers: %w", err)
|
||||||
}
|
}
|
||||||
@@ -653,7 +318,7 @@ func (cli *Client) ListServices(ctx context.Context) ([]api.Service, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, c := range mc.Containers {
|
for _, c := range mc.Containers {
|
||||||
ctr := api.Container{Container: c}
|
ctr := api.Container{ContainerJSON: c}
|
||||||
if _, ok := servicesByID[ctr.ServiceID()]; ok {
|
if _, ok := servicesByID[ctr.ServiceID()]; ok {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,364 @@
|
|||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"math/rand/v2"
|
||||||
|
"slices"
|
||||||
|
"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 {
|
||||||
|
// Type returns the type of the deployment strategy, e.g. "rolling", "blue-green".
|
||||||
|
Type() string
|
||||||
|
// 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 {
|
||||||
|
// MachineFilter optionally restricts which machines can be used for deployment.
|
||||||
|
MachineFilter MachineFilter
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *RollingStrategy) Type() string {
|
||||||
|
return "rolling"
|
||||||
|
}
|
||||||
|
|
||||||
|
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.
|
||||||
|
// For replicated services, we want to maintain a specific number of containers (replicas) across the available machines
|
||||||
|
// in the cluster.
|
||||||
|
func (s *RollingStrategy) planReplicated(
|
||||||
|
ctx context.Context, cli *Client, svc *api.Service, spec api.ServiceSpec,
|
||||||
|
) (Plan, error) {
|
||||||
|
var plan Plan
|
||||||
|
|
||||||
|
// Generate a new service ID for the first service deployment if it doesn't exist yet.
|
||||||
|
if svc != nil {
|
||||||
|
plan.ServiceID = svc.ID
|
||||||
|
} else {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
// Filter machines that are not DOWN and match the machine filter if provided.
|
||||||
|
var availableMachines []*pb.MachineInfo
|
||||||
|
var unmatchedMachines []*pb.MachineInfo
|
||||||
|
var downMachines []*pb.MachineInfo
|
||||||
|
for _, m := range machines {
|
||||||
|
if m.State == pb.MachineMember_DOWN {
|
||||||
|
downMachines = append(downMachines, m.Machine)
|
||||||
|
} else {
|
||||||
|
if s.MachineFilter == nil || s.MachineFilter(m.Machine) {
|
||||||
|
availableMachines = append(availableMachines, m.Machine)
|
||||||
|
} else {
|
||||||
|
unmatchedMachines = append(unmatchedMachines, m.Machine)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(availableMachines) == 0 {
|
||||||
|
if s.MachineFilter != nil {
|
||||||
|
return plan, ErrNoMatchingMachines
|
||||||
|
}
|
||||||
|
return plan, fmt.Errorf("no available machines to deploy service")
|
||||||
|
}
|
||||||
|
// Randomise the order of machines to avoid always deploying to the same machines first.
|
||||||
|
rand.Shuffle(len(availableMachines), func(i, j int) {
|
||||||
|
availableMachines[i], availableMachines[j] = availableMachines[j], availableMachines[i]
|
||||||
|
})
|
||||||
|
|
||||||
|
// Organise existing containers by machine.
|
||||||
|
containersOnMachine := make(map[string][]api.Container)
|
||||||
|
upToDateContainersOnMachine := make(map[string]int)
|
||||||
|
if svc != nil {
|
||||||
|
runningSpecs := make(map[string]api.ServiceSpec)
|
||||||
|
for _, c := range svc.Containers {
|
||||||
|
if !c.Container.State.Running || c.Container.State.Paused {
|
||||||
|
// Skip containers that are not running.
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
cs, err := c.Container.ServiceSpec()
|
||||||
|
if err == nil {
|
||||||
|
runningSpecs[c.Container.ID] = cs
|
||||||
|
if cs.Equals(spec) {
|
||||||
|
upToDateContainersOnMachine[c.MachineID] += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort containers such that containers with the desired spec are first.
|
||||||
|
slices.SortFunc(svc.Containers, func(c1, c2 api.MachineContainer) int {
|
||||||
|
if spec1, ok := runningSpecs[c1.Container.ID]; ok && spec1.Equals(spec) {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
if spec2, ok := runningSpecs[c2.Container.ID]; ok && spec2.Equals(spec) {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
})
|
||||||
|
|
||||||
|
for _, c := range svc.Containers {
|
||||||
|
containersOnMachine[c.MachineID] = append(containersOnMachine[c.MachineID], c.Container)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort machines such that machines with the most up-to-date containers are first, followed by machines with
|
||||||
|
// existing containers, and finally machines without containers.
|
||||||
|
slices.SortFunc(availableMachines, func(m1, m2 *pb.MachineInfo) int {
|
||||||
|
if upToDateContainersOnMachine[m1.Id] > 0 && upToDateContainersOnMachine[m2.Id] > 0 {
|
||||||
|
return upToDateContainersOnMachine[m2.Id] - upToDateContainersOnMachine[m1.Id]
|
||||||
|
}
|
||||||
|
if upToDateContainersOnMachine[m1.Id] > 0 {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
if upToDateContainersOnMachine[m2.Id] > 0 {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return len(containersOnMachine[m2.Id]) - len(containersOnMachine[m1.Id])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Spread the containers across the available machines evenly using a simple round-robin approach, starting with
|
||||||
|
// machines that already have containers and prioritising machines with containers that match the desired spec.
|
||||||
|
for i := 0; i < int(spec.Replicas); i++ {
|
||||||
|
m := availableMachines[i%len(availableMachines)]
|
||||||
|
containers := containersOnMachine[m.Id]
|
||||||
|
|
||||||
|
if len(containers) == 0 {
|
||||||
|
// No more existing containers on this machine, create a new one.
|
||||||
|
plan.Operations = append(plan.Operations, &RunContainerOperation{
|
||||||
|
ServiceID: plan.ServiceID,
|
||||||
|
Spec: spec,
|
||||||
|
MachineID: m.Id,
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
ctr := containers[0]
|
||||||
|
containersOnMachine[m.Id] = containers[1:]
|
||||||
|
|
||||||
|
if ctr.State.Running {
|
||||||
|
ctrSpec, specErr := ctr.ServiceSpec()
|
||||||
|
if specErr == nil && ctrSpec.Equals(spec) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
conflictingPorts, portsErr := ctr.ConflictingServicePorts(spec.Ports)
|
||||||
|
if specErr != nil || portsErr != nil || len(conflictingPorts) > 0 {
|
||||||
|
// Stop the malformed container or the container with conflicting ports.
|
||||||
|
plan.Operations = append(plan.Operations, &StopContainerOperation{
|
||||||
|
ServiceID: plan.ServiceID,
|
||||||
|
ContainerID: ctr.ID,
|
||||||
|
MachineID: m.Id,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run a new container.
|
||||||
|
plan.Operations = append(plan.Operations, &RunContainerOperation{
|
||||||
|
ServiceID: plan.ServiceID,
|
||||||
|
Spec: spec,
|
||||||
|
MachineID: m.Id,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Remove the old container.
|
||||||
|
plan.Operations = append(plan.Operations, &RemoveContainerOperation{
|
||||||
|
ServiceID: plan.ServiceID,
|
||||||
|
ContainerID: ctr.ID,
|
||||||
|
MachineID: m.Id,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove any remaining containers that are not needed.
|
||||||
|
for mid, containers := range containersOnMachine {
|
||||||
|
for _, c := range containers {
|
||||||
|
plan.Operations = append(plan.Operations, &RemoveContainerOperation{
|
||||||
|
ServiceID: plan.ServiceID,
|
||||||
|
ContainerID: c.ID,
|
||||||
|
MachineID: mid,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return plan, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
// Filter machines if a machine filter is provided.
|
||||||
|
// TODO: not sure this is the right behaviour to ignore other machines that might run service containers.
|
||||||
|
// Maybe there should be another filter to specify which machines to deploy to but keep the rest running.
|
||||||
|
// Could be useful to test a new version on a subset of machines before rolling out to all.
|
||||||
|
if s.MachineFilter != nil {
|
||||||
|
machines = slices.DeleteFunc(machines, func(m *pb.MachineMember) bool {
|
||||||
|
return !s.MachineFilter(m.Machine)
|
||||||
|
})
|
||||||
|
if len(machines) == 0 {
|
||||||
|
return plan, ErrNoMatchingMachines
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
plan.Operations = append(plan.Operations, ops...)
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
@@ -14,10 +14,11 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type MachineConnection struct {
|
type MachineConnection struct {
|
||||||
SSH SSHDestination `toml:"ssh,omitempty"`
|
SSH SSHDestination `toml:"ssh,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".
|
||||||
|
|||||||
+22
-10
@@ -17,24 +17,36 @@ type RemoteMachine struct {
|
|||||||
KeyPath string
|
KeyPath string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func installCmd(user string) string {
|
||||||
|
sudoPrefix := "sudo"
|
||||||
|
// Add the SSH user (non-root) to the uncloud group to allow access to the Uncloud daemon unix socket.
|
||||||
|
env := "UNCLOUD_GROUP_ADD_USER=" + user
|
||||||
|
|
||||||
|
curlBashCmd := fmt.Sprintf(
|
||||||
|
"curl -fsSL %s | %s %s bash", sshexec.Quote(installScriptURL), sudoPrefix, sshexec.Quote(env),
|
||||||
|
)
|
||||||
|
|
||||||
|
if user == "root" {
|
||||||
|
curlBashCmd = fmt.Sprintf(
|
||||||
|
"curl -fsSL %s | bash", sshexec.Quote(installScriptURL),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return curlBashCmd
|
||||||
|
}
|
||||||
|
|
||||||
// provisionMachine provisions the remote machine by downloading the Uncloud install script from GitHub and running it.
|
// provisionMachine provisions the remote machine by downloading the Uncloud install script from GitHub and running it.
|
||||||
func provisionMachine(ctx context.Context, exec sshexec.Executor) error {
|
func provisionMachine(ctx context.Context, exec sshexec.Executor) error {
|
||||||
user, err := exec.Run(ctx, "whoami")
|
user, err := exec.Run(ctx, "whoami")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("run whoami: %w", err)
|
return fmt.Errorf("run whoami: %w", err)
|
||||||
}
|
}
|
||||||
sudoPrefix, env := "", ""
|
|
||||||
if user != "root" {
|
installCmd := installCmd(user)
|
||||||
sudoPrefix = "sudo"
|
|
||||||
// Add the SSH user (non-root) to the uncloud group to allow access to the Uncloud daemon unix socket.
|
|
||||||
env = "UNCLOUD_GROUP_ADD_USER=" + user
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Println("Downloading Uncloud install script:", installScriptURL)
|
fmt.Println("Downloading Uncloud install script:", installScriptURL)
|
||||||
curlBashCmd := fmt.Sprintf(
|
|
||||||
"curl -fsSL %s | %s %s bash", sshexec.Quote(installScriptURL), sudoPrefix, sshexec.Quote(env),
|
cmd := sshexec.QuoteCommand("bash", "-c", "set -o pipefail; "+installCmd)
|
||||||
)
|
|
||||||
cmd := sshexec.QuoteCommand("bash", "-c", "set -o pipefail; "+curlBashCmd)
|
|
||||||
if err = exec.Stream(ctx, cmd, os.Stdout, os.Stderr); err != nil {
|
if err = exec.Stream(ctx, cmd, os.Stdout, os.Stderr); err != nil {
|
||||||
return fmt.Errorf("download and run install script: %w", err)
|
return fmt.Errorf("download and run install script: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestInstallCmd(t *testing.T) {
|
||||||
|
t.Run("root", func(t *testing.T) {
|
||||||
|
cmd := installCmd("root")
|
||||||
|
assert.NotContains(t, cmd, "sudo")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("nonroot", func(t *testing.T) {
|
||||||
|
cmd := installCmd("nonroot")
|
||||||
|
assert.Contains(t, cmd, "sudo")
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -5,8 +5,23 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"os/user"
|
"os/user"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func ExpandHomeDir(path string) string {
|
||||||
|
if len(path) == 0 {
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
if path[0] == '~' {
|
||||||
|
home, err := os.UserHomeDir()
|
||||||
|
if err != nil {
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
return strings.Replace(path, "~", home, 1)
|
||||||
|
}
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
|
||||||
// LookupUIDGID returns the user and group IDs for the given username.
|
// LookupUIDGID returns the user and group IDs for the given username.
|
||||||
func LookupUIDGID(username string) (uid, gid int, err error) {
|
func LookupUIDGID(username string) (uid, gid int, err error) {
|
||||||
usr, err := user.Lookup(username)
|
usr, err := user.Lookup(username)
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package fs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestExpandHomeDir(t *testing.T) {
|
||||||
|
t.Run("empty", func(t *testing.T) {
|
||||||
|
assert.Equal(t, "", ExpandHomeDir(""))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("no home", func(t *testing.T) {
|
||||||
|
assert.Equal(t, "/path", ExpandHomeDir("/path"))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("home", func(t *testing.T) {
|
||||||
|
t.Setenv("HOME", "/home/user")
|
||||||
|
assert.Equal(t, "/home/user/path", ExpandHomeDir("~/path"))
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -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",
|
||||||
|
|||||||
@@ -152,6 +152,101 @@ func (x *CreateContainerResponse) GetResponse() []byte {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type InspectContainerRequest struct {
|
||||||
|
state protoimpl.MessageState
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
|
||||||
|
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *InspectContainerRequest) Reset() {
|
||||||
|
*x = InspectContainerRequest{}
|
||||||
|
if protoimpl.UnsafeEnabled {
|
||||||
|
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[2]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *InspectContainerRequest) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*InspectContainerRequest) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *InspectContainerRequest) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[2]
|
||||||
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use InspectContainerRequest.ProtoReflect.Descriptor instead.
|
||||||
|
func (*InspectContainerRequest) Descriptor() ([]byte, []int) {
|
||||||
|
return file_internal_machine_api_pb_docker_proto_rawDescGZIP(), []int{2}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *InspectContainerRequest) GetId() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Id
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
type InspectContainerResponse struct {
|
||||||
|
state protoimpl.MessageState
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
|
||||||
|
// JSON serialized container.InspectResponse.
|
||||||
|
Response []byte `protobuf:"bytes,1,opt,name=response,proto3" json:"response,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *InspectContainerResponse) Reset() {
|
||||||
|
*x = InspectContainerResponse{}
|
||||||
|
if protoimpl.UnsafeEnabled {
|
||||||
|
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[3]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *InspectContainerResponse) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*InspectContainerResponse) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *InspectContainerResponse) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[3]
|
||||||
|
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 InspectContainerResponse.ProtoReflect.Descriptor instead.
|
||||||
|
func (*InspectContainerResponse) Descriptor() ([]byte, []int) {
|
||||||
|
return file_internal_machine_api_pb_docker_proto_rawDescGZIP(), []int{3}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *InspectContainerResponse) GetResponse() []byte {
|
||||||
|
if x != nil {
|
||||||
|
return x.Response
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
type StartContainerRequest struct {
|
type StartContainerRequest struct {
|
||||||
state protoimpl.MessageState
|
state protoimpl.MessageState
|
||||||
sizeCache protoimpl.SizeCache
|
sizeCache protoimpl.SizeCache
|
||||||
@@ -165,7 +260,7 @@ type StartContainerRequest struct {
|
|||||||
func (x *StartContainerRequest) Reset() {
|
func (x *StartContainerRequest) Reset() {
|
||||||
*x = StartContainerRequest{}
|
*x = StartContainerRequest{}
|
||||||
if protoimpl.UnsafeEnabled {
|
if protoimpl.UnsafeEnabled {
|
||||||
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[2]
|
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[4]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@@ -178,7 +273,7 @@ func (x *StartContainerRequest) String() string {
|
|||||||
func (*StartContainerRequest) ProtoMessage() {}
|
func (*StartContainerRequest) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *StartContainerRequest) ProtoReflect() protoreflect.Message {
|
func (x *StartContainerRequest) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[2]
|
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[4]
|
||||||
if protoimpl.UnsafeEnabled && x != nil {
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
@@ -191,7 +286,7 @@ func (x *StartContainerRequest) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use StartContainerRequest.ProtoReflect.Descriptor instead.
|
// Deprecated: Use StartContainerRequest.ProtoReflect.Descriptor instead.
|
||||||
func (*StartContainerRequest) Descriptor() ([]byte, []int) {
|
func (*StartContainerRequest) Descriptor() ([]byte, []int) {
|
||||||
return file_internal_machine_api_pb_docker_proto_rawDescGZIP(), []int{2}
|
return file_internal_machine_api_pb_docker_proto_rawDescGZIP(), []int{4}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *StartContainerRequest) GetId() string {
|
func (x *StartContainerRequest) GetId() string {
|
||||||
@@ -208,6 +303,62 @@ func (x *StartContainerRequest) GetOptions() []byte {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type StopContainerRequest struct {
|
||||||
|
state protoimpl.MessageState
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
|
||||||
|
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
|
||||||
|
// JSON serialized container.StopOptions.
|
||||||
|
Options []byte `protobuf:"bytes,2,opt,name=options,proto3" json:"options,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *StopContainerRequest) Reset() {
|
||||||
|
*x = StopContainerRequest{}
|
||||||
|
if protoimpl.UnsafeEnabled {
|
||||||
|
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[5]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *StopContainerRequest) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*StopContainerRequest) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *StopContainerRequest) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_internal_machine_api_pb_docker_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 StopContainerRequest.ProtoReflect.Descriptor instead.
|
||||||
|
func (*StopContainerRequest) Descriptor() ([]byte, []int) {
|
||||||
|
return file_internal_machine_api_pb_docker_proto_rawDescGZIP(), []int{5}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *StopContainerRequest) GetId() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Id
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *StopContainerRequest) GetOptions() []byte {
|
||||||
|
if x != nil {
|
||||||
|
return x.Options
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
type ListContainersRequest struct {
|
type ListContainersRequest struct {
|
||||||
state protoimpl.MessageState
|
state protoimpl.MessageState
|
||||||
sizeCache protoimpl.SizeCache
|
sizeCache protoimpl.SizeCache
|
||||||
@@ -220,7 +371,7 @@ type ListContainersRequest struct {
|
|||||||
func (x *ListContainersRequest) Reset() {
|
func (x *ListContainersRequest) Reset() {
|
||||||
*x = ListContainersRequest{}
|
*x = ListContainersRequest{}
|
||||||
if protoimpl.UnsafeEnabled {
|
if protoimpl.UnsafeEnabled {
|
||||||
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[3]
|
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[6]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@@ -233,7 +384,7 @@ func (x *ListContainersRequest) String() string {
|
|||||||
func (*ListContainersRequest) ProtoMessage() {}
|
func (*ListContainersRequest) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *ListContainersRequest) ProtoReflect() protoreflect.Message {
|
func (x *ListContainersRequest) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[3]
|
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[6]
|
||||||
if protoimpl.UnsafeEnabled && x != nil {
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
@@ -246,7 +397,7 @@ func (x *ListContainersRequest) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use ListContainersRequest.ProtoReflect.Descriptor instead.
|
// Deprecated: Use ListContainersRequest.ProtoReflect.Descriptor instead.
|
||||||
func (*ListContainersRequest) Descriptor() ([]byte, []int) {
|
func (*ListContainersRequest) Descriptor() ([]byte, []int) {
|
||||||
return file_internal_machine_api_pb_docker_proto_rawDescGZIP(), []int{3}
|
return file_internal_machine_api_pb_docker_proto_rawDescGZIP(), []int{6}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *ListContainersRequest) GetOptions() []byte {
|
func (x *ListContainersRequest) GetOptions() []byte {
|
||||||
@@ -268,7 +419,7 @@ type ListContainersResponse struct {
|
|||||||
func (x *ListContainersResponse) Reset() {
|
func (x *ListContainersResponse) Reset() {
|
||||||
*x = ListContainersResponse{}
|
*x = ListContainersResponse{}
|
||||||
if protoimpl.UnsafeEnabled {
|
if protoimpl.UnsafeEnabled {
|
||||||
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[4]
|
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[7]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@@ -281,7 +432,7 @@ func (x *ListContainersResponse) String() string {
|
|||||||
func (*ListContainersResponse) ProtoMessage() {}
|
func (*ListContainersResponse) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *ListContainersResponse) ProtoReflect() protoreflect.Message {
|
func (x *ListContainersResponse) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[4]
|
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[7]
|
||||||
if protoimpl.UnsafeEnabled && x != nil {
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
@@ -294,7 +445,7 @@ func (x *ListContainersResponse) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use ListContainersResponse.ProtoReflect.Descriptor instead.
|
// Deprecated: Use ListContainersResponse.ProtoReflect.Descriptor instead.
|
||||||
func (*ListContainersResponse) Descriptor() ([]byte, []int) {
|
func (*ListContainersResponse) Descriptor() ([]byte, []int) {
|
||||||
return file_internal_machine_api_pb_docker_proto_rawDescGZIP(), []int{4}
|
return file_internal_machine_api_pb_docker_proto_rawDescGZIP(), []int{7}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *ListContainersResponse) GetMessages() []*MachineContainers {
|
func (x *ListContainersResponse) GetMessages() []*MachineContainers {
|
||||||
@@ -310,14 +461,14 @@ type MachineContainers struct {
|
|||||||
unknownFields protoimpl.UnknownFields
|
unknownFields protoimpl.UnknownFields
|
||||||
|
|
||||||
Metadata *Metadata `protobuf:"bytes,1,opt,name=metadata,proto3" json:"metadata,omitempty"`
|
Metadata *Metadata `protobuf:"bytes,1,opt,name=metadata,proto3" json:"metadata,omitempty"`
|
||||||
// JSON serialized []container.Summary.
|
// JSON serialized []container.ContainerJSON.
|
||||||
Containers []byte `protobuf:"bytes,2,opt,name=containers,proto3" json:"containers,omitempty"`
|
Containers []byte `protobuf:"bytes,2,opt,name=containers,proto3" json:"containers,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *MachineContainers) Reset() {
|
func (x *MachineContainers) Reset() {
|
||||||
*x = MachineContainers{}
|
*x = MachineContainers{}
|
||||||
if protoimpl.UnsafeEnabled {
|
if protoimpl.UnsafeEnabled {
|
||||||
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[5]
|
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[8]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@@ -330,7 +481,7 @@ func (x *MachineContainers) String() string {
|
|||||||
func (*MachineContainers) ProtoMessage() {}
|
func (*MachineContainers) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *MachineContainers) ProtoReflect() protoreflect.Message {
|
func (x *MachineContainers) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[5]
|
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[8]
|
||||||
if protoimpl.UnsafeEnabled && x != nil {
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
@@ -343,7 +494,7 @@ func (x *MachineContainers) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use MachineContainers.ProtoReflect.Descriptor instead.
|
// Deprecated: Use MachineContainers.ProtoReflect.Descriptor instead.
|
||||||
func (*MachineContainers) Descriptor() ([]byte, []int) {
|
func (*MachineContainers) Descriptor() ([]byte, []int) {
|
||||||
return file_internal_machine_api_pb_docker_proto_rawDescGZIP(), []int{5}
|
return file_internal_machine_api_pb_docker_proto_rawDescGZIP(), []int{8}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *MachineContainers) GetMetadata() *Metadata {
|
func (x *MachineContainers) GetMetadata() *Metadata {
|
||||||
@@ -373,7 +524,7 @@ type RemoveContainerRequest struct {
|
|||||||
func (x *RemoveContainerRequest) Reset() {
|
func (x *RemoveContainerRequest) Reset() {
|
||||||
*x = RemoveContainerRequest{}
|
*x = RemoveContainerRequest{}
|
||||||
if protoimpl.UnsafeEnabled {
|
if protoimpl.UnsafeEnabled {
|
||||||
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[6]
|
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[9]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@@ -386,7 +537,7 @@ func (x *RemoveContainerRequest) String() string {
|
|||||||
func (*RemoveContainerRequest) ProtoMessage() {}
|
func (*RemoveContainerRequest) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *RemoveContainerRequest) ProtoReflect() protoreflect.Message {
|
func (x *RemoveContainerRequest) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[6]
|
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[9]
|
||||||
if protoimpl.UnsafeEnabled && x != nil {
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
@@ -399,7 +550,7 @@ func (x *RemoveContainerRequest) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use RemoveContainerRequest.ProtoReflect.Descriptor instead.
|
// Deprecated: Use RemoveContainerRequest.ProtoReflect.Descriptor instead.
|
||||||
func (*RemoveContainerRequest) Descriptor() ([]byte, []int) {
|
func (*RemoveContainerRequest) Descriptor() ([]byte, []int) {
|
||||||
return file_internal_machine_api_pb_docker_proto_rawDescGZIP(), []int{6}
|
return file_internal_machine_api_pb_docker_proto_rawDescGZIP(), []int{9}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *RemoveContainerRequest) GetId() string {
|
func (x *RemoveContainerRequest) GetId() string {
|
||||||
@@ -429,7 +580,7 @@ type PullImageRequest struct {
|
|||||||
func (x *PullImageRequest) Reset() {
|
func (x *PullImageRequest) Reset() {
|
||||||
*x = PullImageRequest{}
|
*x = PullImageRequest{}
|
||||||
if protoimpl.UnsafeEnabled {
|
if protoimpl.UnsafeEnabled {
|
||||||
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[7]
|
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[10]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@@ -442,7 +593,7 @@ func (x *PullImageRequest) String() string {
|
|||||||
func (*PullImageRequest) ProtoMessage() {}
|
func (*PullImageRequest) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *PullImageRequest) ProtoReflect() protoreflect.Message {
|
func (x *PullImageRequest) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[7]
|
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[10]
|
||||||
if protoimpl.UnsafeEnabled && x != nil {
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
@@ -455,7 +606,7 @@ func (x *PullImageRequest) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use PullImageRequest.ProtoReflect.Descriptor instead.
|
// Deprecated: Use PullImageRequest.ProtoReflect.Descriptor instead.
|
||||||
func (*PullImageRequest) Descriptor() ([]byte, []int) {
|
func (*PullImageRequest) Descriptor() ([]byte, []int) {
|
||||||
return file_internal_machine_api_pb_docker_proto_rawDescGZIP(), []int{7}
|
return file_internal_machine_api_pb_docker_proto_rawDescGZIP(), []int{10}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *PullImageRequest) GetImage() string {
|
func (x *PullImageRequest) GetImage() string {
|
||||||
@@ -484,7 +635,7 @@ type JSONMessage struct {
|
|||||||
func (x *JSONMessage) Reset() {
|
func (x *JSONMessage) Reset() {
|
||||||
*x = JSONMessage{}
|
*x = JSONMessage{}
|
||||||
if protoimpl.UnsafeEnabled {
|
if protoimpl.UnsafeEnabled {
|
||||||
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[8]
|
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[11]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@@ -497,7 +648,7 @@ func (x *JSONMessage) String() string {
|
|||||||
func (*JSONMessage) ProtoMessage() {}
|
func (*JSONMessage) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *JSONMessage) ProtoReflect() protoreflect.Message {
|
func (x *JSONMessage) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[8]
|
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[11]
|
||||||
if protoimpl.UnsafeEnabled && x != nil {
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
@@ -510,7 +661,7 @@ func (x *JSONMessage) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use JSONMessage.ProtoReflect.Descriptor instead.
|
// Deprecated: Use JSONMessage.ProtoReflect.Descriptor instead.
|
||||||
func (*JSONMessage) Descriptor() ([]byte, []int) {
|
func (*JSONMessage) Descriptor() ([]byte, []int) {
|
||||||
return file_internal_machine_api_pb_docker_proto_rawDescGZIP(), []int{8}
|
return file_internal_machine_api_pb_docker_proto_rawDescGZIP(), []int{11}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *JSONMessage) GetMessage() []byte {
|
func (x *JSONMessage) GetMessage() []byte {
|
||||||
@@ -544,62 +695,82 @@ var file_internal_machine_api_pb_docker_proto_rawDesc = []byte{
|
|||||||
0x61, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70,
|
0x61, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70,
|
||||||
0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
|
0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
|
||||||
0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
|
0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
|
||||||
0x22, 0x41, 0x0a, 0x15, 0x53, 0x74, 0x61, 0x72, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e,
|
0x22, 0x29, 0x0a, 0x17, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61,
|
||||||
0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18,
|
0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69,
|
||||||
0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x6f, 0x70, 0x74,
|
0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x22, 0x36, 0x0a, 0x18, 0x49,
|
||||||
0x69, 0x6f, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x6f, 0x70, 0x74, 0x69,
|
0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52,
|
||||||
0x6f, 0x6e, 0x73, 0x22, 0x31, 0x0a, 0x15, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61,
|
0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x70, 0x6f,
|
||||||
0x69, 0x6e, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x07,
|
0x6e, 0x73, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, 0x72, 0x65, 0x73, 0x70, 0x6f,
|
||||||
0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x6f,
|
0x6e, 0x73, 0x65, 0x22, 0x41, 0x0a, 0x15, 0x53, 0x74, 0x61, 0x72, 0x74, 0x43, 0x6f, 0x6e, 0x74,
|
||||||
0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x4c, 0x0a, 0x16, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f,
|
0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02,
|
||||||
0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
|
0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x18, 0x0a, 0x07,
|
||||||
0x12, 0x32, 0x0a, 0x08, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03,
|
0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x6f,
|
||||||
0x28, 0x0b, 0x32, 0x16, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65,
|
0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x40, 0x0a, 0x14, 0x53, 0x74, 0x6f, 0x70, 0x43, 0x6f,
|
||||||
0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x52, 0x08, 0x6d, 0x65, 0x73, 0x73,
|
|
||||||
0x61, 0x67, 0x65, 0x73, 0x22, 0x5e, 0x0a, 0x11, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x43,
|
|
||||||
0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x12, 0x29, 0x0a, 0x08, 0x6d, 0x65, 0x74,
|
|
||||||
0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x61, 0x70,
|
|
||||||
0x69, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61,
|
|
||||||
0x64, 0x61, 0x74, 0x61, 0x12, 0x1e, 0x0a, 0x0a, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65,
|
|
||||||
0x72, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0a, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69,
|
|
||||||
0x6e, 0x65, 0x72, 0x73, 0x22, 0x42, 0x0a, 0x16, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x43, 0x6f,
|
|
||||||
0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e,
|
0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e,
|
||||||
0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x18,
|
0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x18,
|
||||||
0x0a, 0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52,
|
0x0a, 0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52,
|
||||||
0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x42, 0x0a, 0x10, 0x50, 0x75, 0x6c, 0x6c,
|
0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x31, 0x0a, 0x15, 0x4c, 0x69, 0x73, 0x74,
|
||||||
0x49, 0x6d, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x14, 0x0a, 0x05,
|
0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
|
||||||
0x69, 0x6d, 0x61, 0x67, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x69, 0x6d, 0x61,
|
0x74, 0x12, 0x18, 0x0a, 0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x01,
|
||||||
0x67, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x02, 0x20,
|
0x28, 0x0c, 0x52, 0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x4c, 0x0a, 0x16, 0x4c,
|
||||||
0x01, 0x28, 0x0c, 0x52, 0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x27, 0x0a, 0x0b,
|
0x69, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73,
|
||||||
0x4a, 0x53, 0x4f, 0x4e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x6d,
|
0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x32, 0x0a, 0x08, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65,
|
||||||
0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x6d, 0x65,
|
0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d, 0x61,
|
||||||
0x73, 0x73, 0x61, 0x67, 0x65, 0x32, 0xe7, 0x02, 0x0a, 0x06, 0x44, 0x6f, 0x63, 0x6b, 0x65, 0x72,
|
0x63, 0x68, 0x69, 0x6e, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x52,
|
||||||
0x12, 0x4c, 0x0a, 0x0f, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69,
|
0x08, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x22, 0x5e, 0x0a, 0x11, 0x4d, 0x61, 0x63,
|
||||||
0x6e, 0x65, 0x72, 0x12, 0x1b, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65,
|
0x68, 0x69, 0x6e, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x12, 0x29,
|
||||||
0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
|
0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b,
|
||||||
0x1a, 0x1c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6e,
|
0x32, 0x0d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52,
|
||||||
0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x44,
|
0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x1e, 0x0a, 0x0a, 0x63, 0x6f, 0x6e,
|
||||||
0x0a, 0x0e, 0x53, 0x74, 0x61, 0x72, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72,
|
0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0a, 0x63,
|
||||||
0x12, 0x1a, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x53, 0x74, 0x61, 0x72, 0x74, 0x43, 0x6f, 0x6e, 0x74,
|
0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x22, 0x42, 0x0a, 0x16, 0x52, 0x65, 0x6d,
|
||||||
0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67,
|
0x6f, 0x76, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75,
|
||||||
0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45,
|
0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52,
|
||||||
0x6d, 0x70, 0x74, 0x79, 0x12, 0x49, 0x0a, 0x0e, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x74,
|
0x02, 0x69, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x02,
|
||||||
0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x12, 0x1a, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73,
|
0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x42, 0x0a,
|
||||||
0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65,
|
0x10, 0x50, 0x75, 0x6c, 0x6c, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
|
||||||
0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, 0x6e,
|
0x74, 0x12, 0x14, 0x0a, 0x05, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
|
||||||
0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12,
|
0x52, 0x05, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f,
|
||||||
0x46, 0x0a, 0x0f, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e,
|
0x6e, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e,
|
||||||
0x65, 0x72, 0x12, 0x1b, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x43,
|
0x73, 0x22, 0x27, 0x0a, 0x0b, 0x4a, 0x53, 0x4f, 0x4e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65,
|
||||||
0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a,
|
0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28,
|
||||||
0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75,
|
0x0c, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x32, 0xfc, 0x03, 0x0a, 0x06, 0x44,
|
||||||
0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x36, 0x0a, 0x09, 0x50, 0x75, 0x6c, 0x6c, 0x49,
|
0x6f, 0x63, 0x6b, 0x65, 0x72, 0x12, 0x4c, 0x0a, 0x0f, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x43,
|
||||||
0x6d, 0x61, 0x67, 0x65, 0x12, 0x15, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x50, 0x75, 0x6c, 0x6c, 0x49,
|
0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x1b, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43,
|
||||||
0x6d, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x10, 0x2e, 0x61, 0x70,
|
0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65,
|
||||||
0x69, 0x2e, 0x4a, 0x53, 0x4f, 0x4e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x30, 0x01, 0x42,
|
0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x72, 0x65, 0x61,
|
||||||
0x37, 0x5a, 0x35, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x70, 0x73,
|
0x74, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f,
|
||||||
0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x6b, 0x69, 0x2f, 0x75, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64,
|
0x6e, 0x73, 0x65, 0x12, 0x4f, 0x0a, 0x10, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x43, 0x6f,
|
||||||
0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e,
|
0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x1c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e,
|
||||||
0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
0x73, 0x70, 0x65, 0x63, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65,
|
||||||
|
0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x73, 0x70,
|
||||||
|
0x65, 0x63, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70,
|
||||||
|
0x6f, 0x6e, 0x73, 0x65, 0x12, 0x44, 0x0a, 0x0e, 0x53, 0x74, 0x61, 0x72, 0x74, 0x43, 0x6f, 0x6e,
|
||||||
|
0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x1a, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x53, 0x74, 0x61,
|
||||||
|
0x72, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65,
|
||||||
|
0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74,
|
||||||
|
0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x42, 0x0a, 0x0d, 0x53, 0x74,
|
||||||
|
0x6f, 0x70, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x19, 0x2e, 0x61, 0x70,
|
||||||
|
0x69, 0x2e, 0x53, 0x74, 0x6f, 0x70, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52,
|
||||||
|
0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e,
|
||||||
|
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x49,
|
||||||
|
0x0a, 0x0e, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73,
|
||||||
|
0x12, 0x1a, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61,
|
||||||
|
0x69, 0x6e, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x61,
|
||||||
|
0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72,
|
||||||
|
0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x46, 0x0a, 0x0f, 0x52, 0x65, 0x6d,
|
||||||
|
0x6f, 0x76, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x1b, 0x2e, 0x61,
|
||||||
|
0x70, 0x69, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e,
|
||||||
|
0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67,
|
||||||
|
0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74,
|
||||||
|
0x79, 0x12, 0x36, 0x0a, 0x09, 0x50, 0x75, 0x6c, 0x6c, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x12, 0x15,
|
||||||
|
0x2e, 0x61, 0x70, 0x69, 0x2e, 0x50, 0x75, 0x6c, 0x6c, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x52, 0x65,
|
||||||
|
0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x10, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4a, 0x53, 0x4f, 0x4e,
|
||||||
|
0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x30, 0x01, 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 (
|
||||||
@@ -614,35 +785,42 @@ func file_internal_machine_api_pb_docker_proto_rawDescGZIP() []byte {
|
|||||||
return file_internal_machine_api_pb_docker_proto_rawDescData
|
return file_internal_machine_api_pb_docker_proto_rawDescData
|
||||||
}
|
}
|
||||||
|
|
||||||
var file_internal_machine_api_pb_docker_proto_msgTypes = make([]protoimpl.MessageInfo, 9)
|
var file_internal_machine_api_pb_docker_proto_msgTypes = make([]protoimpl.MessageInfo, 12)
|
||||||
var file_internal_machine_api_pb_docker_proto_goTypes = []any{
|
var file_internal_machine_api_pb_docker_proto_goTypes = []any{
|
||||||
(*CreateContainerRequest)(nil), // 0: api.CreateContainerRequest
|
(*CreateContainerRequest)(nil), // 0: api.CreateContainerRequest
|
||||||
(*CreateContainerResponse)(nil), // 1: api.CreateContainerResponse
|
(*CreateContainerResponse)(nil), // 1: api.CreateContainerResponse
|
||||||
(*StartContainerRequest)(nil), // 2: api.StartContainerRequest
|
(*InspectContainerRequest)(nil), // 2: api.InspectContainerRequest
|
||||||
(*ListContainersRequest)(nil), // 3: api.ListContainersRequest
|
(*InspectContainerResponse)(nil), // 3: api.InspectContainerResponse
|
||||||
(*ListContainersResponse)(nil), // 4: api.ListContainersResponse
|
(*StartContainerRequest)(nil), // 4: api.StartContainerRequest
|
||||||
(*MachineContainers)(nil), // 5: api.MachineContainers
|
(*StopContainerRequest)(nil), // 5: api.StopContainerRequest
|
||||||
(*RemoveContainerRequest)(nil), // 6: api.RemoveContainerRequest
|
(*ListContainersRequest)(nil), // 6: api.ListContainersRequest
|
||||||
(*PullImageRequest)(nil), // 7: api.PullImageRequest
|
(*ListContainersResponse)(nil), // 7: api.ListContainersResponse
|
||||||
(*JSONMessage)(nil), // 8: api.JSONMessage
|
(*MachineContainers)(nil), // 8: api.MachineContainers
|
||||||
(*Metadata)(nil), // 9: api.Metadata
|
(*RemoveContainerRequest)(nil), // 9: api.RemoveContainerRequest
|
||||||
(*emptypb.Empty)(nil), // 10: google.protobuf.Empty
|
(*PullImageRequest)(nil), // 10: api.PullImageRequest
|
||||||
|
(*JSONMessage)(nil), // 11: api.JSONMessage
|
||||||
|
(*Metadata)(nil), // 12: api.Metadata
|
||||||
|
(*emptypb.Empty)(nil), // 13: google.protobuf.Empty
|
||||||
}
|
}
|
||||||
var file_internal_machine_api_pb_docker_proto_depIdxs = []int32{
|
var file_internal_machine_api_pb_docker_proto_depIdxs = []int32{
|
||||||
5, // 0: api.ListContainersResponse.messages:type_name -> api.MachineContainers
|
8, // 0: api.ListContainersResponse.messages:type_name -> api.MachineContainers
|
||||||
9, // 1: api.MachineContainers.metadata:type_name -> api.Metadata
|
12, // 1: api.MachineContainers.metadata:type_name -> api.Metadata
|
||||||
0, // 2: api.Docker.CreateContainer:input_type -> api.CreateContainerRequest
|
0, // 2: api.Docker.CreateContainer:input_type -> api.CreateContainerRequest
|
||||||
2, // 3: api.Docker.StartContainer:input_type -> api.StartContainerRequest
|
2, // 3: api.Docker.InspectContainer:input_type -> api.InspectContainerRequest
|
||||||
3, // 4: api.Docker.ListContainers:input_type -> api.ListContainersRequest
|
4, // 4: api.Docker.StartContainer:input_type -> api.StartContainerRequest
|
||||||
6, // 5: api.Docker.RemoveContainer:input_type -> api.RemoveContainerRequest
|
5, // 5: api.Docker.StopContainer:input_type -> api.StopContainerRequest
|
||||||
7, // 6: api.Docker.PullImage:input_type -> api.PullImageRequest
|
6, // 6: api.Docker.ListContainers:input_type -> api.ListContainersRequest
|
||||||
1, // 7: api.Docker.CreateContainer:output_type -> api.CreateContainerResponse
|
9, // 7: api.Docker.RemoveContainer:input_type -> api.RemoveContainerRequest
|
||||||
10, // 8: api.Docker.StartContainer:output_type -> google.protobuf.Empty
|
10, // 8: api.Docker.PullImage:input_type -> api.PullImageRequest
|
||||||
4, // 9: api.Docker.ListContainers:output_type -> api.ListContainersResponse
|
1, // 9: api.Docker.CreateContainer:output_type -> api.CreateContainerResponse
|
||||||
10, // 10: api.Docker.RemoveContainer:output_type -> google.protobuf.Empty
|
3, // 10: api.Docker.InspectContainer:output_type -> api.InspectContainerResponse
|
||||||
8, // 11: api.Docker.PullImage:output_type -> api.JSONMessage
|
13, // 11: api.Docker.StartContainer:output_type -> google.protobuf.Empty
|
||||||
7, // [7:12] is the sub-list for method output_type
|
13, // 12: api.Docker.StopContainer:output_type -> google.protobuf.Empty
|
||||||
2, // [2:7] is the sub-list for method input_type
|
7, // 13: api.Docker.ListContainers:output_type -> api.ListContainersResponse
|
||||||
|
13, // 14: api.Docker.RemoveContainer:output_type -> google.protobuf.Empty
|
||||||
|
11, // 15: api.Docker.PullImage:output_type -> api.JSONMessage
|
||||||
|
9, // [9:16] is the sub-list for method output_type
|
||||||
|
2, // [2:9] is the sub-list for method input_type
|
||||||
2, // [2:2] is the sub-list for extension type_name
|
2, // [2:2] is the sub-list for extension type_name
|
||||||
2, // [2:2] is the sub-list for extension extendee
|
2, // [2:2] is the sub-list for extension extendee
|
||||||
0, // [0:2] is the sub-list for field type_name
|
0, // [0:2] is the sub-list for field type_name
|
||||||
@@ -680,7 +858,7 @@ func file_internal_machine_api_pb_docker_proto_init() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
file_internal_machine_api_pb_docker_proto_msgTypes[2].Exporter = func(v any, i int) any {
|
file_internal_machine_api_pb_docker_proto_msgTypes[2].Exporter = func(v any, i int) any {
|
||||||
switch v := v.(*StartContainerRequest); i {
|
switch v := v.(*InspectContainerRequest); i {
|
||||||
case 0:
|
case 0:
|
||||||
return &v.state
|
return &v.state
|
||||||
case 1:
|
case 1:
|
||||||
@@ -692,7 +870,7 @@ func file_internal_machine_api_pb_docker_proto_init() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
file_internal_machine_api_pb_docker_proto_msgTypes[3].Exporter = func(v any, i int) any {
|
file_internal_machine_api_pb_docker_proto_msgTypes[3].Exporter = func(v any, i int) any {
|
||||||
switch v := v.(*ListContainersRequest); i {
|
switch v := v.(*InspectContainerResponse); i {
|
||||||
case 0:
|
case 0:
|
||||||
return &v.state
|
return &v.state
|
||||||
case 1:
|
case 1:
|
||||||
@@ -704,7 +882,7 @@ func file_internal_machine_api_pb_docker_proto_init() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
file_internal_machine_api_pb_docker_proto_msgTypes[4].Exporter = func(v any, i int) any {
|
file_internal_machine_api_pb_docker_proto_msgTypes[4].Exporter = func(v any, i int) any {
|
||||||
switch v := v.(*ListContainersResponse); i {
|
switch v := v.(*StartContainerRequest); i {
|
||||||
case 0:
|
case 0:
|
||||||
return &v.state
|
return &v.state
|
||||||
case 1:
|
case 1:
|
||||||
@@ -716,7 +894,7 @@ func file_internal_machine_api_pb_docker_proto_init() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
file_internal_machine_api_pb_docker_proto_msgTypes[5].Exporter = func(v any, i int) any {
|
file_internal_machine_api_pb_docker_proto_msgTypes[5].Exporter = func(v any, i int) any {
|
||||||
switch v := v.(*MachineContainers); i {
|
switch v := v.(*StopContainerRequest); i {
|
||||||
case 0:
|
case 0:
|
||||||
return &v.state
|
return &v.state
|
||||||
case 1:
|
case 1:
|
||||||
@@ -728,7 +906,7 @@ func file_internal_machine_api_pb_docker_proto_init() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
file_internal_machine_api_pb_docker_proto_msgTypes[6].Exporter = func(v any, i int) any {
|
file_internal_machine_api_pb_docker_proto_msgTypes[6].Exporter = func(v any, i int) any {
|
||||||
switch v := v.(*RemoveContainerRequest); i {
|
switch v := v.(*ListContainersRequest); i {
|
||||||
case 0:
|
case 0:
|
||||||
return &v.state
|
return &v.state
|
||||||
case 1:
|
case 1:
|
||||||
@@ -740,7 +918,7 @@ func file_internal_machine_api_pb_docker_proto_init() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
file_internal_machine_api_pb_docker_proto_msgTypes[7].Exporter = func(v any, i int) any {
|
file_internal_machine_api_pb_docker_proto_msgTypes[7].Exporter = func(v any, i int) any {
|
||||||
switch v := v.(*PullImageRequest); i {
|
switch v := v.(*ListContainersResponse); i {
|
||||||
case 0:
|
case 0:
|
||||||
return &v.state
|
return &v.state
|
||||||
case 1:
|
case 1:
|
||||||
@@ -752,6 +930,42 @@ func file_internal_machine_api_pb_docker_proto_init() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
file_internal_machine_api_pb_docker_proto_msgTypes[8].Exporter = func(v any, i int) any {
|
file_internal_machine_api_pb_docker_proto_msgTypes[8].Exporter = func(v any, i int) any {
|
||||||
|
switch v := v.(*MachineContainers); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file_internal_machine_api_pb_docker_proto_msgTypes[9].Exporter = func(v any, i int) any {
|
||||||
|
switch v := v.(*RemoveContainerRequest); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file_internal_machine_api_pb_docker_proto_msgTypes[10].Exporter = func(v any, i int) any {
|
||||||
|
switch v := v.(*PullImageRequest); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file_internal_machine_api_pb_docker_proto_msgTypes[11].Exporter = func(v any, i int) any {
|
||||||
switch v := v.(*JSONMessage); i {
|
switch v := v.(*JSONMessage); i {
|
||||||
case 0:
|
case 0:
|
||||||
return &v.state
|
return &v.state
|
||||||
@@ -770,7 +984,7 @@ func file_internal_machine_api_pb_docker_proto_init() {
|
|||||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||||
RawDescriptor: file_internal_machine_api_pb_docker_proto_rawDesc,
|
RawDescriptor: file_internal_machine_api_pb_docker_proto_rawDesc,
|
||||||
NumEnums: 0,
|
NumEnums: 0,
|
||||||
NumMessages: 9,
|
NumMessages: 12,
|
||||||
NumExtensions: 0,
|
NumExtensions: 0,
|
||||||
NumServices: 1,
|
NumServices: 1,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -9,7 +9,9 @@ import "internal/machine/api/pb/common.proto";
|
|||||||
|
|
||||||
service Docker {
|
service Docker {
|
||||||
rpc CreateContainer(CreateContainerRequest) returns (CreateContainerResponse);
|
rpc CreateContainer(CreateContainerRequest) returns (CreateContainerResponse);
|
||||||
|
rpc InspectContainer(InspectContainerRequest) returns (InspectContainerResponse);
|
||||||
rpc StartContainer(StartContainerRequest) returns (google.protobuf.Empty);
|
rpc StartContainer(StartContainerRequest) returns (google.protobuf.Empty);
|
||||||
|
rpc StopContainer(StopContainerRequest) returns (google.protobuf.Empty);
|
||||||
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);
|
||||||
@@ -32,12 +34,27 @@ message CreateContainerResponse {
|
|||||||
bytes response = 1;
|
bytes response = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message InspectContainerRequest {
|
||||||
|
string id = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message InspectContainerResponse {
|
||||||
|
// JSON serialized container.InspectResponse.
|
||||||
|
bytes response = 1;
|
||||||
|
}
|
||||||
|
|
||||||
message StartContainerRequest {
|
message StartContainerRequest {
|
||||||
string id = 1;
|
string id = 1;
|
||||||
// JSON serialized container.StartOptions.
|
// JSON serialized container.StartOptions.
|
||||||
bytes options = 2;
|
bytes options = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message StopContainerRequest {
|
||||||
|
string id = 1;
|
||||||
|
// JSON serialized container.StopOptions.
|
||||||
|
bytes options = 2;
|
||||||
|
}
|
||||||
|
|
||||||
message ListContainersRequest {
|
message ListContainersRequest {
|
||||||
// JSON serialized container.ListOptions.
|
// JSON serialized container.ListOptions.
|
||||||
bytes options = 1;
|
bytes options = 1;
|
||||||
@@ -50,7 +67,7 @@ message ListContainersResponse {
|
|||||||
|
|
||||||
message MachineContainers {
|
message MachineContainers {
|
||||||
Metadata metadata = 1;
|
Metadata metadata = 1;
|
||||||
// JSON serialized []container.Summary.
|
// JSON serialized []container.ContainerJSON.
|
||||||
bytes containers = 2;
|
bytes containers = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,11 +20,13 @@ import (
|
|||||||
const _ = grpc.SupportPackageIsVersion9
|
const _ = grpc.SupportPackageIsVersion9
|
||||||
|
|
||||||
const (
|
const (
|
||||||
Docker_CreateContainer_FullMethodName = "/api.Docker/CreateContainer"
|
Docker_CreateContainer_FullMethodName = "/api.Docker/CreateContainer"
|
||||||
Docker_StartContainer_FullMethodName = "/api.Docker/StartContainer"
|
Docker_InspectContainer_FullMethodName = "/api.Docker/InspectContainer"
|
||||||
Docker_ListContainers_FullMethodName = "/api.Docker/ListContainers"
|
Docker_StartContainer_FullMethodName = "/api.Docker/StartContainer"
|
||||||
Docker_RemoveContainer_FullMethodName = "/api.Docker/RemoveContainer"
|
Docker_StopContainer_FullMethodName = "/api.Docker/StopContainer"
|
||||||
Docker_PullImage_FullMethodName = "/api.Docker/PullImage"
|
Docker_ListContainers_FullMethodName = "/api.Docker/ListContainers"
|
||||||
|
Docker_RemoveContainer_FullMethodName = "/api.Docker/RemoveContainer"
|
||||||
|
Docker_PullImage_FullMethodName = "/api.Docker/PullImage"
|
||||||
)
|
)
|
||||||
|
|
||||||
// DockerClient is the client API for Docker service.
|
// DockerClient is the client API for Docker service.
|
||||||
@@ -32,7 +34,9 @@ const (
|
|||||||
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
|
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
|
||||||
type DockerClient interface {
|
type DockerClient interface {
|
||||||
CreateContainer(ctx context.Context, in *CreateContainerRequest, opts ...grpc.CallOption) (*CreateContainerResponse, error)
|
CreateContainer(ctx context.Context, in *CreateContainerRequest, opts ...grpc.CallOption) (*CreateContainerResponse, error)
|
||||||
|
InspectContainer(ctx context.Context, in *InspectContainerRequest, opts ...grpc.CallOption) (*InspectContainerResponse, error)
|
||||||
StartContainer(ctx context.Context, in *StartContainerRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
|
StartContainer(ctx context.Context, in *StartContainerRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
|
||||||
|
StopContainer(ctx context.Context, in *StopContainerRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
|
||||||
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)
|
||||||
@@ -56,6 +60,16 @@ func (c *dockerClient) CreateContainer(ctx context.Context, in *CreateContainerR
|
|||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *dockerClient) InspectContainer(ctx context.Context, in *InspectContainerRequest, opts ...grpc.CallOption) (*InspectContainerResponse, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(InspectContainerResponse)
|
||||||
|
err := c.cc.Invoke(ctx, Docker_InspectContainer_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (c *dockerClient) StartContainer(ctx context.Context, in *StartContainerRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) {
|
func (c *dockerClient) StartContainer(ctx context.Context, in *StartContainerRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) {
|
||||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
out := new(emptypb.Empty)
|
out := new(emptypb.Empty)
|
||||||
@@ -66,6 +80,16 @@ func (c *dockerClient) StartContainer(ctx context.Context, in *StartContainerReq
|
|||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *dockerClient) StopContainer(ctx context.Context, in *StopContainerRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(emptypb.Empty)
|
||||||
|
err := c.cc.Invoke(ctx, Docker_StopContainer_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (c *dockerClient) ListContainers(ctx context.Context, in *ListContainersRequest, opts ...grpc.CallOption) (*ListContainersResponse, error) {
|
func (c *dockerClient) ListContainers(ctx context.Context, in *ListContainersRequest, opts ...grpc.CallOption) (*ListContainersResponse, error) {
|
||||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
out := new(ListContainersResponse)
|
out := new(ListContainersResponse)
|
||||||
@@ -110,7 +134,9 @@ type Docker_PullImageClient = grpc.ServerStreamingClient[JSONMessage]
|
|||||||
// for forward compatibility.
|
// for forward compatibility.
|
||||||
type DockerServer interface {
|
type DockerServer interface {
|
||||||
CreateContainer(context.Context, *CreateContainerRequest) (*CreateContainerResponse, error)
|
CreateContainer(context.Context, *CreateContainerRequest) (*CreateContainerResponse, error)
|
||||||
|
InspectContainer(context.Context, *InspectContainerRequest) (*InspectContainerResponse, error)
|
||||||
StartContainer(context.Context, *StartContainerRequest) (*emptypb.Empty, error)
|
StartContainer(context.Context, *StartContainerRequest) (*emptypb.Empty, error)
|
||||||
|
StopContainer(context.Context, *StopContainerRequest) (*emptypb.Empty, error)
|
||||||
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
|
||||||
@@ -127,9 +153,15 @@ type UnimplementedDockerServer struct{}
|
|||||||
func (UnimplementedDockerServer) CreateContainer(context.Context, *CreateContainerRequest) (*CreateContainerResponse, error) {
|
func (UnimplementedDockerServer) CreateContainer(context.Context, *CreateContainerRequest) (*CreateContainerResponse, error) {
|
||||||
return nil, status.Errorf(codes.Unimplemented, "method CreateContainer not implemented")
|
return nil, status.Errorf(codes.Unimplemented, "method CreateContainer not implemented")
|
||||||
}
|
}
|
||||||
|
func (UnimplementedDockerServer) InspectContainer(context.Context, *InspectContainerRequest) (*InspectContainerResponse, error) {
|
||||||
|
return nil, status.Errorf(codes.Unimplemented, "method InspectContainer not implemented")
|
||||||
|
}
|
||||||
func (UnimplementedDockerServer) StartContainer(context.Context, *StartContainerRequest) (*emptypb.Empty, error) {
|
func (UnimplementedDockerServer) StartContainer(context.Context, *StartContainerRequest) (*emptypb.Empty, error) {
|
||||||
return nil, status.Errorf(codes.Unimplemented, "method StartContainer not implemented")
|
return nil, status.Errorf(codes.Unimplemented, "method StartContainer not implemented")
|
||||||
}
|
}
|
||||||
|
func (UnimplementedDockerServer) StopContainer(context.Context, *StopContainerRequest) (*emptypb.Empty, error) {
|
||||||
|
return nil, status.Errorf(codes.Unimplemented, "method StopContainer not implemented")
|
||||||
|
}
|
||||||
func (UnimplementedDockerServer) ListContainers(context.Context, *ListContainersRequest) (*ListContainersResponse, error) {
|
func (UnimplementedDockerServer) ListContainers(context.Context, *ListContainersRequest) (*ListContainersResponse, error) {
|
||||||
return nil, status.Errorf(codes.Unimplemented, "method ListContainers not implemented")
|
return nil, status.Errorf(codes.Unimplemented, "method ListContainers not implemented")
|
||||||
}
|
}
|
||||||
@@ -178,6 +210,24 @@ func _Docker_CreateContainer_Handler(srv interface{}, ctx context.Context, dec f
|
|||||||
return interceptor(ctx, in, info, handler)
|
return interceptor(ctx, in, info, handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func _Docker_InspectContainer_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).InspectContainer(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: Docker_InspectContainer_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(DockerServer).InspectContainer(ctx, req.(*InspectContainerRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
func _Docker_StartContainer_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
func _Docker_StartContainer_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
in := new(StartContainerRequest)
|
in := new(StartContainerRequest)
|
||||||
if err := dec(in); err != nil {
|
if err := dec(in); err != nil {
|
||||||
@@ -196,6 +246,24 @@ func _Docker_StartContainer_Handler(srv interface{}, ctx context.Context, dec fu
|
|||||||
return interceptor(ctx, in, info, handler)
|
return interceptor(ctx, in, info, handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func _Docker_StopContainer_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(StopContainerRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(DockerServer).StopContainer(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: Docker_StopContainer_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(DockerServer).StopContainer(ctx, req.(*StopContainerRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
func _Docker_ListContainers_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
func _Docker_ListContainers_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
in := new(ListContainersRequest)
|
in := new(ListContainersRequest)
|
||||||
if err := dec(in); err != nil {
|
if err := dec(in); err != nil {
|
||||||
@@ -254,10 +322,18 @@ var Docker_ServiceDesc = grpc.ServiceDesc{
|
|||||||
MethodName: "CreateContainer",
|
MethodName: "CreateContainer",
|
||||||
Handler: _Docker_CreateContainer_Handler,
|
Handler: _Docker_CreateContainer_Handler,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
MethodName: "InspectContainer",
|
||||||
|
Handler: _Docker_InspectContainer_Handler,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
MethodName: "StartContainer",
|
MethodName: "StartContainer",
|
||||||
Handler: _Docker_StartContainer_Handler,
|
Handler: _Docker_StartContainer_Handler,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
MethodName: "StopContainer",
|
||||||
|
Handler: _Docker_StopContainer_Handler,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
MethodName: "ListContainers",
|
MethodName: "ListContainers",
|
||||||
Handler: _Docker_ListContainers_Handler,
|
Handler: _Docker_ListContainers_Handler,
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -0,0 +1,163 @@
|
|||||||
|
package caddyfile
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"github.com/caddyserver/caddy/v2"
|
||||||
|
"github.com/caddyserver/caddy/v2/caddyconfig"
|
||||||
|
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||||
|
"github.com/caddyserver/caddy/v2/modules/caddyhttp/reverseproxy"
|
||||||
|
"log/slog"
|
||||||
|
"maps"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"slices"
|
||||||
|
"strconv"
|
||||||
|
"uncloud/internal/api"
|
||||||
|
"uncloud/internal/machine/docker"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GenerateConfig(containers []api.Container, 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,365 @@
|
|||||||
|
package caddyfile
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/docker/docker/api/types"
|
||||||
|
"github.com/docker/docker/api/types/container"
|
||||||
|
"github.com/docker/docker/api/types/network"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"uncloud/internal/api"
|
||||||
|
"uncloud/internal/machine/docker"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGenerateConfig(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
containers []api.Container
|
||||||
|
want string
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "empty containers",
|
||||||
|
containers: []api.Container{},
|
||||||
|
want: `{
|
||||||
|
"servers": {
|
||||||
|
"http": {
|
||||||
|
"listen": [":80"],
|
||||||
|
"routes": [{
|
||||||
|
"match": [{"path": ["/.uncloud-verify"]}],
|
||||||
|
"handle": [{
|
||||||
|
"body": "verification-response-body",
|
||||||
|
"handler": "static_response",
|
||||||
|
"status_code": 200
|
||||||
|
}]
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
"https": {
|
||||||
|
"listen": [":443"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: "HTTP container",
|
||||||
|
containers: []api.Container{
|
||||||
|
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.Container{
|
||||||
|
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.Container{
|
||||||
|
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.Container{
|
||||||
|
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.Container{
|
||||||
|
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.Container{
|
||||||
|
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.Container{
|
||||||
|
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.Container {
|
||||||
|
portsLabel := strings.Join(ports, ",")
|
||||||
|
return api.Container{
|
||||||
|
ContainerJSON: types.ContainerJSON{
|
||||||
|
ContainerJSONBase: &types.ContainerJSONBase{},
|
||||||
|
NetworkSettings: &types.NetworkSettings{
|
||||||
|
Networks: map[string]*network.EndpointSettings{
|
||||||
|
docker.NetworkName: {
|
||||||
|
IPAddress: ip,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Config: &container.Config{
|
||||||
|
Labels: map[string]string{
|
||||||
|
api.LabelServicePorts: portsLabel,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newContainerWithoutNetwork(ports ...string) api.Container {
|
||||||
|
portsLabel := strings.Join(ports, ",")
|
||||||
|
return api.Container{
|
||||||
|
ContainerJSON: types.ContainerJSON{
|
||||||
|
ContainerJSONBase: &types.ContainerJSONBase{},
|
||||||
|
NetworkSettings: &types.NetworkSettings{
|
||||||
|
Networks: map[string]*network.EndpointSettings{
|
||||||
|
"other-network": {
|
||||||
|
IPAddress: "172.17.0.2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Config: &container.Config{
|
||||||
|
Labels: map[string]string{
|
||||||
|
api.LabelServicePorts: portsLabel,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,34 +3,30 @@ 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"
|
"uncloud/internal/api"
|
||||||
"uncloud/internal/fs"
|
"uncloud/internal/fs"
|
||||||
"uncloud/internal/machine/docker"
|
|
||||||
"uncloud/internal/machine/store"
|
"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 +36,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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,84 +86,21 @@ func (c *Controller) Run(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// filterAvailableContainers filters out containers that are likely unavailable from this machine. 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(containerRecords []store.ContainerRecord) ([]api.Container, error) {
|
||||||
containers := make([]*api.Container, len(containerRecords))
|
containers := make([]api.Container, len(containerRecords))
|
||||||
for i, cr := range containerRecords {
|
for i, cr := range containerRecords {
|
||||||
containers[i] = cr.Container
|
containers[i] = cr.Container
|
||||||
}
|
}
|
||||||
return containers, nil
|
return containers, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Controller) generateConfig(containers []*api.Container) error {
|
func (c *Controller) generateConfig(containers []api.Container) 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 +117,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
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -210,9 +220,13 @@ func (c *Cluster) ListMachines(ctx context.Context, _ *emptypb.Empty) (*pb.ListM
|
|||||||
state := pb.MachineMember_DOWN
|
state := pb.MachineMember_DOWN
|
||||||
addr, _ := m.Network.ManagementIp.ToAddr()
|
addr, _ := m.Network.ManagementIp.ToAddr()
|
||||||
for _, s := range states {
|
for _, s := range states {
|
||||||
if s.Addr.Addr().Compare(addr) == 0 &&
|
if s.Addr.Addr().Compare(addr) == 0 {
|
||||||
(s.State == corrosion.MembershipStateAlive || s.State == corrosion.MembershipStateSuspect) {
|
switch s.State {
|
||||||
state = pb.MachineMember_UP
|
case corrosion.MembershipStateAlive:
|
||||||
|
state = pb.MachineMember_UP
|
||||||
|
case corrosion.MembershipStateSuspect:
|
||||||
|
state = pb.MachineMember_SUSPECT
|
||||||
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -228,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"
|
||||||
|
"google.golang.org/grpc/codes"
|
||||||
|
"google.golang.org/grpc/status"
|
||||||
|
"google.golang.org/protobuf/types/known/emptypb"
|
||||||
|
"uncloud/internal/dns"
|
||||||
|
"uncloud/internal/machine/api/pb"
|
||||||
|
"uncloud/internal/machine/store"
|
||||||
|
)
|
||||||
|
|
||||||
|
// uncloudDNSKey is the key used to store the details of the reserved domain in the store.
|
||||||
|
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
|
||||||
|
}
|
||||||
@@ -86,6 +86,26 @@ func (c *Client) CreateContainer(
|
|||||||
return resp, nil
|
return resp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// InspectContainer returns the container information for the given container ID.
|
||||||
|
func (c *Client) InspectContainer(ctx context.Context, id string) (types.ContainerJSON, error) {
|
||||||
|
var resp types.ContainerJSON
|
||||||
|
|
||||||
|
grpcResp, err := c.grpcClient.InspectContainer(ctx, &pb.InspectContainerRequest{Id: id})
|
||||||
|
if err != nil {
|
||||||
|
if s, ok := status.FromError(err); ok {
|
||||||
|
if s.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
|
||||||
|
}
|
||||||
|
|
||||||
// StartContainer starts a container with the given ID and options.
|
// StartContainer starts a container with the given ID and options.
|
||||||
func (c *Client) StartContainer(ctx context.Context, id string, opts container.StartOptions) error {
|
func (c *Client) StartContainer(ctx context.Context, id string, opts container.StartOptions) error {
|
||||||
optsBytes, err := json.Marshal(opts)
|
optsBytes, err := json.Marshal(opts)
|
||||||
@@ -107,9 +127,30 @@ func (c *Client) StartContainer(ctx context.Context, id string, opts container.S
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// StopContainer stops a container with the given ID and options.
|
||||||
|
func (c *Client) StopContainer(ctx context.Context, id string, opts container.StopOptions) error {
|
||||||
|
optsBytes, err := json.Marshal(opts)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("marshal options: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = c.grpcClient.StopContainer(ctx, &pb.StopContainerRequest{
|
||||||
|
Id: id,
|
||||||
|
Options: optsBytes,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
if s, ok := status.FromError(err); ok {
|
||||||
|
if s.Code() == codes.NotFound {
|
||||||
|
return errdefs.NotFound(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
type MachineContainers struct {
|
type MachineContainers struct {
|
||||||
Metadata *pb.Metadata
|
Metadata *pb.Metadata
|
||||||
Containers []types.Container
|
Containers []types.ContainerJSON
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) ListContainers(ctx context.Context, opts container.ListOptions) ([]MachineContainers, error) {
|
func (c *Client) ListContainers(ctx context.Context, opts container.ListOptions) ([]MachineContainers, error) {
|
||||||
|
|||||||
@@ -149,8 +149,9 @@ func (m *Manager) syncContainersToStore(ctx context.Context) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("list containers from store: %w", err)
|
return fmt.Errorf("list containers from store: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// List only Uncloud service containers identified by their labels.
|
// List only Uncloud service containers identified by their labels.
|
||||||
containers, err := m.client.ContainerList(ctx, dockercontainer.ListOptions{
|
containerSummaries, err := m.client.ContainerList(ctx, dockercontainer.ListOptions{
|
||||||
Filters: filters.NewArgs(
|
Filters: filters.NewArgs(
|
||||||
filters.Arg("label", api.LabelServiceID),
|
filters.Arg("label", api.LabelServiceID),
|
||||||
filters.Arg("label", api.LabelServiceName),
|
filters.Arg("label", api.LabelServiceName),
|
||||||
@@ -161,11 +162,21 @@ func (m *Manager) syncContainersToStore(ctx context.Context) error {
|
|||||||
return fmt.Errorf("list Docker containers: %w", err)
|
return fmt.Errorf("list Docker containers: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete containers that are not present in the Docker daemon from the store.
|
// Inspect each container to get the full container details.
|
||||||
|
containers := make([]api.Container, len(containerSummaries))
|
||||||
|
for i, cs := range containerSummaries {
|
||||||
|
ctr, err := m.client.ContainerInspect(ctx, cs.ID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("inspect container '%s': %w", cs.ID, err)
|
||||||
|
}
|
||||||
|
containers[i] = api.Container{ContainerJSON: ctr}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete containers from the store that are no longer present in the Docker daemon.
|
||||||
var deleteIDs []string
|
var deleteIDs []string
|
||||||
for _, sc := range storeContainers {
|
for _, sc := range storeContainers {
|
||||||
found := false
|
found := false
|
||||||
for i, _ := range containers {
|
for i := range containers {
|
||||||
if containers[i].ID == sc.Container.ID {
|
if containers[i].ID == sc.Container.ID {
|
||||||
found = true
|
found = true
|
||||||
break
|
break
|
||||||
@@ -184,8 +195,7 @@ func (m *Manager) syncContainersToStore(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create or update the current Docker containers in the store.
|
// Create or update the current Docker containers in the store.
|
||||||
for _, dc := range containers {
|
for _, c := range containers {
|
||||||
c := &api.Container{Container: dc}
|
|
||||||
if err = m.store.CreateOrUpdateContainer(ctx, c, m.machineID); err != nil {
|
if err = m.store.CreateOrUpdateContainer(ctx, c, m.machineID); err != nil {
|
||||||
storeErr = errors.Join(storeErr, fmt.Errorf("create or update container %q: %w", c.ID, err))
|
storeErr = errors.Join(storeErr, fmt.Errorf("create or update container %q: %w", c.ID, err))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,6 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// EnsureUncloudNetwork is a stub for darwin.
|
// EnsureUncloudNetwork is a stub for darwin.
|
||||||
func (d *Manager) EnsureUncloudNetwork(ctx context.Context, subnet netip.Prefix) error {
|
func (m *Manager) EnsureUncloudNetwork(ctx context.Context, subnet netip.Prefix) error {
|
||||||
return fmt.Errorf("not supported on darwin")
|
return fmt.Errorf("not supported on darwin")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,10 +14,10 @@ import (
|
|||||||
// EnsureUncloudNetwork creates the Docker bridge network NetworkName with the provided machine subnet
|
// EnsureUncloudNetwork creates the Docker bridge network NetworkName with the provided machine subnet
|
||||||
// if it doesn't exist. If the network exists but has a different subnet, it removes and recreates the network.
|
// if it doesn't exist. If the network exists but has a different subnet, it removes and recreates the network.
|
||||||
// It also configures iptables to allow container access from the WireGuard network.
|
// It also configures iptables to allow container access from the WireGuard network.
|
||||||
func (d *Manager) EnsureUncloudNetwork(ctx context.Context, subnet netip.Prefix) error {
|
func (m *Manager) EnsureUncloudNetwork(ctx context.Context, subnet netip.Prefix) error {
|
||||||
// Ensure the Docker network 'uncloud' is created with the correct subnet.
|
// Ensure the Docker network 'uncloud' is created with the correct subnet.
|
||||||
needsCreation := false
|
needsCreation := false
|
||||||
nw, err := d.client.NetworkInspect(ctx, NetworkName, dnetwork.InspectOptions{})
|
nw, err := m.client.NetworkInspect(ctx, NetworkName, dnetwork.InspectOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !client.IsErrNotFound(err) {
|
if !client.IsErrNotFound(err) {
|
||||||
return fmt.Errorf("inspect Docker network %q: %w", NetworkName, err)
|
return fmt.Errorf("inspect Docker network %q: %w", NetworkName, err)
|
||||||
@@ -29,7 +29,7 @@ func (d *Manager) EnsureUncloudNetwork(ctx context.Context, subnet netip.Prefix)
|
|||||||
slog.Info(
|
slog.Info(
|
||||||
"Removing Docker network with old subnet.", "name", NetworkName, "subnet", nw.IPAM.Config[0].Subnet,
|
"Removing Docker network with old subnet.", "name", NetworkName, "subnet", nw.IPAM.Config[0].Subnet,
|
||||||
)
|
)
|
||||||
if err = d.client.NetworkRemove(ctx, NetworkName); err != nil {
|
if err = m.client.NetworkRemove(ctx, NetworkName); err != nil {
|
||||||
// It can still fail if the network is in use by a container. Leave it to the user to resolve the issue.
|
// It can still fail if the network is in use by a container. Leave it to the user to resolve the issue.
|
||||||
return fmt.Errorf("remove Docker network %q: %w", NetworkName, err)
|
return fmt.Errorf("remove Docker network %q: %w", NetworkName, err)
|
||||||
}
|
}
|
||||||
@@ -37,7 +37,7 @@ func (d *Manager) EnsureUncloudNetwork(ctx context.Context, subnet netip.Prefix)
|
|||||||
}
|
}
|
||||||
|
|
||||||
if needsCreation {
|
if needsCreation {
|
||||||
if _, err = d.client.NetworkCreate(
|
if _, err = m.client.NetworkCreate(
|
||||||
ctx, NetworkName, dnetwork.CreateOptions{
|
ctx, NetworkName, dnetwork.CreateOptions{
|
||||||
Driver: "bridge",
|
Driver: "bridge",
|
||||||
Scope: "local",
|
Scope: "local",
|
||||||
@@ -54,7 +54,7 @@ func (d *Manager) EnsureUncloudNetwork(ctx context.Context, subnet netip.Prefix)
|
|||||||
}
|
}
|
||||||
slog.Info("Docker network created.", "name", NetworkName, "subnet", subnet.String())
|
slog.Info("Docker network created.", "name", NetworkName, "subnet", subnet.String())
|
||||||
|
|
||||||
if nw, err = d.client.NetworkInspect(ctx, NetworkName, dnetwork.InspectOptions{}); err != nil {
|
if nw, err = m.client.NetworkInspect(ctx, NetworkName, dnetwork.InspectOptions{}); err != nil {
|
||||||
return fmt.Errorf("inspect Docker network %q: %w", NetworkName, err)
|
return fmt.Errorf("inspect Docker network %q: %w", NetworkName, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
|
"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"
|
||||||
@@ -53,9 +54,9 @@ func (s *Server) CreateContainer(ctx context.Context, req *pb.CreateContainerReq
|
|||||||
resp, err := s.client.ContainerCreate(ctx, &config, &hostConfig, &networkConfig, &platform, req.Name)
|
resp, err := s.client.ContainerCreate(ctx, &config, &hostConfig, &networkConfig, &platform, req.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if client.IsErrNotFound(err) {
|
if client.IsErrNotFound(err) {
|
||||||
return nil, status.Errorf(codes.NotFound, "create container: %v", err)
|
return nil, status.Errorf(codes.NotFound, err.Error())
|
||||||
}
|
}
|
||||||
return nil, status.Errorf(codes.Internal, "create container: %v", err)
|
return nil, status.Errorf(codes.Internal, err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
respBytes, err := json.Marshal(resp)
|
respBytes, err := json.Marshal(resp)
|
||||||
@@ -66,6 +67,24 @@ func (s *Server) CreateContainer(ctx context.Context, req *pb.CreateContainerReq
|
|||||||
return &pb.CreateContainerResponse{Response: respBytes}, nil
|
return &pb.CreateContainerResponse{Response: respBytes}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// InspectContainer returns the container information for the given container ID.
|
||||||
|
func (s *Server) InspectContainer(ctx context.Context, req *pb.InspectContainerRequest) (*pb.InspectContainerResponse, error) {
|
||||||
|
resp, 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())
|
||||||
|
}
|
||||||
|
|
||||||
|
respBytes, err := json.Marshal(resp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, status.Errorf(codes.Internal, "marshal response: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &pb.InspectContainerResponse{Response: respBytes}, nil
|
||||||
|
}
|
||||||
|
|
||||||
// StartContainer starts a container with the given ID and options.
|
// StartContainer starts a container with the given ID and options.
|
||||||
func (s *Server) StartContainer(ctx context.Context, req *pb.StartContainerRequest) (*emptypb.Empty, error) {
|
func (s *Server) StartContainer(ctx context.Context, req *pb.StartContainerRequest) (*emptypb.Empty, error) {
|
||||||
var opts container.StartOptions
|
var opts container.StartOptions
|
||||||
@@ -77,9 +96,28 @@ func (s *Server) StartContainer(ctx context.Context, req *pb.StartContainerReque
|
|||||||
|
|
||||||
if err := s.client.ContainerStart(ctx, req.Id, opts); err != nil {
|
if err := s.client.ContainerStart(ctx, req.Id, opts); err != nil {
|
||||||
if client.IsErrNotFound(err) {
|
if client.IsErrNotFound(err) {
|
||||||
return nil, status.Errorf(codes.NotFound, "start container: %v", err)
|
return nil, status.Errorf(codes.NotFound, err.Error())
|
||||||
}
|
}
|
||||||
return nil, status.Errorf(codes.Internal, "start container: %v", err)
|
return nil, status.Errorf(codes.Internal, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return &emptypb.Empty{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// StopContainer stops a container with the given ID and options.
|
||||||
|
func (s *Server) StopContainer(ctx context.Context, req *pb.StopContainerRequest) (*emptypb.Empty, error) {
|
||||||
|
var opts container.StopOptions
|
||||||
|
if len(req.Options) > 0 {
|
||||||
|
if err := json.Unmarshal(req.Options, &opts); err != nil {
|
||||||
|
return nil, status.Errorf(codes.InvalidArgument, "unmarshal options: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.client.ContainerStop(ctx, req.Id, opts); err != nil {
|
||||||
|
if client.IsErrNotFound(err) {
|
||||||
|
return nil, status.Errorf(codes.NotFound, err.Error())
|
||||||
|
}
|
||||||
|
return nil, status.Errorf(codes.Internal, err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
return &emptypb.Empty{}, nil
|
return &emptypb.Empty{}, nil
|
||||||
@@ -107,9 +145,17 @@ func (s *Server) ListContainers(ctx context.Context, req *pb.ListContainersReque
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
containers, err := s.client.ContainerList(ctx, opts)
|
containerSummaries, err := s.client.ContainerList(ctx, opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, status.Errorf(codes.Internal, "list container: %v", err)
|
return nil, status.Errorf(codes.Internal, err.Error())
|
||||||
|
}
|
||||||
|
containers := make([]types.ContainerJSON, len(containerSummaries))
|
||||||
|
for i, cs := range containerSummaries {
|
||||||
|
c, err := s.client.ContainerInspect(ctx, cs.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, status.Errorf(codes.Internal, "inspect container %s: %v", cs.ID, err)
|
||||||
|
}
|
||||||
|
containers[i] = c
|
||||||
}
|
}
|
||||||
|
|
||||||
containersBytes, err := json.Marshal(containers)
|
containersBytes, err := json.Marshal(containers)
|
||||||
@@ -137,9 +183,9 @@ func (s *Server) RemoveContainer(ctx context.Context, req *pb.RemoveContainerReq
|
|||||||
|
|
||||||
if err := s.client.ContainerRemove(ctx, req.Id, opts); err != nil {
|
if err := s.client.ContainerRemove(ctx, req.Id, opts); err != nil {
|
||||||
if client.IsErrNotFound(err) {
|
if client.IsErrNotFound(err) {
|
||||||
return nil, status.Errorf(codes.NotFound, "remove container: %v", err)
|
return nil, status.Errorf(codes.NotFound, err.Error())
|
||||||
}
|
}
|
||||||
return nil, status.Errorf(codes.Internal, "remove container: %v", err)
|
return nil, status.Errorf(codes.Internal, err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
return &emptypb.Empty{}, nil
|
return &emptypb.Empty{}, nil
|
||||||
@@ -158,7 +204,7 @@ func (s *Server) PullImage(req *pb.PullImageRequest, stream grpc.ServerStreaming
|
|||||||
|
|
||||||
respBody, err := s.client.ImagePull(ctx, req.Image, opts)
|
respBody, err := s.client.ImagePull(ctx, req.Image, opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return status.Errorf(codes.Internal, "pull image: %v", err)
|
return status.Errorf(codes.Internal, err.Error())
|
||||||
}
|
}
|
||||||
defer respBody.Close()
|
defer respBody.Close()
|
||||||
|
|
||||||
@@ -189,7 +235,7 @@ func (s *Server) PullImage(req *pb.PullImageRequest, stream grpc.ServerStreaming
|
|||||||
case err = <-errCh:
|
case err = <-errCh:
|
||||||
return err
|
return err
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return status.Errorf(codes.Canceled, "pull image: %v", ctx.Err())
|
return status.Errorf(codes.Canceled, ctx.Err().Error())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -357,7 +357,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.CaddyfilePath, 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 +538,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 {
|
||||||
ips = append(ips, publicIP)
|
ips = append(ips, publicIP)
|
||||||
}
|
}
|
||||||
endpoints := make([]*pb.IPPort, len(ips))
|
endpoints := make([]*pb.IPPort, len(ips))
|
||||||
@@ -556,6 +558,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)
|
||||||
@@ -682,7 +690,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 +711,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) {
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type ContainerRecord struct {
|
type ContainerRecord struct {
|
||||||
Container *api.Container
|
Container api.Container
|
||||||
MachineID string
|
MachineID string
|
||||||
SyncStatus string
|
SyncStatus string
|
||||||
UpdatedAt time.Time
|
UpdatedAt time.Time
|
||||||
@@ -47,8 +47,11 @@ type DeleteOptions struct {
|
|||||||
|
|
||||||
// CreateOrUpdateContainer creates a new container record or updates an existing one in the store database.
|
// CreateOrUpdateContainer creates a new container record or updates an existing one in the store database.
|
||||||
// The container is associated with the given machine ID that indicates which machine the container is running on.
|
// The container is associated with the given machine ID that indicates which machine the container is running on.
|
||||||
func (s *Store) CreateOrUpdateContainer(ctx context.Context, c *api.Container, machineID string) error {
|
func (s *Store) CreateOrUpdateContainer(ctx context.Context, ctr api.Container, machineID string) error {
|
||||||
cJSON, err := json.Marshal(c)
|
// Remove the environment variables from the container record before storing it in the database
|
||||||
|
// to avoid leaking secrets.
|
||||||
|
ctr.Config.Env = nil
|
||||||
|
cJSON, err := json.Marshal(ctr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("marshal container: %w", err)
|
return fmt.Errorf("marshal container: %w", err)
|
||||||
}
|
}
|
||||||
@@ -63,19 +66,19 @@ func (s *Store) CreateOrUpdateContainer(ctx context.Context, c *api.Container, m
|
|||||||
updated_at = excluded.updated_at
|
updated_at = excluded.updated_at
|
||||||
WHERE containers.container != excluded.container
|
WHERE containers.container != excluded.container
|
||||||
OR containers.machine_id != excluded.machine_id`,
|
OR containers.machine_id != excluded.machine_id`,
|
||||||
c.ID, string(cJSON), machineID, SyncStatusSynced)
|
ctr.ID, string(cJSON), machineID, SyncStatusSynced)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("upsert query: %w", err)
|
return fmt.Errorf("upsert query: %w", err)
|
||||||
}
|
}
|
||||||
if res.RowsAffected > 0 {
|
if res.RowsAffected > 0 {
|
||||||
slog.Debug("Container record updated in store DB.", "id", c.ID, "machine_id", machineID)
|
slog.Debug("Container record updated in store DB.", "id", ctr.ID, "machine_id", machineID)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListContainers returns a list of container records from the store database that match the given options.
|
// ListContainers returns a list of container records from the store database that match the given options.
|
||||||
func (s *Store) ListContainers(ctx context.Context, opts ListOptions) ([]*ContainerRecord, error) {
|
func (s *Store) ListContainers(ctx context.Context, opts ListOptions) ([]ContainerRecord, error) {
|
||||||
q := sq.Select("container", "machine_id", "sync_status", "updated_at").From("containers").
|
q := sq.Select("container", "machine_id", "sync_status", "updated_at").From("containers").
|
||||||
Where(sq.Eq{"sync_status": SyncStatusSynced})
|
Where(sq.Eq{"sync_status": SyncStatusSynced})
|
||||||
|
|
||||||
@@ -105,7 +108,7 @@ func (s *Store) ListContainers(ctx context.Context, opts ListOptions) ([]*Contai
|
|||||||
}
|
}
|
||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
|
|
||||||
var containers []*ContainerRecord
|
var containers []ContainerRecord
|
||||||
var cJSON, machineID, syncStatus, updatedAtStr string
|
var cJSON, machineID, syncStatus, updatedAtStr string
|
||||||
var updatedAt time.Time
|
var updatedAt time.Time
|
||||||
|
|
||||||
@@ -121,8 +124,8 @@ func (s *Store) ListContainers(ctx context.Context, opts ListOptions) ([]*Contai
|
|||||||
if updatedAt, err = time.Parse(time.DateTime, updatedAtStr); err != nil {
|
if updatedAt, err = time.Parse(time.DateTime, updatedAtStr); err != nil {
|
||||||
return nil, fmt.Errorf("parse updated_at: %w", err)
|
return nil, fmt.Errorf("parse updated_at: %w", err)
|
||||||
}
|
}
|
||||||
containers = append(containers, &ContainerRecord{
|
containers = append(containers, ContainerRecord{
|
||||||
Container: &c,
|
Container: c,
|
||||||
MachineID: machineID,
|
MachineID: machineID,
|
||||||
SyncStatus: syncStatus,
|
SyncStatus: syncStatus,
|
||||||
UpdatedAt: updatedAt,
|
UpdatedAt: updatedAt,
|
||||||
@@ -158,7 +161,7 @@ func (s *Store) DeleteContainers(ctx context.Context, opts DeleteOptions) error
|
|||||||
|
|
||||||
// SubscribeContainers returns a list of containers and a channel that signals changes to the list. The channel doesn't
|
// SubscribeContainers returns a list of containers and a channel that signals changes to the list. The channel doesn't
|
||||||
// receive any values, it just signals when a container(s) has been added, updated, or deleted in the database.
|
// receive any values, it just signals when a container(s) has been added, updated, or deleted in the database.
|
||||||
func (s *Store) SubscribeContainers(ctx context.Context) ([]*ContainerRecord, <-chan struct{}, error) {
|
func (s *Store) SubscribeContainers(ctx context.Context) ([]ContainerRecord, <-chan struct{}, error) {
|
||||||
// TODO: figure out whether we need sync_status at all.
|
// TODO: figure out whether we need sync_status at all.
|
||||||
q := sq.Select("container", "machine_id", "sync_status", "updated_at").From("containers").
|
q := sq.Select("container", "machine_id", "sync_status", "updated_at").From("containers").
|
||||||
Where(sq.Eq{"sync_status": SyncStatusSynced})
|
Where(sq.Eq{"sync_status": SyncStatusSynced})
|
||||||
@@ -172,7 +175,7 @@ func (s *Store) SubscribeContainers(ctx context.Context) ([]*ContainerRecord, <-
|
|||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var containers []*ContainerRecord
|
var containers []ContainerRecord
|
||||||
var cJSON, updatedAtStr string
|
var cJSON, updatedAtStr string
|
||||||
|
|
||||||
rows := sub.Rows()
|
rows := sub.Rows()
|
||||||
@@ -188,7 +191,7 @@ func (s *Store) SubscribeContainers(ctx context.Context) ([]*ContainerRecord, <-
|
|||||||
if cr.UpdatedAt, err = time.Parse(time.DateTime, updatedAtStr); err != nil {
|
if cr.UpdatedAt, err = time.Parse(time.DateTime, updatedAtStr); err != nil {
|
||||||
return nil, nil, fmt.Errorf("parse updated_at: %w", err)
|
return nil, nil, fmt.Errorf("parse updated_at: %w", err)
|
||||||
}
|
}
|
||||||
containers = append(containers, &cr)
|
containers = append(containers, cr)
|
||||||
}
|
}
|
||||||
events, err := sub.Changes()
|
events, err := sub.Changes()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -22,8 +22,8 @@ CREATE TABLE containers
|
|||||||
-- container is a JSON-serialized api.Container struct.
|
-- container is a JSON-serialized api.Container struct.
|
||||||
container TEXT NOT NULL DEFAULT '{}' CHECK (json_valid(container)),
|
container TEXT NOT NULL DEFAULT '{}' CHECK (json_valid(container)),
|
||||||
machine_id TEXT NOT NULL DEFAULT '',
|
machine_id TEXT NOT NULL DEFAULT '',
|
||||||
service_id TEXT AS (json_extract(container, '$.Labels."uncloud.service.id"')),
|
service_id TEXT AS (json_extract(container, '$.Config.Labels."uncloud.service.id"')),
|
||||||
service_name TEXT AS (json_extract(container, '$.Labels."uncloud.service.name"')),
|
service_name TEXT AS (json_extract(container, '$.Config.Labels."uncloud.service.name"')),
|
||||||
-- sync_status indicates if the record reflects the actual Docker state of the container.
|
-- sync_status indicates if the record reflects the actual Docker state of the container.
|
||||||
sync_status TEXT NOT NULL DEFAULT '',
|
sync_status TEXT NOT NULL DEFAULT '',
|
||||||
-- updated_at is the last time the record was updated.
|
-- updated_at is the last time the record was updated.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,12 +2,13 @@ package sshexec
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"golang.org/x/crypto/ssh"
|
|
||||||
"golang.org/x/crypto/ssh/agent"
|
|
||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/ssh"
|
||||||
|
"golang.org/x/crypto/ssh/agent"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Connect(user, host string, port int, sshKeyPath string) (*ssh.Client, error) {
|
func Connect(user, host string, port int, sshKeyPath string) (*ssh.Client, error) {
|
||||||
@@ -34,6 +35,9 @@ func Connect(user, host string, port int, sshKeyPath string) (*ssh.Client, error
|
|||||||
}
|
}
|
||||||
|
|
||||||
keyAuth, err := privateKeyAuth(sshKeyPath)
|
keyAuth, err := privateKeyAuth(sshKeyPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
config := &ssh.ClientConfig{
|
config := &ssh.ClientConfig{
|
||||||
User: user,
|
User: user,
|
||||||
Auth: []ssh.AuthMethod{keyAuth},
|
Auth: []ssh.AuthMethod{keyAuth},
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+6
-6
@@ -112,18 +112,18 @@ install_uncloud_binaries() {
|
|||||||
|
|
||||||
local uncloudd_url
|
local uncloudd_url
|
||||||
if [ "${UNCLOUD_VERSION}" == "latest" ]; then
|
if [ "${UNCLOUD_VERSION}" == "latest" ]; then
|
||||||
uncloudd_url="${UNCLOUD_GITHUB_URL}/releases/latest/download/uncloudd_linux_${file_arch}.gz"
|
uncloudd_url="${UNCLOUD_GITHUB_URL}/releases/latest/download/uncloudd_linux_${file_arch}.tar.gz"
|
||||||
else
|
else
|
||||||
uncloudd_url="${UNCLOUD_GITHUB_URL}/releases/download/${UNCLOUD_VERSION}/uncloudd_linux_${file_arch}.gz"
|
uncloudd_url="${UNCLOUD_GITHUB_URL}/releases/download/${UNCLOUD_VERSION}/uncloudd_linux_${file_arch}.tar.gz"
|
||||||
fi
|
fi
|
||||||
local uncloudd_download_path="${tmp_dir}/uncloudd.gz"
|
local uncloudd_download_path="${tmp_dir}/uncloudd.tar.gz"
|
||||||
|
|
||||||
log "⏳ Downloading uncloudd binary: ${uncloudd_url}"
|
log "⏳ Downloading uncloudd binary: ${uncloudd_url}"
|
||||||
if ! curl -fsSL -o "${uncloudd_download_path}" "${uncloudd_url}"; then
|
if ! curl -fsSL -o "${uncloudd_download_path}" "${uncloudd_url}"; then
|
||||||
error "Failed to download uncloudd binary."
|
error "Failed to download uncloudd binary."
|
||||||
fi
|
fi
|
||||||
gzip -d "${uncloudd_download_path}"
|
tar -xf "${uncloudd_download_path}"
|
||||||
if ! install "${uncloudd_download_path%.gz}" "${uncloudd_install_path}"; then
|
if ! install ./uncloudd "${uncloudd_install_path}"; then
|
||||||
error "Failed to install uncloud binary to ${uncloudd_install_path}"
|
error "Failed to install uncloud binary to ${uncloudd_install_path}"
|
||||||
fi
|
fi
|
||||||
log "✓ uncloudd binary installed: ${uncloudd_install_path}"
|
log "✓ uncloudd binary installed: ${uncloudd_install_path}"
|
||||||
@@ -196,7 +196,7 @@ install_corrosion() {
|
|||||||
if ! curl -fsSL -o "${corrosion_download_path}" "${corrosion_url}"; then
|
if ! curl -fsSL -o "${corrosion_download_path}" "${corrosion_url}"; then
|
||||||
error "Failed to download uncloud-corrosion binary."
|
error "Failed to download uncloud-corrosion binary."
|
||||||
fi
|
fi
|
||||||
tar -xzf "${corrosion_download_path}" -C "${tmp_dir}"
|
tar -xf "${corrosion_download_path}" -C "${tmp_dir}"
|
||||||
if ! install "${tmp_dir}/corrosion" "${corrosion_install_path}"; then
|
if ! install "${tmp_dir}/corrosion" "${corrosion_install_path}"; then
|
||||||
error "Failed to install uncloud-corrosion binary to ${corrosion_install_path}"
|
error "Failed to install uncloud-corrosion binary to ${corrosion_install_path}"
|
||||||
fi
|
fi
|
||||||
|
|||||||
+23
-24
@@ -8,25 +8,25 @@ INSTALL_DIR=${INSTALL_DIR:-/usr/local/bin}
|
|||||||
VERSION=${VERSION:-latest}
|
VERSION=${VERSION:-latest}
|
||||||
|
|
||||||
print_manual_install() {
|
print_manual_install() {
|
||||||
# TODO: review
|
RELEASES_URL="https://github.com/${GITHUB_REPO}/releases/${VERSION}"
|
||||||
echo "You can manually install uncloud by:"
|
echo "Failed while attempting to install uncloud CLI. You can install it manually:"
|
||||||
echo "1. Opening $RELEASES_URL"
|
echo " 1. Open your web browser and go to ${RELEASES_URL}"
|
||||||
echo "2. Downloading uncloud_*_${OS}_${ARCH}.tar.gz for your platform"
|
echo " 2. Download uncloud_<OS>_<ARCH>.tar.gz for your platform (OS: linux/macos, ARCH: amd64/arm64)."
|
||||||
echo "3. Verifying the checksum from checksums.txt"
|
echo " 3. Extract the 'uncloud' binary from the archive: tar -xvf uncloud_*.tar.gz"
|
||||||
echo "4. Extracting the archive: tar xzf uncloud_*_${OS}_${ARCH}.tar.gz"
|
echo " 4. Install the binary to /usr/local/bin: sudo install ./uncloud ${INSTALL_DIR}/uncloud"
|
||||||
echo "5. Installing the binary: sudo install -m 755 uncloud /usr/local/bin/"
|
echo " 5. Optionally create a 'uc' symlink: sudo ln -sf ${INSTALL_DIR}/uncloud ${INSTALL_DIR}/uc"
|
||||||
echo "6. Creating symlink: sudo ln -sf /usr/local/bin/uncloud /usr/local/bin/uc"
|
echo " 6. Delete the downloaded archive and extracted binary: rm uncloud*"
|
||||||
|
echo " 7. Run 'uncloud --help' to verify the installation. Enjoy! ✨"
|
||||||
}
|
}
|
||||||
|
|
||||||
latest_version() {
|
fetch_latest_version() {
|
||||||
api_url="https://api.github.com/repos/${GITHUB_REPO}/releases/latest"
|
api_url="https://api.github.com/repos/${GITHUB_REPO}/releases/latest"
|
||||||
version=$(curl -fsSL "$api_url" | grep -o '"tag_name": "[^"]*' | cut -d'"' -f4)
|
VERSION=$(curl -fsSL "$api_url" | grep -o '"tag_name": "[^"]*' | cut -d'"' -f4)
|
||||||
if [ -z "$version" ]; then
|
if [ -z "$VERSION" ]; then
|
||||||
echo "Failed to fetch the latest version from GitHub."
|
echo "Failed to fetch the latest version from GitHub."
|
||||||
print_manual_install
|
print_manual_install
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
echo "$version"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Check if not running as root and need to use sudo to write to INSTALL_DIR.
|
# Check if not running as root and need to use sudo to write to INSTALL_DIR.
|
||||||
@@ -62,9 +62,9 @@ esac
|
|||||||
|
|
||||||
# Use the latest version if not specified explicitly.
|
# Use the latest version if not specified explicitly.
|
||||||
if [ "$VERSION" = "latest" ]; then
|
if [ "$VERSION" = "latest" ]; then
|
||||||
VERSION=$(latest_version)
|
fetch_latest_version
|
||||||
fi
|
fi
|
||||||
BINARY_NAME="uncloud_${BINARY_OS}_${BINARY_ARCH}.gz"
|
BINARY_NAME="uncloud_${BINARY_OS}_${BINARY_ARCH}.tar.gz"
|
||||||
BINARY_URL="https://github.com/${GITHUB_REPO}/releases/download/${VERSION}/${BINARY_NAME}"
|
BINARY_URL="https://github.com/${GITHUB_REPO}/releases/download/${VERSION}/${BINARY_NAME}"
|
||||||
CHECKSUM_URL="https://github.com/${GITHUB_REPO}/releases/download/$VERSION/checksums.txt"
|
CHECKSUM_URL="https://github.com/${GITHUB_REPO}/releases/download/$VERSION/checksums.txt"
|
||||||
|
|
||||||
@@ -78,25 +78,24 @@ curl -fsSL "$BINARY_URL" -o "${TMP_DIR}/${BINARY_NAME}"
|
|||||||
curl -fsSL "$CHECKSUM_URL" -o "${TMP_DIR}/checksums.txt"
|
curl -fsSL "$CHECKSUM_URL" -o "${TMP_DIR}/checksums.txt"
|
||||||
echo "Download complete."
|
echo "Download complete."
|
||||||
|
|
||||||
# TODO: fix name_template in goreleaser config.
|
echo "Verifying checksum..."
|
||||||
#echo "Verifying checksum..."
|
|
||||||
cd "$TMP_DIR"
|
cd "$TMP_DIR"
|
||||||
#if ! sha256sum --check --ignore-missing "checksums.txt"; then
|
if ! sha256sum --check --ignore-missing "checksums.txt"; then
|
||||||
# echo "Checksum verification failed."
|
echo "Checksum verification failed."
|
||||||
# print_manual_install
|
print_manual_install
|
||||||
# exit 1
|
exit 1
|
||||||
#fi
|
fi
|
||||||
#echo "Checksum is valid."
|
echo "Checksum is valid."
|
||||||
|
|
||||||
# Decompress and install the binary.
|
# Decompress and install the binary.
|
||||||
gzip -d "${BINARY_NAME}"
|
tar -xf "${BINARY_NAME}"
|
||||||
|
|
||||||
if [ -z "${SUDO}" ]; then
|
if [ -z "${SUDO}" ]; then
|
||||||
echo "Installing uncloud binary to ${INSTALL_DIR}"
|
echo "Installing uncloud binary to ${INSTALL_DIR}"
|
||||||
else
|
else
|
||||||
echo "Installing uncloud binary to ${INSTALL_DIR} using sudo. You may be prompted for your password."
|
echo "Installing uncloud binary to ${INSTALL_DIR} using sudo. You may be prompted for your password."
|
||||||
fi
|
fi
|
||||||
if ! $SUDO install "${BINARY_NAME%.gz}" "${INSTALL_DIR}/uncloud"; then
|
if ! $SUDO install ./uncloud "${INSTALL_DIR}/uncloud"; then
|
||||||
echo "Failed to install uncloud binary to ${INSTALL_DIR}"
|
echo "Failed to install uncloud binary to ${INSTALL_DIR}"
|
||||||
print_manual_install
|
print_manual_install
|
||||||
exit 1
|
exit 1
|
||||||
|
|||||||
+813
-24
@@ -2,15 +2,642 @@ package e2e
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
dockerclient "github.com/docker/docker/client"
|
"errors"
|
||||||
|
"github.com/docker/docker/api/types/container"
|
||||||
|
"github.com/docker/go-connections/nat"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
"net/netip"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"uncloud/internal/api"
|
"uncloud/internal/api"
|
||||||
"uncloud/internal/cli/client"
|
"uncloud/internal/cli/client"
|
||||||
|
"uncloud/internal/machine/api/pb"
|
||||||
|
machinedocker "uncloud/internal/machine/docker"
|
||||||
|
"uncloud/internal/secret"
|
||||||
"uncloud/internal/ucind"
|
"uncloud/internal/ucind"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func newServiceID() string {
|
||||||
|
id, err := secret.NewID()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeployment(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
clusterName := "ucind-test.deployment"
|
||||||
|
ctx := context.Background()
|
||||||
|
c, _ := createTestCluster(t, clusterName, ucind.CreateClusterOptions{Machines: 3}, true)
|
||||||
|
|
||||||
|
cli, err := c.Machines[0].Connect(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
t.Run("global", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
name := "global-deployment"
|
||||||
|
t.Cleanup(func() {
|
||||||
|
err := cli.RemoveService(ctx, name)
|
||||||
|
if !errors.Is(err, client.ErrNotFound) {
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = cli.InspectService(ctx, name)
|
||||||
|
require.ErrorIs(t, err, client.ErrNotFound)
|
||||||
|
})
|
||||||
|
|
||||||
|
spec := api.ServiceSpec{
|
||||||
|
Name: name,
|
||||||
|
Mode: api.ServiceModeGlobal,
|
||||||
|
Container: api.ContainerSpec{
|
||||||
|
Image: "portainer/pause:latest",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
deploy, err := cli.NewDeployment(spec, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = deploy.Validate(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
plan, err := deploy.Plan(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Len(t, plan.SequenceOperation.Operations, 3) // 3 run
|
||||||
|
|
||||||
|
svcID, err := deploy.Run(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.NotEmpty(t, svcID)
|
||||||
|
|
||||||
|
svc, err := cli.InspectService(ctx, name)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, name, svc.Name)
|
||||||
|
assert.Equal(t, api.ServiceModeGlobal, svc.Mode)
|
||||||
|
assert.Len(t, svc.Containers, 3)
|
||||||
|
|
||||||
|
svcSpec, err := svc.Containers[0].Container.ServiceSpec()
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.True(t, svcSpec.Equals(spec))
|
||||||
|
|
||||||
|
machines := make(map[string]struct{})
|
||||||
|
for _, ctr := range svc.Containers {
|
||||||
|
machines[ctr.MachineID] = struct{}{}
|
||||||
|
}
|
||||||
|
assert.Len(t, machines, 3, "expected 1 container on each machine")
|
||||||
|
|
||||||
|
// Deploy a published port.
|
||||||
|
specWithPort := api.ServiceSpec{
|
||||||
|
Name: name,
|
||||||
|
Mode: api.ServiceModeGlobal,
|
||||||
|
Container: api.ContainerSpec{
|
||||||
|
Image: "portainer/pause:latest",
|
||||||
|
},
|
||||||
|
Ports: []api.PortSpec{
|
||||||
|
{
|
||||||
|
PublishedPort: 8000,
|
||||||
|
ContainerPort: 8000,
|
||||||
|
Protocol: api.ProtocolTCP,
|
||||||
|
Mode: api.PortModeHost,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
deploy, err = cli.NewDeployment(specWithPort, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
plan, err = deploy.Plan(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Len(t, plan.SequenceOperation.Operations, 6) // 3 run + 3 remove
|
||||||
|
|
||||||
|
svcID, err = deploy.Run(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.NotEmpty(t, svcID)
|
||||||
|
|
||||||
|
svc, err = cli.InspectService(ctx, name)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, name, svc.Name)
|
||||||
|
assert.Equal(t, api.ServiceModeGlobal, svc.Mode)
|
||||||
|
assert.Len(t, svc.Containers, 3)
|
||||||
|
|
||||||
|
svcSpec, err = svc.Containers[0].Container.ServiceSpec()
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.True(t, svcSpec.Equals(specWithPort))
|
||||||
|
|
||||||
|
// Deploy the same conflicting port but with container spec changes
|
||||||
|
init := true
|
||||||
|
specWithPortAndInit := api.ServiceSpec{
|
||||||
|
Name: name,
|
||||||
|
Mode: api.ServiceModeGlobal,
|
||||||
|
Container: api.ContainerSpec{
|
||||||
|
Image: "portainer/pause:latest",
|
||||||
|
Init: &init,
|
||||||
|
},
|
||||||
|
Ports: []api.PortSpec{
|
||||||
|
{
|
||||||
|
PublishedPort: 8000,
|
||||||
|
ContainerPort: 8000,
|
||||||
|
Protocol: api.ProtocolTCP,
|
||||||
|
Mode: api.PortModeHost,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
deploy, err = cli.NewDeployment(specWithPortAndInit, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
plan, err = deploy.Plan(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Len(t, plan.SequenceOperation.Operations, 9) // 3 stop + 3 run + 3 remove
|
||||||
|
|
||||||
|
svcID, err = deploy.Run(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.NotEmpty(t, svcID)
|
||||||
|
|
||||||
|
svc, err = cli.InspectService(ctx, name)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, name, svc.Name)
|
||||||
|
assert.Equal(t, api.ServiceModeGlobal, svc.Mode)
|
||||||
|
assert.Len(t, svc.Containers, 3)
|
||||||
|
|
||||||
|
svcSpec, err = svc.Containers[0].Container.ServiceSpec()
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.True(t, svcSpec.Equals(specWithPortAndInit))
|
||||||
|
|
||||||
|
// Deploying the same spec should be a no-op.
|
||||||
|
deploy, err = cli.NewDeployment(specWithPortAndInit, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
plan, err = deploy.Plan(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Len(t, plan.SequenceOperation.Operations, 0) // no-op
|
||||||
|
|
||||||
|
svcID, err = deploy.Run(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.NotEmpty(t, svcID)
|
||||||
|
|
||||||
|
svc, err = cli.InspectService(ctx, name)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, name, svc.Name)
|
||||||
|
assert.Equal(t, api.ServiceModeGlobal, svc.Mode)
|
||||||
|
assert.Len(t, svc.Containers, 3)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("global with machine filter", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
name := "global-deployment-filtered"
|
||||||
|
t.Cleanup(func() {
|
||||||
|
err := cli.RemoveService(ctx, name)
|
||||||
|
if !errors.Is(err, client.ErrNotFound) {
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// First deploy globally without filter to get containers on all machines.
|
||||||
|
spec := api.ServiceSpec{
|
||||||
|
Name: name,
|
||||||
|
Mode: api.ServiceModeGlobal,
|
||||||
|
Container: api.ContainerSpec{
|
||||||
|
Image: "portainer/pause:latest",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
deploy, err := cli.NewDeployment(spec, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = deploy.Run(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
svc, err := cli.InspectService(ctx, name)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Len(t, svc.Containers, 3, "expected 1 container on each machine")
|
||||||
|
|
||||||
|
// Store initial container IDs by machine.
|
||||||
|
initialContainers := make(map[string]string) // machineID -> containerID
|
||||||
|
for _, ctr := range svc.Containers {
|
||||||
|
initialContainers[ctr.MachineID] = ctr.Container.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update spec with Init=true, but only deploy to machines #0 and #2.
|
||||||
|
init := true
|
||||||
|
specWithInit := spec
|
||||||
|
specWithInit.Container.Init = &init
|
||||||
|
|
||||||
|
filter := func(m *pb.MachineInfo) bool {
|
||||||
|
return m.Name == c.Machines[0].Name || m.Name == c.Machines[2].Name
|
||||||
|
}
|
||||||
|
strategy := &client.RollingStrategy{MachineFilter: filter}
|
||||||
|
|
||||||
|
deploy, err = cli.NewDeployment(specWithInit, strategy)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = deploy.Run(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
svc, err = cli.InspectService(ctx, name)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Len(t, svc.Containers, 3, "still 1 container on each machine")
|
||||||
|
|
||||||
|
// Verify:
|
||||||
|
// 1. Containers on machines #0 and #2 were updated (new IDs, init enabled)
|
||||||
|
// 2. Container on machine #1 remains unchanged (same ID, no init)
|
||||||
|
for _, ctr := range svc.Containers {
|
||||||
|
machine, err := cli.InspectMachine(ctx, ctr.MachineID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
oldContainerID := initialContainers[ctr.MachineID]
|
||||||
|
switch machine.Machine.Name {
|
||||||
|
case c.Machines[0].Name, c.Machines[2].Name:
|
||||||
|
// These containers should be updated.
|
||||||
|
assert.NotEqual(t, oldContainerID, ctr.Container.ID,
|
||||||
|
"Container on machine %s should have been updated", machine.Machine.Name)
|
||||||
|
|
||||||
|
svcSpec, err := ctr.Container.ServiceSpec()
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.NotNil(t, svcSpec.Container.Init)
|
||||||
|
assert.True(t, *svcSpec.Container.Init,
|
||||||
|
"Container on machine %s should have init enabled", machine.Machine.Name)
|
||||||
|
case c.Machines[1].Name:
|
||||||
|
// This container should remain unchanged.
|
||||||
|
assert.Equal(t, oldContainerID, ctr.Container.ID,
|
||||||
|
"Container on machine %s should not have been updated", machine.Machine.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now deploy another update without filter - should affect all machines.
|
||||||
|
init = false
|
||||||
|
specWithPort := spec
|
||||||
|
specWithPort.Ports = []api.PortSpec{
|
||||||
|
{
|
||||||
|
PublishedPort: 8001,
|
||||||
|
ContainerPort: 8001,
|
||||||
|
Protocol: api.ProtocolTCP,
|
||||||
|
Mode: api.PortModeHost,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
deploy, err = cli.NewDeployment(specWithPort, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = deploy.Run(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
svc, err = cli.InspectService(ctx, name)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Len(t, svc.Containers, 3)
|
||||||
|
|
||||||
|
// Verify all containers are updated with a published port.
|
||||||
|
for _, ctr := range svc.Containers {
|
||||||
|
svcSpec, err := ctr.Container.ServiceSpec()
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Nil(t, svcSpec.Container.Init,
|
||||||
|
"Container on machine %s should have init disabled", ctr.MachineID)
|
||||||
|
|
||||||
|
ports, err := ctr.Container.ServicePorts()
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, specWithPort.Ports, ports,
|
||||||
|
"Container on machine %s should have updated port", ctr.MachineID)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("caddy", func(t *testing.T) {
|
||||||
|
t.Cleanup(func() {
|
||||||
|
err := cli.RemoveService(ctx, client.CaddyServiceName)
|
||||||
|
if !errors.Is(err, client.ErrNotFound) {
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
deploy, err := cli.NewCaddyDeployment("", nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = deploy.Run(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
svc, err := cli.InspectService(ctx, client.CaddyServiceName)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, client.CaddyServiceName, svc.Name)
|
||||||
|
assert.Equal(t, api.ServiceModeGlobal, svc.Mode)
|
||||||
|
assert.Len(t, svc.Containers, 3)
|
||||||
|
|
||||||
|
ctr := svc.Containers[0].Container
|
||||||
|
assert.Regexp(t, `^caddy:2\.\d+\.\d+$`, ctr.Config.Image)
|
||||||
|
|
||||||
|
ports, err := ctr.ServicePorts()
|
||||||
|
require.NoError(t, err)
|
||||||
|
expectedPorts := []api.PortSpec{
|
||||||
|
{
|
||||||
|
PublishedPort: 80,
|
||||||
|
ContainerPort: 80,
|
||||||
|
Protocol: api.ProtocolTCP,
|
||||||
|
Mode: api.PortModeHost,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
PublishedPort: 443,
|
||||||
|
ContainerPort: 443,
|
||||||
|
Protocol: api.ProtocolTCP,
|
||||||
|
Mode: api.PortModeHost,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
assert.Equal(t, expectedPorts, ports)
|
||||||
|
|
||||||
|
assert.Equal(t, container.RestartPolicy{
|
||||||
|
Name: container.RestartPolicyAlways,
|
||||||
|
MaximumRetryCount: 0,
|
||||||
|
}, ctr.HostConfig.RestartPolicy)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("caddy with machine filter", func(t *testing.T) {
|
||||||
|
t.Cleanup(func() {
|
||||||
|
err := cli.RemoveService(ctx, client.CaddyServiceName)
|
||||||
|
if !errors.Is(err, client.ErrNotFound) {
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Deploy to machine #0
|
||||||
|
filter := func(m *pb.MachineInfo) bool {
|
||||||
|
return m.Name == c.Machines[0].Name
|
||||||
|
}
|
||||||
|
|
||||||
|
deploy, err := cli.NewCaddyDeployment("", filter)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = deploy.Run(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
svc, err := cli.InspectService(ctx, client.CaddyServiceName)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Len(t, svc.Containers, 1)
|
||||||
|
ctr0 := svc.Containers[0]
|
||||||
|
|
||||||
|
machine0, err := cli.InspectMachine(ctx, ctr0.MachineID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, c.Machines[0].Name, machine0.Machine.Name)
|
||||||
|
|
||||||
|
// Deploy to machines #0 and #2
|
||||||
|
filter = func(m *pb.MachineInfo) bool {
|
||||||
|
return m.Name == c.Machines[0].Name || m.Name == c.Machines[2].Name
|
||||||
|
}
|
||||||
|
|
||||||
|
deploy, err = cli.NewCaddyDeployment("", filter)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = deploy.Run(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
svc, err = cli.InspectService(ctx, client.CaddyServiceName)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Len(t, svc.Containers, 2)
|
||||||
|
|
||||||
|
// Existing container ctr0 on machine #0 should be left unchanged.
|
||||||
|
var ctr2 api.MachineContainer
|
||||||
|
if ctr0.Container.ID == svc.Containers[0].Container.ID {
|
||||||
|
ctr2 = svc.Containers[1]
|
||||||
|
} else {
|
||||||
|
assert.Equal(t, ctr0.Container.ID, svc.Containers[1].Container.ID)
|
||||||
|
ctr2 = svc.Containers[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
machine2, err := cli.InspectMachine(ctx, ctr2.MachineID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, c.Machines[2].Name, machine2.Machine.Name)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("replicated", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
name := "replicated-deployment"
|
||||||
|
t.Cleanup(func() {
|
||||||
|
err := cli.RemoveService(ctx, name)
|
||||||
|
if !errors.Is(err, client.ErrNotFound) {
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 1. Create a basic replicated service with 2 replicas.
|
||||||
|
spec := api.ServiceSpec{
|
||||||
|
Name: name,
|
||||||
|
Mode: api.ServiceModeReplicated,
|
||||||
|
Container: api.ContainerSpec{
|
||||||
|
Image: "portainer/pause:latest",
|
||||||
|
},
|
||||||
|
Replicas: 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
deploy, err := cli.NewDeployment(spec, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = deploy.Validate(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
plan, err := deploy.Plan(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Len(t, plan.SequenceOperation.Operations, 2) // 2 run operations for 2 replicas
|
||||||
|
|
||||||
|
svcID, err := deploy.Run(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.NotEmpty(t, svcID)
|
||||||
|
|
||||||
|
// Verify service was created with correct settings.
|
||||||
|
svc, err := cli.InspectService(ctx, name)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, name, svc.Name)
|
||||||
|
assert.Equal(t, api.ServiceModeReplicated, svc.Mode)
|
||||||
|
assert.Len(t, svc.Containers, 2, "expected 2 replicas")
|
||||||
|
|
||||||
|
// Verify containers are on different machines for balanced distribution.
|
||||||
|
machines := make(map[string]struct{})
|
||||||
|
for _, ctr := range svc.Containers {
|
||||||
|
machines[ctr.MachineID] = struct{}{}
|
||||||
|
|
||||||
|
// Verify container spec matches our deployment spec.
|
||||||
|
svcSpec, err := ctr.Container.ServiceSpec()
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.True(t, svcSpec.Equals(spec))
|
||||||
|
}
|
||||||
|
assert.Len(t, machines, 2, "containers should be on different machines")
|
||||||
|
|
||||||
|
// Store the initial container IDs.
|
||||||
|
initialContainers := make(map[string]string) // machineID -> containerID
|
||||||
|
for _, ctr := range svc.Containers {
|
||||||
|
initialContainers[ctr.MachineID] = ctr.Container.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Update the service with a new configuration.
|
||||||
|
init := true
|
||||||
|
updatedSpec := spec
|
||||||
|
updatedSpec.Container.Init = &init
|
||||||
|
|
||||||
|
deploy, err = cli.NewDeployment(updatedSpec, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
plan, err = deploy.Plan(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Len(t, plan.Operations, 4, "expected 2 run + 2 remove operations")
|
||||||
|
|
||||||
|
_, err = deploy.Run(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Verify service was updated.
|
||||||
|
svc, err = cli.InspectService(ctx, name)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, name, svc.Name)
|
||||||
|
assert.Len(t, svc.Containers, 2)
|
||||||
|
|
||||||
|
// Verify initial containers were updated.
|
||||||
|
for _, ctr := range svc.Containers {
|
||||||
|
initialCtr, ok := initialContainers[ctr.MachineID]
|
||||||
|
require.True(t, ok, "Updated container should have replaced one of the initial containers")
|
||||||
|
|
||||||
|
assert.NotEqual(t, initialCtr, ctr.Container.ID,
|
||||||
|
"Container on machine %s should have been updated", ctr.MachineID)
|
||||||
|
|
||||||
|
svcSpec, err := ctr.Container.ServiceSpec()
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.True(t, svcSpec.Equals(updatedSpec))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Update to 4 replicas with a different configuration.
|
||||||
|
initialContainers = make(map[string]string) // Reset container tracking.
|
||||||
|
for _, ctr := range svc.Containers {
|
||||||
|
initialContainers[ctr.MachineID] = ctr.Container.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
fourReplicaSpec := updatedSpec
|
||||||
|
fourReplicaSpec.Container.Command = []string{"updated"}
|
||||||
|
fourReplicaSpec.Replicas = 4
|
||||||
|
|
||||||
|
deploy, err = cli.NewDeployment(fourReplicaSpec, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
plan, err = deploy.Plan(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Len(t, plan.Operations, 6, "Expected 4 run + 2 remove operations")
|
||||||
|
|
||||||
|
_, err = deploy.Run(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Verify service now has 4 containers.
|
||||||
|
svc, err = cli.InspectService(ctx, name)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, name, svc.Name)
|
||||||
|
assert.Len(t, svc.Containers, 4, "Expected 4 replicas")
|
||||||
|
|
||||||
|
// Count containers per machine.
|
||||||
|
machineContainerCount := make(map[string]int)
|
||||||
|
for _, ctr := range svc.Containers {
|
||||||
|
machineContainerCount[ctr.MachineID]++
|
||||||
|
|
||||||
|
// Verify all containers match the new spec
|
||||||
|
svcSpec, err := ctr.Container.ServiceSpec()
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.True(t, svcSpec.Equals(fourReplicaSpec))
|
||||||
|
|
||||||
|
// For existing machines, verify containers were replaced
|
||||||
|
if initialID, ok := initialContainers[ctr.MachineID]; ok {
|
||||||
|
assert.NotEqual(t, initialID, ctr.Container.ID,
|
||||||
|
"Container on machine %s should have been updated", ctr.MachineID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify even distributions across machines.
|
||||||
|
assert.Len(t, machineContainerCount, 3, "Expected containers on all 3 machines")
|
||||||
|
for _, count := range machineContainerCount {
|
||||||
|
assert.GreaterOrEqual(t, count, 1, "Expected at least 1 container on each machine")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Redeploy the exact same spec and verify it's a noop.
|
||||||
|
deploy, err = cli.NewDeployment(fourReplicaSpec, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
plan, err = deploy.Plan(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
svc, err = cli.InspectService(ctx, name)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Empty(t, plan.Operations, "Redeploying the same spec should be a no-op")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("replicated with machine filter", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
name := "replicated-deployment-filtered"
|
||||||
|
t.Cleanup(func() {
|
||||||
|
err := cli.RemoveService(ctx, name)
|
||||||
|
if !errors.Is(err, client.ErrNotFound) {
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Create a replicated service with 2 replicas but limit to machines 0 and 1
|
||||||
|
spec := api.ServiceSpec{
|
||||||
|
Name: name,
|
||||||
|
Mode: api.ServiceModeReplicated,
|
||||||
|
Container: api.ContainerSpec{
|
||||||
|
Image: "portainer/pause:latest",
|
||||||
|
},
|
||||||
|
Replicas: 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
machine01Filter := func(m *pb.MachineInfo) bool {
|
||||||
|
return m.Name == c.Machines[0].Name || m.Name == c.Machines[1].Name
|
||||||
|
}
|
||||||
|
strategy := &client.RollingStrategy{MachineFilter: machine01Filter}
|
||||||
|
|
||||||
|
deploy, err := cli.NewDeployment(spec, strategy)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = deploy.Run(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Verify service has 2 containers on machines 0 and 1.
|
||||||
|
svc, err := cli.InspectService(ctx, name)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Len(t, svc.Containers, 2)
|
||||||
|
assert.NotEqual(t, svc.Containers[0].MachineID, svc.Containers[1].MachineID,
|
||||||
|
"Expected containers on different machines")
|
||||||
|
|
||||||
|
machineNames := make(map[string]string)
|
||||||
|
for _, ctr := range svc.Containers {
|
||||||
|
machine, err := cli.InspectMachine(ctx, ctr.MachineID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
machineNames[ctr.MachineID] = machine.Machine.Name
|
||||||
|
|
||||||
|
// Should only be on machines 0 or 1.
|
||||||
|
assert.Contains(t, []string{c.Machines[0].Name, c.Machines[1].Name}, machine.Machine.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now update the filter to only allow machine 2
|
||||||
|
machine2Filter := func(m *pb.MachineInfo) bool {
|
||||||
|
return m.Name == c.Machines[2].Name
|
||||||
|
}
|
||||||
|
|
||||||
|
strategy = &client.RollingStrategy{MachineFilter: machine2Filter}
|
||||||
|
deploy, err = cli.NewDeployment(spec, strategy)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = deploy.Run(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Verify service now has containers only on machine 2.
|
||||||
|
svc, err = cli.InspectService(ctx, name)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Len(t, svc.Containers, 2) // Still 2 replicas.
|
||||||
|
assert.Equal(t, svc.Containers[0].MachineID, svc.Containers[1].MachineID,
|
||||||
|
"Expected containers on the same machine")
|
||||||
|
|
||||||
|
machine, err := cli.InspectMachine(ctx, svc.Containers[0].MachineID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, c.Machines[2].Name, machine.Machine.Name, "Containers should only be on machine #2")
|
||||||
|
})
|
||||||
|
|
||||||
|
// TODO: test deployments with unreachable machines. See https://github.com/psviderski/uncloud/issues/29.
|
||||||
|
}
|
||||||
|
|
||||||
func TestRunService(t *testing.T) {
|
func TestRunService(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
@@ -21,13 +648,186 @@ func TestRunService(t *testing.T) {
|
|||||||
cli, err := c.Machines[0].Connect(ctx)
|
cli, err := c.Machines[0].Connect(ctx)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
t.Run("create container with spec defaults", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
serviceID := newServiceID()
|
||||||
|
// Only required fields.
|
||||||
|
spec := api.ServiceSpec{
|
||||||
|
Name: "container-spec-defaults",
|
||||||
|
Container: api.ContainerSpec{
|
||||||
|
Image: "portainer/pause:latest",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := cli.CreateContainer(ctx, serviceID, spec, c.Machines[0].Name)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.NotEmpty(t, resp.ID)
|
||||||
|
|
||||||
|
t.Cleanup(func() {
|
||||||
|
err := cli.RemoveContainer(ctx, serviceID, resp.ID, container.RemoveOptions{Force: true})
|
||||||
|
if !errors.Is(err, client.ErrNotFound) {
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Verify container configuration.
|
||||||
|
mc, err := cli.InspectContainer(ctx, serviceID, resp.ID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
ctr := mc.Container
|
||||||
|
|
||||||
|
assert.True(t, strings.HasPrefix(ctr.Name, "/container-spec-defaults-"))
|
||||||
|
assert.Equal(t, "portainer/pause:latest", ctr.Config.Image)
|
||||||
|
|
||||||
|
// Verify default settings.
|
||||||
|
assert.Empty(t, ctr.Config.Cmd)
|
||||||
|
assert.Nil(t, ctr.HostConfig.Init)
|
||||||
|
assert.Empty(t, ctr.HostConfig.Binds)
|
||||||
|
assert.Empty(t, ctr.HostConfig.PortBindings)
|
||||||
|
assert.Equal(t, container.RestartPolicy{
|
||||||
|
Name: container.RestartPolicyAlways,
|
||||||
|
MaximumRetryCount: 0,
|
||||||
|
}, ctr.HostConfig.RestartPolicy)
|
||||||
|
|
||||||
|
// Verify labels.
|
||||||
|
assert.Equal(t, serviceID, ctr.Config.Labels[api.LabelServiceID])
|
||||||
|
assert.Equal(t, spec.Name, ctr.Config.Labels[api.LabelServiceName])
|
||||||
|
assert.Equal(t, api.ServiceModeReplicated, ctr.Config.Labels[api.LabelServiceMode])
|
||||||
|
assert.NotContains(t, ctr.Config.Labels, api.LabelServicePorts) // No ports set.
|
||||||
|
assert.Contains(t, ctr.Config.Labels, api.LabelManaged)
|
||||||
|
|
||||||
|
// Verify network settings.
|
||||||
|
assert.Len(t, ctr.NetworkSettings.Networks, 1)
|
||||||
|
assert.Contains(t, ctr.NetworkSettings.Networks, machinedocker.NetworkName)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("create container with full spec", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
serviceID := newServiceID()
|
||||||
|
init := true
|
||||||
|
spec := api.ServiceSpec{
|
||||||
|
Name: "container-spec-full",
|
||||||
|
Mode: api.ServiceModeGlobal,
|
||||||
|
Container: api.ContainerSpec{
|
||||||
|
Command: []string{"sleep", "infinity"},
|
||||||
|
Image: "portainer/pause:latest",
|
||||||
|
Init: &init,
|
||||||
|
Volumes: []string{"/host/path:/container/path:ro"},
|
||||||
|
},
|
||||||
|
Ports: []api.PortSpec{
|
||||||
|
{
|
||||||
|
HostIP: netip.MustParseAddr("127.0.0.1"),
|
||||||
|
PublishedPort: 80,
|
||||||
|
ContainerPort: 8080,
|
||||||
|
Protocol: api.ProtocolTCP,
|
||||||
|
Mode: api.PortModeHost,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Hostname: "app.example.com",
|
||||||
|
ContainerPort: 8000,
|
||||||
|
Protocol: api.ProtocolHTTPS,
|
||||||
|
Mode: api.PortModeIngress,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := cli.CreateContainer(ctx, serviceID, spec, c.Machines[0].Name)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.NotEmpty(t, resp.ID)
|
||||||
|
|
||||||
|
t.Cleanup(func() {
|
||||||
|
err := cli.RemoveContainer(ctx, serviceID, resp.ID, container.RemoveOptions{Force: true})
|
||||||
|
if !errors.Is(err, client.ErrNotFound) {
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Verify container configuration.
|
||||||
|
mc, err := cli.InspectContainer(ctx, serviceID, resp.ID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
ctr := mc.Container
|
||||||
|
|
||||||
|
assert.True(t, strings.HasPrefix(ctr.Name, "/container-spec-full-"))
|
||||||
|
assert.Equal(t, "portainer/pause:latest", ctr.Config.Image)
|
||||||
|
|
||||||
|
assert.EqualValues(t, spec.Container.Command, ctr.Config.Cmd)
|
||||||
|
assert.True(t, *ctr.HostConfig.Init)
|
||||||
|
assert.Len(t, ctr.HostConfig.Binds, 1)
|
||||||
|
assert.Contains(t, ctr.HostConfig.Binds, spec.Container.Volumes[0])
|
||||||
|
|
||||||
|
assert.Len(t, ctr.HostConfig.PortBindings, 1)
|
||||||
|
expectedPort := []nat.PortBinding{
|
||||||
|
{
|
||||||
|
HostIP: "127.0.0.1",
|
||||||
|
HostPort: "80",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
assert.Equal(t, expectedPort, ctr.HostConfig.PortBindings[nat.Port("8080/tcp")])
|
||||||
|
|
||||||
|
assert.Equal(t, container.RestartPolicy{
|
||||||
|
Name: container.RestartPolicyAlways,
|
||||||
|
MaximumRetryCount: 0,
|
||||||
|
}, ctr.HostConfig.RestartPolicy)
|
||||||
|
|
||||||
|
// Verify labels.
|
||||||
|
assert.Equal(t, serviceID, ctr.Config.Labels[api.LabelServiceID])
|
||||||
|
assert.Equal(t, spec.Name, ctr.Config.Labels[api.LabelServiceName])
|
||||||
|
assert.Equal(t, api.ServiceModeGlobal, ctr.Config.Labels[api.LabelServiceMode])
|
||||||
|
assert.Equal(t, "127.0.0.1:80:8080/tcp@host,app.example.com:8000/https", ctr.Config.Labels[api.LabelServicePorts])
|
||||||
|
assert.Contains(t, ctr.Config.Labels, api.LabelManaged)
|
||||||
|
|
||||||
|
// Verify network settings.
|
||||||
|
assert.Len(t, ctr.NetworkSettings.Networks, 1)
|
||||||
|
assert.Contains(t, ctr.NetworkSettings.Networks, machinedocker.NetworkName)
|
||||||
|
})
|
||||||
|
|
||||||
|
// TODO: create container invalid spec.
|
||||||
|
// - service Name is required
|
||||||
|
|
||||||
|
t.Run("container lifecycle", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
serviceID := newServiceID()
|
||||||
|
spec := api.ServiceSpec{
|
||||||
|
Name: "container-lifecycle",
|
||||||
|
Container: api.ContainerSpec{
|
||||||
|
Image: "portainer/pause:latest",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
ctr, err := cli.CreateContainer(ctx, serviceID, spec, c.Machines[0].Name)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.NotEmpty(t, ctr.ID)
|
||||||
|
|
||||||
|
t.Cleanup(func() {
|
||||||
|
err := cli.RemoveContainer(ctx, serviceID, ctr.ID, container.RemoveOptions{Force: true})
|
||||||
|
if !errors.Is(err, client.ErrNotFound) {
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
err = cli.StartContainer(ctx, serviceID, ctr.ID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
timeout := 1
|
||||||
|
err = cli.StopContainer(ctx, serviceID, ctr.ID, container.StopOptions{Timeout: &timeout})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = cli.RemoveContainer(ctx, serviceID, ctr.ID, container.RemoveOptions{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = cli.RemoveContainer(ctx, serviceID, ctr.ID, container.RemoveOptions{})
|
||||||
|
require.ErrorIs(t, err, client.ErrNotFound)
|
||||||
|
})
|
||||||
|
|
||||||
t.Run("1 replica", func(t *testing.T) {
|
t.Run("1 replica", func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
name := "busybox-1-replica"
|
name := "1-replica"
|
||||||
t.Cleanup(func() {
|
t.Cleanup(func() {
|
||||||
err := cli.RemoveService(ctx, name)
|
err := cli.RemoveService(ctx, name)
|
||||||
if !dockerclient.IsErrNotFound(err) {
|
if !errors.Is(err, client.ErrNotFound) {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,15 +839,13 @@ func TestRunService(t *testing.T) {
|
|||||||
Name: name,
|
Name: name,
|
||||||
Mode: api.ServiceModeReplicated,
|
Mode: api.ServiceModeReplicated,
|
||||||
Container: api.ContainerSpec{
|
Container: api.ContainerSpec{
|
||||||
Command: []string{"sleep", "infinity"},
|
Image: "portainer/pause:latest",
|
||||||
Image: "busybox:latest",
|
|
||||||
},
|
},
|
||||||
})
|
}, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.NotEmpty(t, resp.ID)
|
assert.NotEmpty(t, resp.ID)
|
||||||
assert.Equal(t, name, resp.Name)
|
assert.Equal(t, name, resp.Name)
|
||||||
assert.Len(t, resp.Containers, 1)
|
|
||||||
|
|
||||||
svc, err := cli.InspectService(ctx, name)
|
svc, err := cli.InspectService(ctx, name)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -76,23 +874,19 @@ func TestRunService(t *testing.T) {
|
|||||||
t.Run("1 replica with ports", func(t *testing.T) {
|
t.Run("1 replica with ports", func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
name := "busybox-1-replica-ports"
|
name := "1-replica-ports"
|
||||||
t.Cleanup(func() {
|
t.Cleanup(func() {
|
||||||
err := cli.RemoveService(ctx, name)
|
err := cli.RemoveService(ctx, name)
|
||||||
if !dockerclient.IsErrNotFound(err) {
|
if !errors.Is(err, client.ErrNotFound) {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = cli.InspectService(ctx, name)
|
|
||||||
require.ErrorIs(t, err, client.ErrNotFound)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
spec := api.ServiceSpec{
|
spec := api.ServiceSpec{
|
||||||
Name: name,
|
Name: name,
|
||||||
Mode: api.ServiceModeReplicated,
|
Mode: api.ServiceModeReplicated,
|
||||||
Container: api.ContainerSpec{
|
Container: api.ContainerSpec{
|
||||||
Command: []string{"sleep", "infinity"},
|
Image: "portainer/pause:latest",
|
||||||
Image: "busybox:latest",
|
|
||||||
},
|
},
|
||||||
Ports: []api.PortSpec{
|
Ports: []api.PortSpec{
|
||||||
{
|
{
|
||||||
@@ -115,7 +909,7 @@ func TestRunService(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
resp, err := cli.RunService(ctx, spec)
|
resp, err := cli.RunService(ctx, spec, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
svc, err := cli.InspectService(ctx, resp.ID)
|
svc, err := cli.InspectService(ctx, resp.ID)
|
||||||
@@ -131,30 +925,25 @@ func TestRunService(t *testing.T) {
|
|||||||
t.Run("global mode", func(t *testing.T) {
|
t.Run("global mode", func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
name := "busybox-global"
|
name := "global"
|
||||||
t.Cleanup(func() {
|
t.Cleanup(func() {
|
||||||
err := cli.RemoveService(ctx, name)
|
err := cli.RemoveService(ctx, name)
|
||||||
if !dockerclient.IsErrNotFound(err) {
|
if !errors.Is(err, client.ErrNotFound) {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = cli.InspectService(ctx, name)
|
|
||||||
require.ErrorIs(t, err, client.ErrNotFound)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
resp, err := cli.RunService(ctx, api.ServiceSpec{
|
resp, err := cli.RunService(ctx, api.ServiceSpec{
|
||||||
Name: name,
|
Name: name,
|
||||||
Mode: api.ServiceModeGlobal,
|
Mode: api.ServiceModeGlobal,
|
||||||
Container: api.ContainerSpec{
|
Container: api.ContainerSpec{
|
||||||
Command: []string{"sleep", "infinity"},
|
Image: "portainer/pause:latest",
|
||||||
Image: "busybox:latest",
|
|
||||||
},
|
},
|
||||||
})
|
}, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.NotEmpty(t, resp.ID)
|
assert.NotEmpty(t, resp.ID)
|
||||||
assert.Equal(t, name, resp.Name)
|
assert.Equal(t, name, resp.Name)
|
||||||
assert.Len(t, resp.Containers, 3, "expected 1 container on each machine")
|
|
||||||
|
|
||||||
svc, err := cli.InspectService(ctx, name)
|
svc, err := cli.InspectService(ctx, name)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 4.9 KiB |
+23
-5
@@ -3,12 +3,30 @@
|
|||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<title>Deploy Docker apps on your own infrastructure | Uncloud</title>
|
<title>Deploy Docker Apps on Your Infrastructure | Uncloud</title>
|
||||||
|
<meta name="description" content="Uncloud helps you deploy container applications on your infrastructure — from cloud VMs to bare metal. Replace Heroku and Render, no Kubernetes required.">
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
<link href="./style.css" rel="stylesheet">
|
<link href="./style.css" rel="stylesheet">
|
||||||
<link rel="apple-touch-icon" href="./images/apple-touch-icon.png" sizes="180x180"/>
|
<link rel="apple-touch-icon" href="./images/apple-touch-icon.png" sizes="180x180"/>
|
||||||
<link rel="icon" type="image/svg+xml" href="./images/logo.svg"/>
|
<link rel="icon" type="image/svg+xml" href="./images/logo.svg"/>
|
||||||
<link rel="alternate icon" type="image/png" href="./images/favicon.png" sizes="96x96"/>
|
<link rel="alternate icon" type="image/png" href="./images/favicon.png" sizes="96x96"/>
|
||||||
|
|
||||||
|
<!-- OpenGraph tags for social media -->
|
||||||
|
<meta property="og:title" content="Deploy Docker Apps on Your Infrastructure | Uncloud" />
|
||||||
|
<meta property="og:description" content="Uncloud helps you deploy container applications on your infrastructure — from cloud VMs to bare metal. Replace Heroku and Render, no Kubernetes required." />
|
||||||
|
<meta property="og:image" content="https://uncloud.run/images/logo-wide.png" />
|
||||||
|
<meta property="og:image:alt" content="Uncloud logo - Deploy container apps with ease" />
|
||||||
|
<meta property="og:url" content="https://uncloud.run" />
|
||||||
|
<meta property="og:type" content="website" />
|
||||||
|
<meta property="og:site_name" content="Uncloud" />
|
||||||
|
<!-- Twitter card tags -->
|
||||||
|
<meta property="twitter:domain" content="uncloud.run">
|
||||||
|
<meta property="twitter:url" content="https://uncloud.run">
|
||||||
|
<meta name="twitter:card" content="summary_large_image">
|
||||||
|
<meta name="twitter:title" content="Deploy Docker Apps on Your Infrastructure | Uncloud">
|
||||||
|
<meta name="twitter:description" content="Uncloud helps you deploy container applications on your infrastructure — from cloud VMs to bare metal. Replace Heroku and Render, no Kubernetes required.">
|
||||||
|
<meta name="twitter:image" content="https://uncloud.run/images/logo-wide.png">
|
||||||
|
<meta name="twitter:image:alt" content="Uncloud logo - Deploy container apps with ease">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="font-inter antialiased bg-white text-zinc-900 tracking-tight">
|
<body class="font-inter antialiased bg-white text-zinc-900 tracking-tight">
|
||||||
@@ -45,7 +63,7 @@
|
|||||||
</li>
|
</li>
|
||||||
<li class="ml-1">
|
<li class="ml-1">
|
||||||
<a class="btn-sm text-zinc-100 bg-zinc-900 hover:bg-zinc-800 w-full shadow"
|
<a class="btn-sm text-zinc-100 bg-zinc-900 hover:bg-zinc-800 w-full shadow"
|
||||||
href="https://github.com/psviderski/uncloud?tab=readme-ov-file#quick-start">Get Started</a>
|
href="https://github.com/psviderski/uncloud/?tab=readme-ov-file#-quick-start">Get Started</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
@@ -76,7 +94,7 @@
|
|||||||
<div class="max-w-xs mx-auto sm:max-w-none sm:inline-flex sm:justify-center space-y-4 sm:space-y-0 sm:space-x-4">
|
<div class="max-w-xs mx-auto sm:max-w-none sm:inline-flex sm:justify-center space-y-4 sm:space-y-0 sm:space-x-4">
|
||||||
<div>
|
<div>
|
||||||
<a class="btn text-zinc-100 bg-zinc-900 hover:bg-zinc-800 w-full shadow"
|
<a class="btn text-zinc-100 bg-zinc-900 hover:bg-zinc-800 w-full shadow"
|
||||||
href="https://github.com/psviderski/uncloud?tab=readme-ov-file#quick-start">Quick Start</a>
|
href="https://github.com/psviderski/uncloud/?tab=readme-ov-file#-quick-start">Quick Start</a>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<a class="btn text-zinc-600 bg-white hover:text-zinc-900 w-full shadow flex items-center justify-center"
|
<a class="btn text-zinc-600 bg-white hover:text-zinc-900 w-full shadow flex items-center justify-center"
|
||||||
@@ -112,7 +130,7 @@
|
|||||||
|
|
||||||
<pre> </pre>
|
<pre> </pre>
|
||||||
<pre class="text-zinc-500 select-none"># Deploy your app with automatic HTTPS</pre>
|
<pre class="text-zinc-500 select-none"># Deploy your app with automatic HTTPS</pre>
|
||||||
<pre><span class="text-emerald-500 select-none">$ </span>uc run --name my-app -p app.example.com:8000/https registry/app</pre>
|
<pre><span class="text-emerald-500 select-none">$ </span>uc run --name my-app -p app.example.com:8000/https app-image:latest</pre>
|
||||||
<pre class="text-zinc-200">✨ Your app is available at <span class="underline">https://app.example.com</span></pre>
|
<pre class="text-zinc-200">✨ Your app is available at <span class="underline">https://app.example.com</span></pre>
|
||||||
|
|
||||||
<pre> </pre>
|
<pre> </pre>
|
||||||
@@ -505,7 +523,7 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- CTA -->
|
<!-- CTA -->
|
||||||
<section>
|
<section id="subscribe">
|
||||||
<div class="py-12 md:py-20">
|
<div class="py-12 md:py-20">
|
||||||
<div class="max-w-6xl mx-auto px-4 sm:px-6">
|
<div class="max-w-6xl mx-auto px-4 sm:px-6">
|
||||||
<div class="relative max-w-3xl mx-auto text-center">
|
<div class="relative max-w-3xl mx-auto text-center">
|
||||||
|
|||||||
+9
-1198
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user