Compare commits

...
88 Commits
Author SHA1 Message Date
Pavel Sviderski 137abde808 feat: set container hostname equal to container name 2025-03-03 16:58:42 +10:00
Pavel Sviderski 342969b7f4 fix(caddy): remove debug prints 2025-03-03 15:05:57 +10:00
Pavel Sviderski d9e350c4dd fix(caddy): config generation for multiple https containers 2025-03-03 15:04:04 +10:00
Pavel Sviderski 3f4f263c05 test: increase test cluster wait timeout 5->30 seconds 2025-03-03 12:21:22 +10:00
Pavel Sviderski 0760ba3b18 test: replicated deployment with machine filter 2025-03-03 11:26:08 +10:00
Pavel Sviderski 0896bd27fb test: basic replicated deployment, prioritise host with the most up-to-date containers 2025-03-02 10:23:49 +10:00
Pavel Sviderski a16b2d1c74 feat: service ls command include ID column if there are multiple services with same name 2025-03-01 22:31:58 +10:00
Pavel Sviderski 52117954a8 fix: specs equals ignores replicas 2025-03-01 22:31:18 +10:00
Pavel Sviderski 435a794fa5 fix: e2e tests 2025-02-28 18:00:02 +10:00
Pavel Sviderski 9642863d7b feat(replicas): support --replicas and --machines for run command 2025-02-28 17:53:58 +10:00
Pavel Sviderski 6bb9bdeb5a feat(replicas): implement rolling strategy for replicated service with basic spread strategy 2025-02-28 17:51:38 +10:00
Pavel Sviderski 30f1e135f1 feat: print ENDPOINTS column when listing services 2025-02-27 20:37:02 +10:00
Pavel Sviderski 58ecc17ac1 feat: print service endpoints on service creation 2025-02-27 20:37:02 +10:00
Anton Ovchinnikov 4c239ed79a fix(smm): Update site description 2025-02-27 00:08:03 +01:00
Anton Ovchinnikov 968dd50705 fix(smm): Use PNG logo, add Twitter tags 2025-02-26 23:55:26 +01:00
Pavel Sviderski d7f437107d fix(dns): error message for ingress port without hostname and no cluster domain 2025-02-26 21:13:18 +10:00
Pavel Sviderski 72548fbd1b feat(dns): allow to specify http(s) ports without hostname to default to cluster domain 2025-02-26 21:10:55 +10:00
Pavel Sviderski a5f6ec4a68 feat(dns): expose service as service-name.cluster-domain if cluster domain reserved 2025-02-26 21:10:21 +10:00
Pavel Sviderski 4cc68e3133 include cloud vm in dev set in Makefile 2025-02-26 15:52:33 +10:00
Pavel Sviderski 2bacebe900 refactor(dns): remove leading dot from domain when interecting with Uncloud DNS 2025-02-26 15:45:28 +10:00
Pavel Sviderski 09bea1d12d feat(dns): update ingress records in Uncloud DNS when deploying Caddy 2025-02-26 14:23:00 +10:00
Pavel Sviderski 5e9b883ad5 fix: increase wait time for machine to 5 min, discard unknown when decoding json to proto 2025-02-26 12:19:04 +10:00
Pavel Sviderski 1046396edc feat(dns): configure and store public IP when adding new machine 2025-02-26 11:16:13 +10:00
Pavel SviderskiandGitHub 35804458b0 Merge pull request #28 from psviderski/tonyo/27-feat-social-tags
feat(smm): Add OpenGraph tags
2025-02-26 10:10:49 +10:00
Anton Ovchinnikov 6bb64ac5dc feat(smm): Add OpenGraph tags 2025-02-26 00:35:33 +01:00
Pavel Sviderski e5f68947f9 feat(dns): missed CLI changes 2025-02-25 21:22:32 +10:00
Pavel Sviderski 289d964b2a feat(dns): configure and store public IP when initialising machine 2025-02-25 21:21:37 +10:00
Pavel Sviderski d0fd9db258 feat(dns): serve machine IP at /.uncloud-verify to verify Caddy reachability 2025-02-25 17:36:44 +10:00
Pavel Sviderski 0ea997e826 feat(dns): update caddy deploy command to create ingress records (stubs for now) 2025-02-24 20:33:54 +10:00
Pavel Sviderski 2d84059d7f feat(dns): client for creating ingress records 2025-02-24 20:33:08 +10:00
Pavel Sviderski ecec42ea60 feat(dns): cluster API for creating DNS records for cluster domain 2025-02-24 20:32:30 +10:00
Pavel Sviderski af4629b679 feat(dns): add CLI commands to reserve, show, delete cluster domain 2025-02-24 17:37:43 +10:00
Pavel Sviderski f6cf5cc250 feat(dns): cluster API for reserving/releasing cluster domain in Uncloud DNS 2025-02-24 17:35:19 +10:00
Pavel Sviderski f37bfd01fe feat: init cluster API for updating domain records in Uncloud DNS 2025-02-22 21:20:16 +10:00
Pavel Sviderski d9897e5d1e feat: set restart policy for all services to always 2025-02-19 16:53:55 +10:00
Pavel Sviderski bcb67a4073 refactor: e2e tests for container config validation 2025-02-19 15:19:00 +10:00
Pavel Sviderski bcb5a2a173 feat: deploy caddy service when adding new machine to cluster 2025-02-18 21:35:36 +10:00
Pavel Sviderski c3b88077ac feat: deploy caddy service when initialising new cluster on remote machine 2025-02-18 18:38:38 +10:00
Pavel Sviderski 98e1bdc9b5 fix: fail global deployment if no matchine machines found, improve caddy deploy output in particular 2025-02-17 20:31:37 +10:00
Pavel Sviderski e13408d79f feat: allow to filter machines for global service deployments 2025-02-17 20:26:40 +10:00
Pavel Sviderski 8610322682 fix: panic when pulling non-existent image 2025-02-17 16:12:37 +10:00
Pavel Sviderski 0be42f1f59 feat: print deployment plan for caddy deployment 2025-02-17 14:29:56 +10:00
Pavel Sviderski dcbfcbd0fb fix: show user-friendly image name when inspecting service 2025-02-16 21:12:56 +10:00
Pavel Sviderski d0e46186ed feat: look up latest caddy version tag when deploying caddy service 2025-02-16 21:10:06 +10:00
Pavel Sviderski 625a8baed3 feat: implement 'caddy deploy' command to deploy global service using caddy:latest image 2025-02-15 10:51:02 +10:00
Pavel Sviderski 98db5ed440 feat: caddy deploy command and stub implementation 2025-02-15 08:52:14 +10:00
Pavel Sviderski c4398f9f06 refactor: deploy global service using Deployment with RollingStrategy 2025-02-14 20:45:23 +10:00
Pavel Sviderski a53fa2c1d8 feat: do not redeploy global service if spec hasn't changed 2025-02-14 17:49:46 +10:00
Pavel Sviderski 5e2b2ac836 refactor: api.Container to use the detailed ContainerJSON struct instead of Container (Summary) 2025-02-14 16:37:40 +10:00
Pavel Sviderski 43f28284f1 feat: rolling deploy strategy for global service, always recreate 2025-02-13 21:32:08 +10:00
Pavel Sviderski b17fd7531f feat: detect conflicing service port on a container 2025-02-13 11:50:34 +10:00
Pavel Sviderski 9859be50a6 feat: add Deployment, Strategy, Operation primitives to compose service deployments 2025-02-12 21:17:30 +10:00
Pavel Sviderski f209851986 feat: progress for removing service(s) 2025-02-11 21:21:50 +10:00
Pavel Sviderski 4da51636fd refactor: remove service with StopContainer and RemoveContainer 2025-02-11 20:43:31 +10:00
Pavel Sviderski 76a9f8e8b6 refactor: client StartContainer pass serviceID 2025-02-11 16:47:52 +10:00
Pavel Sviderski a477db6b16 refactor: client CreateContainer and StartContainer methods 2025-02-11 15:59:36 +10:00
Pavel Sviderski 04fd1b1425 feat: introduce client.Service entity for managing service containers 2025-02-10 20:21:42 +10:00
Pavel Sviderski c0bf1f2930 add contributors section to README, feature @cedws 2025-02-10 11:38:35 +10:00
Pavel Sviderski 0bf759333a fix: populate ssh_key_file in the config when adding machine to cluster 2025-02-10 09:14:54 +10:00
Pavel SviderskiandGitHub b482d836aa Merge pull request #21 from cedws/identityfile
feat: add ssh_key_file field to connections config
2025-02-10 09:03:13 +10:00
Connor Edwards 3d2d9b78e8 fix: rename IdentityFile to SSHKeyFile 2025-02-09 18:39:44 +00:00
Connor Edwards 99219dc376 fix: set identity_file in config after init 2025-02-08 15:59:05 +00:00
Connor Edwards 408703360c fix: use os.UserHomeDir() instead of $HOME 2025-02-08 15:56:18 +00:00
Connor Edwards ac48b8bee6 feat: add identity_file field to config connections config 2025-02-08 15:56:18 +00:00
Pavel SviderskiandGitHub e3a310397b Merge pull request #20 from cedws/init-19
fix: execute valid bash cmd when running init as root
2025-02-08 16:42:14 +10:00
Connor Edwards b2f7c6fb33 fix: execute valid bash cmd when running init as root 2025-02-07 21:14:27 +00:00
Pavel Sviderski 653f47f507 add clean up step to quick start 2025-02-07 14:53:30 +10:00
Pavel Sviderski 5b66cec627 verify checksum in install_cli.sh 2025-02-06 15:17:33 +10:00
Pavel Sviderski 0d9a2b6e07 add brew install command to README 2025-02-06 14:54:55 +10:00
Pavel Sviderski fb68303825 fix: cli install script, add manual instruction 2025-02-06 14:38:55 +10:00
Pavel Sviderski cfe5b018f3 fix: install.sh install command 2025-02-06 14:01:55 +10:00
Pavel Sviderski b451f2cf7c fix: install_cli.sh install command 2025-02-06 13:56:56 +10:00
Pavel Sviderski 7d25bbafdf fix: goreleaser format for homebrew 2025-02-06 13:51:15 +10:00
Pavel Sviderski 58e1c2eea3 goreleaser: specify id for brew 2025-02-06 13:21:52 +10:00
Pavel Sviderski 6c791840bd update token for brew release 2025-02-06 13:15:27 +10:00
Pavel Sviderski 3af97306e1 add homebrew release to goreleaser 2025-02-06 13:09:34 +10:00
Pavel Sviderski d645958631 make DNS A record creation a separate step in quick start 2025-02-06 08:42:39 +10:00
Pavel Sviderski 48bc94828e link design doc 2025-02-05 21:35:16 +10:00
Pavel Sviderski a43f4bf5df minor README 2025-02-05 21:01:55 +10:00
Pavel Sviderski 22f705d6c9 update image name on landing 2025-02-05 20:48:02 +10:00
Pavel Sviderski efcb9ee947 add How it works section to README 2025-02-05 20:47:53 +10:00
Pavel Sviderski a347b32b65 fix typo 2025-02-03 11:55:03 +10:00
Pavel Sviderski 7e865b61c2 update get started links on landing 2025-02-03 11:48:11 +10:00
Pavel Sviderski b959f5d9f3 remove duplicate feature 2025-02-02 22:17:51 +10:00
Pavel Sviderski df64a00813 header padding 2025-02-02 22:15:48 +10:00
Pavel Sviderski 07dc5c373d update README: header, features, why uncloud 2025-02-02 22:12:42 +10:00
Pavel SviderskiandGitHub a6e8616b36 Update curl url in README.md 2025-01-30 15:14:07 +10:00
Pavel Sviderski 7041244728 update curl | sh url 2025-01-24 22:49:57 +10:00
77 changed files with 6490 additions and 2414 deletions
+3
View File
@@ -32,3 +32,6 @@ dist/
# Web
node_modules/
# VS Code
.vscode
+22 -3
View File
@@ -40,7 +40,9 @@ archives:
- id: uncloud
builds:
- uncloud
format: gz
formats:
# gz is not compatible with homebrew releases, so we use tar.gz
- tar.gz
name_template: >-
{{ .Binary }}_
{{- if eq .Os "darwin"}}macos
@@ -49,14 +51,16 @@ archives:
# use zip for windows archives
format_overrides:
- goos: windows
format: zip
formats:
- zip
files:
- none*
- id: uncloudd
builds:
- uncloudd
format: gz
formats:
- tar.gz
name_template: "{{ .Binary }}_{{ .Os }}_{{ .Arch }}"
files:
- none*
@@ -70,3 +74,18 @@ changelog:
exclude:
- "^docs:"
- "^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
View File
@@ -8,6 +8,10 @@ update-dev:
scp uncloudd-linux-amd64 spy@192.168.40.176:~/ && \
ssh spy@192.168.40.176 sudo install ./uncloudd-linux-amd64 /usr/local/bin/uncloudd && \
rm uncloudd-linux-amd64
# GOOS=linux GOARCH=arm64 go build -o uncloudd-linux-arm64 ./cmd/uncloudd && \
# scp uncloudd-linux-arm64 ubuntu@152.67.101.197:~/ && \
# ssh ubuntu@152.67.101.197 sudo install ./uncloudd-linux-arm64 /usr/local/bin/uncloudd && \
# rm uncloudd-linux-arm64
update-restart-dev:
GOOS=linux GOARCH=amd64 go build -o uncloudd-linux-amd64 ./cmd/uncloudd && \
@@ -16,6 +20,10 @@ update-restart-dev:
scp uncloudd-linux-amd64 spy@192.168.40.176:~/ && \
ssh spy@192.168.40.176 "sudo install ./uncloudd-linux-amd64 /usr/local/bin/uncloudd && sudo systemctl restart uncloud" && \
rm uncloudd-linux-amd64
# GOOS=linux GOARCH=arm64 go build -o uncloudd-linux-arm64 ./cmd/uncloudd && \
# scp uncloudd-linux-arm64 ubuntu@152.67.101.197:~/ && \
# ssh ubuntu@152.67.101.197 "sudo install ./uncloudd-linux-arm64 /usr/local/bin/uncloudd && sudo systemctl restart uncloud" && \
# rm uncloudd-linux-arm64
reset-dev:
ssh spy@192.168.40.243 "sudo systemctl stop uncloud && sudo rm -rf /var/lib/uncloud"
+158 -17
View File
@@ -1,16 +1,76 @@
<div align="center">
<img src="./website/images/logo.svg" height="100" alt="Uncloud logo"/>
<h1>Uncloud</h2>
A lightweight Docker clustering tool for running web apps on your own servers — from cloud VMs to bare metal.<br>
Replace Heroku and Render, no Kubernetes required.
<h1>Uncloud</h1>
<p><strong>Docker simplicity. Multi-machine power.</strong></p>
</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:
![Diagram: multi-provider cluster of 3 machines](website/images/diagram.webp)
## 💫 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:
```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:
@@ -19,26 +79,107 @@ curl -fsSL https://raw.githubusercontent.com/psviderski/uncloud/main/scripts/ins
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
automatic HTTPS:
3. Create a DNS A record in your DNS provider (Cloudflare, Namecheap, etc.) that points `app.example.com` to your
server's IP address. Allow a few minutes for DNS propagation.
4. Deploy your app from a Docker image:
```bash
uc run --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 ✨
## 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
share this journey with you. [Subscribe](https://uncloud.run/) to follow the progress, get early insights into new
features, and be the first to know when it's ready for production use.
```bash
uc ls
# 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)
- 💡 Have ideas? [Join the discussion](https://github.com/psviderski/uncloud/discussions)
Check out the [design document](docs/design.md) to understand Uncloud's design philosophy and goals. Here, let's peek
under the hood to see what happens when you run certain commands.
## 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>
+247
View File
@@ -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)
}
}
+16
View File
@@ -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
}
+54
View File
@@ -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
}
+62
View File
@@ -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
}
+22
View File
@@ -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
}
+53
View File
@@ -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
View File
@@ -1,16 +1,29 @@
package machine
import (
"context"
"errors"
"fmt"
"github.com/cenkalti/backoff/v4"
"github.com/docker/compose/v2/pkg/progress"
"github.com/spf13/cobra"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/emptypb"
"net/netip"
"time"
"uncloud/internal/cli"
"uncloud/internal/cli/client"
"uncloud/internal/cli/config"
"uncloud/internal/machine/api/pb"
)
type addOptions struct {
name string
sshKey string
cluster string
name string
noCaddy bool
publicIP string
sshKey string
cluster string
}
func NewAddCommand() *cobra.Command {
@@ -33,10 +46,19 @@ func NewAddCommand() *cobra.Command {
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().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(
&opts.sshKey, "ssh-key", "i", "",
"path to SSH private key for SSH remote login. (default ~/.ssh/id_*)",
@@ -47,3 +69,101 @@ func NewAddCommand() *cobra.Command {
)
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
View File
@@ -1,7 +1,9 @@
package machine
import (
"context"
"fmt"
"github.com/docker/compose/v2/pkg/progress"
"github.com/spf13/cobra"
"net/netip"
"uncloud/internal/cli"
@@ -10,10 +12,12 @@ import (
)
type initOptions struct {
name string
network string
sshKey string
cluster string
name string
network string
noCaddy bool
publicIP string
sshKey string
cluster string
}
func NewInitCommand() *cobra.Command {
@@ -39,19 +43,27 @@ func NewInitCommand() *cobra.Command {
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(
&opts.network, "network", cluster.DefaultNetwork.String(),
"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(
&opts.sshKey, "ssh-key", "i", "",
"path to SSH private key for SSH remote login. (default ~/.ssh/id_*)",
@@ -63,3 +75,51 @@ func NewInitCommand() *cobra.Command {
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 -4
View File
@@ -10,7 +10,6 @@ import (
"text/tabwriter"
"uncloud/internal/cli"
"uncloud/internal/machine/network"
"uncloud/internal/secret"
)
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.
tw := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', 0)
// 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)
}
// Print rows.
@@ -54,14 +53,21 @@ func list(ctx context.Context, uncli *cli.CLI, clusterName string) error {
m := member.Machine
subnet, _ := m.Network.Subnet.ToPrefix()
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))
for i, ep := range m.Network.Endpoints {
addrPort, _ := ep.ToAddrPort()
endpoints[i] = addrPort.String()
}
publicKey := secret.Secret(m.Network.PublicKey)
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 {
return fmt.Errorf("write row: %w", err)
}
+4
View File
@@ -6,6 +6,8 @@ import (
"github.com/spf13/cobra"
"os"
"strings"
"uncloud/cmd/uncloud/caddy"
"uncloud/cmd/uncloud/dns"
"uncloud/cmd/uncloud/machine"
"uncloud/cmd/uncloud/service"
"uncloud/internal/cli"
@@ -41,6 +43,8 @@ func main() {
_ = cmd.MarkPersistentFlagFilename("uncloud-config", "toml")
cmd.AddCommand(
caddy.NewRootCommand(),
dns.NewRootCommand(),
machine.NewRootCommand(),
service.NewRootCommand(),
service.NewInspectCommand(),
+10 -3
View File
@@ -70,21 +70,28 @@ func inspect(ctx context.Context, uncli *cli.CLI, opts inspectOptions) error {
}
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"
machine := machinesNamesByID[ctr.MachineID]
if machine == "" {
machine = ctr.MachineID
}
state, err := ctr.Container.HumanState()
if err != nil {
return fmt.Errorf("get human state: %w", err)
}
_, err = fmt.Fprintf(
tw,
"%s\t%s\t%s\t%s\t%s\n",
stringid.TruncateID(ctr.Container.ID),
ctr.Container.Image,
ctr.Container.Config.Image,
created,
ctr.Container.Status,
state,
machine,
)
if err != nil {
+28 -2
View File
@@ -5,6 +5,7 @@ import (
"fmt"
"github.com/spf13/cobra"
"os"
"strings"
"text/tabwriter"
"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)
}
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.
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)
}
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)
}
}
+9 -4
View File
@@ -3,7 +3,10 @@ package service
import (
"context"
"fmt"
"github.com/docker/cli/cli/streams"
"github.com/docker/compose/v2/pkg/progress"
"github.com/spf13/cobra"
"os"
"uncloud/internal/cli"
)
@@ -40,10 +43,12 @@ func rm(ctx context.Context, uncli *cli.CLI, opts rmOptions) error {
defer client.Close()
for _, s := range opts.services {
if err = client.RemoveService(ctx, s); err != nil {
return fmt.Errorf("remove service %q: %w", s, err)
}
fmt.Printf("Service %q removed.\n", s)
err = progress.RunWithTitle(ctx, func(ctx context.Context) error {
if err = client.RemoveService(ctx, s); err != nil {
return fmt.Errorf("remove service '%s': %w", s, err)
}
return nil
}, streams.NewOut(os.Stdout), "Removing service "+s)
}
return nil
+1
View File
@@ -10,6 +10,7 @@ func NewRootCommand() *cobra.Command {
Short: "Manage services in an Uncloud cluster.",
}
cmd.AddCommand(
NewInspectCommand(),
NewListCommand(),
NewRmCommand(),
NewRunCommand(),
+61 -18
View File
@@ -4,18 +4,23 @@ import (
"context"
"fmt"
"github.com/spf13/cobra"
"slices"
"strings"
"uncloud/internal/api"
"uncloud/internal/cli"
"uncloud/internal/cli/client"
"uncloud/internal/machine/api/pb"
)
type runOptions struct {
command []string
image string
machine string
mode string
name string
publish []string
volumes []string
command []string
image string
machines []string
mode string
name string
publish []string
replicas uint
volumes []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,
fmt.Sprintf("Replication mode of the service: either %q (a specified number of containers across "+
"the machines) or %q (one container on every machine).",
api.ServiceModeReplicated, api.ServiceModeGlobal))
cmd.Flags().StringSliceVarP(&opts.machines, "machine", "m", nil,
"Placement constraint by machine name, limiting which machines the service can run on. Can be specified "+
"multiple times or as a comma-separated list of machine names. (default is any suitable machine)")
cmd.Flags().StringVarP(&opts.name, "name", "n", "",
"Assign a name to the service. A random name is generated if not specified.")
cmd.Flags().StringSliceVarP(&opts.publish, "publish", "p", nil,
"Publish a service port to make it accessible outside the cluster. Can be specified multiple times.\n"+
"Format: [hostname:][load_balancer_port:]container_port[/protocol] or [host_ip:]:host_port:container_port[/protocol]@host\n"+
"Supported protocols: tcp, udp, http, https (default is tcp). If a hostname for http(s) port is not specified, 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"+
" -p 8080/https Publish port 8080 as HTTPS via load balancer with default service-name.cluster-domain hostname\n"+
" -p app.example.com:8080/https Publish port 8080 as HTTPS via load balancer with custom hostname\n"+
" -p 9000:8080 Publish port 8080 as TCP port 9000 via load balancer\n"+
" -p 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,
"Bind mount a host file or directory into a service container using the format "+
"/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 {
switch opts.mode {
case "", api.ServiceModeReplicated, api.ServiceModeGlobal:
case api.ServiceModeReplicated, api.ServiceModeGlobal:
default:
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))
for i, publishPort := range opts.publish {
port, err := api.ParsePortSpec(publishPort)
@@ -93,9 +123,10 @@ func run(ctx context.Context, uncli *cli.CLI, opts runOptions) error {
Image: opts.image,
Volumes: opts.volumes,
},
Mode: opts.mode,
Name: opts.name,
Ports: ports,
Mode: opts.mode,
Name: opts.name,
Ports: ports,
Replicas: opts.replicas,
}
if err := spec.Validate(); err != nil {
return fmt.Errorf("invalid service configuration: %w", err)
@@ -107,9 +138,21 @@ func run(ctx context.Context, uncli *cli.CLI, opts runOptions) error {
}
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)
}
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
}
+28 -20
View File
@@ -1,11 +1,12 @@
module uncloud
go 1.23
go 1.23.0
toolchain go1.23.2
require (
github.com/BurntSushi/toml v1.4.0
github.com/Masterminds/semver v1.5.0
github.com/Masterminds/squirrel v1.5.4
github.com/caddyserver/caddy/v2 v2.8.4
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/dgraph-io/badger/v3 v3.2103.5
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/docker v27.4.0-rc.2+incompatible
github.com/docker/go-connections v0.5.0
github.com/docker/go-units v0.5.0
github.com/google/go-containerregistry v0.20.2
github.com/hashicorp/memberlist v0.5.1
github.com/hashicorp/serf v0.10.1
github.com/ipfs/boxo v0.21.0
@@ -37,15 +39,15 @@ require (
github.com/vishvananda/netlink v1.3.0
go.uber.org/zap v1.27.0
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba
golang.org/x/crypto v0.31.0
golang.org/x/net v0.32.0
golang.org/x/crypto v0.32.0
golang.org/x/net v0.34.0
golang.org/x/sync v0.10.0
golang.org/x/sys v0.28.0
golang.org/x/sys v0.29.0
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6
google.golang.org/genproto/googleapis/rpc v0.0.0-20241007155032-5fefd90f89a9
google.golang.org/grpc v1.68.0
google.golang.org/protobuf v1.36.0
google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576
google.golang.org/grpc v1.68.1
google.golang.org/protobuf v1.36.3
)
require (
@@ -57,6 +59,7 @@ require (
github.com/Masterminds/semver/v3 v3.2.1 // indirect
github.com/Masterminds/sprig/v3 v3.2.3 // 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/armon/circbuf v0.0.0-20190214190532-5111143e8da2 // 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/term v0.2.0 // indirect
github.com/chzyer/readline v1.5.1 // indirect
github.com/cloudflare/cfssl v1.6.4 // indirect
github.com/compose-spec/compose-go/v2 v2.4.5 // indirect
github.com/containerd/console v1.0.4 // indirect
github.com/containerd/containerd v1.7.24 // indirect
@@ -87,9 +91,10 @@ require (
github.com/containerd/errdefs v0.3.0 // indirect
github.com/containerd/log v0.1.0 // 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/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/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // 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/uuid v1.6.0 // 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/go-immutable-radix v1.3.1 // 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/mitchellh/cli v1.1.5 // 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/hashstructure/v2 v2.0.2 // 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/vt100 v0.0.0-20240514184818-90bafcd6abab // 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/zeebo/blake3 v0.2.4 // indirect
go.etcd.io/bbolt v1.3.11 // 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/net/http/httptrace/otelhttptrace v0.46.1 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.52.0 // indirect
go.opentelemetry.io/otel v1.32.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.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/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/metric v1.32.0 // indirect
go.opentelemetry.io/otel/sdk v1.32.0 // indirect
go.opentelemetry.io/otel/metric v1.33.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/trace v1.32.0 // indirect
go.opentelemetry.io/proto/otlp v1.3.1 // indirect
go.opentelemetry.io/otel/trace v1.33.0 // indirect
go.opentelemetry.io/proto/otlp v1.4.0 // indirect
go.step.sm/cli-utils v0.9.0 // indirect
go.step.sm/crypto v0.45.0 // 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/exp v0.0.0-20241215155358-4a5509556b9e // 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/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
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
gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259 // indirect
howett.net/plist v1.0.0 // indirect
+63 -46
View File
@@ -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/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
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.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
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/hcsshim v0.12.5 h1:bpTInLlDy/nDRWFVcefDZZ1+U8tS+rz3MxjKgu9boo0=
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.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-20171204204709-577dee27f20d h1:UrqY+r/OJnIp5u0s1SbQ8dVfLCZJsnvazdBP5hS4iRs=
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/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I=
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 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/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I=
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/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/stargz-snapshotter v0.15.1 h1:fpsP4kf/Z4n2EYnU0WT8ZCE3eiKDwikDhL6VwxIlgeA=
github.com/containerd/stargz-snapshotter/estargz v0.15.1 h1:eXJjw9RbkLFgioVaTG+G/ZW/0kEe2oEKCdS/ZxIyoCU=
github.com/containerd/stargz-snapshotter/estargz v0.15.1/go.mod h1:gr2RNwukQ/S9Nv33Lt6UC7xEx58C+LHRdoqbEKjz1Kk=
github.com/containerd/stargz-snapshotter/estargz v0.16.3 h1:7evrXtoh1mSbGj/pfRccTampEyKpjpOnS3CyiV1Ebr8=
github.com/containerd/stargz-snapshotter/estargz v0.16.3/go.mod h1:uyr4BfYfOj3G9WBVE8cOlQmXAbPN9VEQpBBeJIuOipU=
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/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/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.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc=
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.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
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/docker/buildx v0.18.0 h1:rSauXHeJt90NvtXrLK5J992Eb0UPJZs2vV3u1zTf1nE=
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.4.0-rc.4+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/cli v27.5.0+incompatible h1:aMphQkcGtpHixwwhAXJT1rrK/detk2JIvDaFkLctbGM=
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/go.mod h1:oQq3UDEdsnB3AUO72AxaoeLbkCgmUu1+8tLzvmphmXA=
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.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
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/go.mod h1:FkNVkc6C+IsvDI9Jw1OveJmxGZUUaKxtrpOS47QWKfU=
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/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
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.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0 h1:TmHmbvxPmaegwhDubVz0lICL0J5Ka2vwTzhoePEXsGE=
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/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4=
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/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jmoiron/sqlx v1.3.3 h1:j82X0bf7oQ27XeqxicSZsTU5suPwKElg3oyxNn43iTk=
github.com/jmoiron/sqlx v1.3.3/go.mod h1:2BljVx/86SuTyjE+aPYlHCTNvZrnJXghYGpNiXLBMCQ=
github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=
github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
@@ -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.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
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-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc=
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/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
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.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
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.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc=
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.16 h1:MH0k6uJxdwdeWQTwhSO42Pwr4YLrNLwBtg1MRgTqPdQ=
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.5/go.mod h1:yZbwRsSeGjusneWgA781EKej9HF8vme8okylkAeNKLk=
github.com/vbatts/tar-split v0.11.6 h1:4SjTW5+PU11n6fZenf2IPoV8/tz3AaYHMWjf23envGs=
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/go.mod h1:i6NetklAujEcC6fK0JPjT8qSwWyO0HLn4UKG+hGqeJs=
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-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/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/yuin/goldmark v1.1.27/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/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4=
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/go.mod h1:dksAq7YMXoljX0xu6VF5DMZGbhYYoLUalEiSySYAS4I=
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/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/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/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.52.0/go.mod h1:XLZfZboOJWHNKUv7eH0inh0E9VV6eWDFB/9yJyTLPp0=
go.opentelemetry.io/otel v1.32.0 h1:WnBN+Xjcteh0zdk01SVqV55d/m62NJLJdIyb4y/WO5U=
go.opentelemetry.io/otel v1.32.0/go.mod h1:00DCVSB0RQcnzlwyTfqtxSm+DRr9hpYrHjNGiBHVQIg=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 h1:yd02MEjBdJkG3uabWP9apV+OuWRIXGDuJEUJbOHmCFU=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0/go.mod h1:umTcuxiv1n/s/S6/c2AT/g2CQ7u5C59sHDNmfSwgz7Q=
go.opentelemetry.io/otel v1.33.0 h1:/FerN9bax5LoK51X/sI0SVYrjSE0/yUL7DpxW4K3FWw=
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/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.27.0/go.mod h1:OQFyQVrDlbe+R7xrEyDr/2Wr67Ol0hRUgsfA+V5A95s=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0 h1:Vh5HayB/0HHfOQA7Ctx69E/Y/DcQSMPpKANYVMQ7fBA=
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/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/go.mod h1:HVkSiDhTM9BoUJU8qE6j2eSWLLXvi1USXjyd2BXT8PY=
go.opentelemetry.io/otel/metric v1.32.0 h1:xV2umtmNcThh2/a/aCP+h64Xx5wsj8qqnkYZktzNa0M=
go.opentelemetry.io/otel/metric v1.32.0/go.mod h1:jH7CIbbK6SH2V2wE16W05BHCtIDzauciCRLoc/SyMv8=
go.opentelemetry.io/otel/sdk v1.32.0 h1:RNxepc9vK59A8XsgZQouW8ue8Gkb4jpWtJm9ge5lEG4=
go.opentelemetry.io/otel/sdk v1.32.0/go.mod h1:LqgegDBjKMmb2GC6/PrTnteJG39I8/vJCAP9LlJXEjU=
go.opentelemetry.io/otel/metric v1.33.0 h1:r+JOocAyeRVXD8lZpjdQjzMadVZp2M4WmQ+5WtEnklQ=
go.opentelemetry.io/otel/metric v1.33.0/go.mod h1:L9+Fyctbp6HFTddIxClbQkjtubW6O9QS3Ann/M82u6M=
go.opentelemetry.io/otel/sdk v1.33.0 h1:iax7M131HuAm9QkZotNHEfstof92xM+N8sr3uHXc2IM=
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/go.mod h1:PWeZlq0zt9YkYAp3gjKZ0eicRYvOh1Gd+X99x6GHpCQ=
go.opentelemetry.io/otel/trace v1.32.0 h1:WIC9mYrXf8TmY/EXuULKc8hR17vE+Hjv2cssQDe03fM=
go.opentelemetry.io/otel/trace v1.32.0/go.mod h1:+i4rkvCraA+tG6AzwloGaCtkx53Fa+L+V8e9a7YvhT8=
go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0=
go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8=
go.opentelemetry.io/otel/trace v1.33.0 h1:cCJuF7LRjUFso9LPnEAHJDB2pqzp+hbO8eu1qqW2d/s=
go.opentelemetry.io/otel/trace v1.33.0/go.mod h1:uIcdVUZMpTAmz0tI1z04GoVSezK37CbGV4fr1f2nBck=
go.opentelemetry.io/proto/otlp v1.4.0 h1:TA9WRvW6zMwP+Ssb6fLoUIuirti1gGbP28GcKG1jgeg=
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/go.mod h1:Y/CRoWl1FVR9j+7PnAewufAwKmBOTzR6l9+7EYGAnp8=
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.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.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
golang.org/x/crypto/x509roots/fallback v0.0.0-20240507223354-67b13616a595 h1:TgSqweA595vD0Zt86JzLv3Pb/syKg8gd5KMGGbJPYFw=
golang.org/x/crypto/x509roots/fallback v0.0.0-20240507223354-67b13616a595/go.mod h1:kNa9WdvYnzFwC79zRpLRMJbdEFlhyM5RPFBBZp/wWH8=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@@ -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.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.32.0 h1:ZqPmj8Kzc+Y6e0+skZsuACbx+wzMgo5MQsJh9Qd6aYI=
golang.org/x/net v0.32.0/go.mod h1:CwU0IoeOlnQQWJ6ioyFrfRuomB8GKF6KbYXZVyeXNfs=
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE=
golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
@@ -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.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.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@@ -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.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
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.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg=
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
@@ -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.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.28.0 h1:WuB6qZ4RPCQo5aP3WdKZS7i595EdWqWR8vqJTlwTVK8=
golang.org/x/tools v0.28.0/go.mod h1:dcIOrVd3mfQKTgrDVQHqCPMWy6lnhfhtX3hLXYVLfRw=
golang.org/x/tools v0.29.0 h1:Xx0h3TtM9rzQpQuR4dKLrdglAmCEN5Oi+P74JdhdzXE=
golang.org/x/tools v0.29.0/go.mod h1:KMQVMRsVxU6nHCFXrBPhDB8XncLNLM0lIy/F14RP588=
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -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-20240401170217-c3f982113cda h1:wu/KJm9KJwpfHWhkkZGohVC6KRrc1oJNr4jwtQMOQXw=
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-20240903143218-8af14fe29dc1/go.mod h1:qpvKtACPCQhAdu3PyQgV4l3LMXZEtft7y8QcarRsp9I=
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-20241007155032-5fefd90f89a9/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI=
google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 h1:CkkIfIt50+lT6NHAVoRYEyAvQGFM7xEwXUUywFvEb3Q=
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-20241209162323-e6fa225c2576 h1:8ZmaLZE4XWrtU3MyClkYqqtl6Oegr3235h7jxsDyqCY=
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.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
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.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
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.0/go.mod h1:fmSPC5AsjSBCK54MyHRx48kpOti1/jRfOlwEWywNjWA=
google.golang.org/grpc v1.68.1 h1:oI5oTa11+ng8r8XMMN7jAOmWfPZWbYpCFaMUTACxkM0=
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-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
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.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.36.0 h1:mjIs9gYtt56AzC4ZaffQuh88TZurBGhIJMBZGSxNerQ=
google.golang.org/protobuf v1.36.0/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
google.golang.org/protobuf v1.36.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU=
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/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/cenkalti/backoff.v2 v2.2.1 h1:eJ9UAg01/HIHG987TwxvnzK2MgxXq97YY6rYDpY9aII=
+126 -27
View File
@@ -1,9 +1,11 @@
package api
import (
"fmt"
"github.com/docker/docker/api/types"
"regexp"
"github.com/docker/go-units"
"strings"
"time"
)
const (
@@ -15,30 +17,39 @@ const (
)
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.
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.
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.
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.
func (c *Container) ServicePorts() ([]PortSpec, error) {
encoded, ok := c.Labels[LabelServicePorts]
encoded, ok := c.Config.Labels[LabelServicePorts]
if !ok {
return nil, nil
}
if strings.TrimSpace(encoded) == "" {
return nil, nil
}
publishPorts := strings.Split(encoded, ",")
ports := make([]PortSpec, len(publishPorts))
@@ -53,34 +64,122 @@ func (c *Container) ServicePorts() ([]PortSpec, error) {
return ports, nil
}
// runningStatusRegex matches the status string of a running container.
// - "Up 3 minutes (healthy)" -> groups: ["Up 3 minutes (healthy)", "healthy"]
// - "Up 5 seconds" -> groups: ["Up 5 seconds", ""]
// - "Up 2 hours (unhealthy)" -> groups: ["Up 2 hours (unhealthy)", "unhealthy"]
// - "Up 1 minute (health: starting)" -> groups: ["Up 1 minute (health: starting)", "health: starting"]
// - "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 [^(]+(?:\(([^)]+)\))?$`)
// ServiceSpec constructs a service spec from the container's configuration.
func (c *Container) ServiceSpec() (ServiceSpec, error) {
ports, err := c.ServicePorts()
if err != nil {
return ServiceSpec{}, fmt.Errorf("get service ports: %w", err)
}
// 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.
func (c *Container) Healthy() bool {
if c.State != "running" {
if !c.State.Running || c.State.Paused || c.State.Restarting {
return false
}
matches := runningStatusRegex.FindStringSubmatch(c.Status)
// Not "Up" or invalid format.
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] == "" {
// If there's no health status (no health check configured), container is considered healthy.
if c.State.Health == nil {
return true
}
// If the health status in parentheses is "healthy", the container is considered healthy.
return matches[1] == types.Healthy
return c.State.Health.Status == 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
View File
@@ -2,91 +2,345 @@ package api
import (
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"net/netip"
"reflect"
"testing"
)
func TestContainer_ServiceSpec(t *testing.T) {
t.Parallel()
init := true
ctr := &Container{ContainerJSON: types.ContainerJSON{
ContainerJSONBase: &types.ContainerJSONBase{
HostConfig: &container.HostConfig{
Binds: []string{"/host/path:/container/path"},
Init: &init,
},
},
Config: &container.Config{
Cmd: []string{"/app/server"},
Image: "app:latest",
Labels: map[string]string{
LabelServiceID: "test-service-id",
LabelServiceName: "test-service-name",
LabelServicePorts: "app.example.com:8000/https",
},
},
}}
expectedSpec := ServiceSpec{
Container: ContainerSpec{
Command: []string{"/app/server"},
Image: "app:latest",
Init: &init,
Volumes: []string{"/host/path:/container/path"},
},
Name: "test-service-name",
Ports: []PortSpec{
{
Hostname: "app.example.com",
ContainerPort: 8000,
Protocol: ProtocolHTTPS,
Mode: PortModeIngress,
},
},
}
spec, err := ctr.ServiceSpec()
require.NoError(t, err)
assert.True(t, reflect.DeepEqual(spec, expectedSpec))
}
func TestContainer_Healthy(t *testing.T) {
t.Parallel()
t.Run("exited", func(t *testing.T) {
t.Parallel()
c := &Container{Container: types.Container{
State: "exited",
Status: "Exited (0) 2 minutes ago",
c := &Container{ContainerJSON: types.ContainerJSON{
ContainerJSONBase: &types.ContainerJSONBase{
State: &types.ContainerState{
Running: false,
Dead: false,
ExitCode: 0,
},
},
}}
assert.False(t, c.Healthy())
})
t.Run("running with no health check", func(t *testing.T) {
t.Parallel()
c := &Container{Container: types.Container{
State: "running",
Status: "Up 5 minutes",
c := &Container{ContainerJSON: types.ContainerJSON{
ContainerJSONBase: &types.ContainerJSONBase{
State: &types.ContainerState{
Running: true,
},
},
}}
assert.True(t, c.Healthy())
})
t.Run("running and healthy", func(t *testing.T) {
t.Parallel()
c := &Container{Container: types.Container{
State: "running",
Status: "Up 3 minutes (healthy)",
c := &Container{ContainerJSON: types.ContainerJSON{
ContainerJSONBase: &types.ContainerJSONBase{
State: &types.ContainerState{
Running: true,
Health: &types.Health{
Status: types.Healthy,
},
},
},
}}
assert.True(t, c.Healthy())
})
t.Run("running but unhealthy", func(t *testing.T) {
t.Parallel()
c := &Container{Container: types.Container{
State: "running",
Status: "Up 2 hours (unhealthy)",
c := &Container{ContainerJSON: types.ContainerJSON{
ContainerJSONBase: &types.ContainerJSONBase{
State: &types.ContainerState{
Running: true,
Health: &types.Health{
Status: types.Unhealthy,
},
},
},
}}
assert.False(t, c.Healthy())
})
t.Run("running with health starting", func(t *testing.T) {
t.Parallel()
c := &Container{Container: types.Container{
State: "running",
Status: "Up 1 minute (health: starting)",
c := &Container{ContainerJSON: types.ContainerJSON{
ContainerJSONBase: &types.ContainerJSONBase{
State: &types.ContainerState{
Running: true,
Health: &types.Health{
Status: "starting",
},
},
},
}}
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()
c := &Container{Container: types.Container{
State: "running",
Status: "Up",
}}
assert.False(t, c.Healthy())
})
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",
c := &Container{ContainerJSON: types.ContainerJSON{
ContainerJSONBase: &types.ContainerJSONBase{
State: &types.ContainerState{
Dead: true,
Running: false,
},
},
}}
assert.False(t, c.Healthy())
})
t.Run("restarting", func(t *testing.T) {
t.Parallel()
c := &Container{Container: types.Container{
State: "running",
Status: "Restarting (0) 5 seconds ago",
c := &Container{ContainerJSON: types.ContainerJSON{
ContainerJSONBase: &types.ContainerJSONBase{
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())
})
}
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)
})
}
}
+1 -6
View File
@@ -62,9 +62,6 @@ func (p *PortSpec) Validate() error {
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:
if p.PublishedPort == 0 {
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)
}
} else {
if parts[0] == "" {
return spec, fmt.Errorf("hostname must not be empty")
}
// Hostname may be empty.
spec.Hostname = parts[0]
}
+25 -33
View File
@@ -50,6 +50,22 @@ func TestPortSpec_Validate(t *testing.T) {
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",
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",
},
{
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",
spec: PortSpec{
@@ -468,6 +466,15 @@ func TestParsePortSpec(t *testing.T) {
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",
port: "app.example.com:8080/http",
@@ -602,21 +609,6 @@ func TestParsePortSpec(t *testing.T) {
port: "app.example.com:invalid:8080",
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",
port: "app:8080/http",
+69 -2
View File
@@ -4,6 +4,10 @@ import (
"encoding/json"
"fmt"
"github.com/distribution/reference"
"maps"
"reflect"
"regexp"
"slices"
"uncloud/internal/machine/api/pb"
)
@@ -19,6 +23,8 @@ type ServiceSpec struct {
Name string
// Ports defines what service ports to publish to make the service accessible outside the cluster.
Ports []PortSpec
// Replicas is the number of containers to run for the service. Only valid for a replicated service.
Replicas uint
}
func (s *ServiceSpec) Validate() error {
@@ -33,10 +39,27 @@ func (s *ServiceSpec) Validate() error {
}
// TODO: validate there is no conflict between ports.
// TODO: return error if there are non-HTTP/HTTPS ingress ports that we don't support yet.
return nil
}
// Equals returns true if the service spec is equal to the given spec ignoring the number of replicas.
func (s *ServiceSpec) Equals(spec ServiceSpec) bool {
// TODO: ignore order of ports.
sCopy := *s
// Ignore the number of replicas when comparing.
sCopy.Replicas = 0
spec.Replicas = 0
return reflect.DeepEqual(*s, spec)
}
var serviceIDRegexp = regexp.MustCompile("^[0-9a-f]{32}$")
func ValidateServiceID(id string) bool {
return serviceIDRegexp.MatchString(id)
}
type ContainerSpec struct {
Command []string
Image string
@@ -47,8 +70,7 @@ type ContainerSpec struct {
}
func (s *ContainerSpec) Validate() error {
_, err := reference.ParseDockerRef(s.Image)
if err != nil {
if _, err := reference.ParseDockerRef(s.Image); err != nil {
return fmt.Errorf("invalid image: %w", err)
}
@@ -67,6 +89,51 @@ type MachineContainer struct {
Container Container
}
// Endpoints returns the exposed HTTP and HTTPS endpoints of the service.
func (s *Service) Endpoints() []string {
endpoints := make(map[string]struct{})
// Container specs may differ between containers in the same service, e.g. during a rolling update,
// so we need to collect all unique endpoints.
for _, ctr := range s.Containers {
ports, err := ctr.Container.ServicePorts()
if err != nil {
continue
}
for _, port := range ports {
protocol := ""
switch port.Protocol {
case ProtocolHTTP:
protocol = "http"
case ProtocolHTTPS:
protocol = "https"
default:
continue
}
if port.Hostname == "" {
// There shouldn't be http(s) ports without a hostname but just in case ignore them.
continue
}
endpoint := fmt.Sprintf("%s://%s", protocol, port.Hostname)
if port.PublishedPort != 0 {
// For non-standard ports (80/443), include the port in the URL.
if !(port.Protocol == ProtocolHTTP && port.PublishedPort == 80) &&
!(port.Protocol == ProtocolHTTPS && port.PublishedPort == 443) {
endpoint += fmt.Sprintf(":%d", port.PublishedPort)
}
}
endpoint += fmt.Sprintf(" → :%d", port.ContainerPort)
endpoints[endpoint] = struct{}{}
}
}
return slices.Sorted(maps.Keys(endpoints))
}
func ServiceFromProto(s *pb.Service) (Service, error) {
var err error
containers := make([]MachineContainer, len(s.Containers))
+96 -39
View File
@@ -4,15 +4,19 @@ import (
"context"
"errors"
"fmt"
"github.com/charmbracelet/huh"
"google.golang.org/protobuf/types/known/emptypb"
"github.com/docker/cli/cli/streams"
"net/netip"
"os"
"uncloud/internal/cli/client"
"uncloud/internal/cli/client/connector"
"uncloud/internal/cli/config"
"uncloud/internal/fs"
"uncloud/internal/machine"
"uncloud/internal/machine/api/pb"
"uncloud/internal/sshexec"
"github.com/charmbracelet/huh"
"google.golang.org/protobuf/types/known/emptypb"
)
const defaultClusterName = "default"
@@ -89,10 +93,14 @@ func (cli *CLI) ConnectCluster(ctx context.Context, clusterName string) (*client
if err != nil {
return nil, fmt.Errorf("parse SSH connection %q: %w", conn.SSH, err)
}
keyPath := fs.ExpandHomeDir(conn.SSHKeyFile)
sshConfig := &connector.SSHConnectorConfig{
User: user,
Host: host,
Port: port,
User: user,
Host: host,
Port: port,
KeyPath: keyPath,
}
return client.New(ctx, connector.NewSSHConnector(sshConfig))
} 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")
}
// InitCluster initialises a new cluster on a remote machine and returns a client to interact with the cluster.
// The client should be closed after use by the caller.
func (cli *CLI) InitCluster(
ctx context.Context, remoteMachine *RemoteMachine, clusterName, machineName string, netPrefix netip.Prefix,
) error {
ctx context.Context,
remoteMachine *RemoteMachine,
clusterName,
machineName string,
netPrefix netip.Prefix,
publicIP *netip.Addr,
) (*client.Client, error) {
if remoteMachine != nil {
return cli.initRemoteMachine(ctx, *remoteMachine, clusterName, machineName, netPrefix)
return cli.initRemoteMachine(ctx, *remoteMachine, clusterName, machineName, netPrefix, publicIP)
}
// 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(
ctx context.Context, remoteMachine RemoteMachine, clusterName, machineName string, netPrefix netip.Prefix,
) error {
ctx context.Context,
remoteMachine RemoteMachine,
clusterName,
machineName string,
netPrefix netip.Prefix,
publicIP *netip.Addr,
) (*client.Client, error) {
if clusterName == "" {
clusterName = defaultClusterName
}
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)
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.
minfo, err := machineClient.Inspect(ctx, &emptypb.Empty{})
if err != nil {
return fmt.Errorf("inspect machine: %w", err)
return nil, fmt.Errorf("inspect machine: %w", err)
}
if minfo.Id != "" {
if err = cli.promptResetMachine(); err != nil {
return err
return nil, err
}
}
@@ -142,65 +167,82 @@ func (cli *CLI) initRemoteMachine(
MachineName: machineName,
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)
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)
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.
if len(cli.config.Clusters) == 1 {
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.
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)
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)
if err != nil {
return fmt.Errorf("connect to cluster: %w", err)
return nil, fmt.Errorf("connect to cluster: %w", err)
}
defer func() {
_ = c.Close()
}()
defer c.Close()
machineClient, err := cli.provisionRemoteMachine(ctx, remoteMachine)
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.
minfo, err := machineClient.Inspect(ctx, &emptypb.Empty{})
if err != nil {
return fmt.Errorf("inspect machine: %w", err)
return nil, fmt.Errorf("inspect machine: %w", err)
}
if minfo.Id != "" {
if err = cli.promptResetMachine(); err != nil {
return err
return nil, err
}
}
tokenResp, err := machineClient.Token(ctx, &emptypb.Empty{})
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)
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.
@@ -215,15 +257,24 @@ func (cli *CLI) AddMachine(ctx context.Context, remoteMachine RemoteMachine, clu
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)
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.
machines, err := c.ListMachines(ctx)
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)
for _, m := range machines {
@@ -238,24 +289,25 @@ func (cli *CLI) AddMachine(ctx context.Context, remoteMachine RemoteMachine, clu
OtherMachines: otherMachines,
}
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)
// Save the machine's SSH connection details in the cluster config.
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 == "" {
clusterName = cli.config.CurrentCluster
}
cli.config.Clusters[clusterName].Connections = append(cli.config.Clusters[clusterName].Connections, connCfg)
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
@@ -319,3 +371,8 @@ func (cli *CLI) promptResetMachine() error {
// TODO: implement resetting the remote machine.
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)
}
+102
View File
@@ -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
}
+69
View File
@@ -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)
})
}
+12 -5
View File
@@ -6,6 +6,7 @@ import (
"fmt"
"github.com/docker/cli/cli/streams"
"google.golang.org/grpc"
"google.golang.org/grpc/metadata"
"os"
"uncloud/internal/machine/api/pb"
"uncloud/internal/machine/docker"
@@ -20,12 +21,11 @@ type Client struct {
pb.MachineClient
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.
type Connector interface {
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.ClusterClient = pb.NewClusterClient(c.conn)
c.DockerClient = docker.NewClient(c.conn)
c.Docker = docker.NewClient(c.conn)
return c, nil
}
@@ -58,3 +58,10 @@ func (cli *Client) Close() error {
func (cli *Client) progressOut() *streams.Out {
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)
}
+15
View File
@@ -6,6 +6,21 @@ import (
"uncloud/internal/machine/api/pb"
)
func (cli *Client) InspectMachine(ctx context.Context, id string) (*pb.MachineMember, error) {
machines, err := cli.ListMachines(ctx)
if err != nil {
return nil, err
}
for _, m := range machines {
if m.Machine.Id == id || m.Machine.Name == id {
return m, nil
}
}
return nil, ErrNotFound
}
func (cli *Client) ListMachines(ctx context.Context) ([]*pb.MachineMember, error) {
resp, err := cli.ClusterClient.ListMachines(ctx, &emptypb.Empty{})
if err != nil {
+335
View File
@@ -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
}
+113
View File
@@ -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)
}
+161
View File
@@ -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)",
)
}
+166
View File
@@ -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
}
+116
View File
@@ -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
View File
@@ -4,65 +4,49 @@ import (
"context"
"errors"
"fmt"
"github.com/distribution/reference"
"github.com/docker/compose/v2/pkg/progress"
"github.com/docker/docker/api/types/container"
"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/metadata"
"google.golang.org/grpc/status"
"slices"
"strconv"
"strings"
"sync"
"uncloud/internal/api"
"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 {
ID string
Name string
Containers []MachineContainerID
ID string
Name string
}
type MachineContainerID struct {
MachineID string
ContainerID string
}
func (cli *Client) RunService(ctx context.Context, spec api.ServiceSpec) (RunServiceResponse, error) {
func (cli *Client) RunService(
ctx context.Context, spec api.ServiceSpec, filter MachineFilter,
) (RunServiceResponse, error) {
var resp RunServiceResponse
if err := spec.Validate(); err != nil {
return resp, fmt.Errorf("invalid service spec: %w", err)
}
img, err := reference.ParseDockerRef(spec.Container.Image)
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 {
if spec.Name != "" {
// Optimistically check if a service with the specified name already exists.
_, err := cli.InspectService(ctx, spec.Name)
if err == nil {
@@ -73,346 +57,30 @@ func (cli *Client) RunService(ctx context.Context, spec api.ServiceSpec) (RunSer
}
}
serviceID, err := secret.NewID()
if err != nil {
return resp, fmt.Errorf("generate service ID: %w", err)
var err error
if spec, err = cli.PrepareDeploymentSpec(ctx, spec); err != nil {
return resp, fmt.Errorf("prepare service spec ready for deployment: %w", err)
}
err = progress.RunWithTitle(ctx, func(ctx context.Context) error {
switch spec.Mode {
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)
}
}
deploy, err := cli.NewDeployment(spec, &RollingStrategy{MachineFilter: filter})
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)
return fmt.Errorf("create deployment: %w", err)
}
// 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)
serviceID, err := deploy.Run(ctx)
if err != nil {
return err
}
}
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
}
}, cli.progressOut(), fmt.Sprintf("Running service %s (%s mode)", spec.Name, spec.Mode))
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,
}
return resp, err
}
// 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),
),
}
machineContainers, err := cli.ListContainers(listCtx, opts)
machineContainers, err := cli.Docker.ListContainers(listCtx, opts)
if err != nil {
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 {
ctr := api.Container{Container: c}
ctr := api.Container{ContainerJSON: c}
if ctr.ServiceID() == id || ctr.ServiceName() == id {
containers = append(containers, api.MachineContainer{
MachineID: machineID,
@@ -513,7 +181,7 @@ func (cli *Client) InspectService(ctx context.Context, id string) (api.Service,
serviceID := containers[0].Container.ServiceID()
for _, mc := range containers[1:] {
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() {
defer wg.Done()
machineIP, ok := machineManagementIPByID[mc.MachineID]
if !ok {
errCh <- fmt.Errorf("machine not found by ID: %s", mc.MachineID)
err := cli.StopContainer(ctx, svc.ID, mc.Container.ID, container.StopOptions{})
if err != nil {
errCh <- fmt.Errorf("stop container '%s': %w", mc.Container.ID, err)
return
}
removeCtx := metadata.NewOutgoingContext(ctx, metadata.Pairs("machines", machineIP))
// TODO: gracefully stop the container before removing it without force.
err := cli.RemoveContainer(removeCtx, mc.Container.ID, container.RemoveOptions{Force: true})
if err != nil {
if !dockerclient.IsErrNotFound(err) {
errCh <- fmt.Errorf("remove container '%s': %w", mc.Container.ID, err)
}
err = cli.RemoveContainer(ctx, svc.ID, mc.Container.ID, container.RemoveOptions{})
if err != nil && !errors.Is(err, ErrNotFound) {
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),
),
}
machineContainers, err := cli.ListContainers(listCtx, opts)
machineContainers, err := cli.Docker.ListContainers(listCtx, opts)
if err != nil {
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 {
ctr := api.Container{Container: c}
ctr := api.Container{ContainerJSON: c}
if _, ok := servicesByID[ctr.ServiceID()]; ok {
continue
}
+364
View File
@@ -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
}
+5 -4
View File
@@ -14,10 +14,11 @@ const (
)
type MachineConnection struct {
SSH SSHDestination `toml:"ssh,omitempty"`
TCP netip.AddrPort `toml:"tcp,omitempty"`
Host string `toml:"host,omitempty"`
PublicKey secret.Secret `toml:"public_key,omitempty"`
SSH SSHDestination `toml:"ssh,omitempty"`
TCP netip.AddrPort `toml:"tcp,omitempty"`
Host string `toml:"host,omitempty"`
PublicKey secret.Secret `toml:"public_key,omitempty"`
SSHKeyFile string `toml:"ssh_key_file,omitempty"`
}
// SSHDestination represents an SSH destination string in the canonical form of "user@host:port".
+22 -10
View File
@@ -17,24 +17,36 @@ type RemoteMachine struct {
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.
func provisionMachine(ctx context.Context, exec sshexec.Executor) error {
user, err := exec.Run(ctx, "whoami")
if err != nil {
return fmt.Errorf("run whoami: %w", err)
}
sudoPrefix, env := "", ""
if user != "root" {
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
}
installCmd := installCmd(user)
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; "+curlBashCmd)
cmd := sshexec.QuoteCommand("bash", "-c", "set -o pipefail; "+installCmd)
if err = exec.Stream(ctx, cmd, os.Stdout, os.Stderr); err != nil {
return fmt.Errorf("download and run install script: %w", err)
}
+19
View File
@@ -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")
})
}
+1 -1
View File
@@ -37,5 +37,5 @@ func MachineToken(dataDir string) (machine.Token, error) {
for i, ip := range ips {
endpoints[i] = netip.AddrPortFrom(ip, network.WireGuardPort)
}
return machine.NewToken(state.Network.PublicKey, endpoints), nil
return machine.NewToken(state.Network.PublicKey, publicIP, endpoints), nil
}
+34
View File
@@ -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"`
}
+152
View File
@@ -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
}
+15
View File
@@ -5,8 +5,23 @@ import (
"os"
"os/user"
"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.
func LookupUIDGID(username string) (uid, gid int, err error) {
usr, err := user.Lookup(username)
+22
View File
@@ -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"))
})
}
+501 -69
View File
@@ -78,13 +78,63 @@ func (MachineMember_MembershipState) EnumDescriptor() ([]byte, []int) {
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 {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
Network *NetworkConfig `protobuf:"bytes,2,opt,name=network,proto3" json:"network,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"`
PublicIp *IP `protobuf:"bytes,3,opt,name=public_ip,json=publicIp,proto3" json:"public_ip,omitempty"`
}
func (x *AddMachineRequest) Reset() {
@@ -133,6 +183,13 @@ func (x *AddMachineRequest) GetNetwork() *NetworkConfig {
return nil
}
func (x *AddMachineRequest) GetPublicIp() *IP {
if x != nil {
return x.PublicIp
}
return nil
}
type AddMachineResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
@@ -282,6 +339,257 @@ func (x *ListMachinesResponse) GetMachines() []*MachineMember {
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_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,
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,
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,
0x70, 0x62, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f,
0x22, 0x55, 0x0a, 0x11, 0x41, 0x64, 0x64, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65,
0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20,
0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x2c, 0x0a, 0x07, 0x6e, 0x65, 0x74,
0x77, 0x6f, 0x72, 0x6b, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x61, 0x70, 0x69,
0x2e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x07,
0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x22, 0x40, 0x0a, 0x12, 0x41, 0x64, 0x64, 0x4d, 0x61,
0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2a, 0x0a,
0x07, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10,
0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f,
0x52, 0x07, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x22, 0xb4, 0x01, 0x0a, 0x0d, 0x4d, 0x61,
0x63, 0x68, 0x69, 0x6e, 0x65, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x12, 0x2a, 0x0a, 0x07, 0x6d,
0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x61,
0x70, 0x69, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x07,
0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x12, 0x38, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65,
0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x22, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d, 0x61, 0x63,
0x68, 0x69, 0x6e, 0x65, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x2e, 0x4d, 0x65, 0x6d, 0x62, 0x65,
0x72, 0x73, 0x68, 0x69, 0x70, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74,
0x65, 0x22, 0x3d, 0x0a, 0x0f, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, 0x68, 0x69, 0x70, 0x53,
0x74, 0x61, 0x74, 0x65, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10,
0x00, 0x12, 0x06, 0x0a, 0x02, 0x55, 0x50, 0x10, 0x01, 0x12, 0x0b, 0x0a, 0x07, 0x53, 0x55, 0x53,
0x50, 0x45, 0x43, 0x54, 0x10, 0x02, 0x12, 0x08, 0x0a, 0x04, 0x44, 0x4f, 0x57, 0x4e, 0x10, 0x03,
0x22, 0x46, 0x0a, 0x14, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73,
0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2e, 0x0a, 0x08, 0x6d, 0x61, 0x63, 0x68,
0x69, 0x6e, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x61, 0x70, 0x69,
0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x52, 0x08,
0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x32, 0x8b, 0x01, 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, 0x42, 0x37, 0x5a, 0x35, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62,
0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x70, 0x73, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x6b, 0x69, 0x2f,
0x75, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c,
0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x62, 0x62,
0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
0x70, 0x62, 0x2f, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a,
0x25, 0x69, 0x6e, 0x74, 0x65, 0x72, 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, 0x22, 0x7b, 0x0a, 0x11, 0x41, 0x64, 0x64, 0x4d, 0x61, 0x63,
0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x6e,
0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12,
0x2c, 0x0a, 0x07, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b,
0x32, 0x12, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x43, 0x6f,
0x6e, 0x66, 0x69, 0x67, 0x52, 0x07, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x12, 0x24, 0x0a,
0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x69, 0x70, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b,
0x32, 0x07, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x50, 0x52, 0x08, 0x70, 0x75, 0x62, 0x6c, 0x69,
0x63, 0x49, 0x70, 0x22, 0x40, 0x0a, 0x12, 0x41, 0x64, 0x64, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e,
0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2a, 0x0a, 0x07, 0x6d, 0x61, 0x63,
0x68, 0x69, 0x6e, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x61, 0x70, 0x69,
0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x07, 0x6d, 0x61,
0x63, 0x68, 0x69, 0x6e, 0x65, 0x22, 0xb4, 0x01, 0x0a, 0x0d, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e,
0x65, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x12, 0x2a, 0x0a, 0x07, 0x6d, 0x61, 0x63, 0x68, 0x69,
0x6e, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d,
0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x07, 0x6d, 0x61, 0x63, 0x68,
0x69, 0x6e, 0x65, 0x12, 0x38, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01,
0x28, 0x0e, 0x32, 0x22, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65,
0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x2e, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, 0x68, 0x69,
0x70, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x22, 0x3d, 0x0a,
0x0f, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, 0x68, 0x69, 0x70, 0x53, 0x74, 0x61, 0x74, 0x65,
0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x06, 0x0a,
0x02, 0x55, 0x50, 0x10, 0x01, 0x12, 0x0b, 0x0a, 0x07, 0x53, 0x55, 0x53, 0x50, 0x45, 0x43, 0x54,
0x10, 0x02, 0x12, 0x08, 0x0a, 0x04, 0x44, 0x4f, 0x57, 0x4e, 0x10, 0x03, 0x22, 0x46, 0x0a, 0x14,
0x4c, 0x69, 0x73, 0x74, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70,
0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2e, 0x0a, 0x08, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73,
0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d, 0x61, 0x63,
0x68, 0x69, 0x6e, 0x65, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x52, 0x08, 0x6d, 0x61, 0x63, 0x68,
0x69, 0x6e, 0x65, 0x73, 0x22, 0x1c, 0x0a, 0x06, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x12,
0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61,
0x6d, 0x65, 0x22, 0x32, 0x0a, 0x14, 0x52, 0x65, 0x73, 0x65, 0x72, 0x76, 0x65, 0x44, 0x6f, 0x6d,
0x61, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x65, 0x6e,
0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x65, 0x6e,
0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x22, 0x46, 0x0a, 0x1a, 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, 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, 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 (
@@ -345,33 +697,52 @@ func file_internal_machine_api_pb_cluster_proto_rawDescGZIP() []byte {
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_msgTypes = make([]protoimpl.MessageInfo, 4)
var file_internal_machine_api_pb_cluster_proto_enumTypes = make([]protoimpl.EnumInfo, 2)
var file_internal_machine_api_pb_cluster_proto_msgTypes = make([]protoimpl.MessageInfo, 9)
var file_internal_machine_api_pb_cluster_proto_goTypes = []any{
(MachineMember_MembershipState)(0), // 0: api.MachineMember.MembershipState
(*AddMachineRequest)(nil), // 1: api.AddMachineRequest
(*AddMachineResponse)(nil), // 2: api.AddMachineResponse
(*MachineMember)(nil), // 3: api.MachineMember
(*ListMachinesResponse)(nil), // 4: api.ListMachinesResponse
(*NetworkConfig)(nil), // 5: api.NetworkConfig
(*MachineInfo)(nil), // 6: api.MachineInfo
(*emptypb.Empty)(nil), // 7: google.protobuf.Empty
(MachineMember_MembershipState)(0), // 0: api.MachineMember.MembershipState
(DNSRecord_RecordType)(0), // 1: api.DNSRecord.RecordType
(*AddMachineRequest)(nil), // 2: api.AddMachineRequest
(*AddMachineResponse)(nil), // 3: api.AddMachineResponse
(*MachineMember)(nil), // 4: api.MachineMember
(*ListMachinesResponse)(nil), // 5: api.ListMachinesResponse
(*Domain)(nil), // 6: api.Domain
(*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{
5, // 0: api.AddMachineRequest.network:type_name -> api.NetworkConfig
6, // 1: api.AddMachineResponse.machine:type_name -> api.MachineInfo
6, // 2: api.MachineMember.machine:type_name -> api.MachineInfo
0, // 3: api.MachineMember.state:type_name -> api.MachineMember.MembershipState
3, // 4: api.ListMachinesResponse.machines:type_name -> api.MachineMember
1, // 5: api.Cluster.AddMachine:input_type -> api.AddMachineRequest
7, // 6: api.Cluster.ListMachines:input_type -> google.protobuf.Empty
2, // 7: api.Cluster.AddMachine:output_type -> api.AddMachineResponse
4, // 8: api.Cluster.ListMachines:output_type -> api.ListMachinesResponse
7, // [7:9] is the sub-list for method output_type
5, // [5:7] is the sub-list for method input_type
5, // [5:5] is the sub-list for extension type_name
5, // [5:5] is the sub-list for extension extendee
0, // [0:5] is the sub-list for field type_name
11, // 0: api.AddMachineRequest.network:type_name -> api.NetworkConfig
12, // 1: api.AddMachineRequest.public_ip:type_name -> api.IP
13, // 2: api.AddMachineResponse.machine:type_name -> api.MachineInfo
13, // 3: api.MachineMember.machine:type_name -> api.MachineInfo
0, // 4: api.MachineMember.state:type_name -> api.MachineMember.MembershipState
4, // 5: api.ListMachinesResponse.machines:type_name -> api.MachineMember
10, // 6: api.CreateDomainRecordsRequest.records:type_name -> api.DNSRecord
10, // 7: api.CreateDomainRecordsResponse.records:type_name -> api.DNSRecord
1, // 8: api.DNSRecord.type:type_name -> api.DNSRecord.RecordType
2, // 9: api.Cluster.AddMachine:input_type -> api.AddMachineRequest
14, // 10: api.Cluster.ListMachines:input_type -> google.protobuf.Empty
7, // 11: api.Cluster.ReserveDomain:input_type -> api.ReserveDomainRequest
14, // 12: api.Cluster.GetDomain:input_type -> google.protobuf.Empty
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() }
@@ -379,6 +750,7 @@ func file_internal_machine_api_pb_cluster_proto_init() {
if File_internal_machine_api_pb_cluster_proto != nil {
return
}
file_internal_machine_api_pb_common_proto_init()
file_internal_machine_api_pb_machine_proto_init()
if !protoimpl.UnsafeEnabled {
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
}
}
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{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_internal_machine_api_pb_cluster_proto_rawDesc,
NumEnums: 1,
NumMessages: 4,
NumEnums: 2,
NumMessages: 9,
NumExtensions: 0,
NumServices: 1,
},
+35
View File
@@ -5,16 +5,23 @@ package api;
option go_package = "github.com/psviderski/uncloud/internal/machine/api/pb";
import "google/protobuf/empty.proto";
import "internal/machine/api/pb/common.proto";
import "internal/machine/api/pb/machine.proto";
service Cluster {
rpc AddMachine(AddMachineRequest) returns (AddMachineResponse);
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 {
string name = 1;
NetworkConfig network = 2;
IP public_ip = 3;
}
message AddMachineResponse {
@@ -41,3 +48,31 @@ message MachineMember {
message ListMachinesResponse {
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;
}
+154 -2
View File
@@ -20,8 +20,12 @@ import (
const _ = grpc.SupportPackageIsVersion9
const (
Cluster_AddMachine_FullMethodName = "/api.Cluster/AddMachine"
Cluster_ListMachines_FullMethodName = "/api.Cluster/ListMachines"
Cluster_AddMachine_FullMethodName = "/api.Cluster/AddMachine"
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.
@@ -30,6 +34,10 @@ const (
type ClusterClient interface {
AddMachine(ctx context.Context, in *AddMachineRequest, opts ...grpc.CallOption) (*AddMachineResponse, 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 {
@@ -60,12 +68,56 @@ func (c *clusterClient) ListMachines(ctx context.Context, in *emptypb.Empty, opt
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.
// All implementations must embed UnimplementedClusterServer
// for forward compatibility.
type ClusterServer interface {
AddMachine(context.Context, *AddMachineRequest) (*AddMachineResponse, 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()
}
@@ -82,6 +134,18 @@ func (UnimplementedClusterServer) AddMachine(context.Context, *AddMachineRequest
func (UnimplementedClusterServer) ListMachines(context.Context, *emptypb.Empty) (*ListMachinesResponse, error) {
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) testEmbeddedByValue() {}
@@ -139,6 +203,78 @@ func _Cluster_ListMachines_Handler(srv interface{}, ctx context.Context, dec fun
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.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
@@ -154,6 +290,22 @@ var Cluster_ServiceDesc = grpc.ServiceDesc{
MethodName: "ListMachines",
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{},
Metadata: "internal/machine/api/pb/cluster.proto",
+321 -107
View File
@@ -152,6 +152,101 @@ func (x *CreateContainerResponse) GetResponse() []byte {
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 {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
@@ -165,7 +260,7 @@ type StartContainerRequest struct {
func (x *StartContainerRequest) Reset() {
*x = StartContainerRequest{}
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.StoreMessageInfo(mi)
}
@@ -178,7 +273,7 @@ func (x *StartContainerRequest) String() string {
func (*StartContainerRequest) ProtoMessage() {}
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 {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -191,7 +286,7 @@ func (x *StartContainerRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use StartContainerRequest.ProtoReflect.Descriptor instead.
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 {
@@ -208,6 +303,62 @@ func (x *StartContainerRequest) GetOptions() []byte {
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 {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
@@ -220,7 +371,7 @@ type ListContainersRequest struct {
func (x *ListContainersRequest) Reset() {
*x = ListContainersRequest{}
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.StoreMessageInfo(mi)
}
@@ -233,7 +384,7 @@ func (x *ListContainersRequest) String() string {
func (*ListContainersRequest) ProtoMessage() {}
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 {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -246,7 +397,7 @@ func (x *ListContainersRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use ListContainersRequest.ProtoReflect.Descriptor instead.
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 {
@@ -268,7 +419,7 @@ type ListContainersResponse struct {
func (x *ListContainersResponse) Reset() {
*x = ListContainersResponse{}
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.StoreMessageInfo(mi)
}
@@ -281,7 +432,7 @@ func (x *ListContainersResponse) String() string {
func (*ListContainersResponse) ProtoMessage() {}
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 {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -294,7 +445,7 @@ func (x *ListContainersResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use ListContainersResponse.ProtoReflect.Descriptor instead.
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 {
@@ -310,14 +461,14 @@ type MachineContainers struct {
unknownFields protoimpl.UnknownFields
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"`
}
func (x *MachineContainers) Reset() {
*x = MachineContainers{}
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.StoreMessageInfo(mi)
}
@@ -330,7 +481,7 @@ func (x *MachineContainers) String() string {
func (*MachineContainers) ProtoMessage() {}
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 {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -343,7 +494,7 @@ func (x *MachineContainers) ProtoReflect() protoreflect.Message {
// Deprecated: Use MachineContainers.ProtoReflect.Descriptor instead.
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 {
@@ -373,7 +524,7 @@ type RemoveContainerRequest struct {
func (x *RemoveContainerRequest) Reset() {
*x = RemoveContainerRequest{}
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.StoreMessageInfo(mi)
}
@@ -386,7 +537,7 @@ func (x *RemoveContainerRequest) String() string {
func (*RemoveContainerRequest) ProtoMessage() {}
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 {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -399,7 +550,7 @@ func (x *RemoveContainerRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use RemoveContainerRequest.ProtoReflect.Descriptor instead.
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 {
@@ -429,7 +580,7 @@ type PullImageRequest struct {
func (x *PullImageRequest) Reset() {
*x = PullImageRequest{}
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.StoreMessageInfo(mi)
}
@@ -442,7 +593,7 @@ func (x *PullImageRequest) String() string {
func (*PullImageRequest) ProtoMessage() {}
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 {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -455,7 +606,7 @@ func (x *PullImageRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use PullImageRequest.ProtoReflect.Descriptor instead.
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 {
@@ -484,7 +635,7 @@ type JSONMessage struct {
func (x *JSONMessage) Reset() {
*x = JSONMessage{}
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.StoreMessageInfo(mi)
}
@@ -497,7 +648,7 @@ func (x *JSONMessage) String() string {
func (*JSONMessage) ProtoMessage() {}
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 {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -510,7 +661,7 @@ func (x *JSONMessage) ProtoReflect() protoreflect.Message {
// Deprecated: Use JSONMessage.ProtoReflect.Descriptor instead.
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 {
@@ -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,
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,
0x22, 0x41, 0x0a, 0x15, 0x53, 0x74, 0x61, 0x72, 0x74, 0x43, 0x6f, 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, 0x07, 0x6f, 0x70, 0x74,
0x69, 0x6f, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x6f, 0x70, 0x74, 0x69,
0x6f, 0x6e, 0x73, 0x22, 0x31, 0x0a, 0x15, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61,
0x69, 0x6e, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x07,
0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x6f,
0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x4c, 0x0a, 0x16, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f,
0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
0x12, 0x32, 0x0a, 0x08, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03,
0x28, 0x0b, 0x32, 0x16, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65,
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,
0x22, 0x29, 0x0a, 0x17, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x43, 0x6f, 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, 0x22, 0x36, 0x0a, 0x18, 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, 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, 0x22, 0x41, 0x0a, 0x15, 0x53, 0x74, 0x61, 0x72, 0x74, 0x43, 0x6f, 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, 0x07,
0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x6f,
0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x40, 0x0a, 0x14, 0x53, 0x74, 0x6f, 0x70, 0x43, 0x6f,
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, 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,
0x49, 0x6d, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x14, 0x0a, 0x05,
0x69, 0x6d, 0x61, 0x67, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x69, 0x6d, 0x61,
0x67, 0x65, 0x12, 0x18, 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, 0x27, 0x0a, 0x0b,
0x4a, 0x53, 0x4f, 0x4e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x6d,
0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x6d, 0x65,
0x73, 0x73, 0x61, 0x67, 0x65, 0x32, 0xe7, 0x02, 0x0a, 0x06, 0x44, 0x6f, 0x63, 0x6b, 0x65, 0x72,
0x12, 0x4c, 0x0a, 0x0f, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69,
0x6e, 0x65, 0x72, 0x12, 0x1b, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65,
0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
0x1a, 0x1c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 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, 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,
0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x31, 0x0a, 0x15, 0x4c, 0x69, 0x73, 0x74,
0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
0x74, 0x12, 0x18, 0x0a, 0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x01,
0x28, 0x0c, 0x52, 0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x4c, 0x0a, 0x16, 0x4c,
0x69, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73,
0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x32, 0x0a, 0x08, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65,
0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d, 0x61,
0x63, 0x68, 0x69, 0x6e, 0x65, 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, 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, 0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x42, 0x0a,
0x10, 0x50, 0x75, 0x6c, 0x6c, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
0x74, 0x12, 0x14, 0x0a, 0x05, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
0x52, 0x05, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x12, 0x18, 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, 0x27, 0x0a, 0x0b, 0x4a, 0x53, 0x4f, 0x4e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65,
0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28,
0x0c, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x32, 0xfc, 0x03, 0x0a, 0x06, 0x44,
0x6f, 0x63, 0x6b, 0x65, 0x72, 0x12, 0x4c, 0x0a, 0x0f, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x43,
0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x1b, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43,
0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65,
0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x72, 0x65, 0x61,
0x74, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f,
0x6e, 0x73, 0x65, 0x12, 0x4f, 0x0a, 0x10, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x43, 0x6f,
0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x1c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e,
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 (
@@ -614,35 +785,42 @@ func file_internal_machine_api_pb_docker_proto_rawDescGZIP() []byte {
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{
(*CreateContainerRequest)(nil), // 0: api.CreateContainerRequest
(*CreateContainerResponse)(nil), // 1: api.CreateContainerResponse
(*StartContainerRequest)(nil), // 2: api.StartContainerRequest
(*ListContainersRequest)(nil), // 3: api.ListContainersRequest
(*ListContainersResponse)(nil), // 4: api.ListContainersResponse
(*MachineContainers)(nil), // 5: api.MachineContainers
(*RemoveContainerRequest)(nil), // 6: api.RemoveContainerRequest
(*PullImageRequest)(nil), // 7: api.PullImageRequest
(*JSONMessage)(nil), // 8: api.JSONMessage
(*Metadata)(nil), // 9: api.Metadata
(*emptypb.Empty)(nil), // 10: google.protobuf.Empty
(*CreateContainerRequest)(nil), // 0: api.CreateContainerRequest
(*CreateContainerResponse)(nil), // 1: api.CreateContainerResponse
(*InspectContainerRequest)(nil), // 2: api.InspectContainerRequest
(*InspectContainerResponse)(nil), // 3: api.InspectContainerResponse
(*StartContainerRequest)(nil), // 4: api.StartContainerRequest
(*StopContainerRequest)(nil), // 5: api.StopContainerRequest
(*ListContainersRequest)(nil), // 6: api.ListContainersRequest
(*ListContainersResponse)(nil), // 7: api.ListContainersResponse
(*MachineContainers)(nil), // 8: api.MachineContainers
(*RemoveContainerRequest)(nil), // 9: api.RemoveContainerRequest
(*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{
5, // 0: api.ListContainersResponse.messages:type_name -> api.MachineContainers
9, // 1: api.MachineContainers.metadata:type_name -> api.Metadata
8, // 0: api.ListContainersResponse.messages:type_name -> api.MachineContainers
12, // 1: api.MachineContainers.metadata:type_name -> api.Metadata
0, // 2: api.Docker.CreateContainer:input_type -> api.CreateContainerRequest
2, // 3: api.Docker.StartContainer:input_type -> api.StartContainerRequest
3, // 4: api.Docker.ListContainers:input_type -> api.ListContainersRequest
6, // 5: api.Docker.RemoveContainer:input_type -> api.RemoveContainerRequest
7, // 6: api.Docker.PullImage:input_type -> api.PullImageRequest
1, // 7: api.Docker.CreateContainer:output_type -> api.CreateContainerResponse
10, // 8: api.Docker.StartContainer:output_type -> google.protobuf.Empty
4, // 9: api.Docker.ListContainers:output_type -> api.ListContainersResponse
10, // 10: api.Docker.RemoveContainer:output_type -> google.protobuf.Empty
8, // 11: api.Docker.PullImage:output_type -> api.JSONMessage
7, // [7:12] is the sub-list for method output_type
2, // [2:7] is the sub-list for method input_type
2, // 3: api.Docker.InspectContainer:input_type -> api.InspectContainerRequest
4, // 4: api.Docker.StartContainer:input_type -> api.StartContainerRequest
5, // 5: api.Docker.StopContainer:input_type -> api.StopContainerRequest
6, // 6: api.Docker.ListContainers:input_type -> api.ListContainersRequest
9, // 7: api.Docker.RemoveContainer:input_type -> api.RemoveContainerRequest
10, // 8: api.Docker.PullImage:input_type -> api.PullImageRequest
1, // 9: api.Docker.CreateContainer:output_type -> api.CreateContainerResponse
3, // 10: api.Docker.InspectContainer:output_type -> api.InspectContainerResponse
13, // 11: api.Docker.StartContainer:output_type -> google.protobuf.Empty
13, // 12: api.Docker.StopContainer:output_type -> google.protobuf.Empty
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 extendee
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 {
switch v := v.(*StartContainerRequest); i {
switch v := v.(*InspectContainerRequest); i {
case 0:
return &v.state
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 {
switch v := v.(*ListContainersRequest); i {
switch v := v.(*InspectContainerResponse); i {
case 0:
return &v.state
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 {
switch v := v.(*ListContainersResponse); i {
switch v := v.(*StartContainerRequest); i {
case 0:
return &v.state
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 {
switch v := v.(*MachineContainers); i {
switch v := v.(*StopContainerRequest); i {
case 0:
return &v.state
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 {
switch v := v.(*RemoveContainerRequest); i {
switch v := v.(*ListContainersRequest); i {
case 0:
return &v.state
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 {
switch v := v.(*PullImageRequest); i {
switch v := v.(*ListContainersResponse); i {
case 0:
return &v.state
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 {
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 {
case 0:
return &v.state
@@ -770,7 +984,7 @@ func file_internal_machine_api_pb_docker_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_internal_machine_api_pb_docker_proto_rawDesc,
NumEnums: 0,
NumMessages: 9,
NumMessages: 12,
NumExtensions: 0,
NumServices: 1,
},
+18 -1
View File
@@ -9,7 +9,9 @@ import "internal/machine/api/pb/common.proto";
service Docker {
rpc CreateContainer(CreateContainerRequest) returns (CreateContainerResponse);
rpc InspectContainer(InspectContainerRequest) returns (InspectContainerResponse);
rpc StartContainer(StartContainerRequest) returns (google.protobuf.Empty);
rpc StopContainer(StopContainerRequest) returns (google.protobuf.Empty);
rpc ListContainers(ListContainersRequest) returns (ListContainersResponse);
rpc RemoveContainer(RemoveContainerRequest) returns (google.protobuf.Empty);
rpc PullImage(PullImageRequest) returns (stream JSONMessage);
@@ -32,12 +34,27 @@ message CreateContainerResponse {
bytes response = 1;
}
message InspectContainerRequest {
string id = 1;
}
message InspectContainerResponse {
// JSON serialized container.InspectResponse.
bytes response = 1;
}
message StartContainerRequest {
string id = 1;
// JSON serialized container.StartOptions.
bytes options = 2;
}
message StopContainerRequest {
string id = 1;
// JSON serialized container.StopOptions.
bytes options = 2;
}
message ListContainersRequest {
// JSON serialized container.ListOptions.
bytes options = 1;
@@ -50,7 +67,7 @@ message ListContainersResponse {
message MachineContainers {
Metadata metadata = 1;
// JSON serialized []container.Summary.
// JSON serialized []container.ContainerJSON.
bytes containers = 2;
}
+81 -5
View File
@@ -20,11 +20,13 @@ import (
const _ = grpc.SupportPackageIsVersion9
const (
Docker_CreateContainer_FullMethodName = "/api.Docker/CreateContainer"
Docker_StartContainer_FullMethodName = "/api.Docker/StartContainer"
Docker_ListContainers_FullMethodName = "/api.Docker/ListContainers"
Docker_RemoveContainer_FullMethodName = "/api.Docker/RemoveContainer"
Docker_PullImage_FullMethodName = "/api.Docker/PullImage"
Docker_CreateContainer_FullMethodName = "/api.Docker/CreateContainer"
Docker_InspectContainer_FullMethodName = "/api.Docker/InspectContainer"
Docker_StartContainer_FullMethodName = "/api.Docker/StartContainer"
Docker_StopContainer_FullMethodName = "/api.Docker/StopContainer"
Docker_ListContainers_FullMethodName = "/api.Docker/ListContainers"
Docker_RemoveContainer_FullMethodName = "/api.Docker/RemoveContainer"
Docker_PullImage_FullMethodName = "/api.Docker/PullImage"
)
// 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.
type DockerClient interface {
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)
StopContainer(ctx context.Context, in *StopContainerRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
ListContainers(ctx context.Context, in *ListContainersRequest, opts ...grpc.CallOption) (*ListContainersResponse, error)
RemoveContainer(ctx context.Context, in *RemoveContainerRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
PullImage(ctx context.Context, in *PullImageRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[JSONMessage], error)
@@ -56,6 +60,16 @@ func (c *dockerClient) CreateContainer(ctx context.Context, in *CreateContainerR
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) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(emptypb.Empty)
@@ -66,6 +80,16 @@ func (c *dockerClient) StartContainer(ctx context.Context, in *StartContainerReq
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) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ListContainersResponse)
@@ -110,7 +134,9 @@ type Docker_PullImageClient = grpc.ServerStreamingClient[JSONMessage]
// for forward compatibility.
type DockerServer interface {
CreateContainer(context.Context, *CreateContainerRequest) (*CreateContainerResponse, error)
InspectContainer(context.Context, *InspectContainerRequest) (*InspectContainerResponse, error)
StartContainer(context.Context, *StartContainerRequest) (*emptypb.Empty, error)
StopContainer(context.Context, *StopContainerRequest) (*emptypb.Empty, error)
ListContainers(context.Context, *ListContainersRequest) (*ListContainersResponse, error)
RemoveContainer(context.Context, *RemoveContainerRequest) (*emptypb.Empty, error)
PullImage(*PullImageRequest, grpc.ServerStreamingServer[JSONMessage]) error
@@ -127,9 +153,15 @@ type UnimplementedDockerServer struct{}
func (UnimplementedDockerServer) CreateContainer(context.Context, *CreateContainerRequest) (*CreateContainerResponse, error) {
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) {
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) {
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)
}
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) {
in := new(StartContainerRequest)
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)
}
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) {
in := new(ListContainersRequest)
if err := dec(in); err != nil {
@@ -254,10 +322,18 @@ var Docker_ServiceDesc = grpc.ServiceDesc{
MethodName: "CreateContainer",
Handler: _Docker_CreateContainer_Handler,
},
{
MethodName: "InspectContainer",
Handler: _Docker_InspectContainer_Handler,
},
{
MethodName: "StartContainer",
Handler: _Docker_StartContainer_Handler,
},
{
MethodName: "StopContainer",
Handler: _Docker_StopContainer_Handler,
},
{
MethodName: "ListContainers",
Handler: _Docker_ListContainers_Handler,
+175 -110
View File
@@ -26,9 +26,10 @@ type MachineInfo struct {
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,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"`
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,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"`
PublicIp *IP `protobuf:"bytes,4,opt,name=public_ip,json=publicIp,proto3" json:"public_ip,omitempty"`
}
func (x *MachineInfo) Reset() {
@@ -84,6 +85,13 @@ func (x *MachineInfo) GetNetwork() *NetworkConfig {
return nil
}
func (x *MachineInfo) GetPublicIp() *IP {
if x != nil {
return x.PublicIp
}
return nil
}
type NetworkConfig struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
@@ -92,7 +100,7 @@ type NetworkConfig struct {
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"`
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() {
@@ -162,6 +170,11 @@ type InitClusterRequest struct {
MachineName string `protobuf:"bytes,1,opt,name=machineName,proto3" json:"machineName,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() {
@@ -210,6 +223,43 @@ func (x *InitClusterRequest) GetNetwork() *IPPrefix {
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 {
state protoimpl.MessageState
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,
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,
0x5f, 0x0a, 0x0b, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x0e,
0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x12,
0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61,
0x6d, 0x65, 0x12, 0x2c, 0x0a, 0x07, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x18, 0x03, 0x20,
0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72,
0x6b, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x07, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b,
0x22, 0xad, 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,
0x85, 0x01, 0x0a, 0x0b, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x12,
0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12,
0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e,
0x61, 0x6d, 0x65, 0x12, 0x2c, 0x0a, 0x07, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x18, 0x03,
0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4e, 0x65, 0x74, 0x77, 0x6f,
0x72, 0x6b, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x07, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72,
0x6b, 0x12, 0x24, 0x0a, 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x69, 0x70, 0x18, 0x04,
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,
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, 0x1c, 0x0a, 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x18,
0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79,
0x22, 0x5f, 0x0a, 0x12, 0x49, 0x6e, 0x69, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x52,
0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x20, 0x0a, 0x0b, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e,
0x65, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x6d, 0x61, 0x63,
0x68, 0x69, 0x6e, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x27, 0x0a, 0x07, 0x6e, 0x65, 0x74, 0x77,
0x6f, 0x72, 0x6b, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x61, 0x70, 0x69, 0x2e,
0x49, 0x50, 0x50, 0x72, 0x65, 0x66, 0x69, 0x78, 0x52, 0x07, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72,
0x6b, 0x22, 0x41, 0x0a, 0x13, 0x49, 0x6e, 0x69, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72,
0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2a, 0x0a, 0x07, 0x6d, 0x61, 0x63, 0x68,
0x69, 0x6e, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x61, 0x70, 0x69, 0x2e,
0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x07, 0x6d, 0x61, 0x63,
0x68, 0x69, 0x6e, 0x65, 0x22, 0x79, 0x0a, 0x12, 0x4a, 0x6f, 0x69, 0x6e, 0x43, 0x6c, 0x75, 0x73,
0x74, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2a, 0x0a, 0x07, 0x6d, 0x61,
0x63, 0x68, 0x69, 0x6e, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x61, 0x70,
0x69, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x07, 0x6d,
0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x12, 0x37, 0x0a, 0x0e, 0x6f, 0x74, 0x68, 0x65, 0x72, 0x5f,
0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x10,
0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f,
0x52, 0x0d, 0x6f, 0x74, 0x68, 0x65, 0x72, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x22,
0x25, 0x0a, 0x0d, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52,
0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0xc3, 0x01, 0x0a, 0x07, 0x53, 0x65, 0x72, 0x76, 0x69,
0x63, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02,
0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09,
0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x18, 0x03,
0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x12, 0x36, 0x0a, 0x0a, 0x63, 0x6f,
0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16,
0x2e, 0x61, 0x70, 0x69, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x43, 0x6f, 0x6e,
0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x0a, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65,
0x72, 0x73, 0x1a, 0x48, 0x0a, 0x09, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12,
0x1d, 0x0a, 0x0a, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20,
0x01, 0x28, 0x09, 0x52, 0x09, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x64, 0x12, 0x1c,
0x0a, 0x09, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28,
0x0c, 0x52, 0x09, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x22, 0x27, 0x0a, 0x15,
0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65,
0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28,
0x09, 0x52, 0x02, 0x69, 0x64, 0x22, 0x40, 0x0a, 0x16, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74,
0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12,
0x26, 0x0a, 0x07, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b,
0x32, 0x0c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x07,
0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x32, 0xc0, 0x02, 0x0a, 0x07, 0x4d, 0x61, 0x63, 0x68,
0x69, 0x6e, 0x65, 0x12, 0x40, 0x0a, 0x0b, 0x49, 0x6e, 0x69, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74,
0x65, 0x72, 0x12, 0x17, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x69, 0x74, 0x43, 0x6c, 0x75,
0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x61, 0x70,
0x69, 0x2e, 0x49, 0x6e, 0x69, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x73,
0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3e, 0x0a, 0x0b, 0x4a, 0x6f, 0x69, 0x6e, 0x43, 0x6c, 0x75,
0x73, 0x74, 0x65, 0x72, 0x12, 0x17, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4a, 0x6f, 0x69, 0x6e, 0x43,
0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e,
0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e,
0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x33, 0x0a, 0x05, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x16,
0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66,
0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x12, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x54, 0x6f, 0x6b,
0x65, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x33, 0x0a, 0x07, 0x49, 0x6e,
0x73, 0x70, 0x65, 0x63, 0x74, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70,
0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x10, 0x2e,
0x61, 0x70, 0x69, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x12,
0x49, 0x0a, 0x0e, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63,
0x65, 0x12, 0x1a, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53,
0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e,
0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69,
0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x37, 0x5a, 0x35, 0x67, 0x69,
0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x70, 0x73, 0x76, 0x69, 0x64, 0x65, 0x72,
0x73, 0x6b, 0x69, 0x2f, 0x75, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2f, 0x69, 0x6e, 0x74, 0x65,
0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x2f, 0x61, 0x70, 0x69,
0x2f, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
0x78, 0x52, 0x07, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x12, 0x26, 0x0a, 0x09, 0x70, 0x75,
0x62, 0x6c, 0x69, 0x63, 0x5f, 0x69, 0x70, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x07, 0x2e,
0x61, 0x70, 0x69, 0x2e, 0x49, 0x50, 0x48, 0x00, 0x52, 0x08, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63,
0x49, 0x70, 0x12, 0x26, 0x0a, 0x0e, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x69, 0x70, 0x5f,
0x61, 0x75, 0x74, 0x6f, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x48, 0x00, 0x52, 0x0c, 0x70, 0x75,
0x62, 0x6c, 0x69, 0x63, 0x49, 0x70, 0x41, 0x75, 0x74, 0x6f, 0x42, 0x12, 0x0a, 0x10, 0x70, 0x75,
0x62, 0x6c, 0x69, 0x63, 0x5f, 0x69, 0x70, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0x41,
0x0a, 0x13, 0x49, 0x6e, 0x69, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x73,
0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2a, 0x0a, 0x07, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65,
0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d, 0x61, 0x63,
0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x07, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e,
0x65, 0x22, 0x79, 0x0a, 0x12, 0x4a, 0x6f, 0x69, 0x6e, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72,
0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2a, 0x0a, 0x07, 0x6d, 0x61, 0x63, 0x68, 0x69,
0x6e, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d,
0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x07, 0x6d, 0x61, 0x63, 0x68,
0x69, 0x6e, 0x65, 0x12, 0x37, 0x0a, 0x0e, 0x6f, 0x74, 0x68, 0x65, 0x72, 0x5f, 0x6d, 0x61, 0x63,
0x68, 0x69, 0x6e, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x61, 0x70,
0x69, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x0d, 0x6f,
0x74, 0x68, 0x65, 0x72, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x22, 0x25, 0x0a, 0x0d,
0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x14, 0x0a,
0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x6f,
0x6b, 0x65, 0x6e, 0x22, 0xc3, 0x01, 0x0a, 0x07, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12,
0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12,
0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e,
0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28,
0x09, 0x52, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x12, 0x36, 0x0a, 0x0a, 0x63, 0x6f, 0x6e, 0x74, 0x61,
0x69, 0x6e, 0x65, 0x72, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x61, 0x70,
0x69, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69,
0x6e, 0x65, 0x72, 0x52, 0x0a, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x1a,
0x48, 0x0a, 0x09, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x1d, 0x0a, 0x0a,
0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
0x52, 0x09, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x64, 0x12, 0x1c, 0x0a, 0x09, 0x63,
0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09,
0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x22, 0x27, 0x0a, 0x15, 0x49, 0x6e, 0x73,
0x70, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65,
0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02,
0x69, 0x64, 0x22, 0x40, 0x0a, 0x16, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72,
0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x26, 0x0a, 0x07,
0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0c, 0x2e,
0x61, 0x70, 0x69, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x07, 0x73, 0x65, 0x72,
0x76, 0x69, 0x63, 0x65, 0x32, 0xc0, 0x02, 0x0a, 0x07, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65,
0x12, 0x40, 0x0a, 0x0b, 0x49, 0x6e, 0x69, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x12,
0x17, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x69, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65,
0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49,
0x6e, 0x69, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
0x73, 0x65, 0x12, 0x3e, 0x0a, 0x0b, 0x4a, 0x6f, 0x69, 0x6e, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65,
0x72, 0x12, 0x17, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4a, 0x6f, 0x69, 0x6e, 0x43, 0x6c, 0x75, 0x73,
0x74, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f,
0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70,
0x74, 0x79, 0x12, 0x33, 0x0a, 0x05, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x16, 0x2e, 0x67, 0x6f,
0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d,
0x70, 0x74, 0x79, 0x1a, 0x12, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52,
0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x33, 0x0a, 0x07, 0x49, 0x6e, 0x73, 0x70, 0x65,
0x63, 0x74, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74,
0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x10, 0x2e, 0x61, 0x70, 0x69,
0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x49, 0x0a, 0x0e,
0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x1a,
0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72, 0x76,
0x69, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x61, 0x70, 0x69,
0x2e, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52,
0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x37, 0x5a, 0x35, 0x67, 0x69, 0x74, 0x68, 0x75,
0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x70, 0x73, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x6b, 0x69,
0x2f, 0x75, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61,
0x6c, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x62,
0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
@@ -697,37 +756,39 @@ var file_internal_machine_api_pb_machine_proto_goTypes = []any{
(*InspectServiceRequest)(nil), // 7: api.InspectServiceRequest
(*InspectServiceResponse)(nil), // 8: api.InspectServiceResponse
(*Service_Container)(nil), // 9: api.Service.Container
(*IPPrefix)(nil), // 10: api.IPPrefix
(*IP)(nil), // 11: api.IP
(*IP)(nil), // 10: api.IP
(*IPPrefix)(nil), // 11: api.IPPrefix
(*IPPort)(nil), // 12: api.IPPort
(*emptypb.Empty)(nil), // 13: google.protobuf.Empty
}
var file_internal_machine_api_pb_machine_proto_depIdxs = []int32{
1, // 0: api.MachineInfo.network:type_name -> api.NetworkConfig
10, // 1: api.NetworkConfig.subnet:type_name -> api.IPPrefix
11, // 2: api.NetworkConfig.management_ip:type_name -> api.IP
12, // 3: api.NetworkConfig.endpoints:type_name -> api.IPPort
10, // 4: api.InitClusterRequest.network:type_name -> api.IPPrefix
0, // 5: api.InitClusterResponse.machine:type_name -> api.MachineInfo
0, // 6: api.JoinClusterRequest.machine:type_name -> api.MachineInfo
0, // 7: api.JoinClusterRequest.other_machines:type_name -> api.MachineInfo
9, // 8: api.Service.containers:type_name -> api.Service.Container
6, // 9: api.InspectServiceResponse.service:type_name -> api.Service
2, // 10: api.Machine.InitCluster:input_type -> api.InitClusterRequest
4, // 11: api.Machine.JoinCluster:input_type -> api.JoinClusterRequest
13, // 12: api.Machine.Token:input_type -> google.protobuf.Empty
13, // 13: api.Machine.Inspect:input_type -> google.protobuf.Empty
7, // 14: api.Machine.InspectService:input_type -> api.InspectServiceRequest
3, // 15: api.Machine.InitCluster:output_type -> api.InitClusterResponse
13, // 16: api.Machine.JoinCluster:output_type -> google.protobuf.Empty
5, // 17: api.Machine.Token:output_type -> api.TokenResponse
0, // 18: api.Machine.Inspect:output_type -> api.MachineInfo
8, // 19: api.Machine.InspectService:output_type -> api.InspectServiceResponse
15, // [15:20] is the sub-list for method output_type
10, // [10:15] is the sub-list for method input_type
10, // [10:10] is the sub-list for extension type_name
10, // [10:10] is the sub-list for extension extendee
0, // [0:10] is the sub-list for field type_name
10, // 1: api.MachineInfo.public_ip:type_name -> api.IP
11, // 2: api.NetworkConfig.subnet:type_name -> api.IPPrefix
10, // 3: api.NetworkConfig.management_ip:type_name -> api.IP
12, // 4: api.NetworkConfig.endpoints:type_name -> api.IPPort
11, // 5: api.InitClusterRequest.network:type_name -> api.IPPrefix
10, // 6: api.InitClusterRequest.public_ip:type_name -> api.IP
0, // 7: api.InitClusterResponse.machine:type_name -> api.MachineInfo
0, // 8: api.JoinClusterRequest.machine:type_name -> api.MachineInfo
0, // 9: api.JoinClusterRequest.other_machines:type_name -> api.MachineInfo
9, // 10: api.Service.containers:type_name -> api.Service.Container
6, // 11: api.InspectServiceResponse.service:type_name -> api.Service
2, // 12: api.Machine.InitCluster:input_type -> api.InitClusterRequest
4, // 13: api.Machine.JoinCluster:input_type -> api.JoinClusterRequest
13, // 14: api.Machine.Token:input_type -> google.protobuf.Empty
13, // 15: api.Machine.Inspect:input_type -> google.protobuf.Empty
7, // 16: api.Machine.InspectService:input_type -> api.InspectServiceRequest
3, // 17: api.Machine.InitCluster:output_type -> api.InitClusterResponse
13, // 18: api.Machine.JoinCluster:output_type -> google.protobuf.Empty
5, // 19: api.Machine.Token:output_type -> api.TokenResponse
0, // 20: api.Machine.Inspect:output_type -> api.MachineInfo
8, // 21: api.Machine.InspectService:output_type -> api.InspectServiceResponse
17, // [17:22] is the sub-list for method output_type
12, // [12:17] is the sub-list for method input_type
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() }
@@ -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{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
+7 -1
View File
@@ -19,18 +19,24 @@ message MachineInfo {
string id = 1;
string name = 2;
NetworkConfig network = 3;
IP public_ip = 4;
}
message NetworkConfig {
IPPrefix subnet = 1;
IP management_ip = 2;
repeated IPPort endpoints = 3;
bytes publicKey = 4;
bytes public_key = 4;
}
message InitClusterRequest {
string machineName = 1;
IPPrefix network = 2;
oneof public_ip_config {
IP public_ip = 3;
bool public_ip_auto = 4;
}
}
message InitClusterResponse {
+163
View File
@@ -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),
},
}
}
+365
View File
@@ -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,
},
},
},
}
}
+18 -112
View File
@@ -3,34 +3,30 @@ package caddyfile
import (
"context"
"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"
"net"
"os"
"path/filepath"
"strconv"
"uncloud/internal/api"
"uncloud/internal/fs"
"uncloud/internal/machine/docker"
"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
// proxy. The generated Caddyfile allows Caddy to route external traffic to service containers across the internal
// network.
type Controller struct {
store *store.Store
path string
store *store.Store
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)
if err := os.MkdirAll(dir, 0750); err != nil {
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{
store: store,
path: path,
store: store,
path: path,
verifyResponse: verifyResponse,
}, 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.
// TODO: implement machine membership check using Corrossion Admin client.
func (c *Controller) filterAvailableContainers(containerRecords []*store.ContainerRecord) ([]*api.Container, error) {
containers := make([]*api.Container, len(containerRecords))
func (c *Controller) filterAvailableContainers(containerRecords []store.ContainerRecord) ([]api.Container, error) {
containers := make([]api.Container, len(containerRecords))
for i, cr := range containerRecords {
containers[i] = cr.Container
}
return containers, nil
}
func (c *Controller) generateConfig(containers []*api.Container) 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(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)
func (c *Controller) generateConfig(containers []api.Container) error {
config, err := GenerateConfig(containers, c.verifyResponse)
if err != nil {
return err
}
configBytes, err := json.MarshalIndent(config, "", " ")
@@ -183,31 +117,3 @@ func (c *Controller) generateConfig(containers []*api.Container) error {
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
}
+17 -24
View File
@@ -110,6 +110,15 @@ func (c *Cluster) AddMachine(ctx context.Context, req *pb.AddMachineRequest) (*p
if len(req.Network.Endpoints) == 0 {
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)
if err != nil {
@@ -174,6 +183,7 @@ func (c *Cluster) AddMachine(ctx context.Context, req *pb.AddMachineRequest) (*p
Endpoints: req.Network.Endpoints,
PublicKey: req.Network.PublicKey,
},
PublicIp: req.PublicIp,
}
// 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.
@@ -210,9 +220,13 @@ func (c *Cluster) ListMachines(ctx context.Context, _ *emptypb.Empty) (*pb.ListM
state := pb.MachineMember_DOWN
addr, _ := m.Network.ManagementIp.ToAddr()
for _, s := range states {
if s.Addr.Addr().Compare(addr) == 0 &&
(s.State == corrosion.MembershipStateAlive || s.State == corrosion.MembershipStateSuspect) {
state = pb.MachineMember_UP
if s.Addr.Addr().Compare(addr) == 0 {
switch s.State {
case corrosion.MembershipStateAlive:
state = pb.MachineMember_UP
case corrosion.MembershipStateSuspect:
state = pb.MachineMember_SUSPECT
}
break
}
}
@@ -228,24 +242,3 @@ func (c *Cluster) ListMachines(ctx context.Context, _ *emptypb.Empty) (*pb.ListM
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
//}
+159
View File
@@ -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
}
+42 -1
View File
@@ -86,6 +86,26 @@ func (c *Client) CreateContainer(
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.
func (c *Client) StartContainer(ctx context.Context, id string, opts container.StartOptions) error {
optsBytes, err := json.Marshal(opts)
@@ -107,9 +127,30 @@ func (c *Client) StartContainer(ctx context.Context, id string, opts container.S
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 {
Metadata *pb.Metadata
Containers []types.Container
Containers []types.ContainerJSON
}
func (c *Client) ListContainers(ctx context.Context, opts container.ListOptions) ([]MachineContainers, error) {
+15 -5
View File
@@ -149,8 +149,9 @@ func (m *Manager) syncContainersToStore(ctx context.Context) error {
if err != nil {
return fmt.Errorf("list containers from store: %w", err)
}
// 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.Arg("label", api.LabelServiceID),
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)
}
// 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
for _, sc := range storeContainers {
found := false
for i, _ := range containers {
for i := range containers {
if containers[i].ID == sc.Container.ID {
found = true
break
@@ -184,8 +195,7 @@ func (m *Manager) syncContainersToStore(ctx context.Context) error {
}
// Create or update the current Docker containers in the store.
for _, dc := range containers {
c := &api.Container{Container: dc}
for _, c := range containers {
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))
}
+1 -1
View File
@@ -9,6 +9,6 @@ import (
)
// 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")
}
+5 -5
View File
@@ -14,10 +14,10 @@ import (
// EnsureUncloudNetwork creates the Docker bridge network NetworkName with the provided machine subnet
// if it doesn't exist. If the network exists but has a different subnet, it removes and recreates the network.
// It also configures iptables to allow container access from the WireGuard network.
func (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.
needsCreation := false
nw, err := d.client.NetworkInspect(ctx, NetworkName, dnetwork.InspectOptions{})
nw, err := m.client.NetworkInspect(ctx, NetworkName, dnetwork.InspectOptions{})
if err != nil {
if !client.IsErrNotFound(err) {
return fmt.Errorf("inspect Docker network %q: %w", NetworkName, err)
@@ -29,7 +29,7 @@ func (d *Manager) EnsureUncloudNetwork(ctx context.Context, subnet netip.Prefix)
slog.Info(
"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.
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 _, err = d.client.NetworkCreate(
if _, err = m.client.NetworkCreate(
ctx, NetworkName, dnetwork.CreateOptions{
Driver: "bridge",
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())
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)
}
}
+56 -10
View File
@@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"errors"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters"
"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)
if err != nil {
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)
@@ -66,6 +67,24 @@ func (s *Server) CreateContainer(ctx context.Context, req *pb.CreateContainerReq
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.
func (s *Server) StartContainer(ctx context.Context, req *pb.StartContainerRequest) (*emptypb.Empty, error) {
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 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
@@ -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 {
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)
@@ -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 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
@@ -158,7 +204,7 @@ func (s *Server) PullImage(req *pb.PullImageRequest, stream grpc.ServerStreaming
respBody, err := s.client.ImagePull(ctx, req.Image, opts)
if err != nil {
return status.Errorf(codes.Internal, "pull image: %v", err)
return status.Errorf(codes.Internal, err.Error())
}
defer respBody.Close()
@@ -189,7 +235,7 @@ func (s *Server) PullImage(req *pb.PullImageRequest, stream grpc.ServerStreaming
case err = <-errCh:
return err
case <-ctx.Done():
return status.Errorf(codes.Canceled, "pull image: %v", ctx.Err())
return status.Errorf(codes.Canceled, ctx.Err().Error())
}
}
}
+13 -5
View File
@@ -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 {
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 {
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.
if err == nil {
if pubIPErr == nil {
ips = append(ips, publicIP)
}
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,
},
}
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)
if err != nil {
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)
}
token := NewToken(m.state.Network.PublicKey, endpoints)
token := NewToken(m.state.Network.PublicKey, publicIP, endpoints)
tokenStr, err := token.String()
if err != nil {
return nil, status.Error(codes.Internal, err.Error())
@@ -703,7 +711,7 @@ func (m *Machine) Inspect(_ context.Context, _ *emptypb.Empty) (*pb.MachineInfo,
}, 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(
ctx context.Context, req *pb.InspectServiceRequest,
) (*pb.InspectServiceResponse, error) {
+15 -12
View File
@@ -22,7 +22,7 @@ const (
)
type ContainerRecord struct {
Container *api.Container
Container api.Container
MachineID string
SyncStatus string
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.
// 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 {
cJSON, err := json.Marshal(c)
func (s *Store) CreateOrUpdateContainer(ctx context.Context, ctr api.Container, machineID string) error {
// 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 {
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
WHERE containers.container != excluded.container
OR containers.machine_id != excluded.machine_id`,
c.ID, string(cJSON), machineID, SyncStatusSynced)
ctr.ID, string(cJSON), machineID, SyncStatusSynced)
if err != nil {
return fmt.Errorf("upsert query: %w", err)
}
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
}
// 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").
Where(sq.Eq{"sync_status": SyncStatusSynced})
@@ -105,7 +108,7 @@ func (s *Store) ListContainers(ctx context.Context, opts ListOptions) ([]*Contai
}
defer rows.Close()
var containers []*ContainerRecord
var containers []ContainerRecord
var cJSON, machineID, syncStatus, updatedAtStr string
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 {
return nil, fmt.Errorf("parse updated_at: %w", err)
}
containers = append(containers, &ContainerRecord{
Container: &c,
containers = append(containers, ContainerRecord{
Container: c,
MachineID: machineID,
SyncStatus: syncStatus,
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
// 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.
q := sq.Select("container", "machine_id", "sync_status", "updated_at").From("containers").
Where(sq.Eq{"sync_status": SyncStatusSynced})
@@ -172,7 +175,7 @@ func (s *Store) SubscribeContainers(ctx context.Context) ([]*ContainerRecord, <-
return nil, nil, err
}
var containers []*ContainerRecord
var containers []ContainerRecord
var cJSON, updatedAtStr string
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 {
return nil, nil, fmt.Errorf("parse updated_at: %w", err)
}
containers = append(containers, &cr)
containers = append(containers, cr)
}
events, err := sub.Changes()
if err != nil {
+2 -2
View File
@@ -22,8 +22,8 @@ CREATE TABLE containers
-- container is a JSON-serialized api.Container struct.
container TEXT NOT NULL DEFAULT '{}' CHECK (json_valid(container)),
machine_id TEXT NOT NULL DEFAULT '',
service_id TEXT AS (json_extract(container, '$.Labels."uncloud.service.id"')),
service_name TEXT AS (json_extract(container, '$.Labels."uncloud.service.name"')),
service_id TEXT AS (json_extract(container, '$.Config.Labels."uncloud.service.id"')),
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 TEXT NOT NULL DEFAULT '',
-- updated_at is the last time the record was updated.
+9 -1
View File
@@ -49,6 +49,11 @@ func (s *Store) Put(ctx context.Context, key string, value any) error {
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 {
mJSON, err := protojson.Marshal(m)
if err != nil {
@@ -74,10 +79,13 @@ func (s *Store) ListMachines(ctx context.Context) ([]*pb.MachineInfo, error) {
if err = rows.Scan(&mJSON); err != nil {
return nil, err
}
protojsonParser := protojson.UnmarshalOptions{DiscardUnknown: true}
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)
}
if err = m.Network.Validate(); err != nil {
slog.Error("Invalid network configuration for machine in store", "id", m.Id, "err", err)
continue
+3 -1
View File
@@ -16,13 +16,15 @@ const (
// Token represents the machine's token for joining a cluster.
type Token struct {
PublicKey secret.Secret
PublicIP netip.Addr
Endpoints []netip.AddrPort
}
// 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{
PublicKey: publicKey,
PublicIP: publicIP,
Endpoints: endpoints,
}
}
+6 -2
View File
@@ -2,12 +2,13 @@ package sshexec
import (
"fmt"
"golang.org/x/crypto/ssh"
"golang.org/x/crypto/ssh/agent"
"net"
"os"
"strconv"
"time"
"golang.org/x/crypto/ssh"
"golang.org/x/crypto/ssh/agent"
)
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)
if err != nil {
return nil, err
}
config := &ssh.ClientConfig{
User: user,
Auth: []ssh.AuthMethod{keyAuth},
+1 -1
View File
@@ -89,7 +89,7 @@ func (p *Provisioner) initCluster(ctx context.Context, machines []Machine) error
// Init a new cluster on the first machine.
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)
}
+6 -6
View File
@@ -112,18 +112,18 @@ install_uncloud_binaries() {
local uncloudd_url
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
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
local uncloudd_download_path="${tmp_dir}/uncloudd.gz"
local uncloudd_download_path="${tmp_dir}/uncloudd.tar.gz"
log "⏳ Downloading uncloudd binary: ${uncloudd_url}"
if ! curl -fsSL -o "${uncloudd_download_path}" "${uncloudd_url}"; then
error "Failed to download uncloudd binary."
fi
gzip -d "${uncloudd_download_path}"
if ! install "${uncloudd_download_path%.gz}" "${uncloudd_install_path}"; then
tar -xf "${uncloudd_download_path}"
if ! install ./uncloudd "${uncloudd_install_path}"; then
error "Failed to install uncloud binary to ${uncloudd_install_path}"
fi
log "✓ uncloudd binary installed: ${uncloudd_install_path}"
@@ -196,7 +196,7 @@ install_corrosion() {
if ! curl -fsSL -o "${corrosion_download_path}" "${corrosion_url}"; then
error "Failed to download uncloud-corrosion binary."
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
error "Failed to install uncloud-corrosion binary to ${corrosion_install_path}"
fi
+23 -24
View File
@@ -8,25 +8,25 @@ INSTALL_DIR=${INSTALL_DIR:-/usr/local/bin}
VERSION=${VERSION:-latest}
print_manual_install() {
# TODO: review
echo "You can manually install uncloud by:"
echo "1. Opening $RELEASES_URL"
echo "2. Downloading uncloud_*_${OS}_${ARCH}.tar.gz for your platform"
echo "3. Verifying the checksum from checksums.txt"
echo "4. Extracting the archive: tar xzf uncloud_*_${OS}_${ARCH}.tar.gz"
echo "5. Installing the binary: sudo install -m 755 uncloud /usr/local/bin/"
echo "6. Creating symlink: sudo ln -sf /usr/local/bin/uncloud /usr/local/bin/uc"
RELEASES_URL="https://github.com/${GITHUB_REPO}/releases/${VERSION}"
echo "Failed while attempting to install uncloud CLI. You can install it manually:"
echo " 1. Open your web browser and go to ${RELEASES_URL}"
echo " 2. Download uncloud_<OS>_<ARCH>.tar.gz for your platform (OS: linux/macos, ARCH: amd64/arm64)."
echo " 3. Extract the 'uncloud' binary from the archive: tar -xvf uncloud_*.tar.gz"
echo " 4. Install the binary to /usr/local/bin: sudo install ./uncloud ${INSTALL_DIR}/uncloud"
echo " 5. Optionally create a 'uc' symlink: sudo ln -sf ${INSTALL_DIR}/uncloud ${INSTALL_DIR}/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"
version=$(curl -fsSL "$api_url" | grep -o '"tag_name": "[^"]*' | cut -d'"' -f4)
if [ -z "$version" ]; then
VERSION=$(curl -fsSL "$api_url" | grep -o '"tag_name": "[^"]*' | cut -d'"' -f4)
if [ -z "$VERSION" ]; then
echo "Failed to fetch the latest version from GitHub."
print_manual_install
exit 1
fi
echo "$version"
}
# 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.
if [ "$VERSION" = "latest" ]; then
VERSION=$(latest_version)
fetch_latest_version
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}"
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"
echo "Download complete."
# TODO: fix name_template in goreleaser config.
#echo "Verifying checksum..."
echo "Verifying checksum..."
cd "$TMP_DIR"
#if ! sha256sum --check --ignore-missing "checksums.txt"; then
# echo "Checksum verification failed."
# print_manual_install
# exit 1
#fi
#echo "Checksum is valid."
if ! sha256sum --check --ignore-missing "checksums.txt"; then
echo "Checksum verification failed."
print_manual_install
exit 1
fi
echo "Checksum is valid."
# Decompress and install the binary.
gzip -d "${BINARY_NAME}"
tar -xf "${BINARY_NAME}"
if [ -z "${SUDO}" ]; then
echo "Installing uncloud binary to ${INSTALL_DIR}"
else
echo "Installing uncloud binary to ${INSTALL_DIR} using sudo. You may be prompted for your password."
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}"
print_manual_install
exit 1
+813 -24
View File
@@ -2,15 +2,642 @@ package e2e
import (
"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/require"
"net/netip"
"strings"
"testing"
"uncloud/internal/api"
"uncloud/internal/cli/client"
"uncloud/internal/machine/api/pb"
machinedocker "uncloud/internal/machine/docker"
"uncloud/internal/secret"
"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) {
t.Parallel()
@@ -21,13 +648,186 @@ func TestRunService(t *testing.T) {
cli, err := c.Machines[0].Connect(ctx)
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.Parallel()
name := "busybox-1-replica"
name := "1-replica"
t.Cleanup(func() {
err := cli.RemoveService(ctx, name)
if !dockerclient.IsErrNotFound(err) {
if !errors.Is(err, client.ErrNotFound) {
require.NoError(t, err)
}
@@ -39,15 +839,13 @@ func TestRunService(t *testing.T) {
Name: name,
Mode: api.ServiceModeReplicated,
Container: api.ContainerSpec{
Command: []string{"sleep", "infinity"},
Image: "busybox:latest",
Image: "portainer/pause:latest",
},
})
}, nil)
require.NoError(t, err)
assert.NotEmpty(t, resp.ID)
assert.Equal(t, name, resp.Name)
assert.Len(t, resp.Containers, 1)
svc, err := cli.InspectService(ctx, name)
require.NoError(t, err)
@@ -76,23 +874,19 @@ func TestRunService(t *testing.T) {
t.Run("1 replica with ports", func(t *testing.T) {
t.Parallel()
name := "busybox-1-replica-ports"
name := "1-replica-ports"
t.Cleanup(func() {
err := cli.RemoveService(ctx, name)
if !dockerclient.IsErrNotFound(err) {
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.ServiceModeReplicated,
Container: api.ContainerSpec{
Command: []string{"sleep", "infinity"},
Image: "busybox:latest",
Image: "portainer/pause:latest",
},
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)
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.Parallel()
name := "busybox-global"
name := "global"
t.Cleanup(func() {
err := cli.RemoveService(ctx, name)
if !dockerclient.IsErrNotFound(err) {
if !errors.Is(err, client.ErrNotFound) {
require.NoError(t, err)
}
_, err = cli.InspectService(ctx, name)
require.ErrorIs(t, err, client.ErrNotFound)
})
resp, err := cli.RunService(ctx, api.ServiceSpec{
Name: name,
Mode: api.ServiceModeGlobal,
Container: api.ContainerSpec{
Command: []string{"sleep", "infinity"},
Image: "busybox:latest",
Image: "portainer/pause:latest",
},
})
}, nil)
require.NoError(t, err)
assert.NotEmpty(t, resp.ID)
assert.Equal(t, name, resp.Name)
assert.Len(t, resp.Containers, 3, "expected 1 container on each machine")
svc, err := cli.InspectService(ctx, name)
require.NoError(t, err)
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

+23 -5
View File
@@ -3,12 +3,30 @@
<head>
<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">
<link href="./style.css" rel="stylesheet">
<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="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>
<body class="font-inter antialiased bg-white text-zinc-900 tracking-tight">
@@ -45,7 +63,7 @@
</li>
<li class="ml-1">
<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>
</ul>
</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>
<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>
<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 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> </pre>
@@ -505,7 +523,7 @@
</section>
<!-- CTA -->
<section>
<section id="subscribe">
<div class="py-12 md:py-20">
<div class="max-w-6xl mx-auto px-4 sm:px-6">
<div class="relative max-w-3xl mx-auto text-center">
+9 -1198
View File
File diff suppressed because it is too large Load Diff