mirror of
https://github.com/psviderski/uncloud.git
synced 2026-08-27 11:33:33 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bee101aa3e | ||
|
|
21e70dd846 | ||
|
|
91f870989a | ||
|
|
bda1123ae6 | ||
|
|
190d0de61f | ||
|
|
f2e730796f | ||
|
|
bb77ff9246 | ||
|
|
5061097dff | ||
|
|
911522c388 | ||
|
|
3ee7cb6e8e | ||
|
|
2e00632959 | ||
|
|
8e53fd46fa | ||
|
|
2835edadeb | ||
|
|
f7c4490c10 | ||
|
|
b3102480c0 | ||
|
|
cbe7c563cd | ||
|
|
b016252821 | ||
|
|
6c197085d0 | ||
|
|
c29fa571e1 | ||
|
|
092fe254aa | ||
|
|
6951221b88 | ||
|
|
fa54584128 | ||
|
|
d922f7bfed | ||
|
|
18ec186e81 | ||
|
|
2a898b3eee | ||
|
|
042b44cbb6 | ||
|
|
3f7726e5fb | ||
|
|
2b3f325075 | ||
|
|
be2a26e626 | ||
|
|
4479490db2 | ||
|
|
7556f8cb12 | ||
|
|
bfbcec34e8 | ||
|
|
8eea34d8dc | ||
|
|
6b80ef9db8 | ||
|
|
2134f3c1d9 | ||
|
|
0652123bc7 | ||
|
|
4de7ecd747 | ||
|
|
e8908ced9e | ||
|
|
deedbcb8a9 | ||
|
|
8bfb3b80ed | ||
|
|
848abbd2f8 | ||
|
|
9cb4b35546 | ||
|
|
26df6740e3 | ||
|
|
537789902b | ||
|
|
4088fdfd84 | ||
|
|
01a71b4b38 | ||
|
|
73d41c75b3 | ||
|
|
f85b5dd01d | ||
|
|
1fb8a3d7a0 | ||
|
|
3ce9c8be04 | ||
|
|
3ac166e6cb | ||
|
|
88ce2d5181 | ||
|
|
8bddbec7bd | ||
|
|
a40fd613e8 | ||
|
|
99c9aabdfd | ||
|
|
5504951a8a | ||
|
|
2727c3c97e | ||
|
|
7d47d274a0 | ||
|
|
a4247b0097 | ||
|
|
11612a802c | ||
|
|
d7bad5ed46 | ||
|
|
c0dff56378 | ||
|
|
00dbbec62b | ||
|
|
9755a6973d | ||
|
|
16120b80ba | ||
|
|
386e4e25f3 | ||
|
|
43ddb5c363 | ||
|
|
417427b4d5 | ||
|
|
5da1fc5387 | ||
|
|
5621a84e99 | ||
|
|
8db7ac84a0 | ||
|
|
77890b8a8a | ||
|
|
daada3bbe9 | ||
|
|
21d55dc69a | ||
|
|
6eb373e4de | ||
|
|
6a4a7f2e6a | ||
|
|
349dd0d2ef | ||
|
|
cf5818d963 | ||
|
|
faf3378659 | ||
|
|
bb967461d0 | ||
|
|
0b5dd2f7ba | ||
|
|
d5f0bca4a0 | ||
|
|
bdd034d169 | ||
|
|
48767125f4 | ||
|
|
035f44896d | ||
|
|
3d5fe0a824 | ||
|
|
6d1c3aa5b2 | ||
|
|
cd60d5178f | ||
|
|
7aa7a9fea1 | ||
|
|
73785007cb | ||
|
|
24a824b890 | ||
|
|
085ee285e8 | ||
|
|
6daa1291b5 | ||
|
|
c3d3a4242e | ||
|
|
871fd8c035 | ||
|
|
a6ccd6055a | ||
|
|
ca607ee82d | ||
|
|
1c8e60aac0 | ||
|
|
d02fc4d981 | ||
|
|
410f74425c | ||
|
|
6e0df53f13 | ||
|
|
e9d07687f2 | ||
|
|
95175d10c5 | ||
|
|
df25b0d098 | ||
|
|
5c1a3dd694 | ||
|
|
6d39174b51 | ||
|
|
e851f25b12 | ||
|
|
3c1d2201ce | ||
|
|
b4c2ec3a2a | ||
|
|
bf10b21da5 | ||
|
|
2819ba63aa | ||
|
|
54a8b09c9f | ||
|
|
24371eb401 | ||
|
|
9c9e143dc8 | ||
|
|
19d53c4f67 | ||
|
|
40f929847c | ||
|
|
e73e38957a | ||
|
|
e82b78df49 | ||
|
|
741378d0b7 | ||
|
|
d31595820d | ||
|
|
035601d366 | ||
|
|
5c96f75958 | ||
|
|
2c62e1f6df | ||
|
|
22bca16226 | ||
|
|
a4be45186d | ||
|
|
5e79697505 | ||
|
|
6736839ccb | ||
|
|
913fb2da0c | ||
|
|
bd0e7f44b2 | ||
|
|
81cf5f5605 | ||
|
|
cedafd069d | ||
|
|
93dfa5ca9d | ||
|
|
c70c8a6ec1 | ||
|
|
5c74b09f45 | ||
|
|
1316dd3ffc | ||
|
|
4493bb3c27 | ||
|
|
b23699470b | ||
|
|
34316c77cf | ||
|
|
c2b466b046 | ||
|
|
d36429e6e5 | ||
|
|
17b96a8744 | ||
|
|
a54666a7ac | ||
|
|
5cb765d286 | ||
|
|
5bf3cba2d6 | ||
|
|
8a3f0032ae | ||
|
|
5fe77c688b | ||
|
|
61dc97228d | ||
|
|
e6df331fb8 | ||
|
|
9dbcf1e902 | ||
|
|
2f6c988125 | ||
|
|
411c3398ce | ||
|
|
3a644771ca | ||
|
|
2eff7d1e22 | ||
|
|
f06dd865fa | ||
|
|
ee580df6af | ||
|
|
2c0cf28509 | ||
|
|
7b98ed34df | ||
|
|
5094149406 | ||
|
|
2fd1ee203a | ||
|
|
336bdf9f4b | ||
|
|
c80c75e4e1 | ||
|
|
e5bda92ec6 | ||
|
|
fd3ad91136 | ||
|
|
94186b2ae7 | ||
|
|
1bcb8836ef | ||
|
|
840943b3b0 | ||
|
|
518eae9bd2 | ||
|
|
06a4001080 | ||
|
|
b73577e1a0 | ||
|
|
c3ba43a8fa | ||
|
|
c275273617 | ||
|
|
b688742e5a | ||
|
|
4ec87db8e8 | ||
|
|
c6358474ac | ||
|
|
8fbf406007 | ||
|
|
e123f7214e | ||
|
|
3287691a31 | ||
|
|
6f9fc69c2e | ||
|
|
0effba0adb | ||
|
|
667ecadd30 | ||
|
|
68845e7607 | ||
|
|
6d4698fc1d | ||
|
|
708e14a4e0 | ||
|
|
c80de16ebe | ||
|
|
3e9718f08c | ||
|
|
69c9167355 | ||
|
|
b25938acc1 | ||
|
|
1ec3376be5 | ||
|
|
aa236aac10 | ||
|
|
9fcc319c3a | ||
|
|
2f7463ebe3 | ||
|
|
498d35e866 | ||
|
|
ba67b96734 |
@@ -0,0 +1,11 @@
|
|||||||
|
# Ignore everything and use an allowlist of what is allowed to not package any secrets by accident.
|
||||||
|
*
|
||||||
|
|
||||||
|
# Allow files and directories.
|
||||||
|
!cmd/
|
||||||
|
!internal/
|
||||||
|
!scripts/docker/
|
||||||
|
!go.*
|
||||||
|
|
||||||
|
# Ignore unnecessary files inside allowed directories.
|
||||||
|
**/.DS_Store
|
||||||
@@ -8,6 +8,9 @@
|
|||||||
*.so
|
*.so
|
||||||
*.dylib
|
*.dylib
|
||||||
|
|
||||||
|
# OS X
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
# Test binary, built with `go test -c`
|
# Test binary, built with `go test -c`
|
||||||
*.test
|
*.test
|
||||||
|
|
||||||
@@ -26,3 +29,6 @@ go.work.sum
|
|||||||
|
|
||||||
data
|
data
|
||||||
dist/
|
dist/
|
||||||
|
|
||||||
|
# Web
|
||||||
|
node_modules/
|
||||||
|
|||||||
+1
-1
@@ -45,7 +45,7 @@ archives:
|
|||||||
{{ .Binary }}_
|
{{ .Binary }}_
|
||||||
{{- if eq .Os "darwin"}}macos
|
{{- if eq .Os "darwin"}}macos
|
||||||
{{- else}}{{.Os}}{{end}}_
|
{{- else}}{{.Os}}{{end}}_
|
||||||
{{- .Arch }}"
|
{{- .Arch }}
|
||||||
# use zip for windows archives
|
# use zip for windows archives
|
||||||
format_overrides:
|
format_overrides:
|
||||||
- goos: windows
|
- goos: windows
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
[tools]
|
[tools]
|
||||||
go = "1.22"
|
go = "1.23"
|
||||||
|
|
||||||
[env]
|
[env]
|
||||||
_.file = ".env"
|
_.file = ".env"
|
||||||
|
|||||||
+51
@@ -0,0 +1,51 @@
|
|||||||
|
ARG ALPINE_VERSION=3.20.3
|
||||||
|
|
||||||
|
FROM golang:1.23.2-alpine AS uncloudd
|
||||||
|
|
||||||
|
ARG TARGETOS
|
||||||
|
ARG TARGETARCH
|
||||||
|
|
||||||
|
ENV CGO_ENABLED=0
|
||||||
|
|
||||||
|
WORKDIR /build
|
||||||
|
# Download and cache dependencies and only redownload them in subsequent builds if they change.
|
||||||
|
COPY go.mod go.sum ./
|
||||||
|
RUN go mod download && go mod verify
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
RUN GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -o uncloudd cmd/uncloudd/main.go
|
||||||
|
|
||||||
|
|
||||||
|
FROM alpine:${ALPINE_VERSION} AS corrosion-download
|
||||||
|
RUN wget -q -O /tmp/corrosion.tar.gz \
|
||||||
|
https://github.com/psviderski/corrosion/releases/latest/download/corrosion-aarch64-unknown-linux-gnu.tar.gz \
|
||||||
|
&& tar -xzf /tmp/corrosion.tar.gz -C /tmp \
|
||||||
|
&& install /tmp/corrosion /usr/local/bin/corrosion \
|
||||||
|
&& rm /tmp/corrosion.tar.gz /tmp/corrosion
|
||||||
|
|
||||||
|
FROM chainguard/wolfi-base:latest AS corrosion
|
||||||
|
COPY --from=corrosion-download /usr/local/bin/corrosion /usr/local/bin/corrosion
|
||||||
|
CMD ["corrosion", "agent"]
|
||||||
|
|
||||||
|
|
||||||
|
FROM alpine:${ALPINE_VERSION} AS corrosion-image-tarball
|
||||||
|
ARG CORROSION_IMAGE="ghcr.io/psviderski/corrosion:latest"
|
||||||
|
|
||||||
|
RUN apk --no-cache add crane
|
||||||
|
RUN crane pull "${CORROSION_IMAGE}" /corrosion.tar
|
||||||
|
|
||||||
|
|
||||||
|
# Uncloud-in-Docker (ucind) image for running Uncloud test clusters using Docker.
|
||||||
|
FROM docker:27.3.1-dind AS ucind
|
||||||
|
# Create system group and user 'uncloud'.
|
||||||
|
RUN addgroup -S uncloud && adduser -SHD -h /nonexistent -G uncloud -g "" uncloud
|
||||||
|
RUN apk --no-cache add \
|
||||||
|
socat \
|
||||||
|
wireguard-tools
|
||||||
|
|
||||||
|
COPY --from=corrosion-image-tarball /corrosion.tar /images/corrosion.tar
|
||||||
|
COPY scripts/docker/dind scripts/docker/entrypoint.sh /usr/local/bin/
|
||||||
|
COPY --from=uncloudd /build/uncloudd /usr/local/bin/
|
||||||
|
|
||||||
|
ENTRYPOINT ["entrypoint.sh"]
|
||||||
|
CMD ["uncloudd"]
|
||||||
@@ -1,16 +1,44 @@
|
|||||||
.PHONY: build
|
CORROSION_IMAGE ?= ghcr.io/psviderski/corrosion:latest
|
||||||
uncloud-dev:
|
UCIND_IMAGE ?= ghcr.io/psviderski/ucind:latest
|
||||||
|
|
||||||
|
update-dev:
|
||||||
GOOS=linux GOARCH=amd64 go build -o uncloudd-linux-amd64 ./cmd/uncloudd && \
|
GOOS=linux GOARCH=amd64 go build -o uncloudd-linux-amd64 ./cmd/uncloudd && \
|
||||||
scp uncloudd-linux-amd64 spy@192.168.40.243:~/ && \
|
scp uncloudd-linux-amd64 spy@192.168.40.243:~/ && \
|
||||||
ssh spy@192.168.40.243 sudo install ./uncloudd-linux-amd64 /usr/local/bin/uncloudd
|
ssh spy@192.168.40.243 sudo install ./uncloudd-linux-amd64 /usr/local/bin/uncloudd
|
||||||
scp uncloudd-linux-amd64 spy@192.168.40.176:~/ && \
|
scp uncloudd-linux-amd64 spy@192.168.40.176:~/ && \
|
||||||
ssh spy@192.168.40.176 sudo install ./uncloudd-linux-amd64 /usr/local/bin/uncloudd
|
ssh spy@192.168.40.176 sudo install ./uncloudd-linux-amd64 /usr/local/bin/uncloudd && \
|
||||||
GOOS=linux GOARCH=amd64 go build -o uncloud-linux-amd64 ./cmd/uncloud && \
|
rm uncloudd-linux-amd64
|
||||||
scp uncloud-linux-amd64 spy@192.168.40.243:~/ && \
|
|
||||||
ssh spy@192.168.40.243 sudo install ./uncloud-linux-amd64 /usr/local/bin/uncloud
|
update-restart-dev:
|
||||||
scp uncloud-linux-amd64 spy@192.168.40.176:~/ && \
|
GOOS=linux GOARCH=amd64 go build -o uncloudd-linux-amd64 ./cmd/uncloudd && \
|
||||||
ssh spy@192.168.40.176 sudo install ./uncloud-linux-amd64 /usr/local/bin/uncloud
|
scp uncloudd-linux-amd64 spy@192.168.40.243:~/ && \
|
||||||
|
ssh spy@192.168.40.243 "sudo install ./uncloudd-linux-amd64 /usr/local/bin/uncloudd && sudo systemctl restart uncloud" && \
|
||||||
|
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
|
||||||
|
|
||||||
|
reset-dev:
|
||||||
|
ssh spy@192.168.40.243 "sudo systemctl stop uncloud && sudo rm -rf /var/lib/uncloud"
|
||||||
|
ssh spy@192.168.40.176 "sudo systemctl stop uncloud && sudo rm -rf /var/lib/uncloud"
|
||||||
|
ssh ubuntu@152.67.101.197 "sudo systemctl stop uncloud && sudo rm -rf /var/lib/uncloud"
|
||||||
|
|
||||||
.PHONY: proto
|
.PHONY: proto
|
||||||
proto:
|
proto:
|
||||||
protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative internal/machine/api/pb/*.proto
|
protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative \
|
||||||
|
--proto_path=. --proto_path=internal/machine/api/vendor internal/machine/api/pb/*.proto
|
||||||
|
|
||||||
|
.PHONY: corrosion-image
|
||||||
|
corrosion-image:
|
||||||
|
docker build -t "$(CORROSION_IMAGE)" --target corrosion .
|
||||||
|
|
||||||
|
.PHONY: corrosion-image-push
|
||||||
|
corrosion-image-push: corrosion-image
|
||||||
|
docker push "$(CORROSION_IMAGE)"
|
||||||
|
|
||||||
|
.PHONY: ucind-image
|
||||||
|
ucind-image:
|
||||||
|
docker build -t "$(UCIND_IMAGE)" --target ucind .
|
||||||
|
|
||||||
|
.PHONY: ucind-image-push
|
||||||
|
ucind-image-push: ucind-image
|
||||||
|
docker push "$(UCIND_IMAGE)"
|
||||||
|
|||||||
@@ -1,9 +1,44 @@
|
|||||||
# Uncloud
|
<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.
|
||||||
|
</div>
|
||||||
|
|
||||||
Uncloud is a lightweight tool for deploying and managing containerised applications across a network of Docker hosts. It
|
## Quick start
|
||||||
bridges the gap between simple Docker deployments and complex cloud platforms and container orchestrators like
|
|
||||||
Kubernetes.
|
|
||||||
|
|
||||||
Our goal is to become the go-to self-hosted alternative to platforms like [Fly.io](https://fly.io) and
|
1. Install Uncloud CLI:
|
||||||
[Render](https://render.com/). Whether you're using bare metal servers, Raspberry Pis, or cloud VMs, Uncloud transforms
|
|
||||||
your hardware into a personal cloud platform.
|
```bash
|
||||||
|
curl -fsSL https://raw.githubusercontent.com/psviderski/uncloud/main/scripts/install_cli.sh | sh
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Initialize your first machine:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uc run --name my-app -p app.example.com:8000/https registry/app
|
||||||
|
```
|
||||||
|
|
||||||
|
That's it! Your app is now running and accessible at https://app.example.com ✨
|
||||||
|
|
||||||
|
## Project status
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
I'd also love your input:
|
||||||
|
|
||||||
|
- 🐛 Found a bug? [Open an issue](https://github.com/psviderski/uncloud/issues)
|
||||||
|
- 💡 Have ideas? [Join the discussion](https://github.com/psviderski/uncloud/discussions)
|
||||||
|
|
||||||
|
## Motivation
|
||||||
|
|
||||||
|
TBD
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package cluster
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"uncloud/internal/ucind"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewCreateCommand() *cobra.Command {
|
||||||
|
opts := ucind.CreateClusterOptions{}
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "create [NAME]",
|
||||||
|
Short: "Create a new cluster.",
|
||||||
|
Args: cobra.MaximumNArgs(1),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
p := cmd.Context().Value("provisioner").(*ucind.Provisioner)
|
||||||
|
|
||||||
|
name := DefaultClusterName
|
||||||
|
if len(args) > 0 {
|
||||||
|
name = args[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := p.CreateCluster(cmd.Context(), name, opts); err != nil {
|
||||||
|
return fmt.Errorf("create cluster '%s': %w", name, err)
|
||||||
|
}
|
||||||
|
fmt.Printf("Cluster '%s' created.\n", name)
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Flags().IntVarP(&opts.Machines, "machines", "m", 1, "Number of machines to create.")
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package cluster
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"uncloud/internal/ucind"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewRemoveCommand() *cobra.Command {
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "rm [NAME]",
|
||||||
|
Short: "Remove a cluster.",
|
||||||
|
Args: cobra.MaximumNArgs(1),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
p := cmd.Context().Value("provisioner").(*ucind.Provisioner)
|
||||||
|
|
||||||
|
name := DefaultClusterName
|
||||||
|
if len(args) > 0 {
|
||||||
|
name = args[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := p.RemoveCluster(cmd.Context(), name); err != nil {
|
||||||
|
return fmt.Errorf("remove cluster '%s': %w", name, err)
|
||||||
|
}
|
||||||
|
fmt.Printf("Cluster '%s' removed.\n", name)
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package cluster
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
const DefaultClusterName = "ucind-default"
|
||||||
|
|
||||||
|
func NewRootCommand() *cobra.Command {
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "cluster",
|
||||||
|
Short: "Manage local Docker-based clusters.",
|
||||||
|
}
|
||||||
|
cmd.AddCommand(
|
||||||
|
NewCreateCommand(),
|
||||||
|
//NewListCommand(),
|
||||||
|
NewRemoveCommand(),
|
||||||
|
)
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"github.com/docker/docker/client"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"uncloud/cmd/ucind/cluster"
|
||||||
|
"uncloud/internal/ucind"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
var configPath string
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "ucind",
|
||||||
|
Short: "A CLI tool for running Uncloud test clusters using Docker.",
|
||||||
|
Long: "A CLI tool for running Uncloud test clusters using Docker.\n" +
|
||||||
|
"Machines in a ucind cluster are Docker containers running a ucind image. All machines within a cluster " +
|
||||||
|
"are connected to the same Docker network.",
|
||||||
|
SilenceUsage: true,
|
||||||
|
SilenceErrors: true,
|
||||||
|
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("create Docker client: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasPrefix(configPath, "~/") {
|
||||||
|
home, err := os.UserHomeDir()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("get user home directory to resolve %q: %w", configPath, err)
|
||||||
|
}
|
||||||
|
configPath = strings.Replace(configPath, "~", home, 1)
|
||||||
|
}
|
||||||
|
configUpdater := ucind.NewConfigUpdater(configPath)
|
||||||
|
|
||||||
|
p := ucind.NewProvisioner(cli, configUpdater)
|
||||||
|
// Persist the provisioner in the context so it can be used by subcommands.
|
||||||
|
cmd.SetContext(context.WithValue(cmd.Context(), "provisioner", p))
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: allow to override using UNCLOUD_CONFIG env var.
|
||||||
|
cmd.PersistentFlags().StringVar(&configPath, "uncloud-config", "~/.config/uncloud/config.toml",
|
||||||
|
"path to the Uncloud configuration file.")
|
||||||
|
_ = cmd.MarkPersistentFlagFilename("uncloud-config", "toml")
|
||||||
|
|
||||||
|
cmd.AddCommand(
|
||||||
|
cluster.NewRootCommand(),
|
||||||
|
)
|
||||||
|
cobra.CheckErr(cmd.Execute())
|
||||||
|
}
|
||||||
@@ -36,14 +36,14 @@ func NewAddCommand() *cobra.Command {
|
|||||||
return uncli.AddMachine(cmd.Context(), remoteMachine, opts.cluster, opts.name)
|
return uncli.AddMachine(cmd.Context(), remoteMachine, opts.cluster, opts.name)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
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().StringVarP(
|
cmd.Flags().StringVarP(
|
||||||
&opts.sshKey, "ssh-key", "i", "",
|
&opts.sshKey, "ssh-key", "i", "",
|
||||||
"path to SSH private key for SSH remote login (default ~/.ssh/id_*)",
|
"path to SSH private key for SSH remote login. (default ~/.ssh/id_*)",
|
||||||
)
|
)
|
||||||
cmd.Flags().StringVarP(
|
cmd.Flags().StringVarP(
|
||||||
&opts.cluster, "cluster", "c", "",
|
&opts.cluster, "cluster", "c", "",
|
||||||
"Name of the cluster to add the machine to (default is the current cluster)",
|
"Name of the cluster to add the machine to. (default is the current cluster)",
|
||||||
)
|
)
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-16
@@ -6,14 +6,12 @@ import (
|
|||||||
"net/netip"
|
"net/netip"
|
||||||
"uncloud/internal/cli"
|
"uncloud/internal/cli"
|
||||||
"uncloud/internal/cli/config"
|
"uncloud/internal/cli/config"
|
||||||
"uncloud/internal/machine/network"
|
"uncloud/internal/machine/cluster"
|
||||||
)
|
)
|
||||||
|
|
||||||
type initOptions struct {
|
type initOptions struct {
|
||||||
name string
|
name string
|
||||||
network string
|
network string
|
||||||
userPublicKey string
|
|
||||||
|
|
||||||
sshKey string
|
sshKey string
|
||||||
cluster string
|
cluster string
|
||||||
}
|
}
|
||||||
@@ -21,10 +19,10 @@ type initOptions struct {
|
|||||||
func NewInitCommand() *cobra.Command {
|
func NewInitCommand() *cobra.Command {
|
||||||
opts := initOptions{}
|
opts := initOptions{}
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "init [USER@HOST:PORT]",
|
Use: "init [USER@HOST:PORT]",
|
||||||
Args: cobra.MaximumNArgs(1),
|
|
||||||
// TODO: include usage examples of initialising a local and remote machine.
|
|
||||||
Short: "Initialise a new cluster that consists of the local or remote machine.",
|
Short: "Initialise a new cluster that consists of the local or remote machine.",
|
||||||
|
// TODO: include usage examples of initialising a local and remote machine.
|
||||||
|
Args: cobra.MaximumNArgs(1),
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
uncli := cmd.Context().Value("cli").(*cli.CLI)
|
uncli := cmd.Context().Value("cli").(*cli.CLI)
|
||||||
|
|
||||||
@@ -49,21 +47,18 @@ func NewInitCommand() *cobra.Command {
|
|||||||
return uncli.InitCluster(cmd.Context(), remoteMachine, opts.cluster, opts.name, netPrefix)
|
return uncli.InitCluster(cmd.Context(), remoteMachine, opts.cluster, opts.name, netPrefix)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
cmd.Flags().StringVarP(&opts.name, "name", "n", "", "Assign a name to the machine")
|
cmd.Flags().StringVarP(&opts.name, "name", "n", "", "Assign a name to the machine.")
|
||||||
cmd.Flags().StringVar(
|
cmd.Flags().StringVar(
|
||||||
&opts.network, "network", network.DefaultNetwork.String(),
|
&opts.network, "network", cluster.DefaultNetwork.String(),
|
||||||
"IPv4 network CIDR to use for machines and services",
|
"IPv4 network CIDR to use for machines and services.",
|
||||||
)
|
)
|
||||||
//cmd.Flags().StringVar(&opts.userPublicKey, "user-pubkey", "",
|
|
||||||
// "User's public key which will be able to access the cluster (hex-encoded)")
|
|
||||||
|
|
||||||
cmd.Flags().StringVarP(
|
cmd.Flags().StringVarP(
|
||||||
&opts.sshKey, "ssh-key", "i", "",
|
&opts.sshKey, "ssh-key", "i", "",
|
||||||
"path to SSH private key for SSH remote login (default ~/.ssh/id_*)",
|
"path to SSH private key for SSH remote login. (default ~/.ssh/id_*)",
|
||||||
)
|
)
|
||||||
cmd.Flags().StringVarP(
|
cmd.Flags().StringVarP(
|
||||||
&opts.cluster, "cluster", "c", "",
|
&opts.cluster, "cluster", "c", "",
|
||||||
"Name of the cluster in the local config if initialising a remote machine",
|
"Name of the cluster in the local config if initialising a remote machine.",
|
||||||
)
|
)
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
|
|||||||
@@ -1,24 +1,78 @@
|
|||||||
package machine
|
package machine
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
"net/netip"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"text/tabwriter"
|
||||||
"uncloud/internal/cli"
|
"uncloud/internal/cli"
|
||||||
|
"uncloud/internal/machine/network"
|
||||||
|
"uncloud/internal/secret"
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewListCommand() *cobra.Command {
|
func NewListCommand() *cobra.Command {
|
||||||
opts := addOptions{}
|
var cluster string
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "list",
|
Use: "ls",
|
||||||
Aliases: []string{"ls"},
|
Aliases: []string{"list"},
|
||||||
Short: "List machines in a cluster",
|
Short: "List machines in a cluster.",
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
uncli := cmd.Context().Value("cli").(*cli.CLI)
|
uncli := cmd.Context().Value("cli").(*cli.CLI)
|
||||||
return uncli.ListMachines(cmd.Context(), opts.cluster)
|
return list(cmd.Context(), uncli, cluster)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
cmd.Flags().StringVarP(
|
cmd.Flags().StringVarP(
|
||||||
&opts.cluster, "cluster", "c", "",
|
&cluster, "cluster", "c", "",
|
||||||
"Name of the cluster (default is the current cluster)",
|
"Name of the cluster. (default is the current cluster)",
|
||||||
)
|
)
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func list(ctx context.Context, uncli *cli.CLI, clusterName string) error {
|
||||||
|
client, err := uncli.ConnectCluster(ctx, clusterName)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("connect to cluster: %w", err)
|
||||||
|
}
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
machines, err := client.ListMachines(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("list machines: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
return fmt.Errorf("write header: %w", err)
|
||||||
|
}
|
||||||
|
// Print rows.
|
||||||
|
for _, member := range machines {
|
||||||
|
m := member.Machine
|
||||||
|
subnet, _ := m.Network.Subnet.ToPrefix()
|
||||||
|
subnet = netip.PrefixFrom(network.MachineIP(subnet), subnet.Bits())
|
||||||
|
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, ", "),
|
||||||
|
); err != nil {
|
||||||
|
return fmt.Errorf("write row: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return tw.Flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
// capitalise returns a string where the first character is upper case, and the rest is lower case.
|
||||||
|
func capitalise(s string) string {
|
||||||
|
if s == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return strings.ToUpper(s[:1]) + strings.ToLower(s[1:])
|
||||||
|
}
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ func NewTokenCommand() *cobra.Command {
|
|||||||
}
|
}
|
||||||
|
|
||||||
cmd.Flags().StringVarP(&opts.dataDir, "data-dir", "d", machine.DefaultDataDir,
|
cmd.Flags().StringVarP(&opts.dataDir, "data-dir", "d", machine.DefaultDataDir,
|
||||||
"Directory for storing persistent machine state")
|
"Directory for storing persistent machine state.")
|
||||||
_ = cmd.MarkFlagDirname("data-dir")
|
_ = cmd.MarkFlagDirname("data-dir")
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
|
|||||||
+7
-1
@@ -7,6 +7,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"uncloud/cmd/uncloud/machine"
|
"uncloud/cmd/uncloud/machine"
|
||||||
|
"uncloud/cmd/uncloud/service"
|
||||||
"uncloud/internal/cli"
|
"uncloud/internal/cli"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -36,11 +37,16 @@ func main() {
|
|||||||
}
|
}
|
||||||
// TODO: allow to override using UNCLOUD_CONFIG env var.
|
// TODO: allow to override using UNCLOUD_CONFIG env var.
|
||||||
cmd.PersistentFlags().StringVar(&configPath, "uncloud-config", "~/.config/uncloud/config.toml",
|
cmd.PersistentFlags().StringVar(&configPath, "uncloud-config", "~/.config/uncloud/config.toml",
|
||||||
"path to the Uncloud configuration file")
|
"path to the Uncloud configuration file.")
|
||||||
_ = cmd.MarkPersistentFlagFilename("uncloud-config", "toml")
|
_ = cmd.MarkPersistentFlagFilename("uncloud-config", "toml")
|
||||||
|
|
||||||
cmd.AddCommand(
|
cmd.AddCommand(
|
||||||
machine.NewRootCommand(),
|
machine.NewRootCommand(),
|
||||||
|
service.NewRootCommand(),
|
||||||
|
service.NewInspectCommand(),
|
||||||
|
service.NewListCommand(),
|
||||||
|
service.NewRmCommand(),
|
||||||
|
service.NewRunCommand(),
|
||||||
)
|
)
|
||||||
cobra.CheckErr(cmd.Execute())
|
cobra.CheckErr(cmd.Execute())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"github.com/docker/docker/pkg/stringid"
|
||||||
|
"github.com/docker/go-units"
|
||||||
|
"os"
|
||||||
|
"text/tabwriter"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"uncloud/internal/cli"
|
||||||
|
)
|
||||||
|
|
||||||
|
type inspectOptions struct {
|
||||||
|
service string
|
||||||
|
cluster string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewInspectCommand() *cobra.Command {
|
||||||
|
opts := inspectOptions{}
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "inspect",
|
||||||
|
Short: "Display detailed information on a service.",
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
uncli := cmd.Context().Value("cli").(*cli.CLI)
|
||||||
|
opts.service = args[0]
|
||||||
|
return inspect(cmd.Context(), uncli, opts)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
cmd.Flags().StringVarP(
|
||||||
|
&opts.cluster, "cluster", "c", "",
|
||||||
|
"Name of the cluster. (default is the current cluster)",
|
||||||
|
)
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func inspect(ctx context.Context, uncli *cli.CLI, opts inspectOptions) error {
|
||||||
|
client, err := uncli.ConnectCluster(ctx, opts.cluster)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("connect to cluster: %w", err)
|
||||||
|
}
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
svc, err := client.InspectService(ctx, opts.service)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("inspect service: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
machines, err := client.ListMachines(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("list machines: %w", err)
|
||||||
|
}
|
||||||
|
machinesNamesByID := make(map[string]string)
|
||||||
|
for _, m := range machines {
|
||||||
|
machinesNamesByID[m.Machine.Id] = m.Machine.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("ID: %s\n", svc.ID)
|
||||||
|
fmt.Printf("Name: %s\n", svc.Name)
|
||||||
|
fmt.Printf("Mode: %s\n", svc.Mode)
|
||||||
|
fmt.Println()
|
||||||
|
|
||||||
|
// Print the list of containers in a table format.
|
||||||
|
tw := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', 0)
|
||||||
|
if _, err = fmt.Fprintln(tw, "CONTAINER ID\tIMAGE\tCREATED\tSTATUS\tMACHINE"); err != nil {
|
||||||
|
return fmt.Errorf("write header: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, ctr := range svc.Containers {
|
||||||
|
createdAt := time.Unix(ctr.Container.Created, 0)
|
||||||
|
created := units.HumanDuration(time.Now().UTC().Sub(createdAt)) + " ago"
|
||||||
|
|
||||||
|
machine := machinesNamesByID[ctr.MachineID]
|
||||||
|
if machine == "" {
|
||||||
|
machine = ctr.MachineID
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = fmt.Fprintf(
|
||||||
|
tw,
|
||||||
|
"%s\t%s\t%s\t%s\t%s\n",
|
||||||
|
stringid.TruncateID(ctr.Container.ID),
|
||||||
|
ctr.Container.Image,
|
||||||
|
created,
|
||||||
|
ctr.Container.Status,
|
||||||
|
machine,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("write row: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return tw.Flush()
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"os"
|
||||||
|
"text/tabwriter"
|
||||||
|
"uncloud/internal/cli"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewListCommand() *cobra.Command {
|
||||||
|
var cluster string
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "ls",
|
||||||
|
Aliases: []string{"list"},
|
||||||
|
Short: "List services.",
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
uncli := cmd.Context().Value("cli").(*cli.CLI)
|
||||||
|
return list(cmd.Context(), uncli, cluster)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
cmd.Flags().StringVarP(
|
||||||
|
&cluster, "cluster", "c", "",
|
||||||
|
"Name of the cluster. (default is the current cluster)",
|
||||||
|
)
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func list(ctx context.Context, uncli *cli.CLI, clusterName string) error {
|
||||||
|
client, err := uncli.ConnectCluster(ctx, clusterName)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("connect to cluster: %w", err)
|
||||||
|
}
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
services, err := client.ListServices(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("list services: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
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 {
|
||||||
|
return fmt.Errorf("write row: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return tw.Flush()
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"uncloud/internal/cli"
|
||||||
|
)
|
||||||
|
|
||||||
|
type rmOptions struct {
|
||||||
|
services []string
|
||||||
|
cluster string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRmCommand() *cobra.Command {
|
||||||
|
opts := rmOptions{}
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "rm SERVICE [SERVICE...]",
|
||||||
|
Aliases: []string{"remove", "delete"},
|
||||||
|
Short: "Remove one or more services.",
|
||||||
|
Args: cobra.MinimumNArgs(1),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
uncli := cmd.Context().Value("cli").(*cli.CLI)
|
||||||
|
opts.services = args
|
||||||
|
return rm(cmd.Context(), uncli, opts)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
cmd.Flags().StringVarP(
|
||||||
|
&opts.cluster, "cluster", "c", "",
|
||||||
|
"Name of the cluster. (default is the current cluster)",
|
||||||
|
)
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func rm(ctx context.Context, uncli *cli.CLI, opts rmOptions) error {
|
||||||
|
client, err := uncli.ConnectCluster(ctx, opts.cluster)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("connect to cluster: %w", err)
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewRootCommand() *cobra.Command {
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "service",
|
||||||
|
Short: "Manage services in an Uncloud cluster.",
|
||||||
|
}
|
||||||
|
cmd.AddCommand(
|
||||||
|
NewListCommand(),
|
||||||
|
NewRmCommand(),
|
||||||
|
NewRunCommand(),
|
||||||
|
)
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"uncloud/internal/api"
|
||||||
|
"uncloud/internal/cli"
|
||||||
|
)
|
||||||
|
|
||||||
|
type runOptions struct {
|
||||||
|
command []string
|
||||||
|
image string
|
||||||
|
machine string
|
||||||
|
mode string
|
||||||
|
name string
|
||||||
|
publish []string
|
||||||
|
volumes []string
|
||||||
|
|
||||||
|
cluster string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRunCommand() *cobra.Command {
|
||||||
|
opts := runOptions{}
|
||||||
|
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "run IMAGE [COMMAND...]",
|
||||||
|
Short: "Run a service.",
|
||||||
|
Args: cobra.MinimumNArgs(1),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
uncli := cmd.Context().Value("cli").(*cli.CLI)
|
||||||
|
|
||||||
|
opts.image = args[0]
|
||||||
|
if len(args) > 1 {
|
||||||
|
opts.command = args[1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
return run(cmd.Context(), uncli, opts)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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().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"+
|
||||||
|
"Examples:\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().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.")
|
||||||
|
|
||||||
|
cmd.Flags().StringVarP(
|
||||||
|
&opts.cluster, "cluster", "c", "",
|
||||||
|
"Name of the cluster to run the service in. (default is the current cluster)",
|
||||||
|
)
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func run(ctx context.Context, uncli *cli.CLI, opts runOptions) error {
|
||||||
|
switch opts.mode {
|
||||||
|
case "", api.ServiceModeReplicated, api.ServiceModeGlobal:
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("invalid replication mode: %q", opts.mode)
|
||||||
|
}
|
||||||
|
|
||||||
|
ports := make([]api.PortSpec, len(opts.publish))
|
||||||
|
for i, publishPort := range opts.publish {
|
||||||
|
port, err := api.ParsePortSpec(publishPort)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid service port '%s': %w", publishPort, err)
|
||||||
|
}
|
||||||
|
ports[i] = port
|
||||||
|
}
|
||||||
|
// TODO: parse and validate opts.volumes to fail fast if invalid.
|
||||||
|
|
||||||
|
spec := api.ServiceSpec{
|
||||||
|
Container: api.ContainerSpec{
|
||||||
|
Command: opts.command,
|
||||||
|
Image: opts.image,
|
||||||
|
Volumes: opts.volumes,
|
||||||
|
},
|
||||||
|
Mode: opts.mode,
|
||||||
|
Name: opts.name,
|
||||||
|
Ports: ports,
|
||||||
|
}
|
||||||
|
if err := spec.Validate(); err != nil {
|
||||||
|
return fmt.Errorf("invalid service configuration: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
client, err := uncli.ConnectCluster(ctx, opts.cluster)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("connect to cluster: %w", err)
|
||||||
|
}
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
if _, err = client.RunService(ctx, spec); err != nil {
|
||||||
|
return fmt.Errorf("run service: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -8,11 +8,15 @@ import (
|
|||||||
"os/signal"
|
"os/signal"
|
||||||
"syscall"
|
"syscall"
|
||||||
"uncloud/internal/daemon"
|
"uncloud/internal/daemon"
|
||||||
|
"uncloud/internal/log"
|
||||||
"uncloud/internal/machine"
|
"uncloud/internal/machine"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
slog.SetLogLoggerLevel(slog.LevelDebug)
|
logger := slog.New(log.NewSlogTextHandler(os.Stderr, &slog.HandlerOptions{
|
||||||
|
Level: slog.LevelDebug,
|
||||||
|
}))
|
||||||
|
slog.SetDefault(logger)
|
||||||
|
|
||||||
var dataDir string
|
var dataDir string
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
|
|||||||
@@ -1,16 +1,23 @@
|
|||||||
module uncloud
|
module uncloud
|
||||||
|
|
||||||
go 1.22.2
|
go 1.23
|
||||||
|
|
||||||
toolchain go1.22.5
|
toolchain go1.23.2
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/BurntSushi/toml v1.4.0
|
github.com/BurntSushi/toml v1.4.0
|
||||||
|
github.com/Masterminds/squirrel v1.5.4
|
||||||
|
github.com/caddyserver/caddy/v2 v2.8.4
|
||||||
|
github.com/cenkalti/backoff/v4 v4.3.0
|
||||||
github.com/charmbracelet/huh v0.6.0
|
github.com/charmbracelet/huh v0.6.0
|
||||||
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf
|
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf
|
||||||
github.com/dgraph-io/badger/v3 v3.2103.5
|
github.com/dgraph-io/badger/v3 v3.2103.5
|
||||||
github.com/docker/docker v27.3.0+incompatible
|
github.com/distribution/reference v0.6.0
|
||||||
|
github.com/docker/cli v27.4.0-rc.4+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-connections v0.5.0
|
||||||
|
github.com/docker/go-units v0.5.0
|
||||||
github.com/hashicorp/memberlist v0.5.1
|
github.com/hashicorp/memberlist v0.5.1
|
||||||
github.com/hashicorp/serf v0.10.1
|
github.com/hashicorp/serf v0.10.1
|
||||||
github.com/ipfs/boxo v0.21.0
|
github.com/ipfs/boxo v0.21.0
|
||||||
@@ -21,34 +28,48 @@ require (
|
|||||||
github.com/ipfs/go-ipld-format v0.6.0
|
github.com/ipfs/go-ipld-format v0.6.0
|
||||||
github.com/ipfs/go-log/v2 v2.5.1
|
github.com/ipfs/go-log/v2 v2.5.1
|
||||||
github.com/lmittmann/tint v1.0.5
|
github.com/lmittmann/tint v1.0.5
|
||||||
|
github.com/opencontainers/image-spec v1.1.0
|
||||||
github.com/siderolabs/discovery-api v0.1.4
|
github.com/siderolabs/discovery-api v0.1.4
|
||||||
github.com/siderolabs/discovery-client v0.1.9
|
github.com/siderolabs/discovery-client v0.1.9
|
||||||
|
github.com/siderolabs/grpc-proxy v0.5.1
|
||||||
github.com/spf13/cobra v1.8.1
|
github.com/spf13/cobra v1.8.1
|
||||||
|
github.com/stretchr/testify v1.10.0
|
||||||
github.com/vishvananda/netlink v1.3.0
|
github.com/vishvananda/netlink v1.3.0
|
||||||
go.uber.org/zap v1.27.0
|
go.uber.org/zap v1.27.0
|
||||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba
|
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba
|
||||||
golang.org/x/crypto v0.27.0
|
golang.org/x/crypto v0.31.0
|
||||||
golang.org/x/sync v0.8.0
|
golang.org/x/net v0.32.0
|
||||||
golang.org/x/sys v0.25.0
|
golang.org/x/sync v0.10.0
|
||||||
|
golang.org/x/sys v0.28.0
|
||||||
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173
|
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173
|
||||||
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6
|
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6
|
||||||
google.golang.org/grpc v1.66.0
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20241007155032-5fefd90f89a9
|
||||||
google.golang.org/protobuf v1.34.2
|
google.golang.org/grpc v1.68.0
|
||||||
|
google.golang.org/protobuf v1.36.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
filippo.io/edwards25519 v1.1.0 // indirect
|
||||||
|
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 // indirect
|
||||||
|
github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 // indirect
|
||||||
|
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
|
||||||
github.com/Masterminds/goutils v1.1.1 // indirect
|
github.com/Masterminds/goutils v1.1.1 // indirect
|
||||||
github.com/Masterminds/semver/v3 v3.2.1 // indirect
|
github.com/Masterminds/semver/v3 v3.2.1 // indirect
|
||||||
github.com/Masterminds/sprig/v3 v3.2.3 // indirect
|
github.com/Masterminds/sprig/v3 v3.2.3 // indirect
|
||||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||||
|
github.com/antlr4-go/antlr/v4 v4.13.0 // indirect
|
||||||
github.com/armon/circbuf v0.0.0-20190214190532-5111143e8da2 // indirect
|
github.com/armon/circbuf v0.0.0-20190214190532-5111143e8da2 // indirect
|
||||||
github.com/armon/go-metrics v0.4.1 // indirect
|
github.com/armon/go-metrics v0.4.1 // indirect
|
||||||
github.com/armon/go-radix v1.0.0 // indirect
|
github.com/armon/go-radix v1.0.0 // indirect
|
||||||
|
github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b // indirect
|
||||||
github.com/atotto/clipboard v0.1.4 // indirect
|
github.com/atotto/clipboard v0.1.4 // indirect
|
||||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||||
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
github.com/bgentry/speakeasy v0.2.0 // indirect
|
github.com/bgentry/speakeasy v0.2.0 // indirect
|
||||||
|
github.com/buger/goterm v1.0.4 // indirect
|
||||||
|
github.com/caddyserver/certmagic v0.21.4 // indirect
|
||||||
|
github.com/caddyserver/zerossl v0.1.3 // indirect
|
||||||
github.com/catppuccin/go v0.2.0 // indirect
|
github.com/catppuccin/go v0.2.0 // indirect
|
||||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
|
||||||
github.com/cespare/xxhash v1.1.0 // indirect
|
github.com/cespare/xxhash v1.1.0 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
github.com/charmbracelet/bubbles v0.20.0 // indirect
|
github.com/charmbracelet/bubbles v0.20.0 // indirect
|
||||||
@@ -57,28 +78,60 @@ require (
|
|||||||
github.com/charmbracelet/x/ansi v0.3.2 // indirect
|
github.com/charmbracelet/x/ansi v0.3.2 // indirect
|
||||||
github.com/charmbracelet/x/exp/strings v0.0.0-20240919170804-a4978c8e603a // indirect
|
github.com/charmbracelet/x/exp/strings v0.0.0-20240919170804-a4978c8e603a // indirect
|
||||||
github.com/charmbracelet/x/term v0.2.0 // indirect
|
github.com/charmbracelet/x/term v0.2.0 // indirect
|
||||||
|
github.com/chzyer/readline v1.5.1 // indirect
|
||||||
|
github.com/compose-spec/compose-go/v2 v2.4.5 // indirect
|
||||||
|
github.com/containerd/console v1.0.4 // indirect
|
||||||
|
github.com/containerd/containerd v1.7.24 // indirect
|
||||||
|
github.com/containerd/containerd/api v1.7.19 // indirect
|
||||||
|
github.com/containerd/continuity v0.4.4 // indirect
|
||||||
|
github.com/containerd/errdefs v0.3.0 // indirect
|
||||||
github.com/containerd/log v0.1.0 // indirect
|
github.com/containerd/log v0.1.0 // indirect
|
||||||
|
github.com/containerd/platforms v0.2.1 // indirect
|
||||||
|
github.com/containerd/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/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect
|
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect
|
||||||
|
github.com/dgraph-io/badger v1.6.2 // indirect
|
||||||
|
github.com/dgraph-io/badger/v2 v2.2007.4 // indirect
|
||||||
github.com/dgraph-io/ristretto v0.1.1 // indirect
|
github.com/dgraph-io/ristretto v0.1.1 // indirect
|
||||||
github.com/distribution/reference v0.6.0 // indirect
|
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 // indirect
|
||||||
|
github.com/docker/buildx v0.18.0 // indirect
|
||||||
|
github.com/docker/distribution v2.8.3+incompatible // indirect
|
||||||
|
github.com/docker/docker-credential-helpers v0.8.2 // indirect
|
||||||
|
github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c // indirect
|
||||||
github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c // indirect
|
github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c // indirect
|
||||||
github.com/docker/go-units v0.5.0 // indirect
|
github.com/docker/go-metrics v0.0.1 // indirect
|
||||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
||||||
github.com/fatih/color v1.17.0 // indirect
|
github.com/fatih/color v1.17.0 // indirect
|
||||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||||
|
github.com/fvbommel/sortorder v1.1.0 // indirect
|
||||||
|
github.com/go-jose/go-jose/v3 v3.0.3 // indirect
|
||||||
|
github.com/go-kit/kit v0.13.0 // indirect
|
||||||
|
github.com/go-kit/log v0.2.1 // indirect
|
||||||
|
github.com/go-logfmt/logfmt v0.6.0 // indirect
|
||||||
github.com/go-logr/logr v1.4.2 // indirect
|
github.com/go-logr/logr v1.4.2 // indirect
|
||||||
github.com/go-logr/stdr v1.2.2 // indirect
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
|
github.com/go-sql-driver/mysql v1.7.1 // indirect
|
||||||
|
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
|
||||||
|
github.com/go-viper/mapstructure/v2 v2.0.0 // indirect
|
||||||
github.com/godbus/dbus/v5 v5.1.0 // indirect
|
github.com/godbus/dbus/v5 v5.1.0 // indirect
|
||||||
|
github.com/gofrs/flock v0.12.1 // indirect
|
||||||
github.com/gogo/protobuf v1.3.2 // indirect
|
github.com/gogo/protobuf v1.3.2 // indirect
|
||||||
github.com/golang/glog v1.2.2 // indirect
|
github.com/golang/glog v1.2.2 // indirect
|
||||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||||
github.com/golang/protobuf v1.5.4 // indirect
|
github.com/golang/protobuf v1.5.4 // indirect
|
||||||
github.com/golang/snappy v0.0.4 // indirect
|
github.com/golang/snappy v0.0.4 // indirect
|
||||||
github.com/google/btree v1.1.2 // indirect
|
github.com/google/btree v1.1.2 // indirect
|
||||||
|
github.com/google/cel-go v0.20.1 // indirect
|
||||||
github.com/google/flatbuffers v24.3.25+incompatible // indirect
|
github.com/google/flatbuffers v24.3.25+incompatible // indirect
|
||||||
github.com/google/go-cmp v0.6.0 // indirect
|
github.com/google/go-cmp v0.6.0 // indirect
|
||||||
|
github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad // indirect
|
||||||
|
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
|
github.com/gorilla/mux v1.8.1 // indirect
|
||||||
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect
|
||||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||||
github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
|
github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
|
||||||
github.com/hashicorp/go-msgpack v0.5.5 // indirect
|
github.com/hashicorp/go-msgpack v0.5.5 // indirect
|
||||||
@@ -86,12 +139,14 @@ require (
|
|||||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||||
github.com/hashicorp/go-sockaddr v1.0.6 // indirect
|
github.com/hashicorp/go-sockaddr v1.0.6 // indirect
|
||||||
github.com/hashicorp/go-syslog v1.0.0 // indirect
|
github.com/hashicorp/go-syslog v1.0.0 // indirect
|
||||||
|
github.com/hashicorp/go-version v1.7.0 // indirect
|
||||||
github.com/hashicorp/golang-lru v1.0.2 // indirect
|
github.com/hashicorp/golang-lru v1.0.2 // indirect
|
||||||
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
|
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
|
||||||
github.com/hashicorp/logutils v1.0.0 // indirect
|
github.com/hashicorp/logutils v1.0.0 // indirect
|
||||||
github.com/hashicorp/mdns v1.0.5 // indirect
|
github.com/hashicorp/mdns v1.0.5 // indirect
|
||||||
github.com/huandu/xstrings v1.5.0 // indirect
|
github.com/huandu/xstrings v1.5.0 // indirect
|
||||||
github.com/imdario/mergo v0.3.16 // indirect
|
github.com/imdario/mergo v0.3.16 // indirect
|
||||||
|
github.com/in-toto/in-toto-golang v0.5.0 // indirect
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
github.com/ipfs/bbloom v0.0.4 // indirect
|
github.com/ipfs/bbloom v0.0.4 // indirect
|
||||||
github.com/ipfs/go-block-format v0.2.0 // indirect
|
github.com/ipfs/go-block-format v0.2.0 // indirect
|
||||||
@@ -101,30 +156,52 @@ require (
|
|||||||
github.com/ipld/go-codec-dagpb v1.6.0 // indirect
|
github.com/ipld/go-codec-dagpb v1.6.0 // indirect
|
||||||
github.com/ipld/go-ipld-prime v0.21.0 // indirect
|
github.com/ipld/go-ipld-prime v0.21.0 // indirect
|
||||||
github.com/ishidawataru/sctp v0.0.0-20230406120618-7ff4192f6ff2 // indirect
|
github.com/ishidawataru/sctp v0.0.0-20230406120618-7ff4192f6ff2 // indirect
|
||||||
|
github.com/jackc/chunkreader/v2 v2.0.1 // indirect
|
||||||
|
github.com/jackc/pgconn v1.14.3 // indirect
|
||||||
|
github.com/jackc/pgio v1.0.0 // indirect
|
||||||
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
|
github.com/jackc/pgproto3/v2 v2.3.3 // indirect
|
||||||
|
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
|
||||||
|
github.com/jackc/pgtype v1.14.0 // indirect
|
||||||
|
github.com/jackc/pgx/v4 v4.18.3 // indirect
|
||||||
github.com/jbenet/goprocess v0.1.4 // indirect
|
github.com/jbenet/goprocess v0.1.4 // indirect
|
||||||
github.com/josharian/native v1.1.0 // indirect
|
github.com/josharian/native v1.1.0 // indirect
|
||||||
github.com/klauspost/compress v1.17.9 // indirect
|
github.com/klauspost/compress v1.17.11 // indirect
|
||||||
github.com/klauspost/cpuid/v2 v2.2.8 // indirect
|
github.com/klauspost/cpuid/v2 v2.2.9 // indirect
|
||||||
|
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect
|
||||||
|
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect
|
||||||
|
github.com/libdns/libdns v0.2.2 // indirect
|
||||||
github.com/libp2p/go-buffer-pool v0.1.0 // indirect
|
github.com/libp2p/go-buffer-pool v0.1.0 // indirect
|
||||||
github.com/libp2p/go-libp2p v0.35.4 // indirect
|
github.com/libp2p/go-libp2p v0.35.4 // indirect
|
||||||
github.com/libp2p/go-libp2p-pubsub v0.11.0 // indirect
|
github.com/libp2p/go-libp2p-pubsub v0.11.0 // indirect
|
||||||
github.com/libp2p/go-msgio v0.3.0 // indirect
|
github.com/libp2p/go-msgio v0.3.0 // indirect
|
||||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||||
|
github.com/manifoldco/promptui v0.9.0 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/mattn/go-localereader v0.0.1 // indirect
|
github.com/mattn/go-localereader v0.0.1 // indirect
|
||||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||||
|
github.com/mattn/go-shellwords v1.0.12 // indirect
|
||||||
github.com/mdlayher/genetlink v1.3.2 // indirect
|
github.com/mdlayher/genetlink v1.3.2 // indirect
|
||||||
github.com/mdlayher/netlink v1.7.2 // indirect
|
github.com/mdlayher/netlink v1.7.2 // indirect
|
||||||
github.com/mdlayher/socket v0.5.1 // indirect
|
github.com/mdlayher/socket v0.5.1 // indirect
|
||||||
|
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
|
||||||
|
github.com/mholt/acmez/v2 v2.0.3 // indirect
|
||||||
github.com/miekg/dns v1.1.62 // indirect
|
github.com/miekg/dns v1.1.62 // indirect
|
||||||
|
github.com/miekg/pkcs11 v1.1.1 // indirect
|
||||||
github.com/minio/sha256-simd v1.0.1 // indirect
|
github.com/minio/sha256-simd v1.0.1 // indirect
|
||||||
github.com/mitchellh/cli v1.1.5 // indirect
|
github.com/mitchellh/cli v1.1.5 // indirect
|
||||||
github.com/mitchellh/copystructure v1.2.0 // indirect
|
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||||
|
github.com/mitchellh/go-ps v1.0.0 // indirect
|
||||||
github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect
|
github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect
|
||||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||||
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||||
|
github.com/moby/buildkit v0.17.2 // indirect
|
||||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||||
|
github.com/moby/locker v1.0.1 // indirect
|
||||||
|
github.com/moby/patternmatcher v0.6.0 // indirect
|
||||||
|
github.com/moby/sys/sequential v0.6.0 // indirect
|
||||||
|
github.com/moby/sys/signal v0.7.1 // indirect
|
||||||
github.com/moby/term v0.5.0 // indirect
|
github.com/moby/term v0.5.0 // indirect
|
||||||
github.com/morikuni/aec v1.0.0 // indirect
|
github.com/morikuni/aec v1.0.0 // indirect
|
||||||
github.com/mr-tron/base58 v1.2.0 // indirect
|
github.com/mr-tron/base58 v1.2.0 // indirect
|
||||||
@@ -140,37 +217,85 @@ require (
|
|||||||
github.com/multiformats/go-multihash v0.2.3 // indirect
|
github.com/multiformats/go-multihash v0.2.3 // indirect
|
||||||
github.com/multiformats/go-multistream v0.5.0 // indirect
|
github.com/multiformats/go-multistream v0.5.0 // indirect
|
||||||
github.com/multiformats/go-varint v0.0.7 // indirect
|
github.com/multiformats/go-varint v0.0.7 // indirect
|
||||||
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||||
|
github.com/onsi/ginkgo/v2 v2.22.0 // indirect
|
||||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||||
github.com/opencontainers/image-spec v1.1.0 // indirect
|
github.com/pelletier/go-toml v1.9.5 // indirect
|
||||||
|
github.com/pires/go-proxyproto v0.7.0 // indirect
|
||||||
github.com/pkg/errors v0.9.1 // indirect
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
|
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/polydawn/refmt v0.89.0 // indirect
|
github.com/polydawn/refmt v0.89.0 // indirect
|
||||||
github.com/posener/complete v1.2.3 // indirect
|
github.com/posener/complete v1.2.3 // indirect
|
||||||
|
github.com/prometheus/client_golang v1.20.5 // indirect
|
||||||
|
github.com/prometheus/client_model v0.6.1 // indirect
|
||||||
|
github.com/prometheus/common v0.61.0 // indirect
|
||||||
|
github.com/prometheus/procfs v0.15.1 // indirect
|
||||||
|
github.com/quic-go/qpack v0.5.1 // indirect
|
||||||
|
github.com/quic-go/quic-go v0.48.2 // indirect
|
||||||
github.com/rivo/uniseg v0.4.7 // indirect
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
|
github.com/rs/xid v1.5.0 // indirect
|
||||||
|
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||||
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 // indirect
|
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 // indirect
|
||||||
|
github.com/secure-systems-lab/go-securesystemslib v0.4.0 // indirect
|
||||||
|
github.com/shibumi/go-pathspec v1.3.0 // indirect
|
||||||
github.com/shopspring/decimal v1.4.0 // indirect
|
github.com/shopspring/decimal v1.4.0 // indirect
|
||||||
|
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
|
||||||
github.com/siderolabs/gen v0.4.8 // indirect
|
github.com/siderolabs/gen v0.4.8 // indirect
|
||||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||||
|
github.com/slackhq/nebula v1.6.1 // indirect
|
||||||
|
github.com/smallstep/certificates v0.26.1 // indirect
|
||||||
|
github.com/smallstep/nosql v0.6.1 // indirect
|
||||||
|
github.com/smallstep/pkcs7 v0.0.0-20231024181729-3b98ecc1ca81 // indirect
|
||||||
|
github.com/smallstep/scep v0.0.0-20231024192529-aee96d7ad34d // indirect
|
||||||
|
github.com/smallstep/truststore v0.13.0 // indirect
|
||||||
github.com/spaolacci/murmur3 v1.1.0 // indirect
|
github.com/spaolacci/murmur3 v1.1.0 // indirect
|
||||||
github.com/spf13/cast v1.7.0 // indirect
|
github.com/spf13/cast v1.7.0 // indirect
|
||||||
github.com/spf13/pflag v1.0.5 // indirect
|
github.com/spf13/pflag v1.0.5 // indirect
|
||||||
|
github.com/stoewer/go-strcase v1.2.0 // indirect
|
||||||
|
github.com/tailscale/tscert v0.0.0-20240517230440-bbccfbf48933 // indirect
|
||||||
|
github.com/theupdateframework/notary v0.7.0 // indirect
|
||||||
|
github.com/tonistiigi/dchapes-mode v0.0.0-20241001053921-ca0759fec205 // indirect
|
||||||
|
github.com/tonistiigi/fsutil v0.0.0-20241028165955-397af5306b5c // indirect
|
||||||
|
github.com/tonistiigi/go-csvvalue v0.0.0-20240710180619-ddb21b71c0b4 // indirect
|
||||||
|
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/vishvananda/netns v0.0.4 // 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.opencensus.io v0.24.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/contrib/instrumentation/net/http/otelhttp v0.52.0 // indirect
|
||||||
go.opentelemetry.io/otel v1.30.0 // indirect
|
go.opentelemetry.io/otel v1.32.0 // indirect
|
||||||
go.opentelemetry.io/otel/metric v1.30.0 // indirect
|
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.44.0 // indirect
|
||||||
go.opentelemetry.io/otel/trace v1.30.0 // indirect
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.27.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/metric v1.32.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/sdk v1.32.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/sdk/metric v1.32.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/trace v1.32.0 // indirect
|
||||||
|
go.opentelemetry.io/proto/otlp v1.3.1 // 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
|
||||||
|
go.uber.org/automaxprocs v1.5.3 // indirect
|
||||||
|
go.uber.org/mock v0.5.0 // indirect
|
||||||
go.uber.org/multierr v1.11.0 // indirect
|
go.uber.org/multierr v1.11.0 // indirect
|
||||||
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect
|
go.uber.org/zap/exp v0.2.0 // indirect
|
||||||
golang.org/x/mod v0.21.0 // indirect
|
golang.org/x/crypto/x509roots/fallback v0.0.0-20240507223354-67b13616a595 // indirect
|
||||||
golang.org/x/net v0.29.0 // indirect
|
golang.org/x/exp v0.0.0-20241215155358-4a5509556b9e // indirect
|
||||||
golang.org/x/text v0.18.0 // indirect
|
golang.org/x/mod v0.22.0 // indirect
|
||||||
golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 // indirect
|
golang.org/x/term v0.27.0 // indirect
|
||||||
golang.org/x/tools v0.25.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.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
|
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20240827150818-7e3bb234dfed // indirect
|
google.golang.org/genproto v0.0.0-20240401170217-c3f982113cda // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240827150818-7e3bb234dfed // indirect
|
google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 // indirect
|
||||||
gotest.tools/v3 v3.5.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259 // indirect
|
gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259 // indirect
|
||||||
|
howett.net/plist v1.0.0 // indirect
|
||||||
lukechampine.com/blake3 v1.3.0 // indirect
|
lukechampine.com/blake3 v1.3.0 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/docker/docker/api/types"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
LabelManaged = "uncloud.managed"
|
||||||
|
LabelServiceID = "uncloud.service.id"
|
||||||
|
LabelServiceName = "uncloud.service.name"
|
||||||
|
LabelServiceMode = "uncloud.service.mode"
|
||||||
|
LabelServicePorts = "uncloud.service.ports"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Container struct {
|
||||||
|
types.Container
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServiceID returns the ID of the service this container belongs to.
|
||||||
|
func (c *Container) ServiceID() string {
|
||||||
|
return c.Labels[LabelServiceID]
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServiceName returns the name of the service this container belongs to.
|
||||||
|
func (c *Container) ServiceName() string {
|
||||||
|
return c.Labels[LabelServiceName]
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServiceMode returns the replication mode of the service this container belongs to.
|
||||||
|
func (c *Container) ServiceMode() string {
|
||||||
|
return c.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]
|
||||||
|
if !ok {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
publishPorts := strings.Split(encoded, ",")
|
||||||
|
ports := make([]PortSpec, len(publishPorts))
|
||||||
|
for i, p := range publishPorts {
|
||||||
|
port, err := ParsePortSpec(strings.TrimSpace(p))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
ports[i] = port
|
||||||
|
}
|
||||||
|
|
||||||
|
return ports, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 [^(]+(?:\(([^)]+)\))?$`)
|
||||||
|
|
||||||
|
// Healthy determines if the container is running and healthy based on its status string.
|
||||||
|
// A running container with no health check configured is considered healthy.
|
||||||
|
func (c *Container) Healthy() bool {
|
||||||
|
if c.State != "running" {
|
||||||
|
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] == "" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the health status in parentheses is "healthy", the container is considered healthy.
|
||||||
|
return matches[1] == types.Healthy
|
||||||
|
}
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/docker/docker/api/types"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
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",
|
||||||
|
}}
|
||||||
|
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",
|
||||||
|
}}
|
||||||
|
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)",
|
||||||
|
}}
|
||||||
|
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)",
|
||||||
|
}}
|
||||||
|
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)",
|
||||||
|
}}
|
||||||
|
assert.False(t, c.Healthy())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("invalid up format no time", 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",
|
||||||
|
}}
|
||||||
|
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",
|
||||||
|
}}
|
||||||
|
assert.False(t, c.Healthy())
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,268 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/netip"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
PortModeIngress = "ingress"
|
||||||
|
PortModeHost = "host"
|
||||||
|
|
||||||
|
ProtocolHTTP = "http"
|
||||||
|
ProtocolHTTPS = "https"
|
||||||
|
ProtocolTCP = "tcp"
|
||||||
|
ProtocolUDP = "udp"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PortSpec struct {
|
||||||
|
// Hostname specifies the DNS name that will route to this service. Only valid in ingress mode.
|
||||||
|
Hostname string
|
||||||
|
// HostIP is the host IP to bind the PublishedPort to. Only valid in host mode.
|
||||||
|
HostIP netip.Addr
|
||||||
|
// PublishedPort is the port number exposed outside the container.
|
||||||
|
// In ingress mode, this is the load balancer port. In host mode, this is the port bound on the host.
|
||||||
|
PublishedPort uint16
|
||||||
|
// ContainerPort is the port inside the container that the service listens on.
|
||||||
|
ContainerPort uint16
|
||||||
|
// Protocol specifies the network protocol.
|
||||||
|
Protocol string
|
||||||
|
// Mode specifies how the port is published.
|
||||||
|
Mode string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PortSpec) Validate() error {
|
||||||
|
if p.ContainerPort == 0 {
|
||||||
|
return fmt.Errorf("container port must be non-zero")
|
||||||
|
}
|
||||||
|
|
||||||
|
switch p.Protocol {
|
||||||
|
case "":
|
||||||
|
return fmt.Errorf("protocol must be specified")
|
||||||
|
case ProtocolHTTP, ProtocolHTTPS, ProtocolTCP, ProtocolUDP:
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("invalid protocol '%s', supported protocols: '%s', '%s', '%s', '%s'",
|
||||||
|
p.Protocol, ProtocolHTTP, ProtocolHTTPS, ProtocolTCP, ProtocolUDP)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch p.Mode {
|
||||||
|
case "":
|
||||||
|
return fmt.Errorf("mode must be specified")
|
||||||
|
case PortModeIngress:
|
||||||
|
if p.HostIP.IsValid() {
|
||||||
|
return fmt.Errorf("host IP cannot be specified in %s mode", PortModeIngress)
|
||||||
|
}
|
||||||
|
if p.Hostname != "" {
|
||||||
|
if p.Protocol != ProtocolHTTP && p.Protocol != ProtocolHTTPS {
|
||||||
|
return fmt.Errorf("hostname is only valid with '%s' or '%s' protocols", ProtocolHTTP, ProtocolHTTPS)
|
||||||
|
}
|
||||||
|
if err := validateHostname(p.Hostname); err != nil {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
if p.Protocol != ProtocolTCP && p.Protocol != ProtocolUDP {
|
||||||
|
return fmt.Errorf("unsupported protocol '%s' in %s mode, only '%s' and '%s' are supported",
|
||||||
|
p.Protocol, PortModeHost, ProtocolTCP, ProtocolUDP)
|
||||||
|
}
|
||||||
|
if p.Hostname != "" {
|
||||||
|
return fmt.Errorf("hostname cannot be specified in %s mode", PortModeHost)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("invalid mode: '%s'", p.Mode)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns the port specification in the -p/--publish flag format.
|
||||||
|
// Format:
|
||||||
|
// [hostname:][load_balancer_port:]container_port/protocol for ingress mode (default) or
|
||||||
|
// [host_ip:]:host_port:container_port/protocol@host for host mode.
|
||||||
|
func (p *PortSpec) String() (string, error) {
|
||||||
|
if err := p.Validate(); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
var parts []string
|
||||||
|
|
||||||
|
switch p.Mode {
|
||||||
|
case "", PortModeIngress: // [hostname:][load_balancer_port:]container_port/protocol
|
||||||
|
if p.Hostname != "" {
|
||||||
|
parts = append(parts, p.Hostname)
|
||||||
|
}
|
||||||
|
if p.PublishedPort != 0 {
|
||||||
|
parts = append(parts, fmt.Sprint(p.PublishedPort))
|
||||||
|
}
|
||||||
|
parts = append(parts, fmt.Sprint(p.ContainerPort))
|
||||||
|
|
||||||
|
return fmt.Sprintf("%s/%s", strings.Join(parts, ":"), p.Protocol), nil
|
||||||
|
case PortModeHost: // [host_ip:]:host_port:container_port/protocol@host
|
||||||
|
if p.HostIP.IsValid() {
|
||||||
|
if p.HostIP.Is6() {
|
||||||
|
parts = append(parts, fmt.Sprintf("[%s]", p.HostIP))
|
||||||
|
} else {
|
||||||
|
parts = append(parts, p.HostIP.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
parts = append(parts, fmt.Sprint(p.PublishedPort))
|
||||||
|
parts = append(parts, fmt.Sprint(p.ContainerPort))
|
||||||
|
|
||||||
|
return fmt.Sprintf("%s/%s@host", strings.Join(parts, ":"), p.Protocol), nil
|
||||||
|
default:
|
||||||
|
return "", fmt.Errorf("not implemented for mode: '%s'", p.Mode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParsePortSpec(port string) (PortSpec, error) {
|
||||||
|
spec := PortSpec{
|
||||||
|
Protocol: ProtocolTCP, // Default protocol.
|
||||||
|
Mode: PortModeIngress, // Default mode.
|
||||||
|
}
|
||||||
|
|
||||||
|
// Split off mode first.
|
||||||
|
parts := strings.Split(port, "@")
|
||||||
|
if len(parts) > 2 {
|
||||||
|
return spec, fmt.Errorf("too many '@' symbols")
|
||||||
|
}
|
||||||
|
if len(parts) == 2 {
|
||||||
|
if parts[1] != PortModeHost {
|
||||||
|
return spec, fmt.Errorf("invalid mode: '%s', only 'host' mode is supported", parts[1])
|
||||||
|
}
|
||||||
|
spec.Mode = PortModeHost
|
||||||
|
}
|
||||||
|
port = parts[0]
|
||||||
|
|
||||||
|
// Parse protocol.
|
||||||
|
parts = strings.Split(port, "/")
|
||||||
|
if len(parts) > 2 {
|
||||||
|
return spec, fmt.Errorf("too many '/' symbols")
|
||||||
|
}
|
||||||
|
specifiedProtocol := ""
|
||||||
|
if len(parts) == 2 {
|
||||||
|
protocol := parts[1]
|
||||||
|
switch protocol {
|
||||||
|
case ProtocolHTTP, ProtocolHTTPS, ProtocolTCP, ProtocolUDP:
|
||||||
|
spec.Protocol = protocol
|
||||||
|
specifiedProtocol = protocol
|
||||||
|
default:
|
||||||
|
return spec, fmt.Errorf("unsupported protocol: '%s'", protocol)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
port = parts[0]
|
||||||
|
|
||||||
|
// Parse hostname/host IP and ports.
|
||||||
|
parts = splitPortParts(port)
|
||||||
|
var err error
|
||||||
|
|
||||||
|
switch len(parts) {
|
||||||
|
case 1: // Just container port.
|
||||||
|
if spec.ContainerPort, err = parsePort(parts[0]); err != nil {
|
||||||
|
return spec, fmt.Errorf("invalid container port '%s': %w", parts[0], err)
|
||||||
|
}
|
||||||
|
|
||||||
|
case 2: // hostname:container_port or [load_balancer_port|host_port]:container_port
|
||||||
|
if spec.ContainerPort, err = parsePort(parts[1]); err != nil {
|
||||||
|
return spec, fmt.Errorf("invalid container port '%s': %w", parts[1], err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if parts[0] == "" {
|
||||||
|
return spec, fmt.Errorf("hostname or published port must be specified, format: " +
|
||||||
|
"hostname:container_port or published_port:container_port")
|
||||||
|
}
|
||||||
|
// Try to parse the first part as port.
|
||||||
|
if publishedPort, err := parsePort(parts[0]); err == nil {
|
||||||
|
spec.PublishedPort = publishedPort
|
||||||
|
} else {
|
||||||
|
// It's a hostname.
|
||||||
|
if spec.Mode == PortModeHost {
|
||||||
|
return spec, fmt.Errorf("hostname cannot be specified in host mode")
|
||||||
|
}
|
||||||
|
spec.Hostname = parts[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
case 3: // hostname:load_balancer_port:container_port or host_ip:host_port:container_port
|
||||||
|
if spec.ContainerPort, err = parsePort(parts[2]); err != nil {
|
||||||
|
return spec, fmt.Errorf("invalid container port '%s': %w", parts[2], err)
|
||||||
|
}
|
||||||
|
if spec.PublishedPort, err = parsePort(parts[1]); err != nil {
|
||||||
|
return spec, fmt.Errorf("invalid published port '%s': %w", parts[1], err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if spec.Mode == PortModeHost {
|
||||||
|
// In host mode, the first part must be IP.
|
||||||
|
ip := parts[0]
|
||||||
|
// Strip brackets from IPv6 address if present.
|
||||||
|
if strings.Contains(ip, ":") {
|
||||||
|
if !strings.HasPrefix(ip, "[") {
|
||||||
|
return spec, fmt.Errorf(
|
||||||
|
"invalid host IP '%s': IPv6 address must be enclosed in square brackets", ip)
|
||||||
|
}
|
||||||
|
if !strings.HasSuffix(ip, "]") {
|
||||||
|
return spec, fmt.Errorf("invalid host IP '%s': missing closing bracket", ip)
|
||||||
|
}
|
||||||
|
ip = ip[1 : len(ip)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
if spec.HostIP, err = netip.ParseAddr(ip); err != nil {
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
spec.Hostname = parts[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return spec, fmt.Errorf("unexpected number of parts in port spec: %d", len(parts))
|
||||||
|
}
|
||||||
|
|
||||||
|
if spec.Hostname != "" {
|
||||||
|
if specifiedProtocol == "" {
|
||||||
|
spec.Protocol = ProtocolHTTPS
|
||||||
|
} else if specifiedProtocol != ProtocolHTTP && specifiedProtocol != ProtocolHTTPS {
|
||||||
|
return spec, fmt.Errorf("hostname is only valid with '%s' or '%s' protocols, specified: '%s'",
|
||||||
|
ProtocolHTTP, ProtocolHTTPS, specifiedProtocol)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return spec, spec.Validate()
|
||||||
|
}
|
||||||
|
|
||||||
|
// splitPortParts splits a port specification [hostname|host_ip:][published_port:]container_port into its parts.
|
||||||
|
func splitPortParts(port string) []string {
|
||||||
|
parts := strings.Split(port, ":")
|
||||||
|
n := len(parts)
|
||||||
|
if n > 3 {
|
||||||
|
// Host IP may contain colons if it's IPv6, so we need to join the first n-2 parts.
|
||||||
|
return append([]string{strings.Join(parts[:n-2], ":")}, parts[n-2:]...)
|
||||||
|
}
|
||||||
|
return parts
|
||||||
|
}
|
||||||
|
|
||||||
|
func parsePort(s string) (uint16, error) {
|
||||||
|
port, err := strconv.ParseUint(s, 10, 16)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return uint16(port), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateHostname(hostname string) error {
|
||||||
|
if hostname == "" {
|
||||||
|
return fmt.Errorf("must not be empty")
|
||||||
|
}
|
||||||
|
if !strings.Contains(hostname, ".") {
|
||||||
|
return fmt.Errorf("must be a valid domain name containing at least one dot")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,698 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"net/netip"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPortSpec_Validate(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
spec PortSpec
|
||||||
|
wantErr string
|
||||||
|
}{
|
||||||
|
// Valid ingress mode.
|
||||||
|
{
|
||||||
|
name: "ingress mode tcp",
|
||||||
|
spec: PortSpec{
|
||||||
|
ContainerPort: 8080,
|
||||||
|
Protocol: ProtocolTCP,
|
||||||
|
Mode: PortModeIngress,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ingress mode with published tcp port",
|
||||||
|
spec: PortSpec{
|
||||||
|
PublishedPort: 80,
|
||||||
|
ContainerPort: 8080,
|
||||||
|
Protocol: ProtocolTCP,
|
||||||
|
Mode: PortModeIngress,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ingress mode udp",
|
||||||
|
spec: PortSpec{
|
||||||
|
ContainerPort: 8080,
|
||||||
|
Protocol: ProtocolUDP,
|
||||||
|
Mode: PortModeIngress,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ingress mode with published udp port",
|
||||||
|
spec: PortSpec{
|
||||||
|
PublishedPort: 80,
|
||||||
|
ContainerPort: 8080,
|
||||||
|
Protocol: ProtocolUDP,
|
||||||
|
Mode: PortModeIngress,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ingress mode with hostname and http",
|
||||||
|
spec: PortSpec{
|
||||||
|
Hostname: "app.example.com",
|
||||||
|
ContainerPort: 8080,
|
||||||
|
Protocol: ProtocolHTTP,
|
||||||
|
Mode: PortModeIngress,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ingress mode with hostname and https",
|
||||||
|
spec: PortSpec{
|
||||||
|
Hostname: "app.example.com",
|
||||||
|
ContainerPort: 8080,
|
||||||
|
Protocol: ProtocolHTTPS,
|
||||||
|
Mode: PortModeIngress,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ingress mode with hostname and published port",
|
||||||
|
spec: PortSpec{
|
||||||
|
Hostname: "app.example.com",
|
||||||
|
PublishedPort: 6443,
|
||||||
|
ContainerPort: 8080,
|
||||||
|
Protocol: ProtocolHTTPS,
|
||||||
|
Mode: PortModeIngress,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// Valid host mode.
|
||||||
|
{
|
||||||
|
name: "host mode tcp",
|
||||||
|
spec: PortSpec{
|
||||||
|
PublishedPort: 80,
|
||||||
|
ContainerPort: 8080,
|
||||||
|
Protocol: ProtocolUDP,
|
||||||
|
Mode: PortModeHost,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "host mode udp",
|
||||||
|
spec: PortSpec{
|
||||||
|
PublishedPort: 80,
|
||||||
|
ContainerPort: 8080,
|
||||||
|
Protocol: ProtocolUDP,
|
||||||
|
Mode: PortModeHost,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "host mode with IPv4",
|
||||||
|
spec: PortSpec{
|
||||||
|
HostIP: netip.MustParseAddr("127.0.0.1"),
|
||||||
|
PublishedPort: 80,
|
||||||
|
ContainerPort: 8080,
|
||||||
|
Protocol: ProtocolTCP,
|
||||||
|
Mode: PortModeHost,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "host mode with IPv6",
|
||||||
|
spec: PortSpec{
|
||||||
|
HostIP: netip.MustParseAddr("2001:db8::1234:5678"),
|
||||||
|
PublishedPort: 80,
|
||||||
|
ContainerPort: 8080,
|
||||||
|
Protocol: ProtocolUDP,
|
||||||
|
Mode: PortModeHost,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// Error cases.
|
||||||
|
{
|
||||||
|
name: "missing container port",
|
||||||
|
spec: PortSpec{
|
||||||
|
Protocol: ProtocolTCP,
|
||||||
|
},
|
||||||
|
wantErr: "container port must be non-zero",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid protocol",
|
||||||
|
spec: PortSpec{
|
||||||
|
ContainerPort: 8080,
|
||||||
|
Protocol: "invalid",
|
||||||
|
},
|
||||||
|
wantErr: "invalid protocol 'invalid'",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid mode",
|
||||||
|
spec: PortSpec{
|
||||||
|
ContainerPort: 8080,
|
||||||
|
Protocol: ProtocolTCP,
|
||||||
|
Mode: "invalid",
|
||||||
|
},
|
||||||
|
wantErr: "invalid mode: 'invalid'",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "hostname with non-http protocol",
|
||||||
|
spec: PortSpec{
|
||||||
|
Hostname: "app.example.com",
|
||||||
|
ContainerPort: 8080,
|
||||||
|
Protocol: ProtocolTCP,
|
||||||
|
Mode: PortModeIngress,
|
||||||
|
},
|
||||||
|
wantErr: "hostname is only valid with 'http' or 'https' protocols",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid hostname",
|
||||||
|
spec: PortSpec{
|
||||||
|
Hostname: "app",
|
||||||
|
ContainerPort: 8080,
|
||||||
|
Protocol: ProtocolHTTPS,
|
||||||
|
Mode: PortModeIngress,
|
||||||
|
},
|
||||||
|
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{
|
||||||
|
HostIP: netip.MustParseAddr("127.0.0.1"),
|
||||||
|
ContainerPort: 8080,
|
||||||
|
Protocol: ProtocolTCP,
|
||||||
|
Mode: PortModeIngress,
|
||||||
|
},
|
||||||
|
wantErr: "host IP cannot be specified in ingress mode",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "zero published port in host mode",
|
||||||
|
spec: PortSpec{
|
||||||
|
ContainerPort: 8080,
|
||||||
|
Protocol: ProtocolTCP,
|
||||||
|
Mode: PortModeHost,
|
||||||
|
},
|
||||||
|
wantErr: "published port is required in host mode",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "hostname in host mode",
|
||||||
|
spec: PortSpec{
|
||||||
|
Hostname: "app.example.com",
|
||||||
|
PublishedPort: 80,
|
||||||
|
ContainerPort: 8080,
|
||||||
|
Protocol: ProtocolTCP,
|
||||||
|
Mode: PortModeHost,
|
||||||
|
},
|
||||||
|
wantErr: "hostname cannot be specified in host mode",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "http in host mode",
|
||||||
|
spec: PortSpec{
|
||||||
|
PublishedPort: 80,
|
||||||
|
ContainerPort: 8080,
|
||||||
|
Protocol: ProtocolHTTP,
|
||||||
|
Mode: PortModeHost,
|
||||||
|
},
|
||||||
|
wantErr: "unsupported protocol 'http' in host mode",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "https in host mode",
|
||||||
|
spec: PortSpec{
|
||||||
|
PublishedPort: 80,
|
||||||
|
ContainerPort: 8080,
|
||||||
|
Protocol: ProtocolHTTPS,
|
||||||
|
Mode: PortModeHost,
|
||||||
|
},
|
||||||
|
wantErr: "unsupported protocol 'https' in host mode",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
err := tt.spec.Validate()
|
||||||
|
if tt.wantErr != "" {
|
||||||
|
require.Error(t, err, tt.wantErr)
|
||||||
|
assert.Contains(t, err.Error(), tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
require.NoError(t, err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPortSpec_String(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
spec PortSpec
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
// Ingress mode.
|
||||||
|
{
|
||||||
|
name: "container port only",
|
||||||
|
spec: PortSpec{
|
||||||
|
ContainerPort: 8080,
|
||||||
|
Protocol: ProtocolTCP,
|
||||||
|
Mode: PortModeIngress,
|
||||||
|
},
|
||||||
|
expected: "8080/tcp",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "container port udp",
|
||||||
|
spec: PortSpec{
|
||||||
|
ContainerPort: 8080,
|
||||||
|
Protocol: ProtocolUDP,
|
||||||
|
Mode: PortModeIngress,
|
||||||
|
},
|
||||||
|
expected: "8080/udp",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "published and container port",
|
||||||
|
spec: PortSpec{
|
||||||
|
ContainerPort: 8080,
|
||||||
|
PublishedPort: 80,
|
||||||
|
Protocol: ProtocolTCP,
|
||||||
|
Mode: PortModeIngress,
|
||||||
|
},
|
||||||
|
expected: "80:8080/tcp",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "hostname and container port https",
|
||||||
|
spec: PortSpec{
|
||||||
|
Hostname: "app.example.com",
|
||||||
|
ContainerPort: 8080,
|
||||||
|
Protocol: ProtocolHTTPS,
|
||||||
|
Mode: PortModeIngress,
|
||||||
|
},
|
||||||
|
expected: "app.example.com:8080/https",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "hostname and container port http",
|
||||||
|
spec: PortSpec{
|
||||||
|
Hostname: "app.example.com",
|
||||||
|
ContainerPort: 8080,
|
||||||
|
Protocol: ProtocolHTTP,
|
||||||
|
Mode: PortModeIngress,
|
||||||
|
},
|
||||||
|
expected: "app.example.com:8080/http",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "hostname and published and container port https",
|
||||||
|
spec: PortSpec{
|
||||||
|
Hostname: "app.example.com",
|
||||||
|
PublishedPort: 6443,
|
||||||
|
ContainerPort: 8080,
|
||||||
|
Protocol: ProtocolHTTPS,
|
||||||
|
Mode: PortModeIngress,
|
||||||
|
},
|
||||||
|
expected: "app.example.com:6443:8080/https",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "hostname and published and container port http",
|
||||||
|
spec: PortSpec{
|
||||||
|
Hostname: "app.example.com",
|
||||||
|
PublishedPort: 6443,
|
||||||
|
ContainerPort: 8080,
|
||||||
|
Protocol: ProtocolHTTP,
|
||||||
|
Mode: PortModeIngress,
|
||||||
|
},
|
||||||
|
expected: "app.example.com:6443:8080/http",
|
||||||
|
},
|
||||||
|
|
||||||
|
// Host mode.
|
||||||
|
{
|
||||||
|
name: "host mode tcp",
|
||||||
|
spec: PortSpec{
|
||||||
|
PublishedPort: 80,
|
||||||
|
ContainerPort: 8080,
|
||||||
|
Protocol: ProtocolTCP,
|
||||||
|
Mode: PortModeHost,
|
||||||
|
},
|
||||||
|
expected: "80:8080/tcp@host",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "host mode udp",
|
||||||
|
spec: PortSpec{
|
||||||
|
PublishedPort: 80,
|
||||||
|
ContainerPort: 8080,
|
||||||
|
Protocol: ProtocolUDP,
|
||||||
|
Mode: PortModeHost,
|
||||||
|
},
|
||||||
|
expected: "80:8080/udp@host",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "host mode with IPv4 tcp",
|
||||||
|
spec: PortSpec{
|
||||||
|
HostIP: netip.MustParseAddr("127.0.0.1"),
|
||||||
|
PublishedPort: 80,
|
||||||
|
ContainerPort: 8080,
|
||||||
|
Protocol: ProtocolTCP,
|
||||||
|
Mode: PortModeHost,
|
||||||
|
},
|
||||||
|
expected: "127.0.0.1:80:8080/tcp@host",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "host mode with IPv4 udp",
|
||||||
|
spec: PortSpec{
|
||||||
|
HostIP: netip.MustParseAddr("127.0.0.1"),
|
||||||
|
PublishedPort: 80,
|
||||||
|
ContainerPort: 8080,
|
||||||
|
Protocol: ProtocolUDP,
|
||||||
|
Mode: PortModeHost,
|
||||||
|
},
|
||||||
|
expected: "127.0.0.1:80:8080/udp@host",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "host mode with IPv6",
|
||||||
|
spec: PortSpec{
|
||||||
|
HostIP: netip.MustParseAddr("2001:db8::1234:5678"),
|
||||||
|
PublishedPort: 80,
|
||||||
|
ContainerPort: 8080,
|
||||||
|
Protocol: ProtocolTCP,
|
||||||
|
Mode: PortModeHost,
|
||||||
|
},
|
||||||
|
expected: "[2001:db8::1234:5678]:80:8080/tcp@host",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result, err := tt.spec.String()
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, tt.expected, result)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParsePortSpec(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
port string
|
||||||
|
expected PortSpec
|
||||||
|
wantErr string
|
||||||
|
}{
|
||||||
|
// Ingress mode (default).
|
||||||
|
{
|
||||||
|
name: "container port only",
|
||||||
|
port: "8080",
|
||||||
|
expected: PortSpec{
|
||||||
|
ContainerPort: 8080,
|
||||||
|
Protocol: ProtocolTCP,
|
||||||
|
Mode: PortModeIngress,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "published and container port",
|
||||||
|
port: "8000:8080",
|
||||||
|
expected: PortSpec{
|
||||||
|
PublishedPort: 8000,
|
||||||
|
ContainerPort: 8080,
|
||||||
|
Protocol: ProtocolTCP,
|
||||||
|
Mode: PortModeIngress,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "published port zero",
|
||||||
|
port: "0:8080",
|
||||||
|
expected: PortSpec{
|
||||||
|
ContainerPort: 8080,
|
||||||
|
Protocol: ProtocolTCP,
|
||||||
|
Mode: PortModeIngress,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "tcp protocol explicit",
|
||||||
|
port: "8080/tcp",
|
||||||
|
expected: PortSpec{
|
||||||
|
ContainerPort: 8080,
|
||||||
|
Protocol: ProtocolTCP,
|
||||||
|
Mode: PortModeIngress,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "udp protocol",
|
||||||
|
port: "8080/udp",
|
||||||
|
expected: PortSpec{
|
||||||
|
ContainerPort: 8080,
|
||||||
|
Protocol: ProtocolUDP,
|
||||||
|
Mode: PortModeIngress,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "published port udp",
|
||||||
|
port: "8000:8080/udp",
|
||||||
|
expected: PortSpec{
|
||||||
|
PublishedPort: 8000,
|
||||||
|
ContainerPort: 8080,
|
||||||
|
Protocol: ProtocolUDP,
|
||||||
|
Mode: PortModeIngress,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "hostname and container port",
|
||||||
|
port: "app.example.com:8080",
|
||||||
|
expected: PortSpec{
|
||||||
|
Hostname: "app.example.com",
|
||||||
|
ContainerPort: 8080,
|
||||||
|
Protocol: ProtocolHTTPS,
|
||||||
|
Mode: PortModeIngress,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "hostname and container port http",
|
||||||
|
port: "app.example.com:8080/http",
|
||||||
|
expected: PortSpec{
|
||||||
|
Hostname: "app.example.com",
|
||||||
|
ContainerPort: 8080,
|
||||||
|
Protocol: ProtocolHTTP,
|
||||||
|
Mode: PortModeIngress,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "hostname and published port",
|
||||||
|
port: "app.example.com:6443:8080",
|
||||||
|
expected: PortSpec{
|
||||||
|
Hostname: "app.example.com",
|
||||||
|
PublishedPort: 6443,
|
||||||
|
ContainerPort: 8080,
|
||||||
|
Protocol: ProtocolHTTPS,
|
||||||
|
Mode: PortModeIngress,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "hostname and published port http",
|
||||||
|
port: "app.example.com:8000:8080/http",
|
||||||
|
expected: PortSpec{
|
||||||
|
Hostname: "app.example.com",
|
||||||
|
PublishedPort: 8000,
|
||||||
|
ContainerPort: 8080,
|
||||||
|
Protocol: ProtocolHTTP,
|
||||||
|
Mode: PortModeIngress,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// Host mode.
|
||||||
|
{
|
||||||
|
name: "host mode published with protocol",
|
||||||
|
port: "80:8080/udp@host",
|
||||||
|
expected: PortSpec{
|
||||||
|
PublishedPort: 80,
|
||||||
|
ContainerPort: 8080,
|
||||||
|
Protocol: ProtocolUDP,
|
||||||
|
Mode: PortModeHost,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "host mode with IPv4",
|
||||||
|
port: "127.0.0.1:80:8080@host",
|
||||||
|
expected: PortSpec{
|
||||||
|
HostIP: netip.MustParseAddr("127.0.0.1"),
|
||||||
|
PublishedPort: 80,
|
||||||
|
ContainerPort: 8080,
|
||||||
|
Protocol: ProtocolTCP,
|
||||||
|
Mode: PortModeHost,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "host mode with IPv6",
|
||||||
|
port: "[2001:db8::1234:5678]:80:8080@host",
|
||||||
|
expected: PortSpec{
|
||||||
|
HostIP: netip.MustParseAddr("2001:db8::1234:5678"),
|
||||||
|
PublishedPort: 80,
|
||||||
|
ContainerPort: 8080,
|
||||||
|
Protocol: ProtocolTCP,
|
||||||
|
Mode: PortModeHost,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "host mode with IP and protocol",
|
||||||
|
port: "127.0.0.1:80:8080/udp@host",
|
||||||
|
expected: PortSpec{
|
||||||
|
HostIP: netip.MustParseAddr("127.0.0.1"),
|
||||||
|
PublishedPort: 80,
|
||||||
|
ContainerPort: 8080,
|
||||||
|
Protocol: ProtocolUDP,
|
||||||
|
Mode: PortModeHost,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// Error cases.
|
||||||
|
{
|
||||||
|
name: "empty",
|
||||||
|
port: "",
|
||||||
|
wantErr: "invalid container port",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid container port",
|
||||||
|
port: "invalid",
|
||||||
|
wantErr: "invalid container port",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "container port zero",
|
||||||
|
port: "0",
|
||||||
|
wantErr: "container port must be non-zero",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "out of range container port",
|
||||||
|
port: "100500",
|
||||||
|
wantErr: "invalid container port",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "just protocol",
|
||||||
|
port: "/tcp",
|
||||||
|
wantErr: "invalid container port",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "just mode",
|
||||||
|
port: "@host",
|
||||||
|
wantErr: "invalid container port",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "multiple @ symbols",
|
||||||
|
port: "8080@host@host",
|
||||||
|
wantErr: "too many '@' symbols",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid mode",
|
||||||
|
port: "8080@invalid",
|
||||||
|
wantErr: "invalid mode: 'invalid'",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "multiple protocols",
|
||||||
|
port: "8080/tcp/udp",
|
||||||
|
wantErr: "too many '/' symbols",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid protocol",
|
||||||
|
port: "8080/invalid",
|
||||||
|
wantErr: "unsupported protocol: 'invalid'",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid published port",
|
||||||
|
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",
|
||||||
|
wantErr: "invalid hostname 'app': must be a valid domain name containing at least one dot",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "hostname with tcp protocol",
|
||||||
|
port: "app.example.com:8080/tcp",
|
||||||
|
wantErr: "hostname is only valid with 'http' or 'https' protocols",
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: "missing published port in host mode",
|
||||||
|
port: "8080@host",
|
||||||
|
wantErr: "published port is required in host mode",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing published port with protocol in host mode",
|
||||||
|
port: "8080/udp@host",
|
||||||
|
wantErr: "published port is required in host mode",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid host IPv4",
|
||||||
|
port: "300.0.0.1:80:8080@host",
|
||||||
|
wantErr: "invalid host IP",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid host IPv6",
|
||||||
|
port: "[:::1]:80:8080@host",
|
||||||
|
wantErr: "invalid host IP",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing closing bracket in IPv6",
|
||||||
|
port: "[::1:80:8080@host",
|
||||||
|
wantErr: "invalid host IP",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing brackets in IPv6",
|
||||||
|
port: "2001:db8::1234:5678:80:8080@host",
|
||||||
|
wantErr: "invalid host IP",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "http in host mode",
|
||||||
|
port: "80:8080/http@host",
|
||||||
|
wantErr: "unsupported protocol 'http' in host mode, only 'tcp' and 'udp' are supported",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "https in host mode",
|
||||||
|
port: "80:8080/https@host",
|
||||||
|
wantErr: "unsupported protocol 'https' in host mode, only 'tcp' and 'udp' are supported",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "hostname in host mode",
|
||||||
|
port: "app.example.com:8080@host",
|
||||||
|
wantErr: "hostname cannot be specified in host mode",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "hostname with invalid published port",
|
||||||
|
port: "app.example.com:invalid:8080@host",
|
||||||
|
wantErr: "invalid published port",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
spec, err := ParsePortSpec(tt.port)
|
||||||
|
if tt.wantErr != "" {
|
||||||
|
require.Error(t, err, "Expected error: %s, got nil, spec: %+v", tt.wantErr, spec)
|
||||||
|
assert.Contains(t, err.Error(), tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, tt.expected, spec)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"github.com/distribution/reference"
|
||||||
|
"uncloud/internal/machine/api/pb"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
ServiceModeReplicated = "replicated"
|
||||||
|
ServiceModeGlobal = "global"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ServiceSpec struct {
|
||||||
|
Container ContainerSpec
|
||||||
|
// Mode is the replication mode of the service. Default is ServiceModeReplicated if empty.
|
||||||
|
Mode string
|
||||||
|
Name string
|
||||||
|
// Ports defines what service ports to publish to make the service accessible outside the cluster.
|
||||||
|
Ports []PortSpec
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ServiceSpec) Validate() error {
|
||||||
|
if err := s.Container.Validate(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch s.Mode {
|
||||||
|
case "", ServiceModeGlobal, ServiceModeReplicated:
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("invalid mode: %q", s.Mode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: validate there is no conflict between ports.
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type ContainerSpec struct {
|
||||||
|
Command []string
|
||||||
|
Image string
|
||||||
|
// Run a custom init inside the container. If nil, use the daemon's configured settings.
|
||||||
|
Init *bool
|
||||||
|
// List of volumes to bind mount into the container.
|
||||||
|
Volumes []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ContainerSpec) Validate() error {
|
||||||
|
_, err := reference.ParseDockerRef(s.Image)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid image: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type Service struct {
|
||||||
|
ID string
|
||||||
|
Name string
|
||||||
|
Mode string
|
||||||
|
Containers []MachineContainer
|
||||||
|
}
|
||||||
|
|
||||||
|
type MachineContainer struct {
|
||||||
|
MachineID string
|
||||||
|
Container Container
|
||||||
|
}
|
||||||
|
|
||||||
|
func ServiceFromProto(s *pb.Service) (Service, error) {
|
||||||
|
var err error
|
||||||
|
containers := make([]MachineContainer, len(s.Containers))
|
||||||
|
for i, sc := range s.Containers {
|
||||||
|
containers[i], err = machineContainerFromProto(sc)
|
||||||
|
if err != nil {
|
||||||
|
return Service{}, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Service{
|
||||||
|
ID: s.Id,
|
||||||
|
Name: s.Name,
|
||||||
|
Mode: s.Mode,
|
||||||
|
Containers: containers,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func machineContainerFromProto(sc *pb.Service_Container) (MachineContainer, error) {
|
||||||
|
var c Container
|
||||||
|
if err := json.Unmarshal(sc.Container, &c); err != nil {
|
||||||
|
return MachineContainer{}, fmt.Errorf("unmarshal container: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return MachineContainer{
|
||||||
|
MachineID: sc.MachineId,
|
||||||
|
Container: c,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
+65
-125
@@ -7,24 +7,16 @@ import (
|
|||||||
"github.com/charmbracelet/huh"
|
"github.com/charmbracelet/huh"
|
||||||
"google.golang.org/protobuf/types/known/emptypb"
|
"google.golang.org/protobuf/types/known/emptypb"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
"text/tabwriter"
|
|
||||||
"uncloud/internal/cli/client"
|
"uncloud/internal/cli/client"
|
||||||
"uncloud/internal/cli/client/connector"
|
"uncloud/internal/cli/client/connector"
|
||||||
"uncloud/internal/cli/config"
|
"uncloud/internal/cli/config"
|
||||||
"uncloud/internal/machine"
|
"uncloud/internal/machine"
|
||||||
"uncloud/internal/machine/api/pb"
|
"uncloud/internal/machine/api/pb"
|
||||||
"uncloud/internal/secret"
|
|
||||||
"uncloud/internal/sshexec"
|
"uncloud/internal/sshexec"
|
||||||
)
|
)
|
||||||
|
|
||||||
const defaultClusterName = "default"
|
const defaultClusterName = "default"
|
||||||
|
|
||||||
var (
|
|
||||||
ErrNotFound = errors.New("not found")
|
|
||||||
)
|
|
||||||
|
|
||||||
type CLI struct {
|
type CLI struct {
|
||||||
config *config.Config
|
config *config.Config
|
||||||
}
|
}
|
||||||
@@ -39,35 +31,19 @@ func New(configPath string) (*CLI, error) {
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cli *CLI) CreateCluster(name string, userPrivateKey secret.Secret) error {
|
func (cli *CLI) CreateCluster(name string) error {
|
||||||
if _, ok := cli.config.Clusters[name]; ok {
|
if _, ok := cli.config.Clusters[name]; ok {
|
||||||
return fmt.Errorf("cluster %q already exists", name)
|
return fmt.Errorf("cluster %q already exists", name)
|
||||||
}
|
}
|
||||||
if userPrivateKey == nil {
|
|
||||||
user, err := client.NewUser(nil)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("generate user: %w", err)
|
|
||||||
}
|
|
||||||
userPrivateKey = user.PrivateKey()
|
|
||||||
}
|
|
||||||
|
|
||||||
cli.config.Clusters[name] = &config.Cluster{
|
cli.config.Clusters[name] = &config.Cluster{
|
||||||
Name: name,
|
Name: name,
|
||||||
UserPrivateKey: userPrivateKey,
|
|
||||||
}
|
}
|
||||||
return cli.config.Save()
|
return cli.config.Save()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cli *CLI) CreateDefaultCluster() error {
|
|
||||||
if err := cli.CreateCluster(defaultClusterName, nil); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return cli.SetCurrentCluster(defaultClusterName)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cli *CLI) SetCurrentCluster(name string) error {
|
func (cli *CLI) SetCurrentCluster(name string) error {
|
||||||
if _, ok := cli.config.Clusters[name]; !ok {
|
if _, ok := cli.config.Clusters[name]; !ok {
|
||||||
return ErrNotFound
|
return client.ErrNotFound
|
||||||
}
|
}
|
||||||
cli.config.CurrentCluster = name
|
cli.config.CurrentCluster = name
|
||||||
return cli.config.Save()
|
return cli.config.Save()
|
||||||
@@ -108,16 +84,21 @@ func (cli *CLI) ConnectCluster(ctx context.Context, clusterName string) (*client
|
|||||||
|
|
||||||
// TODO: iterate over all connections and try to connect to the cluster using the first successful connection.
|
// TODO: iterate over all connections and try to connect to the cluster using the first successful connection.
|
||||||
conn := cfg.Connections[0]
|
conn := cfg.Connections[0]
|
||||||
user, host, port, err := conn.SSH.Parse()
|
if conn.SSH != "" {
|
||||||
if err != nil {
|
user, host, port, err := conn.SSH.Parse()
|
||||||
return nil, fmt.Errorf("parse SSH connection %q: %w", conn.SSH, err)
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("parse SSH connection %q: %w", conn.SSH, err)
|
||||||
|
}
|
||||||
|
sshConfig := &connector.SSHConnectorConfig{
|
||||||
|
User: user,
|
||||||
|
Host: host,
|
||||||
|
Port: port,
|
||||||
|
}
|
||||||
|
return client.New(ctx, connector.NewSSHConnector(sshConfig))
|
||||||
|
} else if conn.TCP.IsValid() {
|
||||||
|
return client.New(ctx, connector.NewTCPConnector(conn.TCP))
|
||||||
}
|
}
|
||||||
sshConfig := &connector.SSHConnectorConfig{
|
return nil, errors.New("no valid connection configuration found for the cluster")
|
||||||
User: user,
|
|
||||||
Host: host,
|
|
||||||
Port: port,
|
|
||||||
}
|
|
||||||
return client.New(ctx, connector.NewSSHConnector(sshConfig))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cli *CLI) InitCluster(
|
func (cli *CLI) InitCluster(
|
||||||
@@ -139,29 +120,10 @@ func (cli *CLI) initRemoteMachine(
|
|||||||
if _, ok := cli.config.Clusters[clusterName]; ok {
|
if _, ok := cli.config.Clusters[clusterName]; ok {
|
||||||
return fmt.Errorf("cluster %q already exists", clusterName)
|
return fmt.Errorf("cluster %q already exists", clusterName)
|
||||||
}
|
}
|
||||||
user, err := client.NewUser(nil)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("generate cluster user: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Provision the remote machine by installing the Uncloud daemon and dependencies over SSH.
|
machineClient, err := cli.provisionRemoteMachine(ctx, remoteMachine)
|
||||||
sshClient, err := sshexec.Connect(remoteMachine.User, remoteMachine.Host, remoteMachine.Port, remoteMachine.KeyPath)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf(
|
return err
|
||||||
"SSH login to remote machine %s: %w",
|
|
||||||
config.NewSSHDestination(remoteMachine.User, remoteMachine.Host, remoteMachine.Port), err,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
exec := sshexec.NewRemote(sshClient)
|
|
||||||
// Install and run the Uncloud daemon and dependencies on the remote machine.
|
|
||||||
if err = provisionMachine(ctx, exec); err != nil {
|
|
||||||
return fmt.Errorf("provision machine: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a machine API client over the SSH connection to the remote machine.
|
|
||||||
machineClient, err := client.New(ctx, connector.NewSSHConnectorFromClient(sshClient))
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("connect to remote machine: %w", err)
|
|
||||||
}
|
}
|
||||||
defer machineClient.Close()
|
defer machineClient.Close()
|
||||||
|
|
||||||
@@ -179,12 +141,6 @@ func (cli *CLI) initRemoteMachine(
|
|||||||
req := &pb.InitClusterRequest{
|
req := &pb.InitClusterRequest{
|
||||||
MachineName: machineName,
|
MachineName: machineName,
|
||||||
Network: pb.NewIPPrefix(netPrefix),
|
Network: pb.NewIPPrefix(netPrefix),
|
||||||
User: &pb.User{
|
|
||||||
Network: &pb.NetworkConfig{
|
|
||||||
ManagementIp: pb.NewIP(user.ManagementIP()),
|
|
||||||
PublicKey: user.PublicKey(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
resp, err := machineClient.InitCluster(ctx, req)
|
resp, err := machineClient.InitCluster(ctx, req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -192,7 +148,7 @@ func (cli *CLI) initRemoteMachine(
|
|||||||
}
|
}
|
||||||
fmt.Printf("Cluster %q initialised with machine %q\n", clusterName, resp.Machine.Name)
|
fmt.Printf("Cluster %q initialised with machine %q\n", clusterName, resp.Machine.Name)
|
||||||
|
|
||||||
if err = cli.CreateCluster(clusterName, user.PrivateKey()); err != nil {
|
if err = cli.CreateCluster(clusterName); err != nil {
|
||||||
return fmt.Errorf("save cluster to config: %w", err)
|
return fmt.Errorf("save cluster to config: %w", err)
|
||||||
}
|
}
|
||||||
// Set the current cluster to the just created one if it is the only cluster in the config.
|
// Set the current cluster to the just created one if it is the only cluster in the config.
|
||||||
@@ -221,24 +177,9 @@ func (cli *CLI) AddMachine(ctx context.Context, remoteMachine RemoteMachine, clu
|
|||||||
_ = c.Close()
|
_ = c.Close()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// Provision the remote machine by installing the Uncloud daemon and dependencies over SSH.
|
machineClient, err := cli.provisionRemoteMachine(ctx, remoteMachine)
|
||||||
sshClient, err := sshexec.Connect(remoteMachine.User, remoteMachine.Host, remoteMachine.Port, remoteMachine.KeyPath)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf(
|
return err
|
||||||
"SSH login to remote machine %s: %w",
|
|
||||||
config.NewSSHDestination(remoteMachine.User, remoteMachine.Host, remoteMachine.Port), err,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
exec := sshexec.NewRemote(sshClient)
|
|
||||||
// Install and run the Uncloud daemon and dependencies on the remote machine.
|
|
||||||
if err = provisionMachine(ctx, exec); err != nil {
|
|
||||||
return fmt.Errorf("provision machine: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a machine API client over the SSH connection to the remote machine.
|
|
||||||
machineClient, err := client.New(ctx, connector.NewSSHConnectorFromClient(sshClient))
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("connect to remote machine: %w", err)
|
|
||||||
}
|
}
|
||||||
defer machineClient.Close()
|
defer machineClient.Close()
|
||||||
|
|
||||||
@@ -280,14 +221,14 @@ func (cli *CLI) AddMachine(ctx context.Context, remoteMachine RemoteMachine, clu
|
|||||||
}
|
}
|
||||||
|
|
||||||
// List other machines in the cluster to include them in the join request.
|
// List other machines in the cluster to include them in the join request.
|
||||||
listResp, err := c.ListMachines(ctx, &emptypb.Empty{})
|
machines, err := c.ListMachines(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("list cluster machines: %w", err)
|
return fmt.Errorf("list cluster machines: %w", err)
|
||||||
}
|
}
|
||||||
otherMachines := make([]*pb.MachineInfo, 0, len(listResp.Machines)-1)
|
otherMachines := make([]*pb.MachineInfo, 0, len(machines)-1)
|
||||||
for _, m := range listResp.Machines {
|
for _, m := range machines {
|
||||||
if m.Id != addResp.Machine.Id {
|
if m.Machine.Id != addResp.Machine.Id {
|
||||||
otherMachines = append(otherMachines, m)
|
otherMachines = append(otherMachines, m.Machine)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -317,6 +258,44 @@ func (cli *CLI) AddMachine(ctx context.Context, remoteMachine RemoteMachine, clu
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// provisionRemoteMachine installs the Uncloud daemon and dependencies on the remote machine over SSH and returns
|
||||||
|
// a machine API client to interact with the machine. The client should be closed after use by the caller.
|
||||||
|
func (cli *CLI) provisionRemoteMachine(ctx context.Context, remoteMachine RemoteMachine) (*client.Client, error) {
|
||||||
|
// Provision the remote machine by installing the Uncloud daemon and dependencies over SSH.
|
||||||
|
sshClient, err := sshexec.Connect(remoteMachine.User, remoteMachine.Host, remoteMachine.Port, remoteMachine.KeyPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf(
|
||||||
|
"SSH login to remote machine %s: %w",
|
||||||
|
config.NewSSHDestination(remoteMachine.User, remoteMachine.Host, remoteMachine.Port), err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
exec := sshexec.NewRemote(sshClient)
|
||||||
|
// Install and run the Uncloud daemon and dependencies on the remote machine.
|
||||||
|
if err = provisionMachine(ctx, exec); err != nil {
|
||||||
|
return nil, fmt.Errorf("provision machine: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var machineClient *client.Client
|
||||||
|
if remoteMachine.User == "root" {
|
||||||
|
// Create a machine API client over the established SSH connection to the remote machine.
|
||||||
|
machineClient, err = client.New(ctx, connector.NewSSHConnectorFromClient(sshClient))
|
||||||
|
} else {
|
||||||
|
// Since the user is not root, we need to establish a new SSH connection to make the user's addition
|
||||||
|
// to the uncloud group effective, thus allowing access to the Uncloud daemon Unix socket.
|
||||||
|
sshConfig := &connector.SSHConnectorConfig{
|
||||||
|
User: remoteMachine.User,
|
||||||
|
Host: remoteMachine.Host,
|
||||||
|
Port: remoteMachine.Port,
|
||||||
|
KeyPath: remoteMachine.KeyPath,
|
||||||
|
}
|
||||||
|
machineClient, err = client.New(ctx, connector.NewSSHConnector(sshConfig))
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("connect to remote machine: %w", err)
|
||||||
|
}
|
||||||
|
return machineClient, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (cli *CLI) promptResetMachine() error {
|
func (cli *CLI) promptResetMachine() error {
|
||||||
var confirm bool
|
var confirm bool
|
||||||
form := huh.NewForm(
|
form := huh.NewForm(
|
||||||
@@ -340,42 +319,3 @@ func (cli *CLI) promptResetMachine() error {
|
|||||||
// TODO: implement resetting the remote machine.
|
// TODO: implement resetting the remote machine.
|
||||||
return fmt.Errorf("resetting the remote machine is not implemented yet")
|
return fmt.Errorf("resetting the remote machine is not implemented yet")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cli *CLI) ListMachines(ctx context.Context, clusterName string) error {
|
|
||||||
c, err := cli.ConnectCluster(ctx, clusterName)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("connect to cluster: %w", err)
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
_ = c.Close()
|
|
||||||
}()
|
|
||||||
|
|
||||||
listResp, err := c.ListMachines(ctx, &emptypb.Empty{})
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("list machines: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Print the list of machines in a table format.
|
|
||||||
tw := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', 0)
|
|
||||||
// Print header.
|
|
||||||
// TODO: print ADDRESS instead of SUBNET which is a machine IP with prefix.
|
|
||||||
if _, err = fmt.Fprintln(tw, "NAME\tSUBNET\tPUBLIC KEY\tENDPOINTS"); err != nil {
|
|
||||||
return fmt.Errorf("write header: %w", err)
|
|
||||||
}
|
|
||||||
// Print rows.
|
|
||||||
for _, m := range listResp.Machines {
|
|
||||||
subnet, _ := m.Network.Subnet.ToPrefix()
|
|
||||||
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\n", m.Name, subnet, publicKey, strings.Join(endpoints, ", "),
|
|
||||||
); err != nil {
|
|
||||||
return fmt.Errorf("write row: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return tw.Flush()
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -4,10 +4,15 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/docker/cli/cli/streams"
|
||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
|
"os"
|
||||||
"uncloud/internal/machine/api/pb"
|
"uncloud/internal/machine/api/pb"
|
||||||
|
"uncloud/internal/machine/docker"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var ErrNotFound = errors.New("not found")
|
||||||
|
|
||||||
// Client is a client for the machine API.
|
// Client is a client for the machine API.
|
||||||
type Client struct {
|
type Client struct {
|
||||||
connector Connector
|
connector Connector
|
||||||
@@ -15,8 +20,12 @@ type Client struct {
|
|||||||
|
|
||||||
pb.MachineClient
|
pb.MachineClient
|
||||||
pb.ClusterClient
|
pb.ClusterClient
|
||||||
|
*DockerClient
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DockerClient is a type alias for the Docker client to embed it in Client with a more specific name.
|
||||||
|
type DockerClient = docker.Client
|
||||||
|
|
||||||
// Connector is an interface for establishing a connection to the machine API.
|
// Connector is an interface for establishing a connection to the machine API.
|
||||||
type Connector interface {
|
type Connector interface {
|
||||||
Connect(ctx context.Context) (*grpc.ClientConn, error)
|
Connect(ctx context.Context) (*grpc.ClientConn, error)
|
||||||
@@ -37,10 +46,15 @@ func New(ctx context.Context, connector Connector) (*Client, error) {
|
|||||||
|
|
||||||
c.MachineClient = pb.NewMachineClient(c.conn)
|
c.MachineClient = pb.NewMachineClient(c.conn)
|
||||||
c.ClusterClient = pb.NewClusterClient(c.conn)
|
c.ClusterClient = pb.NewClusterClient(c.conn)
|
||||||
|
c.DockerClient = docker.NewClient(c.conn)
|
||||||
return c, nil
|
return c, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) Close() error {
|
func (cli *Client) Close() error {
|
||||||
err := c.conn.Close()
|
return errors.Join(cli.conn.Close(), cli.connector.Close())
|
||||||
return errors.Join(err, c.connector.Close())
|
}
|
||||||
|
|
||||||
|
// progressOut returns an output stream for progress writer.
|
||||||
|
func (cli *Client) progressOut() *streams.Out {
|
||||||
|
return streams.NewOut(os.Stdout)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"google.golang.org/protobuf/types/known/emptypb"
|
||||||
|
"uncloud/internal/machine/api/pb"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (cli *Client) ListMachines(ctx context.Context) ([]*pb.MachineMember, error) {
|
||||||
|
resp, err := cli.ClusterClient.ListMachines(ctx, &emptypb.Empty{})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return resp.Machines, nil
|
||||||
|
}
|
||||||
@@ -18,7 +18,7 @@ type SSHConnectorConfig struct {
|
|||||||
Port int
|
Port int
|
||||||
KeyPath string
|
KeyPath string
|
||||||
|
|
||||||
APISockPath string
|
SockPath string
|
||||||
}
|
}
|
||||||
|
|
||||||
// SSHConnector establishes a connection to the machine API through an SSH tunnel to the machine.
|
// SSHConnector establishes a connection to the machine API through an SSH tunnel to the machine.
|
||||||
@@ -49,12 +49,12 @@ func (c *SSHConnector) Connect(ctx context.Context) (*grpc.ClientConn, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
apiSockPath := c.config.APISockPath
|
sockPath := c.config.SockPath
|
||||||
if apiSockPath == "" {
|
if sockPath == "" {
|
||||||
apiSockPath = machine.DefaultAPISockPath
|
sockPath = machine.DefaultUncloudSockPath
|
||||||
}
|
}
|
||||||
conn, err := grpc.NewClient(
|
conn, err := grpc.NewClient(
|
||||||
"unix://"+apiSockPath,
|
"unix://"+sockPath,
|
||||||
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
||||||
grpc.WithContextDialer(
|
grpc.WithContextDialer(
|
||||||
func(ctx context.Context, addr string) (net.Conn, error) {
|
func(ctx context.Context, addr string) (net.Conn, error) {
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package connector
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
"google.golang.org/grpc/credentials/insecure"
|
||||||
|
"net/netip"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TCPConnector establishes a connection to the machine API through a direct TCP connection to an API endpoint.
|
||||||
|
type TCPConnector struct {
|
||||||
|
apiAddr netip.AddrPort
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTCPConnector(apiAddr netip.AddrPort) *TCPConnector {
|
||||||
|
return &TCPConnector{apiAddr: apiAddr}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *TCPConnector) Connect(_ context.Context) (*grpc.ClientConn, error) {
|
||||||
|
conn, err := grpc.NewClient(
|
||||||
|
c.apiAddr.String(),
|
||||||
|
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("create machine API client: %w", err)
|
||||||
|
}
|
||||||
|
return conn, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *TCPConnector) Close() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,678 @@
|
|||||||
|
package client
|
||||||
|
|
||||||
|
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"
|
||||||
|
)
|
||||||
|
|
||||||
|
type RunServiceResponse struct {
|
||||||
|
ID string
|
||||||
|
Name string
|
||||||
|
Containers []MachineContainerID
|
||||||
|
}
|
||||||
|
|
||||||
|
type MachineContainerID struct {
|
||||||
|
MachineID string
|
||||||
|
ContainerID string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cli *Client) RunService(ctx context.Context, spec api.ServiceSpec) (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 {
|
||||||
|
// Optimistically check if a service with the specified name already exists.
|
||||||
|
_, err := cli.InspectService(ctx, spec.Name)
|
||||||
|
if err == nil {
|
||||||
|
return resp, fmt.Errorf("service with name '%s' already exists", spec.Name)
|
||||||
|
}
|
||||||
|
if !errors.Is(err, ErrNotFound) {
|
||||||
|
return resp, fmt.Errorf("inspect service: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
serviceID, err := secret.NewID()
|
||||||
|
if err != nil {
|
||||||
|
return resp, fmt.Errorf("generate service ID: %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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// InspectService returns detailed information about a service and its containers.
|
||||||
|
// The id parameter can be either a service ID or name.
|
||||||
|
func (cli *Client) InspectService(ctx context.Context, id string) (api.Service, error) {
|
||||||
|
var svc api.Service
|
||||||
|
|
||||||
|
machines, err := cli.ListMachines(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return svc, fmt.Errorf("list machines: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Broadcast the container list request to all available machines.
|
||||||
|
machineIDByManagementIP := make(map[string]string)
|
||||||
|
md := metadata.New(nil)
|
||||||
|
for _, m := range machines {
|
||||||
|
if m.State == pb.MachineMember_UP || m.State == pb.MachineMember_SUSPECT {
|
||||||
|
machineIP, _ := m.Machine.Network.ManagementIp.ToAddr()
|
||||||
|
md.Append("machines", machineIP.String())
|
||||||
|
|
||||||
|
machineIDByManagementIP[machineIP.String()] = m.Machine.Id
|
||||||
|
}
|
||||||
|
// TODO: warning about machines that are DOWN.
|
||||||
|
}
|
||||||
|
listCtx := metadata.NewOutgoingContext(ctx, md)
|
||||||
|
|
||||||
|
// List only uncloud-managed containers that belong to some service.
|
||||||
|
opts := container.ListOptions{
|
||||||
|
All: true,
|
||||||
|
Filters: filters.NewArgs(
|
||||||
|
filters.Arg("label", api.LabelServiceID),
|
||||||
|
filters.Arg("label", api.LabelManaged),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
machineContainers, err := cli.ListContainers(listCtx, opts)
|
||||||
|
if err != nil {
|
||||||
|
return svc, fmt.Errorf("list containers: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect all containers on all machines that belong to the specified service.
|
||||||
|
foundByID := false
|
||||||
|
var containers []api.MachineContainer
|
||||||
|
for _, mc := range machineContainers {
|
||||||
|
// Metadata can be nil if the request was broadcasted to only one machine.
|
||||||
|
if mc.Metadata == nil && len(machineContainers) > 1 {
|
||||||
|
return svc, errors.New("something went wrong with gRPC proxy: metadata is missing for a machine response")
|
||||||
|
}
|
||||||
|
if mc.Metadata != nil && mc.Metadata.Error != "" {
|
||||||
|
// TODO: return failed machines in the response.
|
||||||
|
fmt.Printf("WARNING: failed to list containers on machine '%s': %s\n",
|
||||||
|
mc.Metadata.Machine, mc.Metadata.Error)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
machineID := ""
|
||||||
|
if mc.Metadata == nil {
|
||||||
|
// ListContainers was proxied to only one machine.
|
||||||
|
for _, v := range machineIDByManagementIP {
|
||||||
|
machineID = v
|
||||||
|
break
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
var ok bool
|
||||||
|
machineID, ok = machineIDByManagementIP[mc.Metadata.Machine]
|
||||||
|
if !ok {
|
||||||
|
return svc, fmt.Errorf("machine name not found for management IP: %s", mc.Metadata.Machine)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, c := range mc.Containers {
|
||||||
|
ctr := api.Container{Container: c}
|
||||||
|
if ctr.ServiceID() == id || ctr.ServiceName() == id {
|
||||||
|
containers = append(containers, api.MachineContainer{
|
||||||
|
MachineID: machineID,
|
||||||
|
Container: ctr,
|
||||||
|
})
|
||||||
|
|
||||||
|
if ctr.ServiceID() == id {
|
||||||
|
foundByID = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(containers) == 0 {
|
||||||
|
return svc, ErrNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
// Containers from different services may share the same service name (distributed and eventually consistent store
|
||||||
|
// may not prevent this), or a service name might match another service's ID. In these cases, matching by ID takes
|
||||||
|
// priority over matching by name.
|
||||||
|
if foundByID {
|
||||||
|
containers = slices.DeleteFunc(containers, func(mc api.MachineContainer) bool {
|
||||||
|
return mc.Container.ServiceID() != id
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// Matched only by name but there could be multiple services with the same name.
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
svc = api.Service{
|
||||||
|
ID: containers[0].Container.ServiceID(),
|
||||||
|
Name: containers[0].Container.ServiceName(),
|
||||||
|
Mode: containers[0].Container.ServiceMode(),
|
||||||
|
Containers: containers,
|
||||||
|
}
|
||||||
|
if svc.Mode == "" {
|
||||||
|
svc.Mode = api.ServiceModeReplicated
|
||||||
|
}
|
||||||
|
|
||||||
|
return svc, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// InspectServiceFromStore returns detailed information about a service and its containers from the distributed store.
|
||||||
|
// Due to eventual consistency of the store, the returned information may not reflect the most recent changes.
|
||||||
|
// The id parameter can be either a service ID or name.
|
||||||
|
func (cli *Client) InspectServiceFromStore(ctx context.Context, id string) (api.Service, error) {
|
||||||
|
var svc api.Service
|
||||||
|
|
||||||
|
resp, err := cli.MachineClient.InspectService(ctx, &pb.InspectServiceRequest{Id: id})
|
||||||
|
if err != nil {
|
||||||
|
if s, ok := status.FromError(err); ok {
|
||||||
|
if s.Code() == codes.NotFound {
|
||||||
|
return svc, ErrNotFound
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return svc, err
|
||||||
|
}
|
||||||
|
|
||||||
|
svc, err = api.ServiceFromProto(resp.Service)
|
||||||
|
if err != nil {
|
||||||
|
return svc, fmt.Errorf("from proto: %w", err)
|
||||||
|
}
|
||||||
|
return svc, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveService removes all containers on all machines that belong to the specified service.
|
||||||
|
// The id parameter can be either a service ID or name.
|
||||||
|
func (cli *Client) RemoveService(ctx context.Context, id string) error {
|
||||||
|
svc, err := cli.InspectService(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
machines, err := cli.ListMachines(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("list machines: %w", err)
|
||||||
|
}
|
||||||
|
machineManagementIPByID := make(map[string]string)
|
||||||
|
for _, m := range machines {
|
||||||
|
machineIP, _ := m.Machine.Network.ManagementIp.ToAddr()
|
||||||
|
machineManagementIPByID[m.Machine.Id] = machineIP.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
wg := sync.WaitGroup{}
|
||||||
|
errCh := make(chan error)
|
||||||
|
|
||||||
|
// Remove all containers on all machines that belong to the service.
|
||||||
|
for _, mc := range svc.Containers {
|
||||||
|
wg.Add(1)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
|
||||||
|
machineIP, ok := machineManagementIPByID[mc.MachineID]
|
||||||
|
if !ok {
|
||||||
|
errCh <- fmt.Errorf("machine not found by ID: %s", mc.MachineID)
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
wg.Wait()
|
||||||
|
close(errCh)
|
||||||
|
}()
|
||||||
|
|
||||||
|
err = nil
|
||||||
|
for e := range errCh {
|
||||||
|
err = errors.Join(err, e)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListServices returns a list of all services and their containers.
|
||||||
|
func (cli *Client) ListServices(ctx context.Context) ([]api.Service, error) {
|
||||||
|
machines, err := cli.ListMachines(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("list machines: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Broadcast the container list request to all available machines.
|
||||||
|
md := metadata.New(nil)
|
||||||
|
for _, m := range machines {
|
||||||
|
if m.State == pb.MachineMember_UP || m.State == pb.MachineMember_SUSPECT {
|
||||||
|
machineIP, _ := m.Machine.Network.ManagementIp.ToAddr()
|
||||||
|
md.Append("machines", machineIP.String())
|
||||||
|
}
|
||||||
|
// TODO: warning about machines that are DOWN.
|
||||||
|
}
|
||||||
|
listCtx := metadata.NewOutgoingContext(ctx, md)
|
||||||
|
|
||||||
|
// List only uncloud-managed containers that belong to some service.
|
||||||
|
opts := container.ListOptions{
|
||||||
|
All: true,
|
||||||
|
Filters: filters.NewArgs(
|
||||||
|
filters.Arg("label", api.LabelServiceID),
|
||||||
|
filters.Arg("label", api.LabelManaged),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
machineContainers, err := cli.ListContainers(listCtx, opts)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("list containers: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: optimise by extracting services from the list of all containers instead of inspecting each service.
|
||||||
|
// Most of the code can be reused in both InspectService and ListServices.
|
||||||
|
servicesByID := make(map[string]api.Service)
|
||||||
|
for _, mc := range machineContainers {
|
||||||
|
if mc.Metadata != nil && mc.Metadata.Error != "" {
|
||||||
|
// TODO: return failed machines in the response.
|
||||||
|
fmt.Printf("WARNING: failed to list containers on machine '%s': %s\n",
|
||||||
|
mc.Metadata.Machine, mc.Metadata.Error)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, c := range mc.Containers {
|
||||||
|
ctr := api.Container{Container: c}
|
||||||
|
if _, ok := servicesByID[ctr.ServiceID()]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
svc, err := cli.InspectService(ctx, ctr.ServiceID())
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, ErrNotFound) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("inspect service: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
servicesByID[ctr.ServiceID()] = svc
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
services := make([]api.Service, 0, len(servicesByID))
|
||||||
|
for _, svc := range servicesByID {
|
||||||
|
services = append(services, svc)
|
||||||
|
}
|
||||||
|
return services, nil
|
||||||
|
}
|
||||||
@@ -1,10 +1,6 @@
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
import "uncloud/internal/secret"
|
|
||||||
|
|
||||||
type Cluster struct {
|
type Cluster struct {
|
||||||
Name string `toml:"-"`
|
Name string `toml:"-"`
|
||||||
Connections []MachineConnection `toml:"connections"`
|
Connections []MachineConnection `toml:"connections"`
|
||||||
// UserPrivateKey is the user's WireGuard private key used to connect to cluster machines.
|
|
||||||
UserPrivateKey secret.Secret `toml:"user_private_key"`
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package config
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"net"
|
"net"
|
||||||
|
"net/netip"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"uncloud/internal/secret"
|
"uncloud/internal/secret"
|
||||||
@@ -14,6 +15,7 @@ const (
|
|||||||
|
|
||||||
type MachineConnection struct {
|
type MachineConnection struct {
|
||||||
SSH SSHDestination `toml:"ssh,omitempty"`
|
SSH SSHDestination `toml:"ssh,omitempty"`
|
||||||
|
TCP netip.AddrPort `toml:"tcp,omitempty"`
|
||||||
Host string `toml:"host,omitempty"`
|
Host string `toml:"host,omitempty"`
|
||||||
PublicKey secret.Secret `toml:"public_key,omitempty"`
|
PublicKey secret.Secret `toml:"public_key,omitempty"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,258 @@
|
|||||||
|
package corrosion
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"net/netip"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AdminClient is a client for the Corrosion admin API.
|
||||||
|
type AdminClient struct {
|
||||||
|
sockPath string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAdminClient(sockPath string) (*AdminClient, error) {
|
||||||
|
return &AdminClient{sockPath: sockPath}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type Response struct {
|
||||||
|
JSON map[string]any
|
||||||
|
// Err is set if the response is an error or if an error occurred while processing the response.
|
||||||
|
Err error
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendCommand sends a command to the Corrosion admin API and returns a channel that will receive responses.
|
||||||
|
// The channel will be closed after sending the last or error response. The caller must read from the channel until
|
||||||
|
// it is closed.
|
||||||
|
func (c *AdminClient) SendCommand(cmd []byte) (<-chan Response, error) {
|
||||||
|
conn, err := net.Dial("unix", c.sockPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("connect to admin socket: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err = conn.Write(encodeFrame(cmd)); err != nil {
|
||||||
|
conn.Close()
|
||||||
|
return nil, fmt.Errorf("send command: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ch := make(chan Response)
|
||||||
|
go func() {
|
||||||
|
defer close(ch)
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
for {
|
||||||
|
r := Response{}
|
||||||
|
|
||||||
|
data, err := readFrame(conn)
|
||||||
|
if err != nil {
|
||||||
|
r.Err = err
|
||||||
|
ch <- r
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var decoded any
|
||||||
|
if err = json.Unmarshal(data, &decoded); err != nil {
|
||||||
|
r.Err = fmt.Errorf("unmarshal response: %w", err)
|
||||||
|
ch <- r
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch v := decoded.(type) {
|
||||||
|
case string:
|
||||||
|
if v == "Success" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Ignore other strings.
|
||||||
|
case map[string]any:
|
||||||
|
if errData, ok := v["Error"].(map[string]any); ok {
|
||||||
|
if errMsg, ok := errData["msg"].(string); ok {
|
||||||
|
r.Err = errors.New(errMsg)
|
||||||
|
} else {
|
||||||
|
r.Err = fmt.Errorf("invalid error response: %v", errData)
|
||||||
|
}
|
||||||
|
ch <- r
|
||||||
|
return
|
||||||
|
} else if jsonData, ok := v["Json"].(map[string]any); ok {
|
||||||
|
r.JSON = jsonData
|
||||||
|
ch <- r
|
||||||
|
}
|
||||||
|
// Ignore other maps.
|
||||||
|
default:
|
||||||
|
// Ignore other types.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return ch, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// encodeFrame encodes a length_delimited Tokio frame by prefacing frame data with a frame head that specifies
|
||||||
|
// the length of the frame.
|
||||||
|
func encodeFrame(data []byte) []byte {
|
||||||
|
encoded := make([]byte, 4+len(data))
|
||||||
|
binary.BigEndian.PutUint32(encoded, uint32(len(data)))
|
||||||
|
copy(encoded[4:], data)
|
||||||
|
return encoded
|
||||||
|
}
|
||||||
|
|
||||||
|
// readFrame reads a length_delimited Tokio frame from the connection by extracting the frame data that follows
|
||||||
|
// the frame head.
|
||||||
|
func readFrame(conn net.Conn) ([]byte, error) {
|
||||||
|
// Read the frame head (4 bytes).
|
||||||
|
head := make([]byte, 4)
|
||||||
|
if _, err := io.ReadFull(conn, head); err != nil {
|
||||||
|
return nil, fmt.Errorf("read frame head: %w", err)
|
||||||
|
}
|
||||||
|
// Read the frame data (length specified in the frame head).
|
||||||
|
length := binary.BigEndian.Uint32(head)
|
||||||
|
data := make([]byte, length)
|
||||||
|
if _, err := io.ReadFull(conn, data); err != nil {
|
||||||
|
return nil, fmt.Errorf("read frame data: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type ClusterMembershipState struct {
|
||||||
|
ID string
|
||||||
|
Addr netip.AddrPort
|
||||||
|
State string
|
||||||
|
Timestamp time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
// MembershipStateAlive indicates that the member is active.
|
||||||
|
MembershipStateAlive string = "Alive"
|
||||||
|
// MembershipStateSuspect indicates that the member is active, but at least one cluster member suspects its down.
|
||||||
|
// For all purposes, a Suspect member is treated as if it were Alive until either it refutes the suspicion
|
||||||
|
// (becoming Alive) or fails to do so (being declared Down).
|
||||||
|
MembershipStateSuspect string = "Suspect"
|
||||||
|
// MembershipStateDown indicates that the member is confirmed Down. A member that reaches this state can't join
|
||||||
|
// the cluster with the same identity until the cluster forgets this knowledge.
|
||||||
|
MembershipStateDown string = "Down"
|
||||||
|
)
|
||||||
|
|
||||||
|
func parseClusterMembershipState(json map[string]any) (ClusterMembershipState, error) {
|
||||||
|
// Example JSON:
|
||||||
|
// {
|
||||||
|
// "id": {
|
||||||
|
// "addr": "[fdcc:1d51:6bae:6bb2:53c0:8796:1be0:b783]:51001",
|
||||||
|
// "cluster_id": 0,
|
||||||
|
// "id": "10d69d6f-6578-4dcf-a285-e860e40c5f06",
|
||||||
|
// "ts": 7435936225798880256
|
||||||
|
// },
|
||||||
|
// "incarnation": 0,
|
||||||
|
// "state": "Down"
|
||||||
|
// }
|
||||||
|
|
||||||
|
var state ClusterMembershipState
|
||||||
|
var err error
|
||||||
|
|
||||||
|
idObj, ok := json["id"].(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
return state, fmt.Errorf("missing or invalid 'id' field")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ID
|
||||||
|
if id, ok := idObj["id"].(string); ok {
|
||||||
|
state.ID = id
|
||||||
|
} else {
|
||||||
|
return state, fmt.Errorf("missing or invalid 'id' field")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Addr
|
||||||
|
if addr, ok := idObj["addr"].(string); ok {
|
||||||
|
state.Addr, err = netip.ParseAddrPort(addr)
|
||||||
|
if err != nil {
|
||||||
|
return state, fmt.Errorf("parse 'addr' field: %w", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return state, fmt.Errorf("missing or invalid 'addr' field")
|
||||||
|
}
|
||||||
|
|
||||||
|
// State
|
||||||
|
if stateStr, ok := json["state"].(string); ok {
|
||||||
|
switch stateStr {
|
||||||
|
case MembershipStateAlive, MembershipStateSuspect, MembershipStateDown:
|
||||||
|
state.State = stateStr
|
||||||
|
default:
|
||||||
|
return state, fmt.Errorf("invalid 'state' field: %s", stateStr)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return state, fmt.Errorf("missing or invalid 'state' field")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Timestamp
|
||||||
|
if ts, ok := idObj["ts"].(float64); ok {
|
||||||
|
state.Timestamp = ntp64ToTime(uint64(ts))
|
||||||
|
} else {
|
||||||
|
return state, fmt.Errorf("missing or invalid 'ts' field")
|
||||||
|
}
|
||||||
|
|
||||||
|
return state, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// nt64ToTime converts a 64-bit NTP timestamp relative to the Unix epoch (1st Jan 1970) to time.Time.
|
||||||
|
// See for more details: https://datatracker.ietf.org/doc/html/rfc5905#section-6
|
||||||
|
func ntp64ToTime(ntp uint64) time.Time {
|
||||||
|
// The NTP timestamp returned from Corrosion is relative to the Unix epoch (1st Jan 1970)
|
||||||
|
// so no need to subtract the 70 years offset.
|
||||||
|
secs := uint32(ntp >> 32)
|
||||||
|
frac := uint32(ntp)
|
||||||
|
// Convert the fraction to nanoseconds: frac * 1e9 / 2^32
|
||||||
|
nsecs := (uint64(frac) * 1000_000_000) >> 32
|
||||||
|
|
||||||
|
return time.Unix(int64(secs), int64(nsecs))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClusterMembershipStates returns the current membership SWIM states of all cluster members.
|
||||||
|
// If latest is true, only the latest state of each member is returned.
|
||||||
|
func (c *AdminClient) ClusterMembershipStates(latest bool) ([]ClusterMembershipState, error) {
|
||||||
|
respCh, err := c.SendCommand([]byte("{\"Cluster\":\"MembershipStates\"}"))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
states []ClusterMembershipState
|
||||||
|
latestStates map[string]ClusterMembershipState
|
||||||
|
parseErr error
|
||||||
|
)
|
||||||
|
if latest {
|
||||||
|
latestStates = make(map[string]ClusterMembershipState)
|
||||||
|
}
|
||||||
|
|
||||||
|
for r := range respCh {
|
||||||
|
if r.Err != nil {
|
||||||
|
// It's safe to return here because the channel is closed after the first error response.
|
||||||
|
return nil, r.Err
|
||||||
|
}
|
||||||
|
|
||||||
|
s, err := parseClusterMembershipState(r.JSON)
|
||||||
|
if err != nil {
|
||||||
|
// Do not return early to drain the channel.
|
||||||
|
parseErr = errors.Join(parseErr, err)
|
||||||
|
} else {
|
||||||
|
if latest {
|
||||||
|
if existing, ok := latestStates[s.ID]; !ok || existing.Timestamp.Before(s.Timestamp) {
|
||||||
|
latestStates[s.ID] = s
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
states = append(states, s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if latest {
|
||||||
|
states = make([]ClusterMembershipState, 0, len(latestStates))
|
||||||
|
for _, s := range latestStates {
|
||||||
|
states = append(states, s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return states, parseErr
|
||||||
|
}
|
||||||
@@ -0,0 +1,233 @@
|
|||||||
|
package corrosion
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"github.com/cenkalti/backoff/v4"
|
||||||
|
"golang.org/x/net/http2"
|
||||||
|
"log/slog"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/netip"
|
||||||
|
"net/url"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// http2ConnectTimeout is the maximum amount of time an HTTP2 client will wait for a connection to be established.
|
||||||
|
http2ConnectTimeout = 3 * time.Second
|
||||||
|
// http2MaxRetryTime is the maximum amount of time an HTTP2 client will retry a request.
|
||||||
|
http2MaxRetryTime = 10 * time.Second
|
||||||
|
// resubscribeMaxRetryTime is the maximum amount of time an API client will retry resubscribing to a query after
|
||||||
|
// an error occurs.
|
||||||
|
resubscribeMaxRetryTime = 60 * time.Second
|
||||||
|
)
|
||||||
|
|
||||||
|
// APIClient is a client for the Corrosion API.
|
||||||
|
type APIClient struct {
|
||||||
|
baseURL *url.URL
|
||||||
|
client *http.Client
|
||||||
|
newResubBackoff func() backoff.BackOff
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewAPIClient creates a new Corrosion API client. The client retries on network errors using an exponential backoff
|
||||||
|
// policy with a maximum interval of 1 second and a maximum elapsed time of 10 seconds.
|
||||||
|
// It automatically resubscribes to active subscriptions if an error occurs using an exponential backoff policy with a
|
||||||
|
// maximum interval of 1 second and a maximum elapsed time of 60 seconds.
|
||||||
|
// Use the WithHTTP2Client option to provide a custom HTTP client and the WithResubscribeBackoff option to change the
|
||||||
|
// backoff policy for resubscribing to a query.
|
||||||
|
func NewAPIClient(addr netip.AddrPort, opts ...APIClientOption) (*APIClient, error) {
|
||||||
|
baseURL, err := url.Parse(fmt.Sprintf("http://%s", addr))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid URL: %w", err)
|
||||||
|
}
|
||||||
|
c := &APIClient{
|
||||||
|
baseURL: baseURL,
|
||||||
|
client: &http.Client{
|
||||||
|
Transport: &RetryRoundTripper{
|
||||||
|
Base: &http2.Transport{
|
||||||
|
AllowHTTP: true,
|
||||||
|
DialTLSContext: func(ctx context.Context, network, addr string, _ *tls.Config) (net.Conn, error) {
|
||||||
|
dialer := &net.Dialer{
|
||||||
|
Timeout: http2ConnectTimeout,
|
||||||
|
}
|
||||||
|
return dialer.DialContext(ctx, network, addr)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
NewBackoff: func() backoff.BackOff {
|
||||||
|
return backoff.NewExponentialBackOff(
|
||||||
|
backoff.WithInitialInterval(100*time.Millisecond),
|
||||||
|
backoff.WithMaxInterval(1*time.Second),
|
||||||
|
backoff.WithMaxElapsedTime(http2MaxRetryTime),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
newResubBackoff: func() backoff.BackOff {
|
||||||
|
return backoff.NewExponentialBackOff(
|
||||||
|
backoff.WithInitialInterval(100*time.Millisecond),
|
||||||
|
backoff.WithMaxInterval(1*time.Second),
|
||||||
|
backoff.WithMaxElapsedTime(resubscribeMaxRetryTime),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, opt := range opts {
|
||||||
|
opt(c)
|
||||||
|
}
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type APIClientOption func(*APIClient)
|
||||||
|
|
||||||
|
func WithHTTP2Client(client *http.Client) APIClientOption {
|
||||||
|
return func(c *APIClient) {
|
||||||
|
c.client = client
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithResubscribeBackoff sets the backoff policy for resubscribing to a query if an error occurs.
|
||||||
|
// Pass nil to disable resubscribing.
|
||||||
|
func WithResubscribeBackoff(newBackoff func() backoff.BackOff) APIClientOption {
|
||||||
|
return func(c *APIClient) {
|
||||||
|
c.newResubBackoff = newBackoff
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type RetryRoundTripper struct {
|
||||||
|
Base http.RoundTripper
|
||||||
|
// NewBackoff creates a new backoff policy for each request.
|
||||||
|
NewBackoff func() backoff.BackOff
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rt *RetryRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||||
|
roundTrip := func() (*http.Response, error) {
|
||||||
|
resp, err := rt.Base.RoundTrip(req)
|
||||||
|
if err != nil {
|
||||||
|
var opErr *net.OpError
|
||||||
|
if errors.As(err, &opErr) {
|
||||||
|
// Not certain, but I expect operational errors should generally be retryable.
|
||||||
|
slog.Debug("Retrying corrosion API request due to network error.", "error", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
// Don't retry on other errors.
|
||||||
|
return nil, backoff.Permanent(err)
|
||||||
|
}
|
||||||
|
// Success, don't retry.
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
boff := backoff.WithContext(rt.NewBackoff(), req.Context())
|
||||||
|
return backoff.RetryWithData(roundTrip, boff)
|
||||||
|
}
|
||||||
|
|
||||||
|
type RetrySubscription struct {
|
||||||
|
ctx context.Context
|
||||||
|
cancel context.CancelFunc
|
||||||
|
|
||||||
|
client *APIClient
|
||||||
|
sub *Subscription
|
||||||
|
changes chan *ChangeEvent
|
||||||
|
lastChangeID uint64
|
||||||
|
err error
|
||||||
|
backoff backoff.BackOff
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRetrySubscription(ctx context.Context, client *APIClient, sub *Subscription, boff backoff.BackOff) *RetrySubscription {
|
||||||
|
ctx, cancel := context.WithCancel(ctx)
|
||||||
|
if boff == nil {
|
||||||
|
boff = backoff.NewExponentialBackOff(
|
||||||
|
backoff.WithInitialInterval(100*time.Millisecond),
|
||||||
|
backoff.WithMaxInterval(1*time.Second),
|
||||||
|
backoff.WithMaxElapsedTime(60*time.Second),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return &RetrySubscription{
|
||||||
|
ctx: ctx,
|
||||||
|
cancel: cancel,
|
||||||
|
client: client,
|
||||||
|
sub: sub,
|
||||||
|
backoff: backoff.WithContext(boff, ctx),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rs *RetrySubscription) ID() string {
|
||||||
|
return rs.sub.ID()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rs *RetrySubscription) Changes() (<-chan *ChangeEvent, error) {
|
||||||
|
if rs.changes != nil {
|
||||||
|
return rs.changes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure the rows are consumed if they have been requested.
|
||||||
|
changes, err := rs.sub.Changes()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
rs.lastChangeID = rs.sub.lastChangeID
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
defer close(rs.changes)
|
||||||
|
|
||||||
|
var change *ChangeEvent
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case change = <-changes:
|
||||||
|
case <-rs.ctx.Done():
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if change != nil {
|
||||||
|
select {
|
||||||
|
case rs.changes <- change:
|
||||||
|
case <-rs.ctx.Done():
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rs.lastChangeID = change.ChangeID
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// The underlying subscription has been closed due to an error or context cancellation.
|
||||||
|
// Return if the context is done or try to resubscribe otherwise.
|
||||||
|
if rs.ctx.Err() != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = rs.resubscribe(); err != nil {
|
||||||
|
// resubscribe returns a permanent error after unsuccessful retries.
|
||||||
|
rs.err = fmt.Errorf("resubscribe to query: %w", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
changes, err = rs.sub.Changes()
|
||||||
|
if err != nil {
|
||||||
|
// Unexpected error but report it anyway.
|
||||||
|
rs.err = err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return rs.changes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rs *RetrySubscription) resubscribe() error {
|
||||||
|
return backoff.Retry(func() error {
|
||||||
|
slog.Debug("Resubscribing to Corrosion query.", "id", rs.ID(), "from_change", rs.lastChangeID)
|
||||||
|
sub, err := rs.client.ResubscribeContext(rs.ctx, rs.ID(), rs.lastChangeID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
rs.sub = sub
|
||||||
|
return nil
|
||||||
|
}, rs.backoff)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rs *RetrySubscription) Err() error {
|
||||||
|
return rs.err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rs *RetrySubscription) Close() {
|
||||||
|
rs.cancel()
|
||||||
|
}
|
||||||
@@ -0,0 +1,327 @@
|
|||||||
|
package corrosion
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Statement struct {
|
||||||
|
Query string `json:"query"`
|
||||||
|
Params []any `json:"params"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExecResponse struct {
|
||||||
|
Results []ExecResult `json:"results"`
|
||||||
|
Time float64 `json:"time"`
|
||||||
|
Version *uint `json:"version"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExecResult struct {
|
||||||
|
RowsAffected uint `json:"rows_affected"`
|
||||||
|
Time float64 `json:"time"`
|
||||||
|
Error *string `json:"error"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExecContext writes changes to the Corrosion database for propagation through the cluster. The args are for any
|
||||||
|
// placeholder parameters in the query. Corrosion does not sync schema changes made using this method. Use Corrosion's
|
||||||
|
// schema_files to create and update the cluster's database schema.
|
||||||
|
func (c *APIClient) ExecContext(ctx context.Context, query string, args ...any) (*ExecResult, error) {
|
||||||
|
statements := []Statement{
|
||||||
|
{
|
||||||
|
Query: query,
|
||||||
|
Params: args,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
resp, err := c.ExecMultiContext(ctx, statements...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(resp.Results) == 0 {
|
||||||
|
return nil, fmt.Errorf("no results: %+v", resp)
|
||||||
|
}
|
||||||
|
return &resp.Results[0], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExecMultiContext writes changes to the Corrosion database for propagation through the cluster.
|
||||||
|
// Unlike ExecContext, this method allows multiple statements to be executed in a single transaction.
|
||||||
|
func (c *APIClient) ExecMultiContext(ctx context.Context, statements ...Statement) (*ExecResponse, error) {
|
||||||
|
body, err := json.Marshal(statements)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("marshal queries: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
transactionsURL := c.baseURL.JoinPath("/v1/transactions").String()
|
||||||
|
req, err := http.NewRequestWithContext(ctx, "POST", transactionsURL, bytes.NewReader(body))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("create request: %w", err)
|
||||||
|
}
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
req.Header.Set("Accept", "application/json")
|
||||||
|
|
||||||
|
resp, err := c.client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("send request: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
var execResp ExecResponse
|
||||||
|
if resp.StatusCode == http.StatusOK {
|
||||||
|
if err = json.NewDecoder(resp.Body).Decode(&execResp); err != nil {
|
||||||
|
return nil, fmt.Errorf("decode response: %w", err)
|
||||||
|
}
|
||||||
|
// The response may still contain DB errors even if the status code is OK. Return them along with the response.
|
||||||
|
var errs []error
|
||||||
|
for _, result := range execResp.Results {
|
||||||
|
if result.Error != nil {
|
||||||
|
errs = append(errs, errors.New(*result.Error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &execResp, errors.Join(errs...)
|
||||||
|
} else if resp.StatusCode == http.StatusInternalServerError {
|
||||||
|
// If the response is an Internal Server Error, the response body may contain the error encoded as ExecResponse.
|
||||||
|
respBody, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("read response body: %w", err)
|
||||||
|
}
|
||||||
|
if err = json.Unmarshal(respBody, &execResp); err != nil {
|
||||||
|
return nil, fmt.Errorf("internal server error: %s", respBody)
|
||||||
|
}
|
||||||
|
if len(execResp.Results) > 0 && execResp.Results[0].Error != nil {
|
||||||
|
return nil, errors.New(*execResp.Results[0].Error)
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("internal server error: %s", respBody)
|
||||||
|
}
|
||||||
|
|
||||||
|
respBody, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("read response body: %w", err)
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("unexpected status code %d: %s", resp.StatusCode, respBody)
|
||||||
|
}
|
||||||
|
|
||||||
|
type QueryEvent struct {
|
||||||
|
Columns []string `json:"columns"`
|
||||||
|
Row *RowEvent `json:"row"`
|
||||||
|
EOQ *EndOfQuery `json:"eoq"`
|
||||||
|
Change *ChangeEvent `json:"change"`
|
||||||
|
// Error is a server-side error that occurred during query execution. It's considered fatal for the client
|
||||||
|
// as it cannot be recovered from server-side.
|
||||||
|
Error *string `json:"error"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type EndOfQuery struct {
|
||||||
|
Time float64 `json:"time"`
|
||||||
|
ChangeID *uint64 `json:"change_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RowEvent struct {
|
||||||
|
RowID uint64
|
||||||
|
Values []json.RawMessage
|
||||||
|
}
|
||||||
|
|
||||||
|
func (re *RowEvent) UnmarshalJSON(data []byte) error {
|
||||||
|
var raw []json.RawMessage
|
||||||
|
if err := json.Unmarshal(data, &raw); err != nil {
|
||||||
|
return fmt.Errorf("invalid row event: %w", err)
|
||||||
|
}
|
||||||
|
if len(raw) != 2 {
|
||||||
|
return fmt.Errorf("invalid row event: expected an array of 2 elements")
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(raw[0], &re.RowID); err != nil {
|
||||||
|
return fmt.Errorf("invalid row event: %w", err)
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(raw[1], &re.Values); err != nil {
|
||||||
|
return fmt.Errorf("invalid row event: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (re *RowEvent) MarshalJSON() ([]byte, error) {
|
||||||
|
return json.Marshal([]any{re.RowID, re.Values})
|
||||||
|
}
|
||||||
|
|
||||||
|
// QueryContext executes a query that returns rows, typically a SELECT.
|
||||||
|
// The args are for any placeholder parameters in the query.
|
||||||
|
func (c *APIClient) QueryContext(ctx context.Context, query string, args ...any) (*Rows, error) {
|
||||||
|
statement := Statement{
|
||||||
|
Query: query,
|
||||||
|
Params: args,
|
||||||
|
}
|
||||||
|
body, err := json.Marshal(statement)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("marshal query: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
queriesURL := c.baseURL.JoinPath("/v1/queries").String()
|
||||||
|
req, err := http.NewRequestWithContext(ctx, "POST", queriesURL, bytes.NewReader(body))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("create request: %w", err)
|
||||||
|
}
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
req.Header.Set("Accept", "application/json")
|
||||||
|
|
||||||
|
resp, err := c.client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("send request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
respBody, err := io.ReadAll(resp.Body)
|
||||||
|
resp.Body.Close()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("read response body: %w", err)
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("unexpected status code %d: %s", resp.StatusCode, respBody)
|
||||||
|
}
|
||||||
|
|
||||||
|
rows, err := newRows(ctx, resp.Body, true)
|
||||||
|
if err != nil {
|
||||||
|
resp.Body.Close()
|
||||||
|
return nil, fmt.Errorf("parse query response: %w", err)
|
||||||
|
}
|
||||||
|
return rows, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rows is the result of a query. Its cursor starts before the first row of the result set.
|
||||||
|
// Use [Rows.Next] to advance from row to row.
|
||||||
|
type Rows struct {
|
||||||
|
ctx context.Context
|
||||||
|
body io.ReadCloser
|
||||||
|
decoder *json.Decoder
|
||||||
|
eoq *EndOfQuery
|
||||||
|
closeOnEOQ bool
|
||||||
|
|
||||||
|
columns []string
|
||||||
|
row RowEvent
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func newRows(ctx context.Context, body io.ReadCloser, closeOnEOQ bool) (*Rows, error) {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return nil, ctx.Err()
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
decoder := json.NewDecoder(body)
|
||||||
|
var e QueryEvent
|
||||||
|
if err := decoder.Decode(&e); err != nil {
|
||||||
|
return nil, fmt.Errorf("decode query event: %w", err)
|
||||||
|
}
|
||||||
|
if e.Columns == nil {
|
||||||
|
return nil, fmt.Errorf("expected columns event, got: %+v", e)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Rows{
|
||||||
|
ctx: ctx,
|
||||||
|
body: body,
|
||||||
|
decoder: decoder,
|
||||||
|
closeOnEOQ: closeOnEOQ,
|
||||||
|
columns: e.Columns,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Columns returns the column names.
|
||||||
|
func (rs *Rows) Columns() []string {
|
||||||
|
return rs.columns
|
||||||
|
}
|
||||||
|
|
||||||
|
// Next prepares the next result row for reading with the [Rows.Scan] method. It returns true on success, or false
|
||||||
|
// if there is no next result row or an error happened while preparing it. [Rows.Err] should be consulted to distinguish
|
||||||
|
// between the two cases.
|
||||||
|
//
|
||||||
|
// Every call to [Rows.Scan], even the first one, must be preceded by a call to [Rows.Next].
|
||||||
|
func (rs *Rows) Next() bool {
|
||||||
|
select {
|
||||||
|
case <-rs.ctx.Done():
|
||||||
|
rs.err = rs.ctx.Err()
|
||||||
|
_ = rs.Close()
|
||||||
|
return false
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
var e QueryEvent
|
||||||
|
if err := rs.decoder.Decode(&e); err != nil {
|
||||||
|
rs.err = fmt.Errorf("decode query event: %w", err)
|
||||||
|
_ = rs.Close()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// Server-side query error.
|
||||||
|
if e.Error != nil {
|
||||||
|
rs.err = fmt.Errorf("query error: %s", *e.Error)
|
||||||
|
_ = rs.Close()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if e.Row != nil {
|
||||||
|
if len(e.Row.Values) != len(rs.columns) {
|
||||||
|
rs.err = fmt.Errorf("expected %d column values, got %d", len(rs.columns), len(e.Row.Values))
|
||||||
|
_ = rs.Close()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
rs.row = *e.Row
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if e.EOQ != nil {
|
||||||
|
rs.eoq = e.EOQ
|
||||||
|
// Rows could be used as part of a subscription, so don't close the body if so.
|
||||||
|
if rs.closeOnEOQ {
|
||||||
|
_ = rs.Close()
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
rs.err = fmt.Errorf("expected row or eof event, got: %+v", e)
|
||||||
|
_ = rs.Close()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Err returns the error, if any, that was encountered during iteration.
|
||||||
|
// Err may be called after an explicit or implicit [Rows.Close].
|
||||||
|
func (rs *Rows) Err() error {
|
||||||
|
return rs.err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scan copies the columns in the current row into the values pointed at by dest.
|
||||||
|
// The number of values in dest must be the same as the number of columns in [Rows].
|
||||||
|
// Scan converts JSON-encoded column values to the provided Go types using [json.Unmarshal].
|
||||||
|
func (rs *Rows) Scan(dest ...any) error {
|
||||||
|
if rs.err != nil {
|
||||||
|
return rs.err
|
||||||
|
}
|
||||||
|
if len(dest) != len(rs.columns) {
|
||||||
|
return fmt.Errorf("expected %d values, got %d", len(rs.columns), len(dest))
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, v := range rs.row.Values {
|
||||||
|
if err := json.Unmarshal(v, dest[i]); err != nil {
|
||||||
|
return fmt.Errorf("unmarshal column value #%d: %w", i, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Time returns the time taken to execute the query in seconds. It's only available after all rows have been consumed.
|
||||||
|
// It doesn't include the time to send the query, receive the response, or iterate over the rows.
|
||||||
|
func (rs *Rows) Time() (float64, error) {
|
||||||
|
if rs.eoq == nil {
|
||||||
|
if rs.Err() != nil {
|
||||||
|
return 0, fmt.Errorf("time is not available: %w", rs.Err())
|
||||||
|
}
|
||||||
|
return 0, errors.New("time is not available until all rows are consumed")
|
||||||
|
}
|
||||||
|
return rs.eoq.Time, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close closes the [Rows], preventing further enumeration. If [Rows.Next] is called and returns false,
|
||||||
|
// the [Rows] are closed automatically and it will suffice to check the result of [Rows.Err].
|
||||||
|
// Close is idempotent and does not affect the result of [Rows.Err].
|
||||||
|
func (rs *Rows) Close() error {
|
||||||
|
return rs.body.Close()
|
||||||
|
}
|
||||||
@@ -0,0 +1,323 @@
|
|||||||
|
package corrosion
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"github.com/cenkalti/backoff/v4"
|
||||||
|
"io"
|
||||||
|
"log/slog"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ChangeType string
|
||||||
|
|
||||||
|
var (
|
||||||
|
ChangeTypeInsert ChangeType = "insert"
|
||||||
|
ChangeTypeUpdate ChangeType = "update"
|
||||||
|
ChangeTypeDelete ChangeType = "delete"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ChangeEvent struct {
|
||||||
|
Type ChangeType
|
||||||
|
RowID uint64
|
||||||
|
Values []json.RawMessage
|
||||||
|
ChangeID uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ce *ChangeEvent) UnmarshalJSON(data []byte) error {
|
||||||
|
var raw []json.RawMessage
|
||||||
|
if err := json.Unmarshal(data, &raw); err != nil {
|
||||||
|
return fmt.Errorf("invalid change event: %w", err)
|
||||||
|
}
|
||||||
|
if len(raw) != 4 {
|
||||||
|
return fmt.Errorf("invalid change event: expected an array of 4 elements")
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(raw[0], &ce.Type); err != nil {
|
||||||
|
return fmt.Errorf("invalid change event type: %w", err)
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(raw[1], &ce.RowID); err != nil {
|
||||||
|
return fmt.Errorf("invalid change event row ID: %w", err)
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(raw[2], &ce.Values); err != nil {
|
||||||
|
return fmt.Errorf("invalid change event values: %w", err)
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(raw[3], &ce.ChangeID); err != nil {
|
||||||
|
return fmt.Errorf("invalid change event change ID: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ce *ChangeEvent) MarshalJSON() ([]byte, error) {
|
||||||
|
return json.Marshal([]any{ce.Type, ce.RowID, ce.Values, ce.ChangeID})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scan copies the column values in the change event into the values pointed at by dest.
|
||||||
|
// The number of values in dest must be the same as the number of columns in the change.
|
||||||
|
// Scan converts JSON-encoded column values to the provided Go types using [json.Unmarshal].
|
||||||
|
func (ce *ChangeEvent) Scan(dest ...any) error {
|
||||||
|
if len(dest) != len(ce.Values) {
|
||||||
|
return fmt.Errorf("expected %d values, got %d", len(ce.Values), len(dest))
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, v := range ce.Values {
|
||||||
|
if err := json.Unmarshal(v, dest[i]); err != nil {
|
||||||
|
return fmt.Errorf("unmarshal column value #%d: %w", i, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subscription receives updates from the Corrosion database for a desired SQL query.
|
||||||
|
type Subscription struct {
|
||||||
|
ctx context.Context
|
||||||
|
cancel context.CancelFunc
|
||||||
|
|
||||||
|
id string
|
||||||
|
rows *Rows
|
||||||
|
body io.ReadCloser
|
||||||
|
decoder *json.Decoder
|
||||||
|
resubscribe func(ctx context.Context, fromChange uint64) (*Subscription, error)
|
||||||
|
changes chan *ChangeEvent
|
||||||
|
lastChangeID uint64
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func newSubscription(
|
||||||
|
ctx context.Context,
|
||||||
|
id string,
|
||||||
|
rows *Rows,
|
||||||
|
body io.ReadCloser,
|
||||||
|
decoder *json.Decoder,
|
||||||
|
resubscribe func(ctx context.Context, fromChange uint64) (*Subscription, error),
|
||||||
|
) *Subscription {
|
||||||
|
ctx, cancel := context.WithCancel(ctx)
|
||||||
|
if decoder == nil {
|
||||||
|
decoder = json.NewDecoder(body)
|
||||||
|
}
|
||||||
|
return &Subscription{
|
||||||
|
ctx: ctx,
|
||||||
|
cancel: cancel,
|
||||||
|
id: id,
|
||||||
|
rows: rows,
|
||||||
|
body: body,
|
||||||
|
decoder: decoder,
|
||||||
|
resubscribe: resubscribe,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ID returns the subscription ID.
|
||||||
|
func (s *Subscription) ID() string {
|
||||||
|
return s.id
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rows returns the rows of the query or nil if skipRows was true when creating the subscription or if the subscription
|
||||||
|
// was created with [APIClient.ResubscribeContext].
|
||||||
|
func (s *Subscription) Rows() *Rows {
|
||||||
|
return s.rows
|
||||||
|
}
|
||||||
|
|
||||||
|
// Changes returns a channel that receives change events for the query. Changes are not available until all rows
|
||||||
|
// are consumed. The channel is closed when the context is done, or an error occurs while reading the changes,
|
||||||
|
// or when the subscription is closed explicitly. If it's closed due to an error, [Subscription.Err] will return
|
||||||
|
// the error.
|
||||||
|
func (s *Subscription) Changes() (<-chan *ChangeEvent, error) {
|
||||||
|
if s.changes != nil {
|
||||||
|
return s.changes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.rows != nil {
|
||||||
|
if s.rows.eoq == nil {
|
||||||
|
return nil, errors.New("changes are not available until all rows are consumed")
|
||||||
|
}
|
||||||
|
s.lastChangeID = *s.rows.eoq.ChangeID
|
||||||
|
}
|
||||||
|
s.changes = make(chan *ChangeEvent)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
// Close the body when the context is done to unblock the decoder in the following goroutine.
|
||||||
|
<-s.ctx.Done()
|
||||||
|
s.body.Close()
|
||||||
|
}()
|
||||||
|
go s.handleChangeEvents()
|
||||||
|
|
||||||
|
return s.changes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Subscription) handleChangeEvents() {
|
||||||
|
defer s.cancel()
|
||||||
|
defer close(s.changes)
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-s.ctx.Done():
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
var e QueryEvent
|
||||||
|
var err error
|
||||||
|
if err = s.decoder.Decode(&e); err != nil {
|
||||||
|
// Do not report an error that occurred due to context cancellation, just return.
|
||||||
|
if s.ctx.Err() != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = fmt.Errorf("decode query event: %w", err)
|
||||||
|
} else if e.Error != nil {
|
||||||
|
err = fmt.Errorf("query error: %s", *e.Error)
|
||||||
|
} else if e.Change == nil {
|
||||||
|
err = fmt.Errorf("expected change event, got: %+v", e)
|
||||||
|
} else if s.lastChangeID != 0 && e.Change.ChangeID != s.lastChangeID+1 {
|
||||||
|
// If skipRows is true, the last change ID is unknown.
|
||||||
|
err = fmt.Errorf("missed a change: expected change ID %d, got %d",
|
||||||
|
s.lastChangeID+1, e.Change.ChangeID)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
s.lastChangeID = e.Change.ChangeID
|
||||||
|
select {
|
||||||
|
case s.changes <- e.Change:
|
||||||
|
case <-s.ctx.Done():
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Report the error if resubscribing is disabled.
|
||||||
|
if s.resubscribe == nil {
|
||||||
|
s.err = err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
slog.Info("Resubscribing to Corrosion query due to an error.",
|
||||||
|
"err", err, "id", s.id, "from_change", s.lastChangeID)
|
||||||
|
sub, sErr := s.resubscribe(s.ctx, s.lastChangeID)
|
||||||
|
if sErr != nil {
|
||||||
|
// resubscribe returns a permanent error after unsuccessful retries.
|
||||||
|
s.err = fmt.Errorf("resubscribe to query with backoff: %w", sErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Reset the subscription to the new one.
|
||||||
|
s.rows = nil
|
||||||
|
s.body = sub.body
|
||||||
|
s.decoder = sub.decoder
|
||||||
|
// Do not close the sub to not close the body.
|
||||||
|
sub.cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Err returns the error, if any, that was encountered during fetching changes.
|
||||||
|
// Err may be called after an explicit or implicit [Subscription.Close].
|
||||||
|
func (s *Subscription) Err() error {
|
||||||
|
return s.err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Subscription) Close() error {
|
||||||
|
s.cancel()
|
||||||
|
return s.body.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// SubscribeContext creates a subscription to receive updates for a desired SQL query. If skipRows is false,
|
||||||
|
// Subscription.Rows must be consumed before Subscription.Changes can be called. If skipRows is true, Subscription.Rows
|
||||||
|
// will return nil.
|
||||||
|
func (c *APIClient) SubscribeContext(
|
||||||
|
ctx context.Context, query string, args []any, skipRows bool,
|
||||||
|
) (*Subscription, error) {
|
||||||
|
statement := Statement{
|
||||||
|
Query: query,
|
||||||
|
Params: args,
|
||||||
|
}
|
||||||
|
body, err := json.Marshal(statement)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("marshal query: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
subURL := c.baseURL.JoinPath("/v1/subscriptions")
|
||||||
|
if skipRows {
|
||||||
|
q := subURL.Query()
|
||||||
|
q.Set("skip_rows", "true")
|
||||||
|
subURL.RawQuery = q.Encode()
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, "POST", subURL.String(), bytes.NewReader(body))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("create request: %w", err)
|
||||||
|
}
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
req.Header.Set("Accept", "application/json")
|
||||||
|
|
||||||
|
resp, err := c.client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("send request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
respBody, err := io.ReadAll(resp.Body)
|
||||||
|
resp.Body.Close()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("read response body: %w", err)
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("unexpected status code %d: %s", resp.StatusCode, respBody)
|
||||||
|
}
|
||||||
|
|
||||||
|
id := resp.Header.Get("corro-query-id")
|
||||||
|
if id == "" {
|
||||||
|
resp.Body.Close()
|
||||||
|
return nil, errors.New("missing corro-query-id header in response")
|
||||||
|
}
|
||||||
|
|
||||||
|
if skipRows {
|
||||||
|
return newSubscription(ctx, id, nil, resp.Body, nil, c.resubscribeWithBackoffFn(id)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
rows, err := newRows(ctx, resp.Body, false)
|
||||||
|
if err != nil {
|
||||||
|
resp.Body.Close()
|
||||||
|
return nil, fmt.Errorf("parse query response: %w", err)
|
||||||
|
}
|
||||||
|
return newSubscription(ctx, id, rows, rows.body, rows.decoder, c.resubscribeWithBackoffFn(id)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *APIClient) resubscribeWithBackoffFn(id string) func(context.Context, uint64) (*Subscription, error) {
|
||||||
|
if c.newResubBackoff == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return func(ctx context.Context, fromChange uint64) (*Subscription, error) {
|
||||||
|
return backoff.RetryWithData(func() (*Subscription, error) {
|
||||||
|
slog.Debug("Retrying to resubscribe to Corrosion query.", "id", id, "from_change", fromChange)
|
||||||
|
return c.ResubscribeContext(ctx, id, fromChange)
|
||||||
|
}, c.newResubBackoff())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *APIClient) ResubscribeContext(ctx context.Context, id string, fromChange uint64) (*Subscription, error) {
|
||||||
|
subURL := c.baseURL.JoinPath("/v1/subscriptions", id)
|
||||||
|
q := subURL.Query()
|
||||||
|
q.Set("from", strconv.FormatUint(fromChange, 10))
|
||||||
|
subURL.RawQuery = q.Encode()
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, "GET", subURL.String(), nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("create request: %w", err)
|
||||||
|
}
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
req.Header.Set("Accept", "application/json")
|
||||||
|
|
||||||
|
resp, err := c.client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("send request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
respBody, err := io.ReadAll(resp.Body)
|
||||||
|
resp.Body.Close()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("read response body: %w", err)
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("unexpected status code %d: %s", resp.StatusCode, respBody)
|
||||||
|
}
|
||||||
|
|
||||||
|
return newSubscription(ctx, id, nil, resp.Body, nil, c.resubscribeWithBackoffFn(id)), nil
|
||||||
|
}
|
||||||
@@ -35,7 +35,7 @@ func (d *Daemon) Run(ctx context.Context) error {
|
|||||||
case <-d.machine.Started():
|
case <-d.machine.Started():
|
||||||
_, err := systemd.SdNotify(false, systemd.SdNotifyReady)
|
_, err := systemd.SdNotify(false, systemd.SdNotifyReady)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("Failed to notify systemd that the daemon is ready.", "error", err)
|
slog.Error("Failed to notify systemd that the daemon is ready.", "err", err)
|
||||||
}
|
}
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package docker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"github.com/cenkalti/backoff/v4"
|
||||||
|
"github.com/docker/docker/client"
|
||||||
|
"log/slog"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// WaitDaemonReady waits for the Docker daemon to start and be ready to serve requests.
|
||||||
|
func WaitDaemonReady(ctx context.Context, cli *client.Client) error {
|
||||||
|
// Retry to ping the Docker daemon until it's ready or the context is canceled.
|
||||||
|
boff := backoff.WithContext(backoff.NewExponentialBackOff(
|
||||||
|
backoff.WithInitialInterval(100*time.Millisecond),
|
||||||
|
backoff.WithMaxInterval(1*time.Second),
|
||||||
|
backoff.WithMaxElapsedTime(0),
|
||||||
|
), ctx)
|
||||||
|
|
||||||
|
waitingLogged := false
|
||||||
|
ping := func() error {
|
||||||
|
_, err := cli.Ping(ctx)
|
||||||
|
if err == nil {
|
||||||
|
if waitingLogged {
|
||||||
|
slog.Info("Docker daemon is ready.")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if !client.IsErrConnectionFailed(err) {
|
||||||
|
return backoff.Permanent(fmt.Errorf("connect to Docker daemon: %w", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
if !waitingLogged {
|
||||||
|
slog.Info("Waiting for Docker daemon to start and be ready.")
|
||||||
|
waitingLogged = true
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := backoff.Retry(ping, boff); err != nil {
|
||||||
|
if errors.Is(err, context.Canceled) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return fmt.Errorf("ping Docker: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
package fs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/user"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("lookup user %q: %w", username, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
uid, err = strconv.Atoi(usr.Uid)
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("parse %q user ID (UID) %q: %w", username, usr.Uid, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
gid, err = strconv.Atoi(usr.Gid)
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("parse %q user group ID (GID) %q: %w", username, usr.Gid, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func Chown(path, username, group string) error {
|
||||||
|
uid, gid := -1, -1
|
||||||
|
if username != "" {
|
||||||
|
usr, err := user.Lookup(username)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("lookup user %q: %w", username, err)
|
||||||
|
}
|
||||||
|
uid, err = strconv.Atoi(usr.Uid)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("parse %q user ID (UID) %q: %w", username, usr.Uid, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if group != "" {
|
||||||
|
grp, err := user.LookupGroup(group)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("lookup group %q: %w", group, err)
|
||||||
|
}
|
||||||
|
gid, err = strconv.Atoi(grp.Gid)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("parse %q group ID (GID) %q: %w", group, grp.Gid, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.Chown(path, uid, gid); err != nil {
|
||||||
|
return fmt.Errorf("chown %q: %w", path, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
package log
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log/slog"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SlogTextHandler extends the standard [slog.TextHandler] to provide custom formatting that is very similar
|
||||||
|
// to the default slog handler. It prefixes each log entry with a right-padded level indicator and message,
|
||||||
|
// while excluding the default time, level, and message attributes from the structured output.
|
||||||
|
//
|
||||||
|
// The output format is:
|
||||||
|
// LEVEL MESSAGE key1=value1 key2=value2
|
||||||
|
type SlogTextHandler struct {
|
||||||
|
*slog.TextHandler
|
||||||
|
opts slog.HandlerOptions
|
||||||
|
mu sync.Mutex
|
||||||
|
w io.Writer
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSlogTextHandler(w io.Writer, opts *slog.HandlerOptions) *SlogTextHandler {
|
||||||
|
if opts == nil {
|
||||||
|
opts = &slog.HandlerOptions{}
|
||||||
|
}
|
||||||
|
// Remove time, level, and message from the default attributes.
|
||||||
|
opts.ReplaceAttr = func(groups []string, a slog.Attr) slog.Attr {
|
||||||
|
if a.Key == slog.TimeKey || a.Key == slog.LevelKey || a.Key == slog.MessageKey {
|
||||||
|
return slog.Attr{}
|
||||||
|
}
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
|
||||||
|
return &SlogTextHandler{
|
||||||
|
TextHandler: slog.NewTextHandler(w, opts),
|
||||||
|
opts: *opts,
|
||||||
|
w: w,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *SlogTextHandler) Enabled(ctx context.Context, level slog.Level) bool {
|
||||||
|
return h.TextHandler.Enabled(ctx, level)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *SlogTextHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
|
||||||
|
return h.TextHandler.WithAttrs(attrs)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *SlogTextHandler) WithGroup(name string) slog.Handler {
|
||||||
|
return h.TextHandler.WithGroup(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *SlogTextHandler) Handle(ctx context.Context, r slog.Record) error {
|
||||||
|
// Only log entries that are at or above the minimum level (INFO by default).
|
||||||
|
minLevel := slog.LevelInfo
|
||||||
|
if h.opts.Level != nil {
|
||||||
|
minLevel = h.opts.Level.Level()
|
||||||
|
}
|
||||||
|
if r.Level < minLevel {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
h.mu.Lock()
|
||||||
|
defer h.mu.Unlock()
|
||||||
|
|
||||||
|
levelMsg := fmt.Sprintf("%-5s %s ", r.Level.String(), r.Message)
|
||||||
|
if _, err := h.w.Write([]byte(levelMsg)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return h.TextHandler.Handle(ctx, r)
|
||||||
|
}
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
package pb
|
package pb
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"google.golang.org/grpc/codes"
|
"google.golang.org/grpc/codes"
|
||||||
"google.golang.org/grpc/status"
|
"google.golang.org/grpc/status"
|
||||||
)
|
)
|
||||||
@@ -40,10 +39,3 @@ func (c *NetworkConfig) Validate() error {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *AddMachineRequest) Validate() error {
|
|
||||||
if r.Network == nil {
|
|
||||||
return fmt.Errorf("network not set")
|
|
||||||
}
|
|
||||||
return r.Network.Validate()
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -21,6 +21,63 @@ const (
|
|||||||
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type MachineMember_MembershipState int32
|
||||||
|
|
||||||
|
const (
|
||||||
|
MachineMember_UNKNOWN MachineMember_MembershipState = 0
|
||||||
|
// The member is active.
|
||||||
|
MachineMember_UP MachineMember_MembershipState = 1
|
||||||
|
// The member is active, but at least one cluster member suspects its down. For all purposes,
|
||||||
|
// a SUSPECT member is treated as if it were UP until either it refutes the suspicion (becoming UP)
|
||||||
|
// or fails to do so (being declared DOWN).
|
||||||
|
MachineMember_SUSPECT MachineMember_MembershipState = 2
|
||||||
|
// The member is confirmed DOWN.
|
||||||
|
MachineMember_DOWN MachineMember_MembershipState = 3
|
||||||
|
)
|
||||||
|
|
||||||
|
// Enum value maps for MachineMember_MembershipState.
|
||||||
|
var (
|
||||||
|
MachineMember_MembershipState_name = map[int32]string{
|
||||||
|
0: "UNKNOWN",
|
||||||
|
1: "UP",
|
||||||
|
2: "SUSPECT",
|
||||||
|
3: "DOWN",
|
||||||
|
}
|
||||||
|
MachineMember_MembershipState_value = map[string]int32{
|
||||||
|
"UNKNOWN": 0,
|
||||||
|
"UP": 1,
|
||||||
|
"SUSPECT": 2,
|
||||||
|
"DOWN": 3,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func (x MachineMember_MembershipState) Enum() *MachineMember_MembershipState {
|
||||||
|
p := new(MachineMember_MembershipState)
|
||||||
|
*p = x
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x MachineMember_MembershipState) String() string {
|
||||||
|
return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (MachineMember_MembershipState) Descriptor() protoreflect.EnumDescriptor {
|
||||||
|
return file_internal_machine_api_pb_cluster_proto_enumTypes[0].Descriptor()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (MachineMember_MembershipState) Type() protoreflect.EnumType {
|
||||||
|
return &file_internal_machine_api_pb_cluster_proto_enumTypes[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x MachineMember_MembershipState) Number() protoreflect.EnumNumber {
|
||||||
|
return protoreflect.EnumNumber(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use MachineMember_MembershipState.Descriptor instead.
|
||||||
|
func (MachineMember_MembershipState) EnumDescriptor() ([]byte, []int) {
|
||||||
|
return file_internal_machine_api_pb_cluster_proto_rawDescGZIP(), []int{2, 0}
|
||||||
|
}
|
||||||
|
|
||||||
type AddMachineRequest struct {
|
type AddMachineRequest struct {
|
||||||
state protoimpl.MessageState
|
state protoimpl.MessageState
|
||||||
sizeCache protoimpl.SizeCache
|
sizeCache protoimpl.SizeCache
|
||||||
@@ -123,18 +180,73 @@ func (x *AddMachineResponse) GetMachine() *MachineInfo {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type MachineMember struct {
|
||||||
|
state protoimpl.MessageState
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
|
||||||
|
Machine *MachineInfo `protobuf:"bytes,1,opt,name=machine,proto3" json:"machine,omitempty"`
|
||||||
|
State MachineMember_MembershipState `protobuf:"varint,2,opt,name=state,proto3,enum=api.MachineMember_MembershipState" json:"state,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *MachineMember) Reset() {
|
||||||
|
*x = MachineMember{}
|
||||||
|
if protoimpl.UnsafeEnabled {
|
||||||
|
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[2]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *MachineMember) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*MachineMember) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *MachineMember) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_internal_machine_api_pb_cluster_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 MachineMember.ProtoReflect.Descriptor instead.
|
||||||
|
func (*MachineMember) Descriptor() ([]byte, []int) {
|
||||||
|
return file_internal_machine_api_pb_cluster_proto_rawDescGZIP(), []int{2}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *MachineMember) GetMachine() *MachineInfo {
|
||||||
|
if x != nil {
|
||||||
|
return x.Machine
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *MachineMember) GetState() MachineMember_MembershipState {
|
||||||
|
if x != nil {
|
||||||
|
return x.State
|
||||||
|
}
|
||||||
|
return MachineMember_UNKNOWN
|
||||||
|
}
|
||||||
|
|
||||||
type ListMachinesResponse struct {
|
type ListMachinesResponse struct {
|
||||||
state protoimpl.MessageState
|
state protoimpl.MessageState
|
||||||
sizeCache protoimpl.SizeCache
|
sizeCache protoimpl.SizeCache
|
||||||
unknownFields protoimpl.UnknownFields
|
unknownFields protoimpl.UnknownFields
|
||||||
|
|
||||||
Machines []*MachineInfo `protobuf:"bytes,1,rep,name=machines,proto3" json:"machines,omitempty"`
|
Machines []*MachineMember `protobuf:"bytes,1,rep,name=machines,proto3" json:"machines,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *ListMachinesResponse) Reset() {
|
func (x *ListMachinesResponse) Reset() {
|
||||||
*x = ListMachinesResponse{}
|
*x = ListMachinesResponse{}
|
||||||
if protoimpl.UnsafeEnabled {
|
if protoimpl.UnsafeEnabled {
|
||||||
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[2]
|
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[3]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@@ -147,7 +259,7 @@ func (x *ListMachinesResponse) String() string {
|
|||||||
func (*ListMachinesResponse) ProtoMessage() {}
|
func (*ListMachinesResponse) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *ListMachinesResponse) ProtoReflect() protoreflect.Message {
|
func (x *ListMachinesResponse) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[2]
|
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[3]
|
||||||
if protoimpl.UnsafeEnabled && x != nil {
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
@@ -160,419 +272,16 @@ func (x *ListMachinesResponse) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use ListMachinesResponse.ProtoReflect.Descriptor instead.
|
// Deprecated: Use ListMachinesResponse.ProtoReflect.Descriptor instead.
|
||||||
func (*ListMachinesResponse) Descriptor() ([]byte, []int) {
|
func (*ListMachinesResponse) Descriptor() ([]byte, []int) {
|
||||||
return file_internal_machine_api_pb_cluster_proto_rawDescGZIP(), []int{2}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *ListMachinesResponse) GetMachines() []*MachineInfo {
|
|
||||||
if x != nil {
|
|
||||||
return x.Machines
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type ListMachineEndpointsRequest struct {
|
|
||||||
state protoimpl.MessageState
|
|
||||||
sizeCache protoimpl.SizeCache
|
|
||||||
unknownFields protoimpl.UnknownFields
|
|
||||||
|
|
||||||
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *ListMachineEndpointsRequest) Reset() {
|
|
||||||
*x = ListMachineEndpointsRequest{}
|
|
||||||
if protoimpl.UnsafeEnabled {
|
|
||||||
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[3]
|
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
|
||||||
ms.StoreMessageInfo(mi)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *ListMachineEndpointsRequest) String() string {
|
|
||||||
return protoimpl.X.MessageStringOf(x)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (*ListMachineEndpointsRequest) ProtoMessage() {}
|
|
||||||
|
|
||||||
func (x *ListMachineEndpointsRequest) ProtoReflect() protoreflect.Message {
|
|
||||||
mi := &file_internal_machine_api_pb_cluster_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 ListMachineEndpointsRequest.ProtoReflect.Descriptor instead.
|
|
||||||
func (*ListMachineEndpointsRequest) Descriptor() ([]byte, []int) {
|
|
||||||
return file_internal_machine_api_pb_cluster_proto_rawDescGZIP(), []int{3}
|
return file_internal_machine_api_pb_cluster_proto_rawDescGZIP(), []int{3}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *ListMachineEndpointsRequest) GetId() string {
|
func (x *ListMachinesResponse) GetMachines() []*MachineMember {
|
||||||
if x != nil {
|
|
||||||
return x.Id
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
type ListMachineEndpointsResponse struct {
|
|
||||||
state protoimpl.MessageState
|
|
||||||
sizeCache protoimpl.SizeCache
|
|
||||||
unknownFields protoimpl.UnknownFields
|
|
||||||
|
|
||||||
Endpoints *MachineEndpoints `protobuf:"bytes,1,opt,name=endpoints,proto3" json:"endpoints,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *ListMachineEndpointsResponse) Reset() {
|
|
||||||
*x = ListMachineEndpointsResponse{}
|
|
||||||
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 *ListMachineEndpointsResponse) String() string {
|
|
||||||
return protoimpl.X.MessageStringOf(x)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (*ListMachineEndpointsResponse) ProtoMessage() {}
|
|
||||||
|
|
||||||
func (x *ListMachineEndpointsResponse) 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 ListMachineEndpointsResponse.ProtoReflect.Descriptor instead.
|
|
||||||
func (*ListMachineEndpointsResponse) Descriptor() ([]byte, []int) {
|
|
||||||
return file_internal_machine_api_pb_cluster_proto_rawDescGZIP(), []int{4}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *ListMachineEndpointsResponse) GetEndpoints() *MachineEndpoints {
|
|
||||||
if x != nil {
|
|
||||||
return x.Endpoints
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type MachineInfo struct {
|
|
||||||
state protoimpl.MessageState
|
|
||||||
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"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *MachineInfo) Reset() {
|
|
||||||
*x = MachineInfo{}
|
|
||||||
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 *MachineInfo) String() string {
|
|
||||||
return protoimpl.X.MessageStringOf(x)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (*MachineInfo) ProtoMessage() {}
|
|
||||||
|
|
||||||
func (x *MachineInfo) 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 MachineInfo.ProtoReflect.Descriptor instead.
|
|
||||||
func (*MachineInfo) Descriptor() ([]byte, []int) {
|
|
||||||
return file_internal_machine_api_pb_cluster_proto_rawDescGZIP(), []int{5}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *MachineInfo) GetId() string {
|
|
||||||
if x != nil {
|
|
||||||
return x.Id
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *MachineInfo) GetName() string {
|
|
||||||
if x != nil {
|
|
||||||
return x.Name
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *MachineInfo) GetNetwork() *NetworkConfig {
|
|
||||||
if x != nil {
|
|
||||||
return x.Network
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type NetworkConfig struct {
|
|
||||||
state protoimpl.MessageState
|
|
||||||
sizeCache protoimpl.SizeCache
|
|
||||||
unknownFields protoimpl.UnknownFields
|
|
||||||
|
|
||||||
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"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *NetworkConfig) Reset() {
|
|
||||||
*x = NetworkConfig{}
|
|
||||||
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 *NetworkConfig) String() string {
|
|
||||||
return protoimpl.X.MessageStringOf(x)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (*NetworkConfig) ProtoMessage() {}
|
|
||||||
|
|
||||||
func (x *NetworkConfig) 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 NetworkConfig.ProtoReflect.Descriptor instead.
|
|
||||||
func (*NetworkConfig) Descriptor() ([]byte, []int) {
|
|
||||||
return file_internal_machine_api_pb_cluster_proto_rawDescGZIP(), []int{6}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *NetworkConfig) GetSubnet() *IPPrefix {
|
|
||||||
if x != nil {
|
|
||||||
return x.Subnet
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *NetworkConfig) GetManagementIp() *IP {
|
|
||||||
if x != nil {
|
|
||||||
return x.ManagementIp
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *NetworkConfig) GetEndpoints() []*IPPort {
|
|
||||||
if x != nil {
|
|
||||||
return x.Endpoints
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *NetworkConfig) GetPublicKey() []byte {
|
|
||||||
if x != nil {
|
|
||||||
return x.PublicKey
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type MachineEndpoints struct {
|
|
||||||
state protoimpl.MessageState
|
|
||||||
sizeCache protoimpl.SizeCache
|
|
||||||
unknownFields protoimpl.UnknownFields
|
|
||||||
|
|
||||||
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
|
|
||||||
Endpoints []*IPPort `protobuf:"bytes,2,rep,name=endpoints,proto3" json:"endpoints,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *MachineEndpoints) Reset() {
|
|
||||||
*x = MachineEndpoints{}
|
|
||||||
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 *MachineEndpoints) String() string {
|
|
||||||
return protoimpl.X.MessageStringOf(x)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (*MachineEndpoints) ProtoMessage() {}
|
|
||||||
|
|
||||||
func (x *MachineEndpoints) 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 MachineEndpoints.ProtoReflect.Descriptor instead.
|
|
||||||
func (*MachineEndpoints) Descriptor() ([]byte, []int) {
|
|
||||||
return file_internal_machine_api_pb_cluster_proto_rawDescGZIP(), []int{7}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *MachineEndpoints) GetId() string {
|
|
||||||
if x != nil {
|
|
||||||
return x.Id
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *MachineEndpoints) GetEndpoints() []*IPPort {
|
|
||||||
if x != nil {
|
|
||||||
return x.Endpoints
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type User struct {
|
|
||||||
state protoimpl.MessageState
|
|
||||||
sizeCache protoimpl.SizeCache
|
|
||||||
unknownFields protoimpl.UnknownFields
|
|
||||||
|
|
||||||
Network *NetworkConfig `protobuf:"bytes,1,opt,name=network,proto3" json:"network,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *User) Reset() {
|
|
||||||
*x = User{}
|
|
||||||
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 *User) String() string {
|
|
||||||
return protoimpl.X.MessageStringOf(x)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (*User) ProtoMessage() {}
|
|
||||||
|
|
||||||
func (x *User) 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 User.ProtoReflect.Descriptor instead.
|
|
||||||
func (*User) Descriptor() ([]byte, []int) {
|
|
||||||
return file_internal_machine_api_pb_cluster_proto_rawDescGZIP(), []int{8}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *User) GetNetwork() *NetworkConfig {
|
|
||||||
if x != nil {
|
|
||||||
return x.Network
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type State struct {
|
|
||||||
state protoimpl.MessageState
|
|
||||||
sizeCache protoimpl.SizeCache
|
|
||||||
unknownFields protoimpl.UnknownFields
|
|
||||||
|
|
||||||
Network *IPPrefix `protobuf:"bytes,1,opt,name=Network,proto3" json:"Network,omitempty"`
|
|
||||||
// The machine configuration in the state is the source of truth set by the administrator.
|
|
||||||
// The machine itself can't update it.
|
|
||||||
Machines map[string]*MachineInfo `protobuf:"bytes,2,rep,name=machines,proto3" json:"machines,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
|
|
||||||
Endpoints map[string]*MachineEndpoints `protobuf:"bytes,3,rep,name=endpoints,proto3" json:"endpoints,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
|
|
||||||
Users []*User `protobuf:"bytes,4,rep,name=users,proto3" json:"users,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *State) Reset() {
|
|
||||||
*x = State{}
|
|
||||||
if protoimpl.UnsafeEnabled {
|
|
||||||
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[9]
|
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
|
||||||
ms.StoreMessageInfo(mi)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *State) String() string {
|
|
||||||
return protoimpl.X.MessageStringOf(x)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (*State) ProtoMessage() {}
|
|
||||||
|
|
||||||
func (x *State) ProtoReflect() protoreflect.Message {
|
|
||||||
mi := &file_internal_machine_api_pb_cluster_proto_msgTypes[9]
|
|
||||||
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 State.ProtoReflect.Descriptor instead.
|
|
||||||
func (*State) Descriptor() ([]byte, []int) {
|
|
||||||
return file_internal_machine_api_pb_cluster_proto_rawDescGZIP(), []int{9}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *State) GetNetwork() *IPPrefix {
|
|
||||||
if x != nil {
|
|
||||||
return x.Network
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *State) GetMachines() map[string]*MachineInfo {
|
|
||||||
if x != nil {
|
if x != nil {
|
||||||
return x.Machines
|
return x.Machines
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *State) GetEndpoints() map[string]*MachineEndpoints {
|
|
||||||
if x != nil {
|
|
||||||
return x.Endpoints
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *State) GetUsers() []*User {
|
|
||||||
if x != nil {
|
|
||||||
return x.Users
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var File_internal_machine_api_pb_cluster_proto protoreflect.FileDescriptor
|
var File_internal_machine_api_pb_cluster_proto protoreflect.FileDescriptor
|
||||||
|
|
||||||
var file_internal_machine_api_pb_cluster_proto_rawDesc = []byte{
|
var file_internal_machine_api_pb_cluster_proto_rawDesc = []byte{
|
||||||
@@ -580,98 +289,48 @@ var file_internal_machine_api_pb_cluster_proto_rawDesc = []byte{
|
|||||||
0x6e, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x62, 0x2f, 0x63, 0x6c, 0x75, 0x73, 0x74, 0x65,
|
0x6e, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x62, 0x2f, 0x63, 0x6c, 0x75, 0x73, 0x74, 0x65,
|
||||||
0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x03, 0x61, 0x70, 0x69, 0x1a, 0x1b, 0x67, 0x6f,
|
0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x03, 0x61, 0x70, 0x69, 0x1a, 0x1b, 0x67, 0x6f,
|
||||||
0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x65, 0x6d,
|
0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x65, 0x6d,
|
||||||
0x70, 0x74, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x24, 0x69, 0x6e, 0x74, 0x65, 0x72,
|
0x70, 0x74, 0x79, 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,
|
0x6e, 0x61, 0x6c, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f,
|
||||||
0x70, 0x62, 0x2f, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22,
|
0x70, 0x62, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f,
|
||||||
0x55, 0x0a, 0x11, 0x41, 0x64, 0x64, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x71,
|
0x22, 0x55, 0x0a, 0x11, 0x41, 0x64, 0x64, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65,
|
||||||
0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01,
|
0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20,
|
||||||
0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x2c, 0x0a, 0x07, 0x6e, 0x65, 0x74, 0x77,
|
0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x2c, 0x0a, 0x07, 0x6e, 0x65, 0x74,
|
||||||
0x6f, 0x72, 0x6b, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x61, 0x70, 0x69, 0x2e,
|
0x77, 0x6f, 0x72, 0x6b, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x61, 0x70, 0x69,
|
||||||
0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x07, 0x6e,
|
0x2e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x07,
|
||||||
0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x22, 0x40, 0x0a, 0x12, 0x41, 0x64, 0x64, 0x4d, 0x61, 0x63,
|
0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x22, 0x40, 0x0a, 0x12, 0x41, 0x64, 0x64, 0x4d, 0x61,
|
||||||
0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2a, 0x0a, 0x07,
|
0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2a, 0x0a,
|
||||||
0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e,
|
0x07, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10,
|
||||||
0x61, 0x70, 0x69, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52,
|
0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f,
|
||||||
0x07, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x22, 0x44, 0x0a, 0x14, 0x4c, 0x69, 0x73, 0x74,
|
0x52, 0x07, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x22, 0xb4, 0x01, 0x0a, 0x0d, 0x4d, 0x61,
|
||||||
0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
|
0x63, 0x68, 0x69, 0x6e, 0x65, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x12, 0x2a, 0x0a, 0x07, 0x6d,
|
||||||
0x12, 0x2c, 0x0a, 0x08, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03,
|
0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x61,
|
||||||
0x28, 0x0b, 0x32, 0x10, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65,
|
0x70, 0x69, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x07,
|
||||||
0x49, 0x6e, 0x66, 0x6f, 0x52, 0x08, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x22, 0x2d,
|
0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x12, 0x38, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65,
|
||||||
0x0a, 0x1b, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x45, 0x6e, 0x64,
|
0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x22, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d, 0x61, 0x63,
|
||||||
0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a,
|
0x68, 0x69, 0x6e, 0x65, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x2e, 0x4d, 0x65, 0x6d, 0x62, 0x65,
|
||||||
0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x22, 0x53, 0x0a,
|
0x72, 0x73, 0x68, 0x69, 0x70, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74,
|
||||||
0x1c, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x45, 0x6e, 0x64, 0x70,
|
0x65, 0x22, 0x3d, 0x0a, 0x0f, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, 0x68, 0x69, 0x70, 0x53,
|
||||||
0x6f, 0x69, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x33, 0x0a,
|
0x74, 0x61, 0x74, 0x65, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10,
|
||||||
0x09, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b,
|
0x00, 0x12, 0x06, 0x0a, 0x02, 0x55, 0x50, 0x10, 0x01, 0x12, 0x0b, 0x0a, 0x07, 0x53, 0x55, 0x53,
|
||||||
0x32, 0x15, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x45, 0x6e,
|
0x50, 0x45, 0x43, 0x54, 0x10, 0x02, 0x12, 0x08, 0x0a, 0x04, 0x44, 0x4f, 0x57, 0x4e, 0x10, 0x03,
|
||||||
0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x52, 0x09, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e,
|
0x22, 0x46, 0x0a, 0x14, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73,
|
||||||
0x74, 0x73, 0x22, 0x5f, 0x0a, 0x0b, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66,
|
0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2e, 0x0a, 0x08, 0x6d, 0x61, 0x63, 0x68,
|
||||||
0x6f, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69,
|
0x69, 0x6e, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x61, 0x70, 0x69,
|
||||||
0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52,
|
0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x52, 0x08,
|
||||||
0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x2c, 0x0a, 0x07, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b,
|
0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x32, 0x8b, 0x01, 0x0a, 0x07, 0x43, 0x6c, 0x75,
|
||||||
0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4e, 0x65, 0x74,
|
0x73, 0x74, 0x65, 0x72, 0x12, 0x3d, 0x0a, 0x0a, 0x41, 0x64, 0x64, 0x4d, 0x61, 0x63, 0x68, 0x69,
|
||||||
0x77, 0x6f, 0x72, 0x6b, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x07, 0x6e, 0x65, 0x74, 0x77,
|
0x6e, 0x65, 0x12, 0x16, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x41, 0x64, 0x64, 0x4d, 0x61, 0x63, 0x68,
|
||||||
0x6f, 0x72, 0x6b, 0x22, 0xad, 0x01, 0x0a, 0x0d, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x43,
|
0x69, 0x6e, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x17, 0x2e, 0x61, 0x70, 0x69,
|
||||||
0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x25, 0x0a, 0x06, 0x73, 0x75, 0x62, 0x6e, 0x65, 0x74, 0x18,
|
0x2e, 0x41, 0x64, 0x64, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f,
|
||||||
0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x50, 0x50, 0x72,
|
0x6e, 0x73, 0x65, 0x12, 0x41, 0x0a, 0x0c, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x61, 0x63, 0x68, 0x69,
|
||||||
0x65, 0x66, 0x69, 0x78, 0x52, 0x06, 0x73, 0x75, 0x62, 0x6e, 0x65, 0x74, 0x12, 0x2c, 0x0a, 0x0d,
|
0x6e, 0x65, 0x73, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f,
|
||||||
0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x70, 0x18, 0x02, 0x20,
|
0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x19, 0x2e, 0x61, 0x70,
|
||||||
0x01, 0x28, 0x0b, 0x32, 0x07, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x50, 0x52, 0x0c, 0x6d, 0x61,
|
0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x52, 0x65,
|
||||||
0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x49, 0x70, 0x12, 0x29, 0x0a, 0x09, 0x65, 0x6e,
|
0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x37, 0x5a, 0x35, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62,
|
||||||
0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0b, 0x2e,
|
0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x70, 0x73, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x6b, 0x69, 0x2f,
|
||||||
0x61, 0x70, 0x69, 0x2e, 0x49, 0x50, 0x50, 0x6f, 0x72, 0x74, 0x52, 0x09, 0x65, 0x6e, 0x64, 0x70,
|
0x75, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c,
|
||||||
0x6f, 0x69, 0x6e, 0x74, 0x73, 0x12, 0x1c, 0x0a, 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b,
|
0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x62, 0x62,
|
||||||
0x65, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63,
|
0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||||
0x4b, 0x65, 0x79, 0x22, 0x4d, 0x0a, 0x10, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x45, 0x6e,
|
|
||||||
0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20,
|
|
||||||
0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x29, 0x0a, 0x09, 0x65, 0x6e, 0x64, 0x70, 0x6f,
|
|
||||||
0x69, 0x6e, 0x74, 0x73, 0x18, 0x02, 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, 0x22, 0x34, 0x0a, 0x04, 0x55, 0x73, 0x65, 0x72, 0x12, 0x2c, 0x0a, 0x07, 0x6e, 0x65,
|
|
||||||
0x74, 0x77, 0x6f, 0x72, 0x6b, 0x18, 0x01, 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, 0xe4, 0x02, 0x0a, 0x05, 0x53, 0x74, 0x61,
|
|
||||||
0x74, 0x65, 0x12, 0x27, 0x0a, 0x07, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x18, 0x01, 0x20,
|
|
||||||
0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x50, 0x50, 0x72, 0x65, 0x66,
|
|
||||||
0x69, 0x78, 0x52, 0x07, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x12, 0x34, 0x0a, 0x08, 0x6d,
|
|
||||||
0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x18, 0x2e,
|
|
||||||
0x61, 0x70, 0x69, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e,
|
|
||||||
0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x08, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65,
|
|
||||||
0x73, 0x12, 0x37, 0x0a, 0x09, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x18, 0x03,
|
|
||||||
0x20, 0x03, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x65,
|
|
||||||
0x2e, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52,
|
|
||||||
0x09, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x12, 0x1f, 0x0a, 0x05, 0x75, 0x73,
|
|
||||||
0x65, 0x72, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x09, 0x2e, 0x61, 0x70, 0x69, 0x2e,
|
|
||||||
0x55, 0x73, 0x65, 0x72, 0x52, 0x05, 0x75, 0x73, 0x65, 0x72, 0x73, 0x1a, 0x4d, 0x0a, 0x0d, 0x4d,
|
|
||||||
0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03,
|
|
||||||
0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x26,
|
|
||||||
0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e,
|
|
||||||
0x61, 0x70, 0x69, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52,
|
|
||||||
0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x1a, 0x53, 0x0a, 0x0e, 0x45, 0x6e,
|
|
||||||
0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03,
|
|
||||||
0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x2b,
|
|
||||||
0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e,
|
|
||||||
0x61, 0x70, 0x69, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x45, 0x6e, 0x64, 0x70, 0x6f,
|
|
||||||
0x69, 0x6e, 0x74, 0x73, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x32,
|
|
||||||
0xe8, 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, 0x12, 0x5b, 0x0a,
|
|
||||||
0x14, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x45, 0x6e, 0x64, 0x70,
|
|
||||||
0x6f, 0x69, 0x6e, 0x74, 0x73, 0x12, 0x20, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74,
|
|
||||||
0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73,
|
|
||||||
0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69,
|
|
||||||
0x73, 0x74, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e,
|
|
||||||
0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x37, 0x5a, 0x35, 0x67, 0x69,
|
|
||||||
0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x70, 0x73, 0x76, 0x69, 0x64, 0x65, 0x72,
|
|
||||||
0x73, 0x6b, 0x69, 0x2f, 0x75, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2f, 0x69, 0x6e, 0x74, 0x65,
|
|
||||||
0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x2f, 0x61, 0x70, 0x69,
|
|
||||||
0x2f, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -686,53 +345,33 @@ func file_internal_machine_api_pb_cluster_proto_rawDescGZIP() []byte {
|
|||||||
return file_internal_machine_api_pb_cluster_proto_rawDescData
|
return file_internal_machine_api_pb_cluster_proto_rawDescData
|
||||||
}
|
}
|
||||||
|
|
||||||
var file_internal_machine_api_pb_cluster_proto_msgTypes = make([]protoimpl.MessageInfo, 12)
|
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_goTypes = []any{
|
var file_internal_machine_api_pb_cluster_proto_goTypes = []any{
|
||||||
(*AddMachineRequest)(nil), // 0: api.AddMachineRequest
|
(MachineMember_MembershipState)(0), // 0: api.MachineMember.MembershipState
|
||||||
(*AddMachineResponse)(nil), // 1: api.AddMachineResponse
|
(*AddMachineRequest)(nil), // 1: api.AddMachineRequest
|
||||||
(*ListMachinesResponse)(nil), // 2: api.ListMachinesResponse
|
(*AddMachineResponse)(nil), // 2: api.AddMachineResponse
|
||||||
(*ListMachineEndpointsRequest)(nil), // 3: api.ListMachineEndpointsRequest
|
(*MachineMember)(nil), // 3: api.MachineMember
|
||||||
(*ListMachineEndpointsResponse)(nil), // 4: api.ListMachineEndpointsResponse
|
(*ListMachinesResponse)(nil), // 4: api.ListMachinesResponse
|
||||||
(*MachineInfo)(nil), // 5: api.MachineInfo
|
(*NetworkConfig)(nil), // 5: api.NetworkConfig
|
||||||
(*NetworkConfig)(nil), // 6: api.NetworkConfig
|
(*MachineInfo)(nil), // 6: api.MachineInfo
|
||||||
(*MachineEndpoints)(nil), // 7: api.MachineEndpoints
|
(*emptypb.Empty)(nil), // 7: google.protobuf.Empty
|
||||||
(*User)(nil), // 8: api.User
|
|
||||||
(*State)(nil), // 9: api.State
|
|
||||||
nil, // 10: api.State.MachinesEntry
|
|
||||||
nil, // 11: api.State.EndpointsEntry
|
|
||||||
(*IPPrefix)(nil), // 12: api.IPPrefix
|
|
||||||
(*IP)(nil), // 13: api.IP
|
|
||||||
(*IPPort)(nil), // 14: api.IPPort
|
|
||||||
(*emptypb.Empty)(nil), // 15: google.protobuf.Empty
|
|
||||||
}
|
}
|
||||||
var file_internal_machine_api_pb_cluster_proto_depIdxs = []int32{
|
var file_internal_machine_api_pb_cluster_proto_depIdxs = []int32{
|
||||||
6, // 0: api.AddMachineRequest.network:type_name -> api.NetworkConfig
|
5, // 0: api.AddMachineRequest.network:type_name -> api.NetworkConfig
|
||||||
5, // 1: api.AddMachineResponse.machine:type_name -> api.MachineInfo
|
6, // 1: api.AddMachineResponse.machine:type_name -> api.MachineInfo
|
||||||
5, // 2: api.ListMachinesResponse.machines:type_name -> api.MachineInfo
|
6, // 2: api.MachineMember.machine:type_name -> api.MachineInfo
|
||||||
7, // 3: api.ListMachineEndpointsResponse.endpoints:type_name -> api.MachineEndpoints
|
0, // 3: api.MachineMember.state:type_name -> api.MachineMember.MembershipState
|
||||||
6, // 4: api.MachineInfo.network:type_name -> api.NetworkConfig
|
3, // 4: api.ListMachinesResponse.machines:type_name -> api.MachineMember
|
||||||
12, // 5: api.NetworkConfig.subnet:type_name -> api.IPPrefix
|
1, // 5: api.Cluster.AddMachine:input_type -> api.AddMachineRequest
|
||||||
13, // 6: api.NetworkConfig.management_ip:type_name -> api.IP
|
7, // 6: api.Cluster.ListMachines:input_type -> google.protobuf.Empty
|
||||||
14, // 7: api.NetworkConfig.endpoints:type_name -> api.IPPort
|
2, // 7: api.Cluster.AddMachine:output_type -> api.AddMachineResponse
|
||||||
14, // 8: api.MachineEndpoints.endpoints:type_name -> api.IPPort
|
4, // 8: api.Cluster.ListMachines:output_type -> api.ListMachinesResponse
|
||||||
6, // 9: api.User.network:type_name -> api.NetworkConfig
|
7, // [7:9] is the sub-list for method output_type
|
||||||
12, // 10: api.State.Network:type_name -> api.IPPrefix
|
5, // [5:7] is the sub-list for method input_type
|
||||||
10, // 11: api.State.machines:type_name -> api.State.MachinesEntry
|
5, // [5:5] is the sub-list for extension type_name
|
||||||
11, // 12: api.State.endpoints:type_name -> api.State.EndpointsEntry
|
5, // [5:5] is the sub-list for extension extendee
|
||||||
8, // 13: api.State.users:type_name -> api.User
|
0, // [0:5] is the sub-list for field type_name
|
||||||
5, // 14: api.State.MachinesEntry.value:type_name -> api.MachineInfo
|
|
||||||
7, // 15: api.State.EndpointsEntry.value:type_name -> api.MachineEndpoints
|
|
||||||
0, // 16: api.Cluster.AddMachine:input_type -> api.AddMachineRequest
|
|
||||||
15, // 17: api.Cluster.ListMachines:input_type -> google.protobuf.Empty
|
|
||||||
3, // 18: api.Cluster.ListMachineEndpoints:input_type -> api.ListMachineEndpointsRequest
|
|
||||||
1, // 19: api.Cluster.AddMachine:output_type -> api.AddMachineResponse
|
|
||||||
2, // 20: api.Cluster.ListMachines:output_type -> api.ListMachinesResponse
|
|
||||||
4, // 21: api.Cluster.ListMachineEndpoints:output_type -> api.ListMachineEndpointsResponse
|
|
||||||
19, // [19:22] is the sub-list for method output_type
|
|
||||||
16, // [16:19] is the sub-list for method input_type
|
|
||||||
16, // [16:16] is the sub-list for extension type_name
|
|
||||||
16, // [16:16] is the sub-list for extension extendee
|
|
||||||
0, // [0:16] is the sub-list for field type_name
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() { file_internal_machine_api_pb_cluster_proto_init() }
|
func init() { file_internal_machine_api_pb_cluster_proto_init() }
|
||||||
@@ -740,7 +379,7 @@ func file_internal_machine_api_pb_cluster_proto_init() {
|
|||||||
if File_internal_machine_api_pb_cluster_proto != nil {
|
if File_internal_machine_api_pb_cluster_proto != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
file_internal_machine_api_pb_common_proto_init()
|
file_internal_machine_api_pb_machine_proto_init()
|
||||||
if !protoimpl.UnsafeEnabled {
|
if !protoimpl.UnsafeEnabled {
|
||||||
file_internal_machine_api_pb_cluster_proto_msgTypes[0].Exporter = func(v any, i int) any {
|
file_internal_machine_api_pb_cluster_proto_msgTypes[0].Exporter = func(v any, i int) any {
|
||||||
switch v := v.(*AddMachineRequest); i {
|
switch v := v.(*AddMachineRequest); i {
|
||||||
@@ -767,7 +406,7 @@ func file_internal_machine_api_pb_cluster_proto_init() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
file_internal_machine_api_pb_cluster_proto_msgTypes[2].Exporter = func(v any, i int) any {
|
file_internal_machine_api_pb_cluster_proto_msgTypes[2].Exporter = func(v any, i int) any {
|
||||||
switch v := v.(*ListMachinesResponse); i {
|
switch v := v.(*MachineMember); i {
|
||||||
case 0:
|
case 0:
|
||||||
return &v.state
|
return &v.state
|
||||||
case 1:
|
case 1:
|
||||||
@@ -779,79 +418,7 @@ func file_internal_machine_api_pb_cluster_proto_init() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
file_internal_machine_api_pb_cluster_proto_msgTypes[3].Exporter = func(v any, i int) any {
|
file_internal_machine_api_pb_cluster_proto_msgTypes[3].Exporter = func(v any, i int) any {
|
||||||
switch v := v.(*ListMachineEndpointsRequest); i {
|
switch v := v.(*ListMachinesResponse); 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[4].Exporter = func(v any, i int) any {
|
|
||||||
switch v := v.(*ListMachineEndpointsResponse); 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.(*MachineInfo); 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.(*NetworkConfig); 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.(*MachineEndpoints); 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.(*User); i {
|
|
||||||
case 0:
|
|
||||||
return &v.state
|
|
||||||
case 1:
|
|
||||||
return &v.sizeCache
|
|
||||||
case 2:
|
|
||||||
return &v.unknownFields
|
|
||||||
default:
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
file_internal_machine_api_pb_cluster_proto_msgTypes[9].Exporter = func(v any, i int) any {
|
|
||||||
switch v := v.(*State); i {
|
|
||||||
case 0:
|
case 0:
|
||||||
return &v.state
|
return &v.state
|
||||||
case 1:
|
case 1:
|
||||||
@@ -868,13 +435,14 @@ func file_internal_machine_api_pb_cluster_proto_init() {
|
|||||||
File: protoimpl.DescBuilder{
|
File: protoimpl.DescBuilder{
|
||||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||||
RawDescriptor: file_internal_machine_api_pb_cluster_proto_rawDesc,
|
RawDescriptor: file_internal_machine_api_pb_cluster_proto_rawDesc,
|
||||||
NumEnums: 0,
|
NumEnums: 1,
|
||||||
NumMessages: 12,
|
NumMessages: 4,
|
||||||
NumExtensions: 0,
|
NumExtensions: 0,
|
||||||
NumServices: 1,
|
NumServices: 1,
|
||||||
},
|
},
|
||||||
GoTypes: file_internal_machine_api_pb_cluster_proto_goTypes,
|
GoTypes: file_internal_machine_api_pb_cluster_proto_goTypes,
|
||||||
DependencyIndexes: file_internal_machine_api_pb_cluster_proto_depIdxs,
|
DependencyIndexes: file_internal_machine_api_pb_cluster_proto_depIdxs,
|
||||||
|
EnumInfos: file_internal_machine_api_pb_cluster_proto_enumTypes,
|
||||||
MessageInfos: file_internal_machine_api_pb_cluster_proto_msgTypes,
|
MessageInfos: file_internal_machine_api_pb_cluster_proto_msgTypes,
|
||||||
}.Build()
|
}.Build()
|
||||||
File_internal_machine_api_pb_cluster_proto = out.File
|
File_internal_machine_api_pb_cluster_proto = out.File
|
||||||
|
|||||||
@@ -5,12 +5,11 @@ package api;
|
|||||||
option go_package = "github.com/psviderski/uncloud/internal/machine/api/pb";
|
option go_package = "github.com/psviderski/uncloud/internal/machine/api/pb";
|
||||||
|
|
||||||
import "google/protobuf/empty.proto";
|
import "google/protobuf/empty.proto";
|
||||||
import "internal/machine/api/pb/common.proto";
|
import "internal/machine/api/pb/machine.proto";
|
||||||
|
|
||||||
service Cluster {
|
service Cluster {
|
||||||
rpc AddMachine(AddMachineRequest) returns (AddMachineResponse);
|
rpc AddMachine(AddMachineRequest) returns (AddMachineResponse);
|
||||||
rpc ListMachines(google.protobuf.Empty) returns (ListMachinesResponse);
|
rpc ListMachines(google.protobuf.Empty) returns (ListMachinesResponse);
|
||||||
rpc ListMachineEndpoints(ListMachineEndpointsRequest) returns (ListMachineEndpointsResponse);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
message AddMachineRequest {
|
message AddMachineRequest {
|
||||||
@@ -22,46 +21,23 @@ message AddMachineResponse {
|
|||||||
MachineInfo machine = 1;
|
MachineInfo machine = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message MachineMember {
|
||||||
|
MachineInfo machine = 1;
|
||||||
|
|
||||||
|
enum MembershipState {
|
||||||
|
UNKNOWN = 0;
|
||||||
|
// The member is active.
|
||||||
|
UP = 1;
|
||||||
|
// The member is active, but at least one cluster member suspects its down. For all purposes,
|
||||||
|
// a SUSPECT member is treated as if it were UP until either it refutes the suspicion (becoming UP)
|
||||||
|
// or fails to do so (being declared DOWN).
|
||||||
|
SUSPECT = 2;
|
||||||
|
// The member is confirmed DOWN.
|
||||||
|
DOWN = 3;
|
||||||
|
}
|
||||||
|
MembershipState state = 2;
|
||||||
|
}
|
||||||
|
|
||||||
message ListMachinesResponse {
|
message ListMachinesResponse {
|
||||||
repeated MachineInfo machines = 1;
|
repeated MachineMember machines = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
message ListMachineEndpointsRequest {
|
|
||||||
string id = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
message ListMachineEndpointsResponse {
|
|
||||||
MachineEndpoints endpoints = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
message MachineInfo {
|
|
||||||
string id = 1;
|
|
||||||
string name = 2;
|
|
||||||
NetworkConfig network = 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
message NetworkConfig {
|
|
||||||
IPPrefix subnet = 1;
|
|
||||||
IP management_ip = 2;
|
|
||||||
repeated IPPort endpoints = 3;
|
|
||||||
bytes publicKey = 4;
|
|
||||||
}
|
|
||||||
|
|
||||||
message MachineEndpoints {
|
|
||||||
string id = 1;
|
|
||||||
repeated IPPort endpoints = 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
message User {
|
|
||||||
NetworkConfig network = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
message State {
|
|
||||||
IPPrefix Network = 1;
|
|
||||||
// The machine configuration in the state is the source of truth set by the administrator.
|
|
||||||
// The machine itself can't update it.
|
|
||||||
map<string, MachineInfo> machines = 2;
|
|
||||||
map<string, MachineEndpoints> endpoints = 3;
|
|
||||||
repeated User users = 4;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|||||||
@@ -20,9 +20,8 @@ import (
|
|||||||
const _ = grpc.SupportPackageIsVersion9
|
const _ = grpc.SupportPackageIsVersion9
|
||||||
|
|
||||||
const (
|
const (
|
||||||
Cluster_AddMachine_FullMethodName = "/api.Cluster/AddMachine"
|
Cluster_AddMachine_FullMethodName = "/api.Cluster/AddMachine"
|
||||||
Cluster_ListMachines_FullMethodName = "/api.Cluster/ListMachines"
|
Cluster_ListMachines_FullMethodName = "/api.Cluster/ListMachines"
|
||||||
Cluster_ListMachineEndpoints_FullMethodName = "/api.Cluster/ListMachineEndpoints"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// ClusterClient is the client API for Cluster service.
|
// ClusterClient is the client API for Cluster service.
|
||||||
@@ -31,7 +30,6 @@ const (
|
|||||||
type ClusterClient interface {
|
type ClusterClient interface {
|
||||||
AddMachine(ctx context.Context, in *AddMachineRequest, opts ...grpc.CallOption) (*AddMachineResponse, error)
|
AddMachine(ctx context.Context, in *AddMachineRequest, opts ...grpc.CallOption) (*AddMachineResponse, error)
|
||||||
ListMachines(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*ListMachinesResponse, error)
|
ListMachines(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*ListMachinesResponse, error)
|
||||||
ListMachineEndpoints(ctx context.Context, in *ListMachineEndpointsRequest, opts ...grpc.CallOption) (*ListMachineEndpointsResponse, error)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type clusterClient struct {
|
type clusterClient struct {
|
||||||
@@ -62,23 +60,12 @@ func (c *clusterClient) ListMachines(ctx context.Context, in *emptypb.Empty, opt
|
|||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *clusterClient) ListMachineEndpoints(ctx context.Context, in *ListMachineEndpointsRequest, opts ...grpc.CallOption) (*ListMachineEndpointsResponse, error) {
|
|
||||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
|
||||||
out := new(ListMachineEndpointsResponse)
|
|
||||||
err := c.cc.Invoke(ctx, Cluster_ListMachineEndpoints_FullMethodName, in, out, cOpts...)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return out, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ClusterServer is the server API for Cluster service.
|
// ClusterServer is the server API for Cluster service.
|
||||||
// All implementations must embed UnimplementedClusterServer
|
// All implementations must embed UnimplementedClusterServer
|
||||||
// for forward compatibility.
|
// for forward compatibility.
|
||||||
type ClusterServer interface {
|
type ClusterServer interface {
|
||||||
AddMachine(context.Context, *AddMachineRequest) (*AddMachineResponse, error)
|
AddMachine(context.Context, *AddMachineRequest) (*AddMachineResponse, error)
|
||||||
ListMachines(context.Context, *emptypb.Empty) (*ListMachinesResponse, error)
|
ListMachines(context.Context, *emptypb.Empty) (*ListMachinesResponse, error)
|
||||||
ListMachineEndpoints(context.Context, *ListMachineEndpointsRequest) (*ListMachineEndpointsResponse, error)
|
|
||||||
mustEmbedUnimplementedClusterServer()
|
mustEmbedUnimplementedClusterServer()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,9 +82,6 @@ func (UnimplementedClusterServer) AddMachine(context.Context, *AddMachineRequest
|
|||||||
func (UnimplementedClusterServer) ListMachines(context.Context, *emptypb.Empty) (*ListMachinesResponse, error) {
|
func (UnimplementedClusterServer) ListMachines(context.Context, *emptypb.Empty) (*ListMachinesResponse, error) {
|
||||||
return nil, status.Errorf(codes.Unimplemented, "method ListMachines not implemented")
|
return nil, status.Errorf(codes.Unimplemented, "method ListMachines not implemented")
|
||||||
}
|
}
|
||||||
func (UnimplementedClusterServer) ListMachineEndpoints(context.Context, *ListMachineEndpointsRequest) (*ListMachineEndpointsResponse, error) {
|
|
||||||
return nil, status.Errorf(codes.Unimplemented, "method ListMachineEndpoints not implemented")
|
|
||||||
}
|
|
||||||
func (UnimplementedClusterServer) mustEmbedUnimplementedClusterServer() {}
|
func (UnimplementedClusterServer) mustEmbedUnimplementedClusterServer() {}
|
||||||
func (UnimplementedClusterServer) testEmbeddedByValue() {}
|
func (UnimplementedClusterServer) testEmbeddedByValue() {}
|
||||||
|
|
||||||
@@ -155,24 +139,6 @@ func _Cluster_ListMachines_Handler(srv interface{}, ctx context.Context, dec fun
|
|||||||
return interceptor(ctx, in, info, handler)
|
return interceptor(ctx, in, info, handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
func _Cluster_ListMachineEndpoints_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
|
||||||
in := new(ListMachineEndpointsRequest)
|
|
||||||
if err := dec(in); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if interceptor == nil {
|
|
||||||
return srv.(ClusterServer).ListMachineEndpoints(ctx, in)
|
|
||||||
}
|
|
||||||
info := &grpc.UnaryServerInfo{
|
|
||||||
Server: srv,
|
|
||||||
FullMethod: Cluster_ListMachineEndpoints_FullMethodName,
|
|
||||||
}
|
|
||||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
|
||||||
return srv.(ClusterServer).ListMachineEndpoints(ctx, req.(*ListMachineEndpointsRequest))
|
|
||||||
}
|
|
||||||
return interceptor(ctx, in, info, handler)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cluster_ServiceDesc is the grpc.ServiceDesc for Cluster service.
|
// Cluster_ServiceDesc is the grpc.ServiceDesc for Cluster service.
|
||||||
// It's only intended for direct use with grpc.RegisterService,
|
// It's only intended for direct use with grpc.RegisterService,
|
||||||
// and not to be introspected or modified (even as a copy)
|
// and not to be introspected or modified (even as a copy)
|
||||||
@@ -188,10 +154,6 @@ var Cluster_ServiceDesc = grpc.ServiceDesc{
|
|||||||
MethodName: "ListMachines",
|
MethodName: "ListMachines",
|
||||||
Handler: _Cluster_ListMachines_Handler,
|
Handler: _Cluster_ListMachines_Handler,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
MethodName: "ListMachineEndpoints",
|
|
||||||
Handler: _Cluster_ListMachineEndpoints_Handler,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
Streams: []grpc.StreamDesc{},
|
Streams: []grpc.StreamDesc{},
|
||||||
Metadata: "internal/machine/api/pb/cluster.proto",
|
Metadata: "internal/machine/api/pb/cluster.proto",
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
package pb
|
package pb
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
status "google.golang.org/genproto/googleapis/rpc/status"
|
||||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||||
reflect "reflect"
|
reflect "reflect"
|
||||||
@@ -20,6 +21,171 @@ const (
|
|||||||
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Common metadata message nested in all reply message types, injected by the gRPC proxy to provide information
|
||||||
|
// about the machine that responded to the request.
|
||||||
|
type Metadata struct {
|
||||||
|
state protoimpl.MessageState
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
|
||||||
|
// Address of the machine the response came from.
|
||||||
|
Machine string `protobuf:"bytes,1,opt,name=machine,proto3" json:"machine,omitempty"`
|
||||||
|
// error is set if the request to upstream failed. The rest of the response is undefined.
|
||||||
|
Error string `protobuf:"bytes,2,opt,name=error,proto3" json:"error,omitempty"`
|
||||||
|
// error as a gRPC Status message.
|
||||||
|
Status *status.Status `protobuf:"bytes,3,opt,name=status,proto3" json:"status,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Metadata) Reset() {
|
||||||
|
*x = Metadata{}
|
||||||
|
if protoimpl.UnsafeEnabled {
|
||||||
|
mi := &file_internal_machine_api_pb_common_proto_msgTypes[0]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Metadata) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*Metadata) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *Metadata) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_internal_machine_api_pb_common_proto_msgTypes[0]
|
||||||
|
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 Metadata.ProtoReflect.Descriptor instead.
|
||||||
|
func (*Metadata) Descriptor() ([]byte, []int) {
|
||||||
|
return file_internal_machine_api_pb_common_proto_rawDescGZIP(), []int{0}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Metadata) GetMachine() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Machine
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Metadata) GetError() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Error
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Metadata) GetStatus() *status.Status {
|
||||||
|
if x != nil {
|
||||||
|
return x.Status
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// A helper message for marshalling the metadata field and injecting it into the response by the gRPC proxy.
|
||||||
|
type Empty struct {
|
||||||
|
state protoimpl.MessageState
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
|
||||||
|
Metadata *Metadata `protobuf:"bytes,1,opt,name=metadata,proto3" json:"metadata,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Empty) Reset() {
|
||||||
|
*x = Empty{}
|
||||||
|
if protoimpl.UnsafeEnabled {
|
||||||
|
mi := &file_internal_machine_api_pb_common_proto_msgTypes[1]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Empty) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*Empty) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *Empty) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_internal_machine_api_pb_common_proto_msgTypes[1]
|
||||||
|
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 Empty.ProtoReflect.Descriptor instead.
|
||||||
|
func (*Empty) Descriptor() ([]byte, []int) {
|
||||||
|
return file_internal_machine_api_pb_common_proto_rawDescGZIP(), []int{1}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Empty) GetMetadata() *Metadata {
|
||||||
|
if x != nil {
|
||||||
|
return x.Metadata
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// EmptyResponse is a response message to be returned by the gRPC proxy when a request to the upstream failed.
|
||||||
|
// The nested Empty.Metadata message should contain an error.
|
||||||
|
type EmptyResponse struct {
|
||||||
|
state protoimpl.MessageState
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
|
||||||
|
Messages []*Empty `protobuf:"bytes,1,rep,name=messages,proto3" json:"messages,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *EmptyResponse) Reset() {
|
||||||
|
*x = EmptyResponse{}
|
||||||
|
if protoimpl.UnsafeEnabled {
|
||||||
|
mi := &file_internal_machine_api_pb_common_proto_msgTypes[2]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *EmptyResponse) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*EmptyResponse) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *EmptyResponse) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_internal_machine_api_pb_common_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 EmptyResponse.ProtoReflect.Descriptor instead.
|
||||||
|
func (*EmptyResponse) Descriptor() ([]byte, []int) {
|
||||||
|
return file_internal_machine_api_pb_common_proto_rawDescGZIP(), []int{2}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *EmptyResponse) GetMessages() []*Empty {
|
||||||
|
if x != nil {
|
||||||
|
return x.Messages
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
type IP struct {
|
type IP struct {
|
||||||
state protoimpl.MessageState
|
state protoimpl.MessageState
|
||||||
sizeCache protoimpl.SizeCache
|
sizeCache protoimpl.SizeCache
|
||||||
@@ -31,7 +197,7 @@ type IP struct {
|
|||||||
func (x *IP) Reset() {
|
func (x *IP) Reset() {
|
||||||
*x = IP{}
|
*x = IP{}
|
||||||
if protoimpl.UnsafeEnabled {
|
if protoimpl.UnsafeEnabled {
|
||||||
mi := &file_internal_machine_api_pb_common_proto_msgTypes[0]
|
mi := &file_internal_machine_api_pb_common_proto_msgTypes[3]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@@ -44,7 +210,7 @@ func (x *IP) String() string {
|
|||||||
func (*IP) ProtoMessage() {}
|
func (*IP) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *IP) ProtoReflect() protoreflect.Message {
|
func (x *IP) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_internal_machine_api_pb_common_proto_msgTypes[0]
|
mi := &file_internal_machine_api_pb_common_proto_msgTypes[3]
|
||||||
if protoimpl.UnsafeEnabled && x != nil {
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
@@ -57,7 +223,7 @@ func (x *IP) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use IP.ProtoReflect.Descriptor instead.
|
// Deprecated: Use IP.ProtoReflect.Descriptor instead.
|
||||||
func (*IP) Descriptor() ([]byte, []int) {
|
func (*IP) Descriptor() ([]byte, []int) {
|
||||||
return file_internal_machine_api_pb_common_proto_rawDescGZIP(), []int{0}
|
return file_internal_machine_api_pb_common_proto_rawDescGZIP(), []int{3}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *IP) GetIp() []byte {
|
func (x *IP) GetIp() []byte {
|
||||||
@@ -79,7 +245,7 @@ type IPPort struct {
|
|||||||
func (x *IPPort) Reset() {
|
func (x *IPPort) Reset() {
|
||||||
*x = IPPort{}
|
*x = IPPort{}
|
||||||
if protoimpl.UnsafeEnabled {
|
if protoimpl.UnsafeEnabled {
|
||||||
mi := &file_internal_machine_api_pb_common_proto_msgTypes[1]
|
mi := &file_internal_machine_api_pb_common_proto_msgTypes[4]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@@ -92,7 +258,7 @@ func (x *IPPort) String() string {
|
|||||||
func (*IPPort) ProtoMessage() {}
|
func (*IPPort) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *IPPort) ProtoReflect() protoreflect.Message {
|
func (x *IPPort) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_internal_machine_api_pb_common_proto_msgTypes[1]
|
mi := &file_internal_machine_api_pb_common_proto_msgTypes[4]
|
||||||
if protoimpl.UnsafeEnabled && x != nil {
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
@@ -105,7 +271,7 @@ func (x *IPPort) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use IPPort.ProtoReflect.Descriptor instead.
|
// Deprecated: Use IPPort.ProtoReflect.Descriptor instead.
|
||||||
func (*IPPort) Descriptor() ([]byte, []int) {
|
func (*IPPort) Descriptor() ([]byte, []int) {
|
||||||
return file_internal_machine_api_pb_common_proto_rawDescGZIP(), []int{1}
|
return file_internal_machine_api_pb_common_proto_rawDescGZIP(), []int{4}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *IPPort) GetIp() *IP {
|
func (x *IPPort) GetIp() *IP {
|
||||||
@@ -134,7 +300,7 @@ type IPPrefix struct {
|
|||||||
func (x *IPPrefix) Reset() {
|
func (x *IPPrefix) Reset() {
|
||||||
*x = IPPrefix{}
|
*x = IPPrefix{}
|
||||||
if protoimpl.UnsafeEnabled {
|
if protoimpl.UnsafeEnabled {
|
||||||
mi := &file_internal_machine_api_pb_common_proto_msgTypes[2]
|
mi := &file_internal_machine_api_pb_common_proto_msgTypes[5]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@@ -147,7 +313,7 @@ func (x *IPPrefix) String() string {
|
|||||||
func (*IPPrefix) ProtoMessage() {}
|
func (*IPPrefix) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *IPPrefix) ProtoReflect() protoreflect.Message {
|
func (x *IPPrefix) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_internal_machine_api_pb_common_proto_msgTypes[2]
|
mi := &file_internal_machine_api_pb_common_proto_msgTypes[5]
|
||||||
if protoimpl.UnsafeEnabled && x != nil {
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
@@ -160,7 +326,7 @@ func (x *IPPrefix) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use IPPrefix.ProtoReflect.Descriptor instead.
|
// Deprecated: Use IPPrefix.ProtoReflect.Descriptor instead.
|
||||||
func (*IPPrefix) Descriptor() ([]byte, []int) {
|
func (*IPPrefix) Descriptor() ([]byte, []int) {
|
||||||
return file_internal_machine_api_pb_common_proto_rawDescGZIP(), []int{2}
|
return file_internal_machine_api_pb_common_proto_rawDescGZIP(), []int{5}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *IPPrefix) GetIp() *IP {
|
func (x *IPPrefix) GetIp() *IP {
|
||||||
@@ -182,20 +348,34 @@ var File_internal_machine_api_pb_common_proto protoreflect.FileDescriptor
|
|||||||
var file_internal_machine_api_pb_common_proto_rawDesc = []byte{
|
var file_internal_machine_api_pb_common_proto_rawDesc = []byte{
|
||||||
0x0a, 0x24, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69,
|
0x0a, 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,
|
0x6e, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x62, 0x2f, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e,
|
||||||
0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x03, 0x61, 0x70, 0x69, 0x22, 0x14, 0x0a, 0x02, 0x49,
|
0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x03, 0x61, 0x70, 0x69, 0x1a, 0x17, 0x67, 0x6f, 0x6f,
|
||||||
0x50, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x02, 0x69,
|
0x67, 0x6c, 0x65, 0x2f, 0x72, 0x70, 0x63, 0x2f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x2e, 0x70,
|
||||||
0x70, 0x22, 0x35, 0x0a, 0x06, 0x49, 0x50, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x17, 0x0a, 0x02, 0x69,
|
0x72, 0x6f, 0x74, 0x6f, 0x22, 0x66, 0x0a, 0x08, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61,
|
||||||
0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x07, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x50,
|
0x12, 0x18, 0x0a, 0x07, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28,
|
||||||
0x52, 0x02, 0x69, 0x70, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x02, 0x20, 0x01,
|
0x09, 0x52, 0x07, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72,
|
||||||
0x28, 0x0d, 0x52, 0x04, 0x70, 0x6f, 0x72, 0x74, 0x22, 0x37, 0x0a, 0x08, 0x49, 0x50, 0x50, 0x72,
|
0x72, 0x6f, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72,
|
||||||
0x65, 0x66, 0x69, 0x78, 0x12, 0x17, 0x0a, 0x02, 0x69, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b,
|
0x12, 0x2a, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b,
|
||||||
0x32, 0x07, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x50, 0x52, 0x02, 0x69, 0x70, 0x12, 0x12, 0x0a,
|
0x32, 0x12, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x74,
|
||||||
0x04, 0x62, 0x69, 0x74, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x04, 0x62, 0x69, 0x74,
|
0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x32, 0x0a, 0x05,
|
||||||
0x73, 0x42, 0x37, 0x5a, 0x35, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f,
|
0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x29, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74,
|
||||||
0x70, 0x73, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x6b, 0x69, 0x2f, 0x75, 0x6e, 0x63, 0x6c, 0x6f,
|
0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d, 0x65,
|
||||||
0x75, 0x64, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x6d, 0x61, 0x63, 0x68,
|
0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61,
|
||||||
0x69, 0x6e, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74,
|
0x22, 0x37, 0x0a, 0x0d, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
|
||||||
0x6f, 0x33,
|
0x65, 0x12, 0x26, 0x0a, 0x08, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x18, 0x01, 0x20,
|
||||||
|
0x03, 0x28, 0x0b, 0x32, 0x0a, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x52,
|
||||||
|
0x08, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x22, 0x14, 0x0a, 0x02, 0x49, 0x50, 0x12,
|
||||||
|
0x0e, 0x0a, 0x02, 0x69, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x02, 0x69, 0x70, 0x22,
|
||||||
|
0x35, 0x0a, 0x06, 0x49, 0x50, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x17, 0x0a, 0x02, 0x69, 0x70, 0x18,
|
||||||
|
0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x07, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x50, 0x52, 0x02,
|
||||||
|
0x69, 0x70, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d,
|
||||||
|
0x52, 0x04, 0x70, 0x6f, 0x72, 0x74, 0x22, 0x37, 0x0a, 0x08, 0x49, 0x50, 0x50, 0x72, 0x65, 0x66,
|
||||||
|
0x69, 0x78, 0x12, 0x17, 0x0a, 0x02, 0x69, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x07,
|
||||||
|
0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x50, 0x52, 0x02, 0x69, 0x70, 0x12, 0x12, 0x0a, 0x04, 0x62,
|
||||||
|
0x69, 0x74, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x04, 0x62, 0x69, 0x74, 0x73, 0x42,
|
||||||
|
0x37, 0x5a, 0x35, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x70, 0x73,
|
||||||
|
0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x6b, 0x69, 0x2f, 0x75, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64,
|
||||||
|
0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e,
|
||||||
|
0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -210,20 +390,27 @@ func file_internal_machine_api_pb_common_proto_rawDescGZIP() []byte {
|
|||||||
return file_internal_machine_api_pb_common_proto_rawDescData
|
return file_internal_machine_api_pb_common_proto_rawDescData
|
||||||
}
|
}
|
||||||
|
|
||||||
var file_internal_machine_api_pb_common_proto_msgTypes = make([]protoimpl.MessageInfo, 3)
|
var file_internal_machine_api_pb_common_proto_msgTypes = make([]protoimpl.MessageInfo, 6)
|
||||||
var file_internal_machine_api_pb_common_proto_goTypes = []any{
|
var file_internal_machine_api_pb_common_proto_goTypes = []any{
|
||||||
(*IP)(nil), // 0: api.IP
|
(*Metadata)(nil), // 0: api.Metadata
|
||||||
(*IPPort)(nil), // 1: api.IPPort
|
(*Empty)(nil), // 1: api.Empty
|
||||||
(*IPPrefix)(nil), // 2: api.IPPrefix
|
(*EmptyResponse)(nil), // 2: api.EmptyResponse
|
||||||
|
(*IP)(nil), // 3: api.IP
|
||||||
|
(*IPPort)(nil), // 4: api.IPPort
|
||||||
|
(*IPPrefix)(nil), // 5: api.IPPrefix
|
||||||
|
(*status.Status)(nil), // 6: google.rpc.Status
|
||||||
}
|
}
|
||||||
var file_internal_machine_api_pb_common_proto_depIdxs = []int32{
|
var file_internal_machine_api_pb_common_proto_depIdxs = []int32{
|
||||||
0, // 0: api.IPPort.ip:type_name -> api.IP
|
6, // 0: api.Metadata.status:type_name -> google.rpc.Status
|
||||||
0, // 1: api.IPPrefix.ip:type_name -> api.IP
|
0, // 1: api.Empty.metadata:type_name -> api.Metadata
|
||||||
2, // [2:2] is the sub-list for method output_type
|
1, // 2: api.EmptyResponse.messages:type_name -> api.Empty
|
||||||
2, // [2:2] is the sub-list for method input_type
|
3, // 3: api.IPPort.ip:type_name -> api.IP
|
||||||
2, // [2:2] is the sub-list for extension type_name
|
3, // 4: api.IPPrefix.ip:type_name -> api.IP
|
||||||
2, // [2:2] is the sub-list for extension extendee
|
5, // [5:5] is the sub-list for method output_type
|
||||||
0, // [0:2] is the sub-list for field type_name
|
5, // [5:5] 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
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() { file_internal_machine_api_pb_common_proto_init() }
|
func init() { file_internal_machine_api_pb_common_proto_init() }
|
||||||
@@ -233,7 +420,7 @@ func file_internal_machine_api_pb_common_proto_init() {
|
|||||||
}
|
}
|
||||||
if !protoimpl.UnsafeEnabled {
|
if !protoimpl.UnsafeEnabled {
|
||||||
file_internal_machine_api_pb_common_proto_msgTypes[0].Exporter = func(v any, i int) any {
|
file_internal_machine_api_pb_common_proto_msgTypes[0].Exporter = func(v any, i int) any {
|
||||||
switch v := v.(*IP); i {
|
switch v := v.(*Metadata); i {
|
||||||
case 0:
|
case 0:
|
||||||
return &v.state
|
return &v.state
|
||||||
case 1:
|
case 1:
|
||||||
@@ -245,7 +432,7 @@ func file_internal_machine_api_pb_common_proto_init() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
file_internal_machine_api_pb_common_proto_msgTypes[1].Exporter = func(v any, i int) any {
|
file_internal_machine_api_pb_common_proto_msgTypes[1].Exporter = func(v any, i int) any {
|
||||||
switch v := v.(*IPPort); i {
|
switch v := v.(*Empty); i {
|
||||||
case 0:
|
case 0:
|
||||||
return &v.state
|
return &v.state
|
||||||
case 1:
|
case 1:
|
||||||
@@ -257,6 +444,42 @@ func file_internal_machine_api_pb_common_proto_init() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
file_internal_machine_api_pb_common_proto_msgTypes[2].Exporter = func(v any, i int) any {
|
file_internal_machine_api_pb_common_proto_msgTypes[2].Exporter = func(v any, i int) any {
|
||||||
|
switch v := v.(*EmptyResponse); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file_internal_machine_api_pb_common_proto_msgTypes[3].Exporter = func(v any, i int) any {
|
||||||
|
switch v := v.(*IP); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file_internal_machine_api_pb_common_proto_msgTypes[4].Exporter = func(v any, i int) any {
|
||||||
|
switch v := v.(*IPPort); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file_internal_machine_api_pb_common_proto_msgTypes[5].Exporter = func(v any, i int) any {
|
||||||
switch v := v.(*IPPrefix); i {
|
switch v := v.(*IPPrefix); i {
|
||||||
case 0:
|
case 0:
|
||||||
return &v.state
|
return &v.state
|
||||||
@@ -275,7 +498,7 @@ func file_internal_machine_api_pb_common_proto_init() {
|
|||||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||||
RawDescriptor: file_internal_machine_api_pb_common_proto_rawDesc,
|
RawDescriptor: file_internal_machine_api_pb_common_proto_rawDesc,
|
||||||
NumEnums: 0,
|
NumEnums: 0,
|
||||||
NumMessages: 3,
|
NumMessages: 6,
|
||||||
NumExtensions: 0,
|
NumExtensions: 0,
|
||||||
NumServices: 0,
|
NumServices: 0,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -4,6 +4,31 @@ package api;
|
|||||||
|
|
||||||
option go_package = "github.com/psviderski/uncloud/internal/machine/api/pb";
|
option go_package = "github.com/psviderski/uncloud/internal/machine/api/pb";
|
||||||
|
|
||||||
|
// Vendored at internal/machine/api/vendor/google/rpc/status.proto.
|
||||||
|
import "google/rpc/status.proto";
|
||||||
|
|
||||||
|
// Common metadata message nested in all reply message types, injected by the gRPC proxy to provide information
|
||||||
|
// about the machine that responded to the request.
|
||||||
|
message Metadata {
|
||||||
|
// Address of the machine the response came from.
|
||||||
|
string machine = 1;
|
||||||
|
// error is set if the request to upstream failed. The rest of the response is undefined.
|
||||||
|
string error = 2;
|
||||||
|
// error as a gRPC Status message.
|
||||||
|
google.rpc.Status status = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A helper message for marshalling the metadata field and injecting it into the response by the gRPC proxy.
|
||||||
|
message Empty {
|
||||||
|
Metadata metadata = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// EmptyResponse is a response message to be returned by the gRPC proxy when a request to the upstream failed.
|
||||||
|
// The nested Empty.Metadata message should contain an error.
|
||||||
|
message EmptyResponse {
|
||||||
|
repeated Empty messages = 1;
|
||||||
|
}
|
||||||
|
|
||||||
message IP {
|
message IP {
|
||||||
bytes ip = 1;
|
bytes ip = 1;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,785 @@
|
|||||||
|
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// protoc-gen-go v1.34.2
|
||||||
|
// protoc v5.27.3
|
||||||
|
// source: internal/machine/api/pb/docker.proto
|
||||||
|
|
||||||
|
package pb
|
||||||
|
|
||||||
|
import (
|
||||||
|
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||||
|
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||||
|
emptypb "google.golang.org/protobuf/types/known/emptypb"
|
||||||
|
reflect "reflect"
|
||||||
|
sync "sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Verify that this generated code is sufficiently up-to-date.
|
||||||
|
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
|
||||||
|
// Verify that runtime/protoimpl is sufficiently up-to-date.
|
||||||
|
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
||||||
|
)
|
||||||
|
|
||||||
|
type CreateContainerRequest struct {
|
||||||
|
state protoimpl.MessageState
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
|
||||||
|
// JSON serialized container.Config.
|
||||||
|
Config []byte `protobuf:"bytes,1,opt,name=config,proto3" json:"config,omitempty"`
|
||||||
|
// JSON serialized container.HostConfig.
|
||||||
|
HostConfig []byte `protobuf:"bytes,2,opt,name=host_config,json=hostConfig,proto3" json:"host_config,omitempty"`
|
||||||
|
// JSON serialized network.NetworkingConfig.
|
||||||
|
NetworkConfig []byte `protobuf:"bytes,3,opt,name=network_config,json=networkConfig,proto3" json:"network_config,omitempty"`
|
||||||
|
// JSON serialized ocispec.Platform.
|
||||||
|
Platform []byte `protobuf:"bytes,4,opt,name=platform,proto3" json:"platform,omitempty"`
|
||||||
|
Name string `protobuf:"bytes,5,opt,name=name,proto3" json:"name,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *CreateContainerRequest) Reset() {
|
||||||
|
*x = CreateContainerRequest{}
|
||||||
|
if protoimpl.UnsafeEnabled {
|
||||||
|
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[0]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *CreateContainerRequest) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*CreateContainerRequest) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *CreateContainerRequest) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[0]
|
||||||
|
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 CreateContainerRequest.ProtoReflect.Descriptor instead.
|
||||||
|
func (*CreateContainerRequest) Descriptor() ([]byte, []int) {
|
||||||
|
return file_internal_machine_api_pb_docker_proto_rawDescGZIP(), []int{0}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *CreateContainerRequest) GetConfig() []byte {
|
||||||
|
if x != nil {
|
||||||
|
return x.Config
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *CreateContainerRequest) GetHostConfig() []byte {
|
||||||
|
if x != nil {
|
||||||
|
return x.HostConfig
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *CreateContainerRequest) GetNetworkConfig() []byte {
|
||||||
|
if x != nil {
|
||||||
|
return x.NetworkConfig
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *CreateContainerRequest) GetPlatform() []byte {
|
||||||
|
if x != nil {
|
||||||
|
return x.Platform
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *CreateContainerRequest) GetName() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Name
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateContainerResponse struct {
|
||||||
|
state protoimpl.MessageState
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
|
||||||
|
// JSON serialized container.CreateResponse.
|
||||||
|
Response []byte `protobuf:"bytes,1,opt,name=response,proto3" json:"response,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *CreateContainerResponse) Reset() {
|
||||||
|
*x = CreateContainerResponse{}
|
||||||
|
if protoimpl.UnsafeEnabled {
|
||||||
|
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[1]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *CreateContainerResponse) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*CreateContainerResponse) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *CreateContainerResponse) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[1]
|
||||||
|
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 CreateContainerResponse.ProtoReflect.Descriptor instead.
|
||||||
|
func (*CreateContainerResponse) Descriptor() ([]byte, []int) {
|
||||||
|
return file_internal_machine_api_pb_docker_proto_rawDescGZIP(), []int{1}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *CreateContainerResponse) GetResponse() []byte {
|
||||||
|
if x != nil {
|
||||||
|
return x.Response
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type StartContainerRequest 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.StartOptions.
|
||||||
|
Options []byte `protobuf:"bytes,2,opt,name=options,proto3" json:"options,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *StartContainerRequest) Reset() {
|
||||||
|
*x = StartContainerRequest{}
|
||||||
|
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 *StartContainerRequest) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*StartContainerRequest) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *StartContainerRequest) 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 StartContainerRequest.ProtoReflect.Descriptor instead.
|
||||||
|
func (*StartContainerRequest) Descriptor() ([]byte, []int) {
|
||||||
|
return file_internal_machine_api_pb_docker_proto_rawDescGZIP(), []int{2}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *StartContainerRequest) GetId() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Id
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *StartContainerRequest) GetOptions() []byte {
|
||||||
|
if x != nil {
|
||||||
|
return x.Options
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListContainersRequest struct {
|
||||||
|
state protoimpl.MessageState
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
|
||||||
|
// JSON serialized container.ListOptions.
|
||||||
|
Options []byte `protobuf:"bytes,1,opt,name=options,proto3" json:"options,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ListContainersRequest) Reset() {
|
||||||
|
*x = ListContainersRequest{}
|
||||||
|
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 *ListContainersRequest) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*ListContainersRequest) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *ListContainersRequest) 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 ListContainersRequest.ProtoReflect.Descriptor instead.
|
||||||
|
func (*ListContainersRequest) Descriptor() ([]byte, []int) {
|
||||||
|
return file_internal_machine_api_pb_docker_proto_rawDescGZIP(), []int{3}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ListContainersRequest) GetOptions() []byte {
|
||||||
|
if x != nil {
|
||||||
|
return x.Options
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListContainersResponse structure allows broadcasting ListContainers requests to multiple machines.
|
||||||
|
type ListContainersResponse struct {
|
||||||
|
state protoimpl.MessageState
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
|
||||||
|
Messages []*MachineContainers `protobuf:"bytes,1,rep,name=messages,proto3" json:"messages,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ListContainersResponse) Reset() {
|
||||||
|
*x = ListContainersResponse{}
|
||||||
|
if protoimpl.UnsafeEnabled {
|
||||||
|
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[4]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ListContainersResponse) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*ListContainersResponse) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *ListContainersResponse) ProtoReflect() protoreflect.Message {
|
||||||
|
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 {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use ListContainersResponse.ProtoReflect.Descriptor instead.
|
||||||
|
func (*ListContainersResponse) Descriptor() ([]byte, []int) {
|
||||||
|
return file_internal_machine_api_pb_docker_proto_rawDescGZIP(), []int{4}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ListContainersResponse) GetMessages() []*MachineContainers {
|
||||||
|
if x != nil {
|
||||||
|
return x.Messages
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type MachineContainers struct {
|
||||||
|
state protoimpl.MessageState
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
|
||||||
|
Metadata *Metadata `protobuf:"bytes,1,opt,name=metadata,proto3" json:"metadata,omitempty"`
|
||||||
|
// JSON serialized []container.Summary.
|
||||||
|
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]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *MachineContainers) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*MachineContainers) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *MachineContainers) 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 MachineContainers.ProtoReflect.Descriptor instead.
|
||||||
|
func (*MachineContainers) Descriptor() ([]byte, []int) {
|
||||||
|
return file_internal_machine_api_pb_docker_proto_rawDescGZIP(), []int{5}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *MachineContainers) GetMetadata() *Metadata {
|
||||||
|
if x != nil {
|
||||||
|
return x.Metadata
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *MachineContainers) GetContainers() []byte {
|
||||||
|
if x != nil {
|
||||||
|
return x.Containers
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type RemoveContainerRequest 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.RemoveOptions.
|
||||||
|
Options []byte `protobuf:"bytes,2,opt,name=options,proto3" json:"options,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *RemoveContainerRequest) Reset() {
|
||||||
|
*x = RemoveContainerRequest{}
|
||||||
|
if protoimpl.UnsafeEnabled {
|
||||||
|
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[6]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *RemoveContainerRequest) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*RemoveContainerRequest) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *RemoveContainerRequest) ProtoReflect() protoreflect.Message {
|
||||||
|
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 {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use RemoveContainerRequest.ProtoReflect.Descriptor instead.
|
||||||
|
func (*RemoveContainerRequest) Descriptor() ([]byte, []int) {
|
||||||
|
return file_internal_machine_api_pb_docker_proto_rawDescGZIP(), []int{6}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *RemoveContainerRequest) GetId() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Id
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *RemoveContainerRequest) GetOptions() []byte {
|
||||||
|
if x != nil {
|
||||||
|
return x.Options
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type PullImageRequest struct {
|
||||||
|
state protoimpl.MessageState
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
|
||||||
|
Image string `protobuf:"bytes,1,opt,name=image,proto3" json:"image,omitempty"`
|
||||||
|
// JSON serialized image.PullOptions.
|
||||||
|
Options []byte `protobuf:"bytes,2,opt,name=options,proto3" json:"options,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *PullImageRequest) Reset() {
|
||||||
|
*x = PullImageRequest{}
|
||||||
|
if protoimpl.UnsafeEnabled {
|
||||||
|
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[7]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *PullImageRequest) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*PullImageRequest) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *PullImageRequest) ProtoReflect() protoreflect.Message {
|
||||||
|
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 {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use PullImageRequest.ProtoReflect.Descriptor instead.
|
||||||
|
func (*PullImageRequest) Descriptor() ([]byte, []int) {
|
||||||
|
return file_internal_machine_api_pb_docker_proto_rawDescGZIP(), []int{7}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *PullImageRequest) GetImage() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Image
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *PullImageRequest) GetOptions() []byte {
|
||||||
|
if x != nil {
|
||||||
|
return x.Options
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type JSONMessage struct {
|
||||||
|
state protoimpl.MessageState
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
|
||||||
|
// JSON serialized jsonmessage.JSONMessage.
|
||||||
|
Message []byte `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *JSONMessage) Reset() {
|
||||||
|
*x = JSONMessage{}
|
||||||
|
if protoimpl.UnsafeEnabled {
|
||||||
|
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[8]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *JSONMessage) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*JSONMessage) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *JSONMessage) ProtoReflect() protoreflect.Message {
|
||||||
|
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 {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use JSONMessage.ProtoReflect.Descriptor instead.
|
||||||
|
func (*JSONMessage) Descriptor() ([]byte, []int) {
|
||||||
|
return file_internal_machine_api_pb_docker_proto_rawDescGZIP(), []int{8}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *JSONMessage) GetMessage() []byte {
|
||||||
|
if x != nil {
|
||||||
|
return x.Message
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var File_internal_machine_api_pb_docker_proto protoreflect.FileDescriptor
|
||||||
|
|
||||||
|
var file_internal_machine_api_pb_docker_proto_rawDesc = []byte{
|
||||||
|
0x0a, 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, 0x64, 0x6f, 0x63, 0x6b, 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, 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, 0xa8,
|
||||||
|
0x01, 0x0a, 0x16, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e,
|
||||||
|
0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x63, 0x6f, 0x6e,
|
||||||
|
0x66, 0x69, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69,
|
||||||
|
0x67, 0x12, 0x1f, 0x0a, 0x0b, 0x68, 0x6f, 0x73, 0x74, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67,
|
||||||
|
0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0a, 0x68, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66,
|
||||||
|
0x69, 0x67, 0x12, 0x25, 0x0a, 0x0e, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x5f, 0x63, 0x6f,
|
||||||
|
0x6e, 0x66, 0x69, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0d, 0x6e, 0x65, 0x74, 0x77,
|
||||||
|
0x6f, 0x72, 0x6b, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x6c, 0x61,
|
||||||
|
0x74, 0x66, 0x6f, 0x72, 0x6d, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, 0x70, 0x6c, 0x61,
|
||||||
|
0x74, 0x66, 0x6f, 0x72, 0x6d, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x05, 0x20,
|
||||||
|
0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x35, 0x0a, 0x17, 0x43, 0x72, 0x65,
|
||||||
|
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,
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
file_internal_machine_api_pb_docker_proto_rawDescOnce sync.Once
|
||||||
|
file_internal_machine_api_pb_docker_proto_rawDescData = file_internal_machine_api_pb_docker_proto_rawDesc
|
||||||
|
)
|
||||||
|
|
||||||
|
func file_internal_machine_api_pb_docker_proto_rawDescGZIP() []byte {
|
||||||
|
file_internal_machine_api_pb_docker_proto_rawDescOnce.Do(func() {
|
||||||
|
file_internal_machine_api_pb_docker_proto_rawDescData = protoimpl.X.CompressGZIP(file_internal_machine_api_pb_docker_proto_rawDescData)
|
||||||
|
})
|
||||||
|
return file_internal_machine_api_pb_docker_proto_rawDescData
|
||||||
|
}
|
||||||
|
|
||||||
|
var file_internal_machine_api_pb_docker_proto_msgTypes = make([]protoimpl.MessageInfo, 9)
|
||||||
|
var file_internal_machine_api_pb_docker_proto_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
|
||||||
|
}
|
||||||
|
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
|
||||||
|
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, // [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
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() { file_internal_machine_api_pb_docker_proto_init() }
|
||||||
|
func file_internal_machine_api_pb_docker_proto_init() {
|
||||||
|
if File_internal_machine_api_pb_docker_proto != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
file_internal_machine_api_pb_common_proto_init()
|
||||||
|
if !protoimpl.UnsafeEnabled {
|
||||||
|
file_internal_machine_api_pb_docker_proto_msgTypes[0].Exporter = func(v any, i int) any {
|
||||||
|
switch v := v.(*CreateContainerRequest); 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[1].Exporter = func(v any, i int) any {
|
||||||
|
switch v := v.(*CreateContainerResponse); 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[2].Exporter = func(v any, i int) any {
|
||||||
|
switch v := v.(*StartContainerRequest); 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[3].Exporter = func(v any, i int) any {
|
||||||
|
switch v := v.(*ListContainersRequest); 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[4].Exporter = func(v any, i int) any {
|
||||||
|
switch v := v.(*ListContainersResponse); 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[5].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[6].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[7].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[8].Exporter = func(v any, i int) any {
|
||||||
|
switch v := v.(*JSONMessage); 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_docker_proto_rawDesc,
|
||||||
|
NumEnums: 0,
|
||||||
|
NumMessages: 9,
|
||||||
|
NumExtensions: 0,
|
||||||
|
NumServices: 1,
|
||||||
|
},
|
||||||
|
GoTypes: file_internal_machine_api_pb_docker_proto_goTypes,
|
||||||
|
DependencyIndexes: file_internal_machine_api_pb_docker_proto_depIdxs,
|
||||||
|
MessageInfos: file_internal_machine_api_pb_docker_proto_msgTypes,
|
||||||
|
}.Build()
|
||||||
|
File_internal_machine_api_pb_docker_proto = out.File
|
||||||
|
file_internal_machine_api_pb_docker_proto_rawDesc = nil
|
||||||
|
file_internal_machine_api_pb_docker_proto_goTypes = nil
|
||||||
|
file_internal_machine_api_pb_docker_proto_depIdxs = nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
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";
|
||||||
|
|
||||||
|
service Docker {
|
||||||
|
rpc CreateContainer(CreateContainerRequest) returns (CreateContainerResponse);
|
||||||
|
rpc StartContainer(StartContainerRequest) returns (google.protobuf.Empty);
|
||||||
|
rpc ListContainers(ListContainersRequest) returns (ListContainersResponse);
|
||||||
|
rpc RemoveContainer(RemoveContainerRequest) returns (google.protobuf.Empty);
|
||||||
|
rpc PullImage(PullImageRequest) returns (stream JSONMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
message CreateContainerRequest {
|
||||||
|
// JSON serialized container.Config.
|
||||||
|
bytes config = 1;
|
||||||
|
// JSON serialized container.HostConfig.
|
||||||
|
bytes host_config = 2;
|
||||||
|
// JSON serialized network.NetworkingConfig.
|
||||||
|
bytes network_config = 3;
|
||||||
|
// JSON serialized ocispec.Platform.
|
||||||
|
bytes platform = 4;
|
||||||
|
string name = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
message CreateContainerResponse {
|
||||||
|
// JSON serialized container.CreateResponse.
|
||||||
|
bytes response = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message StartContainerRequest {
|
||||||
|
string id = 1;
|
||||||
|
// JSON serialized container.StartOptions.
|
||||||
|
bytes options = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ListContainersRequest {
|
||||||
|
// JSON serialized container.ListOptions.
|
||||||
|
bytes options = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListContainersResponse structure allows broadcasting ListContainers requests to multiple machines.
|
||||||
|
message ListContainersResponse {
|
||||||
|
repeated MachineContainers messages = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message MachineContainers {
|
||||||
|
Metadata metadata = 1;
|
||||||
|
// JSON serialized []container.Summary.
|
||||||
|
bytes containers = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message RemoveContainerRequest {
|
||||||
|
string id = 1;
|
||||||
|
// JSON serialized container.RemoveOptions.
|
||||||
|
bytes options = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message PullImageRequest {
|
||||||
|
string image = 1;
|
||||||
|
// JSON serialized image.PullOptions.
|
||||||
|
bytes options = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message JSONMessage {
|
||||||
|
// JSON serialized jsonmessage.JSONMessage.
|
||||||
|
bytes message = 1;
|
||||||
|
}
|
||||||
@@ -0,0 +1,278 @@
|
|||||||
|
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// - protoc-gen-go-grpc v1.5.1
|
||||||
|
// - protoc v5.27.3
|
||||||
|
// source: internal/machine/api/pb/docker.proto
|
||||||
|
|
||||||
|
package pb
|
||||||
|
|
||||||
|
import (
|
||||||
|
context "context"
|
||||||
|
grpc "google.golang.org/grpc"
|
||||||
|
codes "google.golang.org/grpc/codes"
|
||||||
|
status "google.golang.org/grpc/status"
|
||||||
|
emptypb "google.golang.org/protobuf/types/known/emptypb"
|
||||||
|
)
|
||||||
|
|
||||||
|
// This is a compile-time assertion to ensure that this generated file
|
||||||
|
// is compatible with the grpc package it is being compiled against.
|
||||||
|
// Requires gRPC-Go v1.64.0 or later.
|
||||||
|
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"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DockerClient is the client API for Docker service.
|
||||||
|
//
|
||||||
|
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
|
||||||
|
type DockerClient interface {
|
||||||
|
CreateContainer(ctx context.Context, in *CreateContainerRequest, opts ...grpc.CallOption) (*CreateContainerResponse, error)
|
||||||
|
StartContainer(ctx context.Context, in *StartContainerRequest, 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
type dockerClient struct {
|
||||||
|
cc grpc.ClientConnInterface
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDockerClient(cc grpc.ClientConnInterface) DockerClient {
|
||||||
|
return &dockerClient{cc}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *dockerClient) CreateContainer(ctx context.Context, in *CreateContainerRequest, opts ...grpc.CallOption) (*CreateContainerResponse, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(CreateContainerResponse)
|
||||||
|
err := c.cc.Invoke(ctx, Docker_CreateContainer_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)
|
||||||
|
err := c.cc.Invoke(ctx, Docker_StartContainer_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)
|
||||||
|
err := c.cc.Invoke(ctx, Docker_ListContainers_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *dockerClient) RemoveContainer(ctx context.Context, in *RemoveContainerRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(emptypb.Empty)
|
||||||
|
err := c.cc.Invoke(ctx, Docker_RemoveContainer_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *dockerClient) PullImage(ctx context.Context, in *PullImageRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[JSONMessage], error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
stream, err := c.cc.NewStream(ctx, &Docker_ServiceDesc.Streams[0], Docker_PullImage_FullMethodName, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
x := &grpc.GenericClientStream[PullImageRequest, JSONMessage]{ClientStream: stream}
|
||||||
|
if err := x.ClientStream.SendMsg(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := x.ClientStream.CloseSend(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return x, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
|
||||||
|
type Docker_PullImageClient = grpc.ServerStreamingClient[JSONMessage]
|
||||||
|
|
||||||
|
// DockerServer is the server API for Docker service.
|
||||||
|
// All implementations must embed UnimplementedDockerServer
|
||||||
|
// for forward compatibility.
|
||||||
|
type DockerServer interface {
|
||||||
|
CreateContainer(context.Context, *CreateContainerRequest) (*CreateContainerResponse, error)
|
||||||
|
StartContainer(context.Context, *StartContainerRequest) (*emptypb.Empty, error)
|
||||||
|
ListContainers(context.Context, *ListContainersRequest) (*ListContainersResponse, error)
|
||||||
|
RemoveContainer(context.Context, *RemoveContainerRequest) (*emptypb.Empty, error)
|
||||||
|
PullImage(*PullImageRequest, grpc.ServerStreamingServer[JSONMessage]) error
|
||||||
|
mustEmbedUnimplementedDockerServer()
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnimplementedDockerServer must be embedded to have
|
||||||
|
// forward compatible implementations.
|
||||||
|
//
|
||||||
|
// NOTE: this should be embedded by value instead of pointer to avoid a nil
|
||||||
|
// pointer dereference when methods are called.
|
||||||
|
type UnimplementedDockerServer struct{}
|
||||||
|
|
||||||
|
func (UnimplementedDockerServer) CreateContainer(context.Context, *CreateContainerRequest) (*CreateContainerResponse, error) {
|
||||||
|
return nil, status.Errorf(codes.Unimplemented, "method CreateContainer not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedDockerServer) StartContainer(context.Context, *StartContainerRequest) (*emptypb.Empty, error) {
|
||||||
|
return nil, status.Errorf(codes.Unimplemented, "method StartContainer not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedDockerServer) ListContainers(context.Context, *ListContainersRequest) (*ListContainersResponse, error) {
|
||||||
|
return nil, status.Errorf(codes.Unimplemented, "method ListContainers not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedDockerServer) RemoveContainer(context.Context, *RemoveContainerRequest) (*emptypb.Empty, error) {
|
||||||
|
return nil, status.Errorf(codes.Unimplemented, "method RemoveContainer not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedDockerServer) PullImage(*PullImageRequest, grpc.ServerStreamingServer[JSONMessage]) error {
|
||||||
|
return status.Errorf(codes.Unimplemented, "method PullImage not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedDockerServer) mustEmbedUnimplementedDockerServer() {}
|
||||||
|
func (UnimplementedDockerServer) testEmbeddedByValue() {}
|
||||||
|
|
||||||
|
// UnsafeDockerServer may be embedded to opt out of forward compatibility for this service.
|
||||||
|
// Use of this interface is not recommended, as added methods to DockerServer will
|
||||||
|
// result in compilation errors.
|
||||||
|
type UnsafeDockerServer interface {
|
||||||
|
mustEmbedUnimplementedDockerServer()
|
||||||
|
}
|
||||||
|
|
||||||
|
func RegisterDockerServer(s grpc.ServiceRegistrar, srv DockerServer) {
|
||||||
|
// If the following call pancis, it indicates UnimplementedDockerServer was
|
||||||
|
// embedded by pointer and is nil. This will cause panics if an
|
||||||
|
// unimplemented method is ever invoked, so we test this at initialization
|
||||||
|
// time to prevent it from happening at runtime later due to I/O.
|
||||||
|
if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
|
||||||
|
t.testEmbeddedByValue()
|
||||||
|
}
|
||||||
|
s.RegisterService(&Docker_ServiceDesc, srv)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _Docker_CreateContainer_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(CreateContainerRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(DockerServer).CreateContainer(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: Docker_CreateContainer_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(DockerServer).CreateContainer(ctx, req.(*CreateContainerRequest))
|
||||||
|
}
|
||||||
|
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 {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(DockerServer).StartContainer(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: Docker_StartContainer_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(DockerServer).StartContainer(ctx, req.(*StartContainerRequest))
|
||||||
|
}
|
||||||
|
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 {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(DockerServer).ListContainers(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: Docker_ListContainers_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(DockerServer).ListContainers(ctx, req.(*ListContainersRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _Docker_RemoveContainer_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(RemoveContainerRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(DockerServer).RemoveContainer(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: Docker_RemoveContainer_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(DockerServer).RemoveContainer(ctx, req.(*RemoveContainerRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _Docker_PullImage_Handler(srv interface{}, stream grpc.ServerStream) error {
|
||||||
|
m := new(PullImageRequest)
|
||||||
|
if err := stream.RecvMsg(m); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return srv.(DockerServer).PullImage(m, &grpc.GenericServerStream[PullImageRequest, JSONMessage]{ServerStream: stream})
|
||||||
|
}
|
||||||
|
|
||||||
|
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
|
||||||
|
type Docker_PullImageServer = grpc.ServerStreamingServer[JSONMessage]
|
||||||
|
|
||||||
|
// Docker_ServiceDesc is the grpc.ServiceDesc for Docker service.
|
||||||
|
// It's only intended for direct use with grpc.RegisterService,
|
||||||
|
// and not to be introspected or modified (even as a copy)
|
||||||
|
var Docker_ServiceDesc = grpc.ServiceDesc{
|
||||||
|
ServiceName: "api.Docker",
|
||||||
|
HandlerType: (*DockerServer)(nil),
|
||||||
|
Methods: []grpc.MethodDesc{
|
||||||
|
{
|
||||||
|
MethodName: "CreateContainer",
|
||||||
|
Handler: _Docker_CreateContainer_Handler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MethodName: "StartContainer",
|
||||||
|
Handler: _Docker_StartContainer_Handler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MethodName: "ListContainers",
|
||||||
|
Handler: _Docker_ListContainers_Handler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MethodName: "RemoveContainer",
|
||||||
|
Handler: _Docker_RemoveContainer_Handler,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Streams: []grpc.StreamDesc{
|
||||||
|
{
|
||||||
|
StreamName: "PullImage",
|
||||||
|
Handler: _Docker_PullImage_Handler,
|
||||||
|
ServerStreams: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Metadata: "internal/machine/api/pb/docker.proto",
|
||||||
|
}
|
||||||
@@ -21,6 +21,140 @@ const (
|
|||||||
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type MachineInfo struct {
|
||||||
|
state protoimpl.MessageState
|
||||||
|
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"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *MachineInfo) Reset() {
|
||||||
|
*x = MachineInfo{}
|
||||||
|
if protoimpl.UnsafeEnabled {
|
||||||
|
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[0]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *MachineInfo) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*MachineInfo) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *MachineInfo) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[0]
|
||||||
|
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 MachineInfo.ProtoReflect.Descriptor instead.
|
||||||
|
func (*MachineInfo) Descriptor() ([]byte, []int) {
|
||||||
|
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{0}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *MachineInfo) GetId() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Id
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *MachineInfo) GetName() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Name
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *MachineInfo) GetNetwork() *NetworkConfig {
|
||||||
|
if x != nil {
|
||||||
|
return x.Network
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type NetworkConfig struct {
|
||||||
|
state protoimpl.MessageState
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
|
||||||
|
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"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *NetworkConfig) Reset() {
|
||||||
|
*x = NetworkConfig{}
|
||||||
|
if protoimpl.UnsafeEnabled {
|
||||||
|
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[1]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *NetworkConfig) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*NetworkConfig) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *NetworkConfig) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[1]
|
||||||
|
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 NetworkConfig.ProtoReflect.Descriptor instead.
|
||||||
|
func (*NetworkConfig) Descriptor() ([]byte, []int) {
|
||||||
|
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{1}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *NetworkConfig) GetSubnet() *IPPrefix {
|
||||||
|
if x != nil {
|
||||||
|
return x.Subnet
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *NetworkConfig) GetManagementIp() *IP {
|
||||||
|
if x != nil {
|
||||||
|
return x.ManagementIp
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *NetworkConfig) GetEndpoints() []*IPPort {
|
||||||
|
if x != nil {
|
||||||
|
return x.Endpoints
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *NetworkConfig) GetPublicKey() []byte {
|
||||||
|
if x != nil {
|
||||||
|
return x.PublicKey
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
type InitClusterRequest struct {
|
type InitClusterRequest struct {
|
||||||
state protoimpl.MessageState
|
state protoimpl.MessageState
|
||||||
sizeCache protoimpl.SizeCache
|
sizeCache protoimpl.SizeCache
|
||||||
@@ -28,13 +162,12 @@ type InitClusterRequest struct {
|
|||||||
|
|
||||||
MachineName string `protobuf:"bytes,1,opt,name=machineName,proto3" json:"machineName,omitempty"`
|
MachineName string `protobuf:"bytes,1,opt,name=machineName,proto3" json:"machineName,omitempty"`
|
||||||
Network *IPPrefix `protobuf:"bytes,2,opt,name=network,proto3" json:"network,omitempty"`
|
Network *IPPrefix `protobuf:"bytes,2,opt,name=network,proto3" json:"network,omitempty"`
|
||||||
User *User `protobuf:"bytes,3,opt,name=user,proto3" json:"user,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *InitClusterRequest) Reset() {
|
func (x *InitClusterRequest) Reset() {
|
||||||
*x = InitClusterRequest{}
|
*x = InitClusterRequest{}
|
||||||
if protoimpl.UnsafeEnabled {
|
if protoimpl.UnsafeEnabled {
|
||||||
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[0]
|
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[2]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@@ -47,7 +180,7 @@ func (x *InitClusterRequest) String() string {
|
|||||||
func (*InitClusterRequest) ProtoMessage() {}
|
func (*InitClusterRequest) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *InitClusterRequest) ProtoReflect() protoreflect.Message {
|
func (x *InitClusterRequest) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[0]
|
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[2]
|
||||||
if protoimpl.UnsafeEnabled && x != nil {
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
@@ -60,7 +193,7 @@ func (x *InitClusterRequest) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use InitClusterRequest.ProtoReflect.Descriptor instead.
|
// Deprecated: Use InitClusterRequest.ProtoReflect.Descriptor instead.
|
||||||
func (*InitClusterRequest) Descriptor() ([]byte, []int) {
|
func (*InitClusterRequest) Descriptor() ([]byte, []int) {
|
||||||
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{0}
|
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{2}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *InitClusterRequest) GetMachineName() string {
|
func (x *InitClusterRequest) GetMachineName() string {
|
||||||
@@ -77,13 +210,6 @@ func (x *InitClusterRequest) GetNetwork() *IPPrefix {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *InitClusterRequest) GetUser() *User {
|
|
||||||
if x != nil {
|
|
||||||
return x.User
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type InitClusterResponse struct {
|
type InitClusterResponse struct {
|
||||||
state protoimpl.MessageState
|
state protoimpl.MessageState
|
||||||
sizeCache protoimpl.SizeCache
|
sizeCache protoimpl.SizeCache
|
||||||
@@ -95,7 +221,7 @@ type InitClusterResponse struct {
|
|||||||
func (x *InitClusterResponse) Reset() {
|
func (x *InitClusterResponse) Reset() {
|
||||||
*x = InitClusterResponse{}
|
*x = InitClusterResponse{}
|
||||||
if protoimpl.UnsafeEnabled {
|
if protoimpl.UnsafeEnabled {
|
||||||
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[1]
|
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[3]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@@ -108,7 +234,7 @@ func (x *InitClusterResponse) String() string {
|
|||||||
func (*InitClusterResponse) ProtoMessage() {}
|
func (*InitClusterResponse) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *InitClusterResponse) ProtoReflect() protoreflect.Message {
|
func (x *InitClusterResponse) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[1]
|
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[3]
|
||||||
if protoimpl.UnsafeEnabled && x != nil {
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
@@ -121,7 +247,7 @@ func (x *InitClusterResponse) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use InitClusterResponse.ProtoReflect.Descriptor instead.
|
// Deprecated: Use InitClusterResponse.ProtoReflect.Descriptor instead.
|
||||||
func (*InitClusterResponse) Descriptor() ([]byte, []int) {
|
func (*InitClusterResponse) Descriptor() ([]byte, []int) {
|
||||||
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{1}
|
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{3}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *InitClusterResponse) GetMachine() *MachineInfo {
|
func (x *InitClusterResponse) GetMachine() *MachineInfo {
|
||||||
@@ -143,7 +269,7 @@ type JoinClusterRequest struct {
|
|||||||
func (x *JoinClusterRequest) Reset() {
|
func (x *JoinClusterRequest) Reset() {
|
||||||
*x = JoinClusterRequest{}
|
*x = JoinClusterRequest{}
|
||||||
if protoimpl.UnsafeEnabled {
|
if protoimpl.UnsafeEnabled {
|
||||||
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[2]
|
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[4]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@@ -156,7 +282,7 @@ func (x *JoinClusterRequest) String() string {
|
|||||||
func (*JoinClusterRequest) ProtoMessage() {}
|
func (*JoinClusterRequest) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *JoinClusterRequest) ProtoReflect() protoreflect.Message {
|
func (x *JoinClusterRequest) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[2]
|
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[4]
|
||||||
if protoimpl.UnsafeEnabled && x != nil {
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
@@ -169,7 +295,7 @@ func (x *JoinClusterRequest) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use JoinClusterRequest.ProtoReflect.Descriptor instead.
|
// Deprecated: Use JoinClusterRequest.ProtoReflect.Descriptor instead.
|
||||||
func (*JoinClusterRequest) Descriptor() ([]byte, []int) {
|
func (*JoinClusterRequest) Descriptor() ([]byte, []int) {
|
||||||
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{2}
|
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{4}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *JoinClusterRequest) GetMachine() *MachineInfo {
|
func (x *JoinClusterRequest) GetMachine() *MachineInfo {
|
||||||
@@ -197,7 +323,7 @@ type TokenResponse struct {
|
|||||||
func (x *TokenResponse) Reset() {
|
func (x *TokenResponse) Reset() {
|
||||||
*x = TokenResponse{}
|
*x = TokenResponse{}
|
||||||
if protoimpl.UnsafeEnabled {
|
if protoimpl.UnsafeEnabled {
|
||||||
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[3]
|
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[5]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@@ -210,7 +336,7 @@ func (x *TokenResponse) String() string {
|
|||||||
func (*TokenResponse) ProtoMessage() {}
|
func (*TokenResponse) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *TokenResponse) ProtoReflect() protoreflect.Message {
|
func (x *TokenResponse) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[3]
|
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[5]
|
||||||
if protoimpl.UnsafeEnabled && x != nil {
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
@@ -223,7 +349,7 @@ func (x *TokenResponse) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use TokenResponse.ProtoReflect.Descriptor instead.
|
// Deprecated: Use TokenResponse.ProtoReflect.Descriptor instead.
|
||||||
func (*TokenResponse) Descriptor() ([]byte, []int) {
|
func (*TokenResponse) Descriptor() ([]byte, []int) {
|
||||||
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{3}
|
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{5}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *TokenResponse) GetToken() string {
|
func (x *TokenResponse) GetToken() string {
|
||||||
@@ -233,6 +359,227 @@ func (x *TokenResponse) GetToken() string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Service struct {
|
||||||
|
state protoimpl.MessageState
|
||||||
|
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"`
|
||||||
|
Mode string `protobuf:"bytes,3,opt,name=mode,proto3" json:"mode,omitempty"`
|
||||||
|
Containers []*Service_Container `protobuf:"bytes,4,rep,name=containers,proto3" json:"containers,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Service) Reset() {
|
||||||
|
*x = Service{}
|
||||||
|
if protoimpl.UnsafeEnabled {
|
||||||
|
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[6]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Service) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*Service) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *Service) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[6]
|
||||||
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use Service.ProtoReflect.Descriptor instead.
|
||||||
|
func (*Service) Descriptor() ([]byte, []int) {
|
||||||
|
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{6}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Service) GetId() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Id
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Service) GetName() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Name
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Service) GetMode() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Mode
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Service) GetContainers() []*Service_Container {
|
||||||
|
if x != nil {
|
||||||
|
return x.Containers
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type InspectServiceRequest struct {
|
||||||
|
state protoimpl.MessageState
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
|
||||||
|
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *InspectServiceRequest) Reset() {
|
||||||
|
*x = InspectServiceRequest{}
|
||||||
|
if protoimpl.UnsafeEnabled {
|
||||||
|
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[7]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *InspectServiceRequest) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*InspectServiceRequest) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *InspectServiceRequest) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[7]
|
||||||
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use InspectServiceRequest.ProtoReflect.Descriptor instead.
|
||||||
|
func (*InspectServiceRequest) Descriptor() ([]byte, []int) {
|
||||||
|
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{7}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *InspectServiceRequest) GetId() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Id
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
type InspectServiceResponse struct {
|
||||||
|
state protoimpl.MessageState
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
|
||||||
|
Service *Service `protobuf:"bytes,1,opt,name=service,proto3" json:"service,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *InspectServiceResponse) Reset() {
|
||||||
|
*x = InspectServiceResponse{}
|
||||||
|
if protoimpl.UnsafeEnabled {
|
||||||
|
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[8]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *InspectServiceResponse) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*InspectServiceResponse) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *InspectServiceResponse) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[8]
|
||||||
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use InspectServiceResponse.ProtoReflect.Descriptor instead.
|
||||||
|
func (*InspectServiceResponse) Descriptor() ([]byte, []int) {
|
||||||
|
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{8}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *InspectServiceResponse) GetService() *Service {
|
||||||
|
if x != nil {
|
||||||
|
return x.Service
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type Service_Container struct {
|
||||||
|
state protoimpl.MessageState
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
|
||||||
|
MachineId string `protobuf:"bytes,1,opt,name=machine_id,json=machineId,proto3" json:"machine_id,omitempty"`
|
||||||
|
// JSON encoded Docker types.Container.
|
||||||
|
Container []byte `protobuf:"bytes,2,opt,name=container,proto3" json:"container,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Service_Container) Reset() {
|
||||||
|
*x = Service_Container{}
|
||||||
|
if protoimpl.UnsafeEnabled {
|
||||||
|
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[9]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Service_Container) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*Service_Container) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *Service_Container) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[9]
|
||||||
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use Service_Container.ProtoReflect.Descriptor instead.
|
||||||
|
func (*Service_Container) Descriptor() ([]byte, []int) {
|
||||||
|
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{6, 0}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Service_Container) GetMachineId() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.MachineId
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Service_Container) GetContainer() []byte {
|
||||||
|
if x != nil {
|
||||||
|
return x.Container
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
var File_internal_machine_api_pb_machine_proto protoreflect.FileDescriptor
|
var File_internal_machine_api_pb_machine_proto protoreflect.FileDescriptor
|
||||||
|
|
||||||
var file_internal_machine_api_pb_machine_proto_rawDesc = []byte{
|
var file_internal_machine_api_pb_machine_proto_rawDesc = []byte{
|
||||||
@@ -242,52 +589,88 @@ var file_internal_machine_api_pb_machine_proto_rawDesc = []byte{
|
|||||||
0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x65, 0x6d,
|
0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x65, 0x6d,
|
||||||
0x70, 0x74, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x24, 0x69, 0x6e, 0x74, 0x65, 0x72,
|
0x70, 0x74, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x24, 0x69, 0x6e, 0x74, 0x65, 0x72,
|
||||||
0x6e, 0x61, 0x6c, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f,
|
0x6e, 0x61, 0x6c, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f,
|
||||||
0x70, 0x62, 0x2f, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a,
|
0x70, 0x62, 0x2f, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22,
|
||||||
0x25, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e,
|
0x5f, 0x0a, 0x0b, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x0e,
|
||||||
0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x62, 0x2f, 0x63, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72,
|
0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x12,
|
||||||
0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x7e, 0x0a, 0x12, 0x49, 0x6e, 0x69, 0x74, 0x43, 0x6c,
|
0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61,
|
||||||
0x75, 0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x20, 0x0a, 0x0b,
|
0x6d, 0x65, 0x12, 0x2c, 0x0a, 0x07, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x18, 0x03, 0x20,
|
||||||
0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28,
|
0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72,
|
||||||
0x09, 0x52, 0x0b, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x27,
|
0x6b, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x07, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b,
|
||||||
0x0a, 0x07, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32,
|
0x22, 0xad, 0x01, 0x0a, 0x0d, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x43, 0x6f, 0x6e, 0x66,
|
||||||
0x0d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x50, 0x50, 0x72, 0x65, 0x66, 0x69, 0x78, 0x52, 0x07,
|
0x69, 0x67, 0x12, 0x25, 0x0a, 0x06, 0x73, 0x75, 0x62, 0x6e, 0x65, 0x74, 0x18, 0x01, 0x20, 0x01,
|
||||||
0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x12, 0x1d, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18,
|
0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x50, 0x50, 0x72, 0x65, 0x66, 0x69,
|
||||||
0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x09, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x55, 0x73, 0x65, 0x72,
|
0x78, 0x52, 0x06, 0x73, 0x75, 0x62, 0x6e, 0x65, 0x74, 0x12, 0x2c, 0x0a, 0x0d, 0x6d, 0x61, 0x6e,
|
||||||
0x52, 0x04, 0x75, 0x73, 0x65, 0x72, 0x22, 0x41, 0x0a, 0x13, 0x49, 0x6e, 0x69, 0x74, 0x43, 0x6c,
|
0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x70, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b,
|
||||||
0x75, 0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2a, 0x0a,
|
0x32, 0x07, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x50, 0x52, 0x0c, 0x6d, 0x61, 0x6e, 0x61, 0x67,
|
||||||
0x07, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10,
|
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,
|
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,
|
0x52, 0x0d, 0x6f, 0x74, 0x68, 0x65, 0x72, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x22,
|
||||||
0x6e, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12,
|
0x25, 0x0a, 0x0d, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
|
||||||
0x2a, 0x0a, 0x07, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b,
|
0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52,
|
||||||
0x32, 0x10, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e,
|
0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0xc3, 0x01, 0x0a, 0x07, 0x53, 0x65, 0x72, 0x76, 0x69,
|
||||||
0x66, 0x6f, 0x52, 0x07, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x12, 0x37, 0x0a, 0x0e, 0x6f,
|
0x63, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02,
|
||||||
0x74, 0x68, 0x65, 0x72, 0x5f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x18, 0x03, 0x20,
|
0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09,
|
||||||
0x03, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e,
|
0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x18, 0x03,
|
||||||
0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x0d, 0x6f, 0x74, 0x68, 0x65, 0x72, 0x4d, 0x61, 0x63, 0x68,
|
0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x12, 0x36, 0x0a, 0x0a, 0x63, 0x6f,
|
||||||
0x69, 0x6e, 0x65, 0x73, 0x22, 0x25, 0x0a, 0x0d, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x73,
|
0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16,
|
||||||
0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01,
|
0x2e, 0x61, 0x70, 0x69, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x43, 0x6f, 0x6e,
|
||||||
0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x32, 0xf5, 0x01, 0x0a, 0x07,
|
0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x0a, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65,
|
||||||
0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x12, 0x40, 0x0a, 0x0b, 0x49, 0x6e, 0x69, 0x74, 0x43,
|
0x72, 0x73, 0x1a, 0x48, 0x0a, 0x09, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12,
|
||||||
0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x12, 0x17, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x69,
|
0x1d, 0x0a, 0x0a, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20,
|
||||||
0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a,
|
0x01, 0x28, 0x09, 0x52, 0x09, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x64, 0x12, 0x1c,
|
||||||
0x18, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x69, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65,
|
0x0a, 0x09, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28,
|
||||||
0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3e, 0x0a, 0x0b, 0x4a, 0x6f, 0x69,
|
0x0c, 0x52, 0x09, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x22, 0x27, 0x0a, 0x15,
|
||||||
0x6e, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x12, 0x17, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4a,
|
0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65,
|
||||||
0x6f, 0x69, 0x6e, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
|
0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28,
|
||||||
0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f,
|
0x09, 0x52, 0x02, 0x69, 0x64, 0x22, 0x40, 0x0a, 0x16, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74,
|
||||||
0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x33, 0x0a, 0x05, 0x54, 0x6f, 0x6b,
|
0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12,
|
||||||
0x65, 0x6e, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74,
|
0x26, 0x0a, 0x07, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b,
|
||||||
0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x12, 0x2e, 0x61, 0x70, 0x69,
|
0x32, 0x0c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x07,
|
||||||
0x2e, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x33,
|
0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x32, 0xc0, 0x02, 0x0a, 0x07, 0x4d, 0x61, 0x63, 0x68,
|
||||||
0x0a, 0x07, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67,
|
0x69, 0x6e, 0x65, 0x12, 0x40, 0x0a, 0x0b, 0x49, 0x6e, 0x69, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74,
|
||||||
0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74,
|
0x65, 0x72, 0x12, 0x17, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x69, 0x74, 0x43, 0x6c, 0x75,
|
||||||
0x79, 0x1a, 0x10, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49,
|
0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x61, 0x70,
|
||||||
0x6e, 0x66, 0x6f, 0x42, 0x37, 0x5a, 0x35, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f,
|
0x69, 0x2e, 0x49, 0x6e, 0x69, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x73,
|
||||||
0x6d, 0x2f, 0x70, 0x73, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x6b, 0x69, 0x2f, 0x75, 0x6e, 0x63,
|
0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3e, 0x0a, 0x0b, 0x4a, 0x6f, 0x69, 0x6e, 0x43, 0x6c, 0x75,
|
||||||
0x6c, 0x6f, 0x75, 0x64, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x6d, 0x61,
|
0x73, 0x74, 0x65, 0x72, 0x12, 0x17, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4a, 0x6f, 0x69, 0x6e, 0x43,
|
||||||
0x63, 0x68, 0x69, 0x6e, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72,
|
0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e,
|
||||||
0x6f, 0x74, 0x6f, 0x33,
|
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 (
|
var (
|
||||||
@@ -302,36 +685,49 @@ func file_internal_machine_api_pb_machine_proto_rawDescGZIP() []byte {
|
|||||||
return file_internal_machine_api_pb_machine_proto_rawDescData
|
return file_internal_machine_api_pb_machine_proto_rawDescData
|
||||||
}
|
}
|
||||||
|
|
||||||
var file_internal_machine_api_pb_machine_proto_msgTypes = make([]protoimpl.MessageInfo, 4)
|
var file_internal_machine_api_pb_machine_proto_msgTypes = make([]protoimpl.MessageInfo, 10)
|
||||||
var file_internal_machine_api_pb_machine_proto_goTypes = []any{
|
var file_internal_machine_api_pb_machine_proto_goTypes = []any{
|
||||||
(*InitClusterRequest)(nil), // 0: api.InitClusterRequest
|
(*MachineInfo)(nil), // 0: api.MachineInfo
|
||||||
(*InitClusterResponse)(nil), // 1: api.InitClusterResponse
|
(*NetworkConfig)(nil), // 1: api.NetworkConfig
|
||||||
(*JoinClusterRequest)(nil), // 2: api.JoinClusterRequest
|
(*InitClusterRequest)(nil), // 2: api.InitClusterRequest
|
||||||
(*TokenResponse)(nil), // 3: api.TokenResponse
|
(*InitClusterResponse)(nil), // 3: api.InitClusterResponse
|
||||||
(*IPPrefix)(nil), // 4: api.IPPrefix
|
(*JoinClusterRequest)(nil), // 4: api.JoinClusterRequest
|
||||||
(*User)(nil), // 5: api.User
|
(*TokenResponse)(nil), // 5: api.TokenResponse
|
||||||
(*MachineInfo)(nil), // 6: api.MachineInfo
|
(*Service)(nil), // 6: api.Service
|
||||||
(*emptypb.Empty)(nil), // 7: google.protobuf.Empty
|
(*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
|
||||||
|
(*IPPort)(nil), // 12: api.IPPort
|
||||||
|
(*emptypb.Empty)(nil), // 13: google.protobuf.Empty
|
||||||
}
|
}
|
||||||
var file_internal_machine_api_pb_machine_proto_depIdxs = []int32{
|
var file_internal_machine_api_pb_machine_proto_depIdxs = []int32{
|
||||||
4, // 0: api.InitClusterRequest.network:type_name -> api.IPPrefix
|
1, // 0: api.MachineInfo.network:type_name -> api.NetworkConfig
|
||||||
5, // 1: api.InitClusterRequest.user:type_name -> api.User
|
10, // 1: api.NetworkConfig.subnet:type_name -> api.IPPrefix
|
||||||
6, // 2: api.InitClusterResponse.machine:type_name -> api.MachineInfo
|
11, // 2: api.NetworkConfig.management_ip:type_name -> api.IP
|
||||||
6, // 3: api.JoinClusterRequest.machine:type_name -> api.MachineInfo
|
12, // 3: api.NetworkConfig.endpoints:type_name -> api.IPPort
|
||||||
6, // 4: api.JoinClusterRequest.other_machines:type_name -> api.MachineInfo
|
10, // 4: api.InitClusterRequest.network:type_name -> api.IPPrefix
|
||||||
0, // 5: api.Machine.InitCluster:input_type -> api.InitClusterRequest
|
0, // 5: api.InitClusterResponse.machine:type_name -> api.MachineInfo
|
||||||
2, // 6: api.Machine.JoinCluster:input_type -> api.JoinClusterRequest
|
0, // 6: api.JoinClusterRequest.machine:type_name -> api.MachineInfo
|
||||||
7, // 7: api.Machine.Token:input_type -> google.protobuf.Empty
|
0, // 7: api.JoinClusterRequest.other_machines:type_name -> api.MachineInfo
|
||||||
7, // 8: api.Machine.Inspect:input_type -> google.protobuf.Empty
|
9, // 8: api.Service.containers:type_name -> api.Service.Container
|
||||||
1, // 9: api.Machine.InitCluster:output_type -> api.InitClusterResponse
|
6, // 9: api.InspectServiceResponse.service:type_name -> api.Service
|
||||||
7, // 10: api.Machine.JoinCluster:output_type -> google.protobuf.Empty
|
2, // 10: api.Machine.InitCluster:input_type -> api.InitClusterRequest
|
||||||
3, // 11: api.Machine.Token:output_type -> api.TokenResponse
|
4, // 11: api.Machine.JoinCluster:input_type -> api.JoinClusterRequest
|
||||||
6, // 12: api.Machine.Inspect:output_type -> api.MachineInfo
|
13, // 12: api.Machine.Token:input_type -> google.protobuf.Empty
|
||||||
9, // [9:13] is the sub-list for method output_type
|
13, // 13: api.Machine.Inspect:input_type -> google.protobuf.Empty
|
||||||
5, // [5:9] is the sub-list for method input_type
|
7, // 14: api.Machine.InspectService:input_type -> api.InspectServiceRequest
|
||||||
5, // [5:5] is the sub-list for extension type_name
|
3, // 15: api.Machine.InitCluster:output_type -> api.InitClusterResponse
|
||||||
5, // [5:5] is the sub-list for extension extendee
|
13, // 16: api.Machine.JoinCluster:output_type -> google.protobuf.Empty
|
||||||
0, // [0:5] is the sub-list for field type_name
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() { file_internal_machine_api_pb_machine_proto_init() }
|
func init() { file_internal_machine_api_pb_machine_proto_init() }
|
||||||
@@ -340,10 +736,9 @@ func file_internal_machine_api_pb_machine_proto_init() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
file_internal_machine_api_pb_common_proto_init()
|
file_internal_machine_api_pb_common_proto_init()
|
||||||
file_internal_machine_api_pb_cluster_proto_init()
|
|
||||||
if !protoimpl.UnsafeEnabled {
|
if !protoimpl.UnsafeEnabled {
|
||||||
file_internal_machine_api_pb_machine_proto_msgTypes[0].Exporter = func(v any, i int) any {
|
file_internal_machine_api_pb_machine_proto_msgTypes[0].Exporter = func(v any, i int) any {
|
||||||
switch v := v.(*InitClusterRequest); i {
|
switch v := v.(*MachineInfo); i {
|
||||||
case 0:
|
case 0:
|
||||||
return &v.state
|
return &v.state
|
||||||
case 1:
|
case 1:
|
||||||
@@ -355,7 +750,7 @@ func file_internal_machine_api_pb_machine_proto_init() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
file_internal_machine_api_pb_machine_proto_msgTypes[1].Exporter = func(v any, i int) any {
|
file_internal_machine_api_pb_machine_proto_msgTypes[1].Exporter = func(v any, i int) any {
|
||||||
switch v := v.(*InitClusterResponse); i {
|
switch v := v.(*NetworkConfig); i {
|
||||||
case 0:
|
case 0:
|
||||||
return &v.state
|
return &v.state
|
||||||
case 1:
|
case 1:
|
||||||
@@ -367,7 +762,7 @@ func file_internal_machine_api_pb_machine_proto_init() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
file_internal_machine_api_pb_machine_proto_msgTypes[2].Exporter = func(v any, i int) any {
|
file_internal_machine_api_pb_machine_proto_msgTypes[2].Exporter = func(v any, i int) any {
|
||||||
switch v := v.(*JoinClusterRequest); i {
|
switch v := v.(*InitClusterRequest); i {
|
||||||
case 0:
|
case 0:
|
||||||
return &v.state
|
return &v.state
|
||||||
case 1:
|
case 1:
|
||||||
@@ -379,6 +774,30 @@ func file_internal_machine_api_pb_machine_proto_init() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
file_internal_machine_api_pb_machine_proto_msgTypes[3].Exporter = func(v any, i int) any {
|
file_internal_machine_api_pb_machine_proto_msgTypes[3].Exporter = func(v any, i int) any {
|
||||||
|
switch v := v.(*InitClusterResponse); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file_internal_machine_api_pb_machine_proto_msgTypes[4].Exporter = func(v any, i int) any {
|
||||||
|
switch v := v.(*JoinClusterRequest); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file_internal_machine_api_pb_machine_proto_msgTypes[5].Exporter = func(v any, i int) any {
|
||||||
switch v := v.(*TokenResponse); i {
|
switch v := v.(*TokenResponse); i {
|
||||||
case 0:
|
case 0:
|
||||||
return &v.state
|
return &v.state
|
||||||
@@ -390,6 +809,54 @@ func file_internal_machine_api_pb_machine_proto_init() {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
file_internal_machine_api_pb_machine_proto_msgTypes[6].Exporter = func(v any, i int) any {
|
||||||
|
switch v := v.(*Service); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file_internal_machine_api_pb_machine_proto_msgTypes[7].Exporter = func(v any, i int) any {
|
||||||
|
switch v := v.(*InspectServiceRequest); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file_internal_machine_api_pb_machine_proto_msgTypes[8].Exporter = func(v any, i int) any {
|
||||||
|
switch v := v.(*InspectServiceResponse); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file_internal_machine_api_pb_machine_proto_msgTypes[9].Exporter = func(v any, i int) any {
|
||||||
|
switch v := v.(*Service_Container); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
type x struct{}
|
type x struct{}
|
||||||
out := protoimpl.TypeBuilder{
|
out := protoimpl.TypeBuilder{
|
||||||
@@ -397,7 +864,7 @@ func file_internal_machine_api_pb_machine_proto_init() {
|
|||||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||||
RawDescriptor: file_internal_machine_api_pb_machine_proto_rawDesc,
|
RawDescriptor: file_internal_machine_api_pb_machine_proto_rawDesc,
|
||||||
NumEnums: 0,
|
NumEnums: 0,
|
||||||
NumMessages: 4,
|
NumMessages: 10,
|
||||||
NumExtensions: 0,
|
NumExtensions: 0,
|
||||||
NumServices: 1,
|
NumServices: 1,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -6,19 +6,31 @@ option go_package = "github.com/psviderski/uncloud/internal/machine/api/pb";
|
|||||||
|
|
||||||
import "google/protobuf/empty.proto";
|
import "google/protobuf/empty.proto";
|
||||||
import "internal/machine/api/pb/common.proto";
|
import "internal/machine/api/pb/common.proto";
|
||||||
import "internal/machine/api/pb/cluster.proto";
|
|
||||||
|
|
||||||
service Machine {
|
service Machine {
|
||||||
rpc InitCluster(InitClusterRequest) returns (InitClusterResponse);
|
rpc InitCluster(InitClusterRequest) returns (InitClusterResponse);
|
||||||
rpc JoinCluster(JoinClusterRequest) returns (google.protobuf.Empty);
|
rpc JoinCluster(JoinClusterRequest) returns (google.protobuf.Empty);
|
||||||
rpc Token(google.protobuf.Empty) returns (TokenResponse);
|
rpc Token(google.protobuf.Empty) returns (TokenResponse);
|
||||||
rpc Inspect(google.protobuf.Empty) returns (MachineInfo);
|
rpc Inspect(google.protobuf.Empty) returns (MachineInfo);
|
||||||
|
rpc InspectService(InspectServiceRequest) returns (InspectServiceResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
message MachineInfo {
|
||||||
|
string id = 1;
|
||||||
|
string name = 2;
|
||||||
|
NetworkConfig network = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message NetworkConfig {
|
||||||
|
IPPrefix subnet = 1;
|
||||||
|
IP management_ip = 2;
|
||||||
|
repeated IPPort endpoints = 3;
|
||||||
|
bytes publicKey = 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
message InitClusterRequest {
|
message InitClusterRequest {
|
||||||
string machineName = 1;
|
string machineName = 1;
|
||||||
IPPrefix network = 2;
|
IPPrefix network = 2;
|
||||||
User user = 3;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
message InitClusterResponse {
|
message InitClusterResponse {
|
||||||
@@ -33,3 +45,25 @@ message JoinClusterRequest {
|
|||||||
message TokenResponse {
|
message TokenResponse {
|
||||||
string token = 1;
|
string token = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message Service {
|
||||||
|
string id = 1;
|
||||||
|
string name = 2;
|
||||||
|
string mode = 3;
|
||||||
|
|
||||||
|
message Container {
|
||||||
|
string machine_id = 1;
|
||||||
|
// JSON encoded Docker types.Container.
|
||||||
|
bytes container = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
repeated Container containers = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message InspectServiceRequest {
|
||||||
|
string id = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message InspectServiceResponse {
|
||||||
|
Service service = 1;
|
||||||
|
}
|
||||||
|
|||||||
@@ -20,10 +20,11 @@ import (
|
|||||||
const _ = grpc.SupportPackageIsVersion9
|
const _ = grpc.SupportPackageIsVersion9
|
||||||
|
|
||||||
const (
|
const (
|
||||||
Machine_InitCluster_FullMethodName = "/api.Machine/InitCluster"
|
Machine_InitCluster_FullMethodName = "/api.Machine/InitCluster"
|
||||||
Machine_JoinCluster_FullMethodName = "/api.Machine/JoinCluster"
|
Machine_JoinCluster_FullMethodName = "/api.Machine/JoinCluster"
|
||||||
Machine_Token_FullMethodName = "/api.Machine/Token"
|
Machine_Token_FullMethodName = "/api.Machine/Token"
|
||||||
Machine_Inspect_FullMethodName = "/api.Machine/Inspect"
|
Machine_Inspect_FullMethodName = "/api.Machine/Inspect"
|
||||||
|
Machine_InspectService_FullMethodName = "/api.Machine/InspectService"
|
||||||
)
|
)
|
||||||
|
|
||||||
// MachineClient is the client API for Machine service.
|
// MachineClient is the client API for Machine service.
|
||||||
@@ -34,6 +35,7 @@ type MachineClient interface {
|
|||||||
JoinCluster(ctx context.Context, in *JoinClusterRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
|
JoinCluster(ctx context.Context, in *JoinClusterRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
|
||||||
Token(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*TokenResponse, error)
|
Token(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*TokenResponse, error)
|
||||||
Inspect(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*MachineInfo, error)
|
Inspect(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*MachineInfo, error)
|
||||||
|
InspectService(ctx context.Context, in *InspectServiceRequest, opts ...grpc.CallOption) (*InspectServiceResponse, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type machineClient struct {
|
type machineClient struct {
|
||||||
@@ -84,6 +86,16 @@ func (c *machineClient) Inspect(ctx context.Context, in *emptypb.Empty, opts ...
|
|||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *machineClient) InspectService(ctx context.Context, in *InspectServiceRequest, opts ...grpc.CallOption) (*InspectServiceResponse, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(InspectServiceResponse)
|
||||||
|
err := c.cc.Invoke(ctx, Machine_InspectService_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
// MachineServer is the server API for Machine service.
|
// MachineServer is the server API for Machine service.
|
||||||
// All implementations must embed UnimplementedMachineServer
|
// All implementations must embed UnimplementedMachineServer
|
||||||
// for forward compatibility.
|
// for forward compatibility.
|
||||||
@@ -92,6 +104,7 @@ type MachineServer interface {
|
|||||||
JoinCluster(context.Context, *JoinClusterRequest) (*emptypb.Empty, error)
|
JoinCluster(context.Context, *JoinClusterRequest) (*emptypb.Empty, error)
|
||||||
Token(context.Context, *emptypb.Empty) (*TokenResponse, error)
|
Token(context.Context, *emptypb.Empty) (*TokenResponse, error)
|
||||||
Inspect(context.Context, *emptypb.Empty) (*MachineInfo, error)
|
Inspect(context.Context, *emptypb.Empty) (*MachineInfo, error)
|
||||||
|
InspectService(context.Context, *InspectServiceRequest) (*InspectServiceResponse, error)
|
||||||
mustEmbedUnimplementedMachineServer()
|
mustEmbedUnimplementedMachineServer()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,6 +127,9 @@ func (UnimplementedMachineServer) Token(context.Context, *emptypb.Empty) (*Token
|
|||||||
func (UnimplementedMachineServer) Inspect(context.Context, *emptypb.Empty) (*MachineInfo, error) {
|
func (UnimplementedMachineServer) Inspect(context.Context, *emptypb.Empty) (*MachineInfo, error) {
|
||||||
return nil, status.Errorf(codes.Unimplemented, "method Inspect not implemented")
|
return nil, status.Errorf(codes.Unimplemented, "method Inspect not implemented")
|
||||||
}
|
}
|
||||||
|
func (UnimplementedMachineServer) InspectService(context.Context, *InspectServiceRequest) (*InspectServiceResponse, error) {
|
||||||
|
return nil, status.Errorf(codes.Unimplemented, "method InspectService not implemented")
|
||||||
|
}
|
||||||
func (UnimplementedMachineServer) mustEmbedUnimplementedMachineServer() {}
|
func (UnimplementedMachineServer) mustEmbedUnimplementedMachineServer() {}
|
||||||
func (UnimplementedMachineServer) testEmbeddedByValue() {}
|
func (UnimplementedMachineServer) testEmbeddedByValue() {}
|
||||||
|
|
||||||
@@ -207,6 +223,24 @@ func _Machine_Inspect_Handler(srv interface{}, ctx context.Context, dec func(int
|
|||||||
return interceptor(ctx, in, info, handler)
|
return interceptor(ctx, in, info, handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func _Machine_InspectService_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(InspectServiceRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(MachineServer).InspectService(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: Machine_InspectService_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(MachineServer).InspectService(ctx, req.(*InspectServiceRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
// Machine_ServiceDesc is the grpc.ServiceDesc for Machine service.
|
// Machine_ServiceDesc is the grpc.ServiceDesc for Machine service.
|
||||||
// It's only intended for direct use with grpc.RegisterService,
|
// It's only intended for direct use with grpc.RegisterService,
|
||||||
// and not to be introspected or modified (even as a copy)
|
// and not to be introspected or modified (even as a copy)
|
||||||
@@ -230,6 +264,10 @@ var Machine_ServiceDesc = grpc.ServiceDesc{
|
|||||||
MethodName: "Inspect",
|
MethodName: "Inspect",
|
||||||
Handler: _Machine_Inspect_Handler,
|
Handler: _Machine_Inspect_Handler,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
MethodName: "InspectService",
|
||||||
|
Handler: _Machine_InspectService_Handler,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
Streams: []grpc.StreamDesc{},
|
Streams: []grpc.StreamDesc{},
|
||||||
Metadata: "internal/machine/api/pb/machine.proto",
|
Metadata: "internal/machine/api/pb/machine.proto",
|
||||||
|
|||||||
@@ -0,0 +1,144 @@
|
|||||||
|
package proxy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"google.golang.org/grpc/status"
|
||||||
|
"google.golang.org/protobuf/encoding/protowire"
|
||||||
|
"google.golang.org/protobuf/proto"
|
||||||
|
"uncloud/internal/machine/api/pb"
|
||||||
|
)
|
||||||
|
|
||||||
|
// One2ManyResponder converts upstream responses into messages from upstreams, so that multiple
|
||||||
|
// successful and failure responses might be returned in One2Many mode.
|
||||||
|
type One2ManyResponder struct {
|
||||||
|
machine string
|
||||||
|
}
|
||||||
|
|
||||||
|
// AppendInfo is called to enhance response from the backend with additional data.
|
||||||
|
//
|
||||||
|
// AppendInfo enhances upstream response with machine metadata (target).
|
||||||
|
//
|
||||||
|
// This method depends on grpc protobuf response structure, each response should
|
||||||
|
// look like:
|
||||||
|
//
|
||||||
|
// message SomeResponse {
|
||||||
|
// repeated SomeReply messages = 1; // please note field ID == 1
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// message SomeReply {
|
||||||
|
// common.Metadata metadata = 1;
|
||||||
|
// <other fields go here ...>
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// As 'SomeReply' is repeated in 'SomeResponse', if we concatenate protobuf representation
|
||||||
|
// of several 'SomeResponse' messages, we still get valid 'SomeResponse' representation but with more
|
||||||
|
// entries (feature of protobuf binary representation).
|
||||||
|
//
|
||||||
|
// If we look at binary representation of any unary 'SomeResponse' message, it will always contain one
|
||||||
|
// protobuf field with field ID 1 (see above) and type 2 (embedded message SomeReply is encoded
|
||||||
|
// as string with length). So if we want to add fields to 'SomeReply', we can simply read field
|
||||||
|
// header, adjust length for new 'SomeReply' representation, and prepend new field header.
|
||||||
|
//
|
||||||
|
// At the same time, we can add 'common.Metadata' structure to 'SomeReply' by simply
|
||||||
|
// appending or prepending 'common.Metadata' as a single field. This requires 'metadata'
|
||||||
|
// field to be not defined in original response. (This is due to the fact that protobuf message
|
||||||
|
// representation is concatenation of each field representation).
|
||||||
|
//
|
||||||
|
// To build only single field (Metadata) we use helper message which contains exactly this
|
||||||
|
// field with same field ID as in every other 'SomeReply':
|
||||||
|
//
|
||||||
|
// message Empty {
|
||||||
|
// common.Metadata metadata = 1;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// As streaming replies are not wrapped into 'SomeResponse' with 'repeated', handling is simpler: we just
|
||||||
|
// need to append Empty with details.
|
||||||
|
//
|
||||||
|
// So AppendInfo does the following: validates that response contains field ID 1 encoded as string,
|
||||||
|
// cuts field header, rest is representation of some reply. Marshal 'Empty' as protobuf,
|
||||||
|
// which builds 'common.Metadata' field, append it to original response message, build new header
|
||||||
|
// for new length of some response, and add back new field header.
|
||||||
|
func (b *One2ManyResponder) AppendInfo(streaming bool, resp []byte) ([]byte, error) {
|
||||||
|
payload, err := proto.Marshal(&pb.Empty{
|
||||||
|
Metadata: &pb.Metadata{
|
||||||
|
Machine: b.machine,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if streaming {
|
||||||
|
return append(resp, payload...), err
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
metadataField = 1 // field number in proto definition for repeated response
|
||||||
|
metadataType = 2 // "string" for embedded messages
|
||||||
|
)
|
||||||
|
|
||||||
|
// decode protobuf embedded header
|
||||||
|
|
||||||
|
typ, n1 := protowire.ConsumeVarint(resp)
|
||||||
|
if n1 < 0 {
|
||||||
|
return nil, protowire.ParseError(n1)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, n2 := protowire.ConsumeVarint(resp[n1:]) // length
|
||||||
|
if n2 < 0 {
|
||||||
|
return nil, protowire.ParseError(n2)
|
||||||
|
}
|
||||||
|
|
||||||
|
if typ != (metadataField<<3)|metadataType {
|
||||||
|
return nil, fmt.Errorf("unexpected message format: %d", typ)
|
||||||
|
}
|
||||||
|
|
||||||
|
if n1+n2 > len(resp) {
|
||||||
|
return nil, fmt.Errorf("unexpected message size: %d", len(resp))
|
||||||
|
}
|
||||||
|
|
||||||
|
// cut off embedded message header
|
||||||
|
resp = resp[n1+n2:]
|
||||||
|
// build new embedded message header
|
||||||
|
prefix := protowire.AppendVarint(
|
||||||
|
protowire.AppendVarint(nil, (metadataField<<3)|metadataType),
|
||||||
|
uint64(len(resp)+len(payload)),
|
||||||
|
)
|
||||||
|
resp = append(prefix, resp...)
|
||||||
|
|
||||||
|
return append(resp, payload...), err
|
||||||
|
}
|
||||||
|
|
||||||
|
// BuildError converts upstream error into message from upstream, so that multiple
|
||||||
|
// successful and failure responses might be returned.
|
||||||
|
//
|
||||||
|
// This simply relies on the fact that any response contains 'Empty' message.
|
||||||
|
// So if 'Empty' is unmarshalled into any other reply message, all the fields
|
||||||
|
// are undefined but 'Metadata':
|
||||||
|
//
|
||||||
|
// message Empty {
|
||||||
|
// common.Metadata metadata = 1;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// message EmptyResponse {
|
||||||
|
// repeated Empty messages = 1;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// Streaming responses are not wrapped into Empty, so we simply marshall EmptyResponse
|
||||||
|
// message.
|
||||||
|
func (b *One2ManyResponder) BuildError(streaming bool, err error) ([]byte, error) {
|
||||||
|
var resp proto.Message = &pb.Empty{
|
||||||
|
Metadata: &pb.Metadata{
|
||||||
|
Machine: b.machine,
|
||||||
|
Error: err.Error(),
|
||||||
|
Status: status.Convert(err).Proto(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if !streaming {
|
||||||
|
resp = &pb.EmptyResponse{
|
||||||
|
Messages: []*pb.Empty{
|
||||||
|
resp.(*pb.Empty),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return proto.Marshal(resp)
|
||||||
|
}
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
package proxy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"github.com/siderolabs/grpc-proxy/proxy"
|
||||||
|
"google.golang.org/grpc/codes"
|
||||||
|
"google.golang.org/grpc/metadata"
|
||||||
|
"google.golang.org/grpc/status"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Director manages routing of gRPC requests between local and remote backends.
|
||||||
|
type Director struct {
|
||||||
|
localBackend *LocalBackend
|
||||||
|
remotePort uint16
|
||||||
|
remoteBackends sync.Map
|
||||||
|
// mu synchronizes access to localAddress.
|
||||||
|
mu sync.RWMutex
|
||||||
|
localAddress string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDirector(localSockPath string, remotePort uint16) *Director {
|
||||||
|
return &Director{
|
||||||
|
localBackend: NewLocalBackend(localSockPath, ""),
|
||||||
|
remotePort: remotePort,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateLocalAddress updates the local machine address used to identify which requests should be proxied
|
||||||
|
// to the local gRPC server.
|
||||||
|
func (d *Director) UpdateLocalAddress(addr string) {
|
||||||
|
d.mu.Lock()
|
||||||
|
defer d.mu.Unlock()
|
||||||
|
|
||||||
|
d.localAddress = addr
|
||||||
|
// Replace the local backend with the one that has local address set.
|
||||||
|
d.localBackend = NewLocalBackend(d.localBackend.sockPath, addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Director implements proxy.StreamDirector for grpc-proxy, routing requests to local or remote backends based
|
||||||
|
// on gRPC metadata in the context. Each machine metadata is injected into the response messages by the proxy
|
||||||
|
// if the request is proxied to multiple backends.
|
||||||
|
func (d *Director) Director(ctx context.Context, fullMethodName string) (proxy.Mode, []proxy.Backend, error) {
|
||||||
|
md, ok := metadata.FromIncomingContext(ctx)
|
||||||
|
if !ok {
|
||||||
|
return proxy.One2One, []proxy.Backend{d.localBackend}, nil
|
||||||
|
}
|
||||||
|
// If the request is already proxied, send it to the local backend.
|
||||||
|
if _, ok = md["proxy-authority"]; ok {
|
||||||
|
return proxy.One2One, []proxy.Backend{d.localBackend}, nil
|
||||||
|
}
|
||||||
|
// If the request metadata doesn't contain machines to proxy to, send it to the local backend.
|
||||||
|
machines, ok := md["machines"]
|
||||||
|
if !ok {
|
||||||
|
return proxy.One2One, []proxy.Backend{d.localBackend}, nil
|
||||||
|
}
|
||||||
|
if len(machines) == 0 {
|
||||||
|
return proxy.One2One, nil, status.Error(codes.InvalidArgument, "no machines specified")
|
||||||
|
}
|
||||||
|
|
||||||
|
d.mu.RLock()
|
||||||
|
localAddress := d.localAddress
|
||||||
|
localBackend := d.localBackend
|
||||||
|
d.mu.RUnlock()
|
||||||
|
|
||||||
|
backends := make([]proxy.Backend, len(machines))
|
||||||
|
for i, addr := range machines {
|
||||||
|
if addr == localAddress {
|
||||||
|
backends[i] = localBackend
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
backend, err := d.remoteBackend(addr)
|
||||||
|
if err != nil {
|
||||||
|
return proxy.One2One, nil, status.Error(codes.Internal, err.Error())
|
||||||
|
}
|
||||||
|
backends[i] = backend
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(backends) == 1 {
|
||||||
|
return proxy.One2One, backends, nil
|
||||||
|
}
|
||||||
|
return proxy.One2Many, backends, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// remoteBackend returns a RemoteBackend for the given address from the cache or creates a new one.
|
||||||
|
func (d *Director) remoteBackend(addr string) (*RemoteBackend, error) {
|
||||||
|
b, ok := d.remoteBackends.Load(addr)
|
||||||
|
if ok {
|
||||||
|
return b.(*RemoteBackend), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
backend, err := NewRemoteBackend(addr, d.remotePort)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
existing, loaded := d.remoteBackends.LoadOrStore(addr, backend)
|
||||||
|
if loaded {
|
||||||
|
// A concurrent remoteBackend call built a different backend.
|
||||||
|
backend.Close()
|
||||||
|
return existing.(*RemoteBackend), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return backend, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// FlushRemoteBackends closes all remote backend connections and removes them from the cache.
|
||||||
|
func (d *Director) FlushRemoteBackends() {
|
||||||
|
d.remoteBackends.Range(func(key, value interface{}) bool {
|
||||||
|
backend, ok := value.(*RemoteBackend)
|
||||||
|
if !ok {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
backend.Close()
|
||||||
|
d.remoteBackends.Delete(key)
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close closes all backend connections.
|
||||||
|
func (d *Director) Close() {
|
||||||
|
d.localBackend.Close()
|
||||||
|
d.FlushRemoteBackends()
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
package proxy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"github.com/siderolabs/grpc-proxy/proxy"
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
"google.golang.org/grpc/credentials/insecure"
|
||||||
|
"google.golang.org/grpc/metadata"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LocalBackend is a proxy.One2ManyResponder implementation that proxies to a local gRPC server listening on a Unix socket.
|
||||||
|
type LocalBackend struct {
|
||||||
|
One2ManyResponder
|
||||||
|
sockPath string
|
||||||
|
|
||||||
|
mu sync.RWMutex
|
||||||
|
conn *grpc.ClientConn
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ proxy.Backend = (*LocalBackend)(nil)
|
||||||
|
|
||||||
|
// NewLocalBackend returns a new LocalBackend for the given Unix socket path. The addr parameter is the local address
|
||||||
|
// of the current machine which could be empty if it's not known. The address is used to populate response metadata
|
||||||
|
// in one2many mode.
|
||||||
|
func NewLocalBackend(sockPath, addr string) *LocalBackend {
|
||||||
|
return &LocalBackend{
|
||||||
|
One2ManyResponder: One2ManyResponder{
|
||||||
|
machine: addr,
|
||||||
|
},
|
||||||
|
sockPath: sockPath,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *LocalBackend) String() string {
|
||||||
|
return b.machine
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetConnection returns a gRPC connection to the local server listening on the Unix socket.
|
||||||
|
func (b *LocalBackend) GetConnection(ctx context.Context, _ string) (context.Context, *grpc.ClientConn, error) {
|
||||||
|
md, _ := metadata.FromIncomingContext(ctx)
|
||||||
|
outCtx := metadata.NewOutgoingContext(ctx, md)
|
||||||
|
|
||||||
|
b.mu.RLock()
|
||||||
|
if b.conn != nil {
|
||||||
|
defer b.mu.RUnlock()
|
||||||
|
return outCtx, b.conn, nil
|
||||||
|
}
|
||||||
|
b.mu.RUnlock()
|
||||||
|
|
||||||
|
b.mu.Lock()
|
||||||
|
defer b.mu.Unlock()
|
||||||
|
|
||||||
|
var err error
|
||||||
|
b.conn, err = grpc.NewClient(
|
||||||
|
"unix://"+b.sockPath,
|
||||||
|
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
||||||
|
grpc.WithDefaultCallOptions(
|
||||||
|
grpc.ForceCodecV2(proxy.Codec()),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
return outCtx, b.conn, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close closes the upstream gRPC connection.
|
||||||
|
func (b *LocalBackend) Close() {
|
||||||
|
b.mu.Lock()
|
||||||
|
defer b.mu.Unlock()
|
||||||
|
|
||||||
|
if b.conn != nil {
|
||||||
|
b.conn.Close()
|
||||||
|
b.conn = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
package proxy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"github.com/siderolabs/grpc-proxy/proxy"
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
"google.golang.org/grpc/backoff"
|
||||||
|
"google.golang.org/grpc/credentials/insecure"
|
||||||
|
"google.golang.org/grpc/metadata"
|
||||||
|
"net/netip"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RemoteBackend is a proxy.One2ManyResponder implementation that proxies to a remote gRPC server, injecting machine metadata
|
||||||
|
// into the response.
|
||||||
|
//
|
||||||
|
// Based on the Talos apid implementation:
|
||||||
|
// https://github.com/siderolabs/talos/blob/59a78da42cdea8fbccc35d0851f9b0eef928261b/internal/app/apid/pkg/backend/apid.go
|
||||||
|
type RemoteBackend struct {
|
||||||
|
One2ManyResponder
|
||||||
|
target string
|
||||||
|
|
||||||
|
mu sync.RWMutex
|
||||||
|
conn *grpc.ClientConn
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ proxy.Backend = (*RemoteBackend)(nil)
|
||||||
|
|
||||||
|
// NewRemoteBackend creates a new instance of RemoteBackend for the given IPv6 address and port.
|
||||||
|
func NewRemoteBackend(addr string, port uint16) (*RemoteBackend, error) {
|
||||||
|
ip, err := netip.ParseAddr(addr)
|
||||||
|
if err != nil || !ip.Is6() {
|
||||||
|
return nil, fmt.Errorf("address must be a valid IPv6 address: %s", addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &RemoteBackend{
|
||||||
|
One2ManyResponder: One2ManyResponder{
|
||||||
|
machine: addr,
|
||||||
|
},
|
||||||
|
target: netip.AddrPortFrom(ip, port).String(),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *RemoteBackend) String() string {
|
||||||
|
return b.machine
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetConnection returns a gRPC connection to the remote server.
|
||||||
|
func (b *RemoteBackend) GetConnection(ctx context.Context, _ string) (context.Context, *grpc.ClientConn, error) {
|
||||||
|
md, _ := metadata.FromIncomingContext(ctx)
|
||||||
|
if authority := md[":authority"]; len(authority) > 0 {
|
||||||
|
md.Set("proxy-authority", authority...)
|
||||||
|
} else {
|
||||||
|
md.Set("proxy-authority", "unknown")
|
||||||
|
}
|
||||||
|
delete(md, ":authority")
|
||||||
|
delete(md, "machines")
|
||||||
|
|
||||||
|
outCtx := metadata.NewOutgoingContext(ctx, md)
|
||||||
|
|
||||||
|
b.mu.RLock()
|
||||||
|
if b.conn != nil {
|
||||||
|
defer b.mu.RUnlock()
|
||||||
|
return outCtx, b.conn, nil
|
||||||
|
}
|
||||||
|
b.mu.RUnlock()
|
||||||
|
|
||||||
|
b.mu.Lock()
|
||||||
|
defer b.mu.Unlock()
|
||||||
|
|
||||||
|
// Override the max delay to avoid excessive backoff when the another node is unavailable, e.g. rebooted
|
||||||
|
// or the WireGuard connection is temporary down.
|
||||||
|
//
|
||||||
|
// Default max delay is 2 minutes, which is too long for our use case.
|
||||||
|
backoffConfig := backoff.DefaultConfig
|
||||||
|
// The maximum wait time between attempts.
|
||||||
|
backoffConfig.MaxDelay = 15 * time.Second
|
||||||
|
|
||||||
|
var err error
|
||||||
|
b.conn, err = grpc.NewClient(
|
||||||
|
b.target,
|
||||||
|
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
||||||
|
grpc.WithConnectParams(grpc.ConnectParams{
|
||||||
|
Backoff: backoffConfig,
|
||||||
|
// Not published as a constant in gRPC library.
|
||||||
|
// See: https://github.com/grpc/grpc-go/blob/d5dee5fdbdeb52f6ea10b37b2cc7ce37814642d7/clientconn.go#L55-L56
|
||||||
|
// Each connection attempt can take up to MinConnectTimeout.
|
||||||
|
MinConnectTimeout: 20 * time.Second,
|
||||||
|
}),
|
||||||
|
grpc.WithDefaultCallOptions(
|
||||||
|
grpc.ForceCodecV2(proxy.Codec()),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
return outCtx, b.conn, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close closes the upstream gRPC connection.
|
||||||
|
func (b *RemoteBackend) Close() {
|
||||||
|
b.mu.Lock()
|
||||||
|
defer b.mu.Unlock()
|
||||||
|
|
||||||
|
if b.conn != nil {
|
||||||
|
b.conn.Close()
|
||||||
|
b.conn = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
// Copyright 2024 Google LLC
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
package google.rpc;
|
||||||
|
|
||||||
|
import "google/protobuf/any.proto";
|
||||||
|
|
||||||
|
option cc_enable_arenas = true;
|
||||||
|
option go_package = "google.golang.org/genproto/googleapis/rpc/status;status";
|
||||||
|
option java_multiple_files = true;
|
||||||
|
option java_outer_classname = "StatusProto";
|
||||||
|
option java_package = "com.google.rpc";
|
||||||
|
option objc_class_prefix = "RPC";
|
||||||
|
|
||||||
|
// The `Status` type defines a logical error model that is suitable for
|
||||||
|
// different programming environments, including REST APIs and RPC APIs. It is
|
||||||
|
// used by [gRPC](https://github.com/grpc). Each `Status` message contains
|
||||||
|
// three pieces of data: error code, error message, and error details.
|
||||||
|
//
|
||||||
|
// You can find out more about this error model and how to work with it in the
|
||||||
|
// [API Design Guide](https://cloud.google.com/apis/design/errors).
|
||||||
|
message Status {
|
||||||
|
// The status code, which should be an enum value of
|
||||||
|
// [google.rpc.Code][google.rpc.Code].
|
||||||
|
int32 code = 1;
|
||||||
|
|
||||||
|
// A developer-facing error message, which should be in English. Any
|
||||||
|
// user-facing error message should be localized and sent in the
|
||||||
|
// [google.rpc.Status.details][google.rpc.Status.details] field, or localized
|
||||||
|
// by the client.
|
||||||
|
string message = 2;
|
||||||
|
|
||||||
|
// A list of messages that carry the error details. There is a common set of
|
||||||
|
// message types for APIs to use.
|
||||||
|
repeated google.protobuf.Any details = 3;
|
||||||
|
}
|
||||||
@@ -0,0 +1,213 @@
|
|||||||
|
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"
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewController(store *store.Store, path string) (*Controller, error) {
|
||||||
|
dir := filepath.Dir(path)
|
||||||
|
if err := os.MkdirAll(dir, 0750); err != nil {
|
||||||
|
return nil, fmt.Errorf("create parent directory for Caddy configuration '%s': %w", dir, err)
|
||||||
|
}
|
||||||
|
if err := fs.Chown(dir, "", CaddyGroup); err != nil {
|
||||||
|
return nil, fmt.Errorf("change owner of parent directory for Caddy configuration '%s': %w", dir, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Controller{
|
||||||
|
store: store,
|
||||||
|
path: path,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Controller) Run(ctx context.Context) error {
|
||||||
|
containerRecords, changes, err := c.store.SubscribeContainers(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("subscribe to container changes: %w", err)
|
||||||
|
}
|
||||||
|
slog.Info("Subscribed to container changes in the cluster to generate Caddy configuration.")
|
||||||
|
|
||||||
|
containers, err := c.filterAvailableContainers(containerRecords)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("filter available containers: %w", err)
|
||||||
|
}
|
||||||
|
if err = c.generateConfig(containers); err != nil {
|
||||||
|
return fmt.Errorf("generate Caddy configuration: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case _, ok := <-changes:
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("containers subscription failed")
|
||||||
|
}
|
||||||
|
slog.Debug("Cluster containers changed, updating Caddy configuration.")
|
||||||
|
|
||||||
|
containerRecords, err = c.store.ListContainers(ctx, store.ListOptions{})
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("Failed to list containers.", "err", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
containers, err = c.filterAvailableContainers(containerRecords)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("Failed to filter available containers.", "err", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err = c.generateConfig(containers); err != nil {
|
||||||
|
slog.Error("Failed to generate Caddy configuration.", "err", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
slog.Debug("Updated Caddy configuration.", "path", c.path)
|
||||||
|
case <-ctx.Done():
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// filterAvailableContainers filters out containers that are likely unavailable from this machine. 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))
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
configBytes, err := json.MarshalIndent(config, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("marshal Caddy configuration: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = os.WriteFile(c.path, configBytes, 0640); err != nil {
|
||||||
|
return fmt.Errorf("write Caddy configuration to file '%s': %w", c.path, err)
|
||||||
|
}
|
||||||
|
if err = fs.Chown(c.path, "", CaddyGroup); err != nil {
|
||||||
|
return fmt.Errorf("change owner of Caddy configuration file '%s': %w", c.path, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
+162
-114
@@ -3,130 +3,156 @@ package cluster
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"google.golang.org/grpc/codes"
|
"google.golang.org/grpc/codes"
|
||||||
"google.golang.org/grpc/status"
|
"google.golang.org/grpc/status"
|
||||||
"google.golang.org/protobuf/proto"
|
|
||||||
"google.golang.org/protobuf/types/known/emptypb"
|
"google.golang.org/protobuf/types/known/emptypb"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
|
"time"
|
||||||
|
"uncloud/internal/corrosion"
|
||||||
"uncloud/internal/machine/api/pb"
|
"uncloud/internal/machine/api/pb"
|
||||||
"uncloud/internal/machine/network"
|
"uncloud/internal/machine/network"
|
||||||
|
"uncloud/internal/machine/store"
|
||||||
"uncloud/internal/secret"
|
"uncloud/internal/secret"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Cluster struct {
|
type Cluster struct {
|
||||||
pb.UnimplementedClusterServer
|
pb.UnimplementedClusterServer
|
||||||
|
|
||||||
state *State
|
store *store.Store
|
||||||
|
corroAdmin *corrosion.AdminClient
|
||||||
// TODO: temporary channel until the state is replaced with networkDB.
|
// machineID is the ID of the current machine that is running the cluster service.
|
||||||
newMachinesCh chan *pb.MachineInfo
|
machineID string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewCluster(state *State) *Cluster {
|
func NewCluster(store *store.Store, corroAdmin *corrosion.AdminClient) *Cluster {
|
||||||
return &Cluster{
|
return &Cluster{
|
||||||
state: state,
|
store: store,
|
||||||
newMachinesCh: make(chan *pb.MachineInfo, 1),
|
corroAdmin: corroAdmin,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Cluster) SetState(state *State) {
|
// UpdateMachineID updates the current machine ID that is running the cluster service.
|
||||||
c.state = state
|
func (c *Cluster) UpdateMachineID(mid string) {
|
||||||
|
c.machineID = mid
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Cluster) Network() (netip.Prefix, error) {
|
func (c *Cluster) Init(ctx context.Context, network netip.Prefix) error {
|
||||||
if c.state == nil {
|
initialised, err := c.Initialised(ctx)
|
||||||
return netip.Prefix{}, status.Error(codes.FailedPrecondition, "cluster is not initialized")
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if initialised {
|
||||||
|
return fmt.Errorf("cluster is already initialised")
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.state.State.Network == nil {
|
if err = c.store.Put(ctx, "network", network.String()); err != nil {
|
||||||
return netip.Prefix{}, fmt.Errorf("network not set")
|
return fmt.Errorf("put network to store: %w", err)
|
||||||
}
|
}
|
||||||
return c.state.State.Network.ToPrefix()
|
if err = c.store.Put(ctx, "created_at", time.Now().UTC().Format(time.RFC3339)); err != nil {
|
||||||
}
|
return fmt.Errorf("put created_at to store: %w", err)
|
||||||
|
|
||||||
func (c *Cluster) SetNetwork(network *pb.IPPrefix) error {
|
|
||||||
if c.state == nil {
|
|
||||||
return status.Error(codes.FailedPrecondition, "cluster is not initialized")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.state.State.Network != nil {
|
|
||||||
return fmt.Errorf("network already set and cannot be changed")
|
|
||||||
}
|
|
||||||
if network == nil {
|
|
||||||
return fmt.Errorf("network not set")
|
|
||||||
}
|
|
||||||
c.state.State.Network = network
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: this is a temporary watcher for PoC until the state is state is replaced with networkDB.
|
func (c *Cluster) Initialised(ctx context.Context) (bool, error) {
|
||||||
func (c *Cluster) WatchNewMachines() <-chan *pb.MachineInfo {
|
var createdAt string
|
||||||
return c.newMachinesCh
|
if err := c.store.Get(ctx, "created_at", &createdAt); err != nil {
|
||||||
|
if errors.Is(err, store.ErrKeyNotFound) {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
return false, status.Errorf(codes.Internal, "get created_at from store: %v", err)
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cluster) checkInitialised(ctx context.Context) error {
|
||||||
|
initialised, err := c.Initialised(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !initialised {
|
||||||
|
return status.Error(codes.FailedPrecondition, "cluster is not initialised")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cluster) Network(ctx context.Context) (netip.Prefix, error) {
|
||||||
|
if err := c.checkInitialised(ctx); err != nil {
|
||||||
|
return netip.Prefix{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var net string
|
||||||
|
if err := c.store.Get(ctx, "network", &net); err != nil {
|
||||||
|
return netip.Prefix{}, status.Errorf(codes.Internal, "get network from store: %v", err)
|
||||||
|
}
|
||||||
|
prefix, err := netip.ParsePrefix(net)
|
||||||
|
if err != nil {
|
||||||
|
return netip.Prefix{}, status.Errorf(codes.Internal, "parse network prefix: %v", err)
|
||||||
|
}
|
||||||
|
return prefix, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddMachine adds a machine to the cluster.
|
// AddMachine adds a machine to the cluster.
|
||||||
func (c *Cluster) AddMachine(ctx context.Context, req *pb.AddMachineRequest) (*pb.AddMachineResponse, error) {
|
func (c *Cluster) AddMachine(ctx context.Context, req *pb.AddMachineRequest) (*pb.AddMachineResponse, error) {
|
||||||
if c.state == nil {
|
if err := c.checkInitialised(ctx); err != nil {
|
||||||
return nil, status.Error(codes.FailedPrecondition, "cluster is not initialized")
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := req.Validate(); err != nil {
|
if req.Network == nil {
|
||||||
|
return nil, status.Error(codes.InvalidArgument, "network not set")
|
||||||
|
}
|
||||||
|
if err := req.Network.Validate(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if len(req.Network.Endpoints) == 0 {
|
if len(req.Network.Endpoints) == 0 {
|
||||||
return nil, status.Error(codes.InvalidArgument, "endpoints not set")
|
return nil, status.Error(codes.InvalidArgument, "endpoints not set")
|
||||||
}
|
}
|
||||||
|
|
||||||
machines := c.state.State.Machines
|
machines, err := c.store.ListMachines(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, status.Errorf(codes.Internal, "list machines: %v", err)
|
||||||
|
}
|
||||||
allocatedSubnets := make([]netip.Prefix, len(machines))
|
allocatedSubnets := make([]netip.Prefix, len(machines))
|
||||||
var err error
|
for i, m := range machines {
|
||||||
i := 0
|
|
||||||
for _, m := range machines {
|
|
||||||
if req.Name != "" && m.Name == req.Name {
|
if req.Name != "" && m.Name == req.Name {
|
||||||
return nil, status.Errorf(codes.AlreadyExists, "machine with name %q already exists", req.Name)
|
return nil, status.Errorf(codes.AlreadyExists, "machine with name %q already exists", req.Name)
|
||||||
}
|
}
|
||||||
if m.Network.ManagementIp != nil && req.Network.ManagementIp != nil &&
|
if req.Network.ManagementIp != nil && req.Network.ManagementIp.Equal(m.Network.ManagementIp) {
|
||||||
m.Network.ManagementIp.Equal(req.Network.ManagementIp) {
|
|
||||||
manageIP, _ := req.Network.ManagementIp.ToAddr()
|
manageIP, _ := req.Network.ManagementIp.ToAddr()
|
||||||
return nil, status.Errorf(codes.AlreadyExists, "machine with management IP %q already exists", manageIP)
|
return nil, status.Errorf(
|
||||||
|
codes.AlreadyExists, "machine with management IP %q already exists under the name %q",
|
||||||
|
manageIP, m.Name,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
if bytes.Equal(m.Network.PublicKey, req.Network.PublicKey) {
|
if bytes.Equal(m.Network.PublicKey, req.Network.PublicKey) {
|
||||||
publicKey := secret.Secret(m.Network.PublicKey)
|
publicKey := secret.Secret(m.Network.PublicKey)
|
||||||
return nil, status.Errorf(codes.AlreadyExists, "machine with public key %q already exists", publicKey)
|
return nil, status.Errorf(
|
||||||
|
codes.AlreadyExists, "machine with public key %q already exists under the name %q",
|
||||||
|
publicKey, m.Name,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
allocatedSubnets[i], _ = m.Network.Subnet.ToPrefix()
|
||||||
if allocatedSubnets[i], err = m.Network.Subnet.ToPrefix(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
i++
|
|
||||||
}
|
}
|
||||||
|
|
||||||
mid, err := NewMachineID()
|
mid, err := NewMachineID()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, status.Errorf(codes.Internal, "generate machine ID: %v", err)
|
return nil, status.Errorf(codes.Internal, "generate machine ID: %v", err)
|
||||||
}
|
}
|
||||||
|
name := req.Name
|
||||||
|
if name == "" {
|
||||||
|
if name, err = NewRandomMachineName(); err != nil {
|
||||||
|
return nil, status.Errorf(codes.Internal, "generate machine name: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
manageIP := req.Network.ManagementIp
|
manageIP := req.Network.ManagementIp
|
||||||
if manageIP == nil {
|
if manageIP == nil {
|
||||||
manageIP = pb.NewIP(network.ManagementIP(req.Network.PublicKey))
|
manageIP = pb.NewIP(network.ManagementIP(req.Network.PublicKey))
|
||||||
}
|
}
|
||||||
m := &pb.MachineInfo{
|
// Allocate a subnet for the machine from the cluster network.
|
||||||
Id: mid,
|
clusterNetwork, err := c.Network(ctx)
|
||||||
Name: req.Name,
|
|
||||||
Network: &pb.NetworkConfig{
|
|
||||||
ManagementIp: manageIP,
|
|
||||||
PublicKey: req.Network.PublicKey,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
if m.Name == "" {
|
|
||||||
m.Name, err = NewRandomMachineName()
|
|
||||||
if err != nil {
|
|
||||||
return nil, status.Errorf(codes.Internal, "generate machine name: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
clusterNetwork, err := c.Network()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, status.Errorf(codes.Internal, "get cluster network: %v", err)
|
return nil, status.Errorf(codes.Internal, "get cluster network: %v", err)
|
||||||
}
|
}
|
||||||
@@ -134,70 +160,92 @@ func (c *Cluster) AddMachine(ctx context.Context, req *pb.AddMachineRequest) (*p
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, status.Errorf(codes.Internal, "create IPAM manager: %v", err)
|
return nil, status.Errorf(codes.Internal, "create IPAM manager: %v", err)
|
||||||
}
|
}
|
||||||
subnet, err := ipam.AllocateSubnetLen(network.DefaultSubnetBits)
|
subnet, err := ipam.AllocateSubnetLen(DefaultSubnetBits)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, status.Errorf(codes.Internal, "allocate subnet for machine: %v", err)
|
return nil, status.Errorf(codes.Internal, "allocate subnet for machine: %v", err)
|
||||||
}
|
}
|
||||||
m.Network.Subnet = pb.NewIPPrefix(subnet)
|
|
||||||
|
|
||||||
// TODO: announce the new machine to the cluster network using Serf and achieve consensus.
|
m := &pb.MachineInfo{
|
||||||
mCopy := proto.Clone(m).(*pb.MachineInfo)
|
Id: mid,
|
||||||
c.state.State.Machines[mCopy.Id] = mCopy
|
Name: name,
|
||||||
// Store machine endpoints in a separate collection to allow modifying them with limited permissions.
|
Network: &pb.NetworkConfig{
|
||||||
c.state.State.Endpoints[mCopy.Id] = &pb.MachineEndpoints{
|
Subnet: pb.NewIPPrefix(subnet),
|
||||||
Id: mCopy.Id,
|
ManagementIp: manageIP,
|
||||||
Endpoints: req.Network.Endpoints,
|
Endpoints: req.Network.Endpoints,
|
||||||
|
PublicKey: req.Network.PublicKey,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
if err = c.state.Save(); err != nil {
|
// TODO: announce the new machine to the cluster members and achieve consensus.
|
||||||
return nil, status.Errorf(codes.Internal, "save state: %v", err)
|
// We should perhaps not proceed if this machine is in a minority partition.
|
||||||
|
if err = c.store.CreateMachine(ctx, m); err != nil {
|
||||||
|
return nil, status.Errorf(codes.Internal, "create machine: %v", err)
|
||||||
}
|
}
|
||||||
slog.Info("Machine added to the cluster.", "id", m.Id, "name", m.Name)
|
slog.Info("Machine added to the cluster.",
|
||||||
|
"id", m.Id, "name", m.Name, "subnet", subnet, "public_key", secret.Secret(m.Network.PublicKey))
|
||||||
// Include the machine endpoints in the response.
|
|
||||||
m.Network.Endpoints = req.Network.Endpoints
|
|
||||||
|
|
||||||
// TODO: notify all cluster machines about the new machine so they can update their peers config.
|
|
||||||
// In PoC we just notify the local machine.
|
|
||||||
c.newMachinesCh <- m
|
|
||||||
|
|
||||||
resp := &pb.AddMachineResponse{Machine: m}
|
resp := &pb.AddMachineResponse{Machine: m}
|
||||||
return resp, nil
|
return resp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ListMachines lists all machines in the cluster including their membership states.
|
||||||
func (c *Cluster) ListMachines(ctx context.Context, _ *emptypb.Empty) (*pb.ListMachinesResponse, error) {
|
func (c *Cluster) ListMachines(ctx context.Context, _ *emptypb.Empty) (*pb.ListMachinesResponse, error) {
|
||||||
if c.state == nil {
|
if err := c.checkInitialised(ctx); err != nil {
|
||||||
return nil, status.Error(codes.FailedPrecondition, "cluster is not initialized")
|
return nil, err
|
||||||
}
|
|
||||||
// TODO: consider creating MachineInfo type that pb.MachineInfo is mapped to always carry valid data to reduce
|
|
||||||
// error handling caused by conversions and nil pointers.
|
|
||||||
machines := make([]*pb.MachineInfo, 0, len(c.state.State.Machines))
|
|
||||||
for _, sm := range c.state.State.Machines {
|
|
||||||
m := proto.Clone(sm).(*pb.MachineInfo)
|
|
||||||
m.Network.Endpoints = c.state.State.Endpoints[m.Id].Endpoints
|
|
||||||
machines = append(machines, m)
|
|
||||||
}
|
|
||||||
return &pb.ListMachinesResponse{Machines: machines}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Cluster) ListMachineEndpoints(
|
|
||||||
ctx context.Context, req *pb.ListMachineEndpointsRequest,
|
|
||||||
) (*pb.ListMachineEndpointsResponse, error) {
|
|
||||||
if c.state == nil {
|
|
||||||
return nil, status.Error(codes.FailedPrecondition, "cluster is not initialized")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
endpoints, ok := c.state.State.Endpoints[req.Id]
|
machines, err := c.store.ListMachines(ctx)
|
||||||
if !ok {
|
if err != nil {
|
||||||
return nil, status.Errorf(codes.NotFound, "machine %q not found", req.Id)
|
return nil, status.Error(codes.Internal, err.Error())
|
||||||
}
|
}
|
||||||
return &pb.ListMachineEndpointsResponse{Endpoints: endpoints}, nil
|
|
||||||
|
states, err := c.corroAdmin.ClusterMembershipStates(true)
|
||||||
|
if err != nil {
|
||||||
|
return nil, status.Errorf(codes.Internal, "get cluster membership states: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
members := make([]*pb.MachineMember, len(machines))
|
||||||
|
for i, m := range machines {
|
||||||
|
// If the machine is not in the cluster membership states or its state is not ALIVE or SUSPECT, it is DOWN.
|
||||||
|
// The exception is the current machine which is always UP as it is serving this request.
|
||||||
|
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
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If the machine is the current machine, it is UP.
|
||||||
|
if m.Id == c.machineID {
|
||||||
|
state = pb.MachineMember_UP
|
||||||
|
}
|
||||||
|
members[i] = &pb.MachineMember{
|
||||||
|
Machine: m,
|
||||||
|
State: state,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &pb.ListMachinesResponse{Machines: members}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Cluster) AddUser(user *pb.User) error {
|
//func (c *Cluster) ListServices(ctx context.Context, _ *emptypb.Empty) (*pb.ListServicesResponse, error) {
|
||||||
c.state.State.Users = append(c.state.State.Users, user)
|
// if err := c.checkInitialised(ctx); err != nil {
|
||||||
return c.state.Save()
|
// return nil, err
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
func (c *Cluster) ListUsers() []*pb.User {
|
// return &pb.ListServicesResponse{
|
||||||
return c.state.State.Users
|
// Messages: []*pb.Services{
|
||||||
}
|
// {
|
||||||
|
// Services: []*pb.Service{
|
||||||
|
// {
|
||||||
|
// Name: "service1",
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// Name: "service2",
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// }, nil
|
||||||
|
//}
|
||||||
|
|||||||
@@ -7,6 +7,10 @@ import (
|
|||||||
"net/netip"
|
"net/netip"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const DefaultSubnetBits = 24
|
||||||
|
|
||||||
|
var DefaultNetwork = netip.MustParsePrefix("10.210.0.0/16")
|
||||||
|
|
||||||
// IPAM is an in-memory IP address manager for allocating and releasing subnets for machines from a cluster network.
|
// IPAM is an in-memory IP address manager for allocating and releasing subnets for machines from a cluster network.
|
||||||
type IPAM struct {
|
type IPAM struct {
|
||||||
network netip.Prefix
|
network netip.Prefix
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
package cluster
|
package cluster
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/rand"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"math/big"
|
|
||||||
"uncloud/internal/secret"
|
"uncloud/internal/secret"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -14,14 +12,9 @@ func NewMachineID() (string, error) {
|
|||||||
|
|
||||||
// NewRandomMachineName generates a random machine name in the format "machine-xxxx".
|
// NewRandomMachineName generates a random machine name in the format "machine-xxxx".
|
||||||
func NewRandomMachineName() (string, error) {
|
func NewRandomMachineName() (string, error) {
|
||||||
const charset = "abcdefghijklmnopqrstuvwxyz0123456789"
|
suffix, err := secret.RandomAlphaNumeric(4)
|
||||||
suffix := make([]byte, 4)
|
if err != nil {
|
||||||
for i := range suffix {
|
return "", fmt.Errorf("generate random suffix: %w", err)
|
||||||
randIdx, err := rand.Int(rand.Reader, big.NewInt(int64(len(charset))))
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("get random number: %w", err)
|
|
||||||
}
|
|
||||||
suffix[i] = charset[randIdx.Int64()]
|
|
||||||
}
|
}
|
||||||
return "machine-" + string(suffix), nil
|
return "machine-" + suffix, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,60 +0,0 @@
|
|||||||
package cluster
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"google.golang.org/protobuf/proto"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"uncloud/internal/machine/api/pb"
|
|
||||||
)
|
|
||||||
|
|
||||||
const StateFile = "cluster.pb"
|
|
||||||
|
|
||||||
type State struct {
|
|
||||||
State *pb.State
|
|
||||||
path string
|
|
||||||
}
|
|
||||||
|
|
||||||
func StatePath(dataDir string) string {
|
|
||||||
return filepath.Join(dataDir, StateFile)
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewState(path string) *State {
|
|
||||||
return &State{
|
|
||||||
State: &pb.State{
|
|
||||||
Machines: make(map[string]*pb.MachineInfo),
|
|
||||||
Endpoints: make(map[string]*pb.MachineEndpoints),
|
|
||||||
},
|
|
||||||
path: path,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *State) Load() error {
|
|
||||||
data, err := os.ReadFile(s.path)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("read state file %q: %w", s.path, err)
|
|
||||||
}
|
|
||||||
if err = proto.Unmarshal(data, s.State); err != nil {
|
|
||||||
return fmt.Errorf("parse state file %q: %w", s.path, err)
|
|
||||||
}
|
|
||||||
if s.State.Machines == nil {
|
|
||||||
s.State.Machines = make(map[string]*pb.MachineInfo)
|
|
||||||
}
|
|
||||||
if s.State.Endpoints == nil {
|
|
||||||
s.State.Endpoints = make(map[string]*pb.MachineEndpoints)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *State) Save() error {
|
|
||||||
dir, _ := filepath.Split(s.path)
|
|
||||||
if err := os.MkdirAll(dir, 0700); err != nil {
|
|
||||||
return fmt.Errorf("create state directory %q: %w", dir, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
data, err := proto.Marshal(s.State)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("marshal state: %w", err)
|
|
||||||
}
|
|
||||||
return os.WriteFile(s.path, data, 0600)
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
package corroservice
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"github.com/BurntSushi/toml"
|
||||||
|
"net/netip"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"uncloud/internal/fs"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
DefaultUser = "uncloud"
|
||||||
|
DefaultGossipPort = 51001
|
||||||
|
DefaultAPIPort = 51002
|
||||||
|
)
|
||||||
|
|
||||||
|
// Config represents the Corrosion config.
|
||||||
|
type Config struct {
|
||||||
|
DB DBConfig `toml:"db"`
|
||||||
|
Gossip GossipConfig `toml:"gossip"`
|
||||||
|
API APIConfig `toml:"api"`
|
||||||
|
Admin AdminConfig `toml:"admin"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DBConfig struct {
|
||||||
|
Path string `toml:"path"`
|
||||||
|
SchemaPaths []string `toml:"schema_paths"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GossipConfig struct {
|
||||||
|
Addr netip.AddrPort `toml:"addr"`
|
||||||
|
Bootstrap []string `toml:"bootstrap"`
|
||||||
|
Plaintext bool `toml:"plaintext"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type APIConfig struct {
|
||||||
|
Addr netip.AddrPort `toml:"addr"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AdminConfig struct {
|
||||||
|
Path string `toml:"path"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) Write(path, owner string) error {
|
||||||
|
var data bytes.Buffer
|
||||||
|
encoder := toml.NewEncoder(&data)
|
||||||
|
encoder.Indent = "" // Disable indentation.
|
||||||
|
if err := encoder.Encode(c); err != nil {
|
||||||
|
return fmt.Errorf("encode config: %w", err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(path, data.Bytes(), 0600); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := fs.Chown(path, owner, owner); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func MkDataDir(dir, owner string) error {
|
||||||
|
parent, _ := filepath.Split(dir)
|
||||||
|
// Use 0711 for parent directories to allow `owner` to access its nested data directory.
|
||||||
|
if err := os.MkdirAll(parent, 0711); err != nil {
|
||||||
|
return fmt.Errorf("create directory %q: %w", parent, err)
|
||||||
|
}
|
||||||
|
if err := os.Mkdir(dir, 0700); err != nil {
|
||||||
|
if !os.IsExist(err) {
|
||||||
|
return fmt.Errorf("create directory %q: %w", dir, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if owner != "" {
|
||||||
|
if err := fs.Chown(dir, owner, owner); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
package corroservice
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"github.com/docker/docker/api/types/container"
|
||||||
|
"github.com/docker/docker/api/types/image"
|
||||||
|
"github.com/docker/docker/api/types/mount"
|
||||||
|
"github.com/docker/docker/api/types/network"
|
||||||
|
"github.com/docker/docker/client"
|
||||||
|
"io"
|
||||||
|
"log/slog"
|
||||||
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
LatestImage = "ghcr.io/psviderski/corrosion:latest"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DockerService struct {
|
||||||
|
Client *client.Client
|
||||||
|
Image string
|
||||||
|
Name string
|
||||||
|
DataDir string
|
||||||
|
User string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDockerService(cli *client.Client, image, name, dataDir string) *DockerService {
|
||||||
|
return &DockerService{
|
||||||
|
Client: cli,
|
||||||
|
Image: image,
|
||||||
|
Name: name,
|
||||||
|
DataDir: dataDir,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *DockerService) Start(ctx context.Context) error {
|
||||||
|
_, err := s.Client.ContainerInspect(ctx, s.Name)
|
||||||
|
if err != nil {
|
||||||
|
if client.IsErrNotFound(err) {
|
||||||
|
return s.startNewContainer(ctx)
|
||||||
|
}
|
||||||
|
return fmt.Errorf("inspect container %q: %w", s.Name, err)
|
||||||
|
}
|
||||||
|
// TODO: recreate only if the container configuration has to be changed.
|
||||||
|
if err = s.Client.ContainerRemove(ctx, s.Name, container.RemoveOptions{Force: true}); err != nil {
|
||||||
|
return fmt.Errorf("remove container %q: %w", s.Name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.startNewContainer(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *DockerService) Restart(ctx context.Context) error {
|
||||||
|
if err := s.Client.ContainerRestart(ctx, s.Name, container.StopOptions{}); err != nil {
|
||||||
|
return fmt.Errorf("restart container %q: %w", s.Name, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *DockerService) Running() bool {
|
||||||
|
c, err := s.Client.ContainerInspect(context.Background(), s.Name)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.State.Running
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *DockerService) containerConfig() *container.Config {
|
||||||
|
return &container.Config{
|
||||||
|
Image: s.Image,
|
||||||
|
Cmd: []string{"corrosion", "agent", "-c", filepath.Join(s.DataDir, "config.toml")},
|
||||||
|
User: s.User,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *DockerService) hostConfig() *container.HostConfig {
|
||||||
|
return &container.HostConfig{
|
||||||
|
NetworkMode: network.NetworkHost,
|
||||||
|
RestartPolicy: container.RestartPolicy{
|
||||||
|
Name: container.RestartPolicyAlways,
|
||||||
|
},
|
||||||
|
Mounts: []mount.Mount{
|
||||||
|
// Bind mount the data directory at the same path inside the container to simplify path handling.
|
||||||
|
{
|
||||||
|
Type: mount.TypeBind,
|
||||||
|
Source: s.DataDir,
|
||||||
|
Target: s.DataDir,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *DockerService) startNewContainer(ctx context.Context) error {
|
||||||
|
_, err := s.Client.ContainerCreate(ctx, s.containerConfig(), s.hostConfig(), nil, nil, s.Name)
|
||||||
|
if err != nil {
|
||||||
|
if !client.IsErrNotFound(err) {
|
||||||
|
return fmt.Errorf("create container: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
slog.Info("Pulling Docker image for corrosion service.", "image", s.Image)
|
||||||
|
start := time.Now()
|
||||||
|
|
||||||
|
respBody, err := s.Client.ImagePull(ctx, s.Image, image.PullOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("pull image: %w", err)
|
||||||
|
}
|
||||||
|
defer respBody.Close()
|
||||||
|
|
||||||
|
// Wait for pull to complete.
|
||||||
|
if _, err := io.Copy(io.Discard, respBody); err != nil {
|
||||||
|
return fmt.Errorf("read pull response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
slog.Info("Docker image pulled.", "image", s.Image, "duration", time.Since(start).String())
|
||||||
|
|
||||||
|
// Create container again after image pull.
|
||||||
|
if _, err = s.Client.ContainerCreate(ctx, s.containerConfig(), s.hostConfig(), nil, nil, s.Name); err != nil {
|
||||||
|
return fmt.Errorf("create container: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = s.Client.ContainerStart(ctx, s.Name, container.StartOptions{}); err != nil {
|
||||||
|
return fmt.Errorf("start container: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package corroservice
|
||||||
|
|
||||||
|
import "context"
|
||||||
|
|
||||||
|
type Service interface {
|
||||||
|
Start(ctx context.Context) error
|
||||||
|
Restart(ctx context.Context) error
|
||||||
|
Running() bool
|
||||||
|
}
|
||||||
@@ -0,0 +1,157 @@
|
|||||||
|
package corroservice
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"sync"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
DefaultCommand = "corrosion"
|
||||||
|
DefaultDataDir = "/var/lib/uncloud/corrosion"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SubprocessService implements the Service interface by running the service as a subprocess.
|
||||||
|
type SubprocessService struct {
|
||||||
|
Command string
|
||||||
|
DataDir string
|
||||||
|
|
||||||
|
cmd *exec.Cmd
|
||||||
|
running bool
|
||||||
|
mu sync.Mutex
|
||||||
|
cancelWatch context.CancelFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
func DefaultSubprocessService() *SubprocessService {
|
||||||
|
return &SubprocessService{
|
||||||
|
Command: DefaultCommand,
|
||||||
|
DataDir: DefaultDataDir,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: maybe stop the process if this ctx is cancelled.
|
||||||
|
func (s *SubprocessService) Start(ctx context.Context) error {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
|
||||||
|
if s.running {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return s.startProcess(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SubprocessService) Restart(ctx context.Context) error {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
|
||||||
|
if s.running {
|
||||||
|
if err := s.stopProcess(); err != nil {
|
||||||
|
return fmt.Errorf("stop process: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return s.startProcess(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SubprocessService) Running() bool {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
return s.running
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SubprocessService) startProcess(ctx context.Context) error {
|
||||||
|
s.cmd = exec.Command(s.Command, "agent", "-c", filepath.Join(s.DataDir, "config.toml"))
|
||||||
|
|
||||||
|
// Redirect stdout and stderr to the logger.
|
||||||
|
stdout, err := s.cmd.StdoutPipe()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("create stdout pipe: %w", err)
|
||||||
|
}
|
||||||
|
stderr, err := s.cmd.StderrPipe()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("create stderr pipe: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
scanner := bufio.NewScanner(stdout)
|
||||||
|
for scanner.Scan() {
|
||||||
|
slog.Info("[corrosion]: " + scanner.Text())
|
||||||
|
}
|
||||||
|
// TODO: remove
|
||||||
|
slog.Info("######## corrosion redirect go routine end ########")
|
||||||
|
}()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
scanner := bufio.NewScanner(stderr)
|
||||||
|
for scanner.Scan() {
|
||||||
|
slog.Error("[corrosion]: " + scanner.Text())
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
if err = s.cmd.Start(); err != nil {
|
||||||
|
return fmt.Errorf("start process: %w", err)
|
||||||
|
}
|
||||||
|
s.running = true
|
||||||
|
|
||||||
|
// Watch for process exit to update running status.
|
||||||
|
go func() {
|
||||||
|
if err := s.cmd.Wait(); err != nil {
|
||||||
|
slog.Error("corrosion process exited with error.", "code", s.cmd.ProcessState.ExitCode(), "err", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.mu.Lock()
|
||||||
|
s.running = false
|
||||||
|
s.mu.Unlock()
|
||||||
|
}()
|
||||||
|
|
||||||
|
// TODO: figure out the waiting process
|
||||||
|
// Wait for initialization
|
||||||
|
//timer := time.NewTimer(2 * time.Second)
|
||||||
|
//defer timer.Stop()
|
||||||
|
|
||||||
|
//select {
|
||||||
|
////case <-timer.C:
|
||||||
|
//// s.running = true
|
||||||
|
//// return nil
|
||||||
|
//case <-watchCtx.Done():
|
||||||
|
// return fmt.Errorf("process failed to start")
|
||||||
|
//case <-ctx.Done():
|
||||||
|
// s.stopProcess()
|
||||||
|
// return ctx.Err()
|
||||||
|
//}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SubprocessService) stopProcess() error {
|
||||||
|
if s.cmd == nil || s.cmd.Process == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.cmd.Process.Signal(syscall.SIGTERM); err != nil {
|
||||||
|
return fmt.Errorf("send SIGTERM: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait up to 5 seconds for graceful shutdown before killing the process.
|
||||||
|
done := make(chan error, 1)
|
||||||
|
go func() {
|
||||||
|
done <- s.cmd.Wait()
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-time.After(5 * time.Second):
|
||||||
|
if err := s.cmd.Process.Kill(); err != nil {
|
||||||
|
return fmt.Errorf("kill process: %w", err)
|
||||||
|
}
|
||||||
|
case err := <-done:
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("process exited with error: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
package corroservice
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"os/exec"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const DefaultSystemdUnit = "uncloud-corrosion.service"
|
||||||
|
|
||||||
|
type SystemdService struct {
|
||||||
|
DataDir string
|
||||||
|
Unit string
|
||||||
|
running bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func DefaultSystemdService(dataDir string) *SystemdService {
|
||||||
|
return &SystemdService{
|
||||||
|
DataDir: dataDir,
|
||||||
|
Unit: DefaultSystemdUnit,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SystemdService) Start(ctx context.Context) error {
|
||||||
|
return s.startOrRestart(ctx, "start")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SystemdService) Restart(ctx context.Context) error {
|
||||||
|
return s.startOrRestart(ctx, "restart")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SystemdService) startOrRestart(ctx context.Context, cmd string) error {
|
||||||
|
if _, err := exec.Command("systemctl", cmd, s.Unit).Output(); err != nil {
|
||||||
|
return fmt.Errorf("systemctl %s %s: %w", cmd, s.Unit, err)
|
||||||
|
}
|
||||||
|
slog.Info(fmt.Sprintf("Corrosion systemd service %sed.", cmd), "unit", s.Unit)
|
||||||
|
|
||||||
|
// Optimistically wait for the corrosion service to start and initialise the database schema before proceeding.
|
||||||
|
timer := time.NewTimer(2 * time.Second)
|
||||||
|
defer timer.Stop()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-timer.C:
|
||||||
|
case <-ctx.Done():
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: run a goroutine to check the status of the service and log any errors in the uncloud log.
|
||||||
|
s.running = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SystemdService) Running() bool {
|
||||||
|
return s.running
|
||||||
|
}
|
||||||
@@ -0,0 +1,198 @@
|
|||||||
|
package docker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"github.com/docker/docker/api/types"
|
||||||
|
"github.com/docker/docker/api/types/container"
|
||||||
|
"github.com/docker/docker/api/types/network"
|
||||||
|
"github.com/docker/docker/errdefs"
|
||||||
|
"github.com/docker/docker/pkg/jsonmessage"
|
||||||
|
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
"google.golang.org/grpc/codes"
|
||||||
|
"google.golang.org/grpc/status"
|
||||||
|
"io"
|
||||||
|
"uncloud/internal/machine/api/pb"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Client is a gRPC client for the Docker service that provides a similar interface to the Docker HTTP client.
|
||||||
|
type Client struct {
|
||||||
|
conn *grpc.ClientConn
|
||||||
|
grpcClient pb.DockerClient
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewClient creates a new Docker gRPC client with the provided gRPC connection.
|
||||||
|
func NewClient(conn *grpc.ClientConn) *Client {
|
||||||
|
return &Client{
|
||||||
|
conn: conn,
|
||||||
|
grpcClient: pb.NewDockerClient(conn),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close closes the gRPC connection.
|
||||||
|
func (c *Client) Close() error {
|
||||||
|
return c.conn.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateContainer creates a new container based on the given configuration.
|
||||||
|
func (c *Client) CreateContainer(
|
||||||
|
ctx context.Context,
|
||||||
|
config *container.Config,
|
||||||
|
hostConfig *container.HostConfig,
|
||||||
|
networkingConfig *network.NetworkingConfig,
|
||||||
|
platform *ocispec.Platform,
|
||||||
|
name string,
|
||||||
|
) (container.CreateResponse, error) {
|
||||||
|
var resp container.CreateResponse
|
||||||
|
// Serialize configs to JSON.
|
||||||
|
configBytes, err := json.Marshal(config)
|
||||||
|
if err != nil {
|
||||||
|
return resp, fmt.Errorf("marshal container config: %w", err)
|
||||||
|
}
|
||||||
|
hostConfigBytes, err := json.Marshal(hostConfig)
|
||||||
|
if err != nil {
|
||||||
|
return resp, fmt.Errorf("marshal host config: %w", err)
|
||||||
|
}
|
||||||
|
networkingConfigBytes, err := json.Marshal(networkingConfig)
|
||||||
|
if err != nil {
|
||||||
|
return resp, fmt.Errorf("marshal networking config: %w", err)
|
||||||
|
}
|
||||||
|
platformBytes, err := json.Marshal(platform)
|
||||||
|
if err != nil {
|
||||||
|
return resp, fmt.Errorf("marshal platform: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
grpcResp, err := c.grpcClient.CreateContainer(ctx, &pb.CreateContainerRequest{
|
||||||
|
Config: configBytes,
|
||||||
|
HostConfig: hostConfigBytes,
|
||||||
|
NetworkConfig: networkingConfigBytes,
|
||||||
|
Platform: platformBytes,
|
||||||
|
Name: name,
|
||||||
|
})
|
||||||
|
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)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("marshal options: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = c.grpcClient.StartContainer(ctx, &pb.StartContainerRequest{
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) ListContainers(ctx context.Context, opts container.ListOptions) ([]MachineContainers, error) {
|
||||||
|
optsBytes, err := json.Marshal(opts)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("marshal options: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := c.grpcClient.ListContainers(ctx, &pb.ListContainersRequest{Options: optsBytes})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
machineContainers := make([]MachineContainers, len(resp.Messages))
|
||||||
|
for i, msg := range resp.Messages {
|
||||||
|
machineContainers[i].Metadata = msg.Metadata
|
||||||
|
if msg.Metadata != nil && msg.Metadata.Error != "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = json.Unmarshal(msg.Containers, &machineContainers[i].Containers); err != nil {
|
||||||
|
return nil, fmt.Errorf("unmarshal containers: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return machineContainers, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveContainer stops (kills after grace period) and removes a container with the given ID.
|
||||||
|
func (c *Client) RemoveContainer(ctx context.Context, id string, opts container.RemoveOptions) error {
|
||||||
|
optsBytes, err := json.Marshal(opts)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("marshal options: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = c.grpcClient.RemoveContainer(ctx, &pb.RemoveContainerRequest{
|
||||||
|
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 PullImageMessage struct {
|
||||||
|
Message jsonmessage.JSONMessage
|
||||||
|
Err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) PullImage(ctx context.Context, image string) (<-chan PullImageMessage, error) {
|
||||||
|
stream, err := c.grpcClient.PullImage(ctx, &pb.PullImageRequest{Image: image})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ch := make(chan PullImageMessage)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
defer close(ch)
|
||||||
|
|
||||||
|
for {
|
||||||
|
msg, err := stream.Recv()
|
||||||
|
if err == io.EOF {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
ch <- PullImageMessage{Err: err}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var jm jsonmessage.JSONMessage
|
||||||
|
if err = json.Unmarshal(msg.Message, &jm); err != nil {
|
||||||
|
ch <- PullImageMessage{Err: fmt.Errorf("unmarshal JSON message: %w", err)}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ch <- PullImageMessage{Message: jm}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return ch, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,194 @@
|
|||||||
|
package docker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
dockercontainer "github.com/docker/docker/api/types/container"
|
||||||
|
"github.com/docker/docker/api/types/events"
|
||||||
|
"github.com/docker/docker/api/types/filters"
|
||||||
|
"github.com/docker/docker/client"
|
||||||
|
"log/slog"
|
||||||
|
"time"
|
||||||
|
"uncloud/internal/api"
|
||||||
|
"uncloud/internal/machine/store"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
NetworkName = "uncloud"
|
||||||
|
UserChain = "DOCKER-USER"
|
||||||
|
// EventsDebounceInterval defines how long to wait before processing the next Docker event. Multiple events
|
||||||
|
// occurring within this window will be processed together as a single event to prevent system overload.
|
||||||
|
EventsDebounceInterval = 100 * time.Millisecond
|
||||||
|
// SyncInterval defines a regular interval to sync containers to the cluster store.
|
||||||
|
SyncInterval = 30 * time.Second
|
||||||
|
)
|
||||||
|
|
||||||
|
type Manager struct {
|
||||||
|
client *client.Client
|
||||||
|
// machineID is the ID of the machine where the managed Docker daemon is running.
|
||||||
|
machineID string
|
||||||
|
store *store.Store
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewManager(client *client.Client, machineID string, store *store.Store) *Manager {
|
||||||
|
return &Manager{
|
||||||
|
client: client,
|
||||||
|
machineID: machineID,
|
||||||
|
store: store,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WaitDaemonReady waits for the Docker daemon to start and be ready to serve requests.
|
||||||
|
func (m *Manager) WaitDaemonReady(ctx context.Context) error {
|
||||||
|
ticker := time.NewTicker(1 * time.Second)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
ready, waitingLogged := false, false
|
||||||
|
for !ready {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
case <-ticker.C:
|
||||||
|
_, err := m.client.Ping(ctx)
|
||||||
|
if err == nil {
|
||||||
|
ready = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if !client.IsErrConnectionFailed(err) {
|
||||||
|
return fmt.Errorf("connect to Docker daemon: %w", err)
|
||||||
|
}
|
||||||
|
if !waitingLogged {
|
||||||
|
slog.Info("Waiting for Docker daemon to start and be ready.")
|
||||||
|
waitingLogged = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) WatchAndSyncContainers(ctx context.Context) error {
|
||||||
|
ctx, cancel := context.WithCancel(ctx)
|
||||||
|
defer cancel()
|
||||||
|
// Filter only local container events.
|
||||||
|
opts := events.ListOptions{
|
||||||
|
Filters: filters.NewArgs(
|
||||||
|
filters.Arg("scope", "local"),
|
||||||
|
filters.Arg("type", string(events.ContainerEventType)),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subscribe to Docker events before running the initial sync to avoid missing any events.
|
||||||
|
eventCh, errCh := m.client.Events(ctx, opts)
|
||||||
|
slog.Debug("Syncing containers to cluster store before processing Docker events.")
|
||||||
|
if err := m.syncContainersToStore(ctx); err != nil {
|
||||||
|
// The deferred cancel will stop the event subscription.
|
||||||
|
return fmt.Errorf("sync containers to cluster store: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
// debouncer is used to debounce multiple Docker events into a single event sent to the debouncerCh
|
||||||
|
// to prevent system overload.
|
||||||
|
debouncer *time.Timer
|
||||||
|
debouncerCh = make(chan events.Message)
|
||||||
|
// ticker is used to trigger a regular sync of containers to the cluster store as a fallback.
|
||||||
|
ticker = time.NewTicker(SyncInterval)
|
||||||
|
)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case e := <-eventCh:
|
||||||
|
switch e.Action {
|
||||||
|
// Actions that may trigger a container state change or creation/deletion of a container.
|
||||||
|
case events.ActionCreate,
|
||||||
|
events.ActionStart,
|
||||||
|
events.ActionStop,
|
||||||
|
events.ActionPause,
|
||||||
|
events.ActionUnPause,
|
||||||
|
events.ActionKill,
|
||||||
|
events.ActionDie,
|
||||||
|
events.ActionOOM,
|
||||||
|
events.ActionDestroy,
|
||||||
|
events.ActionHealthStatusHealthy,
|
||||||
|
events.ActionHealthStatusUnhealthy:
|
||||||
|
|
||||||
|
if debouncer == nil {
|
||||||
|
debouncer = time.AfterFunc(EventsDebounceInterval, func() {
|
||||||
|
debouncerCh <- e
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case e := <-debouncerCh:
|
||||||
|
debouncer = nil
|
||||||
|
slog.Debug("Syncing containers to cluster store triggered by a Docker container event.",
|
||||||
|
"container_id", e.Actor.ID,
|
||||||
|
"container_name", e.Actor.Attributes["name"],
|
||||||
|
"action", e.Action)
|
||||||
|
|
||||||
|
if err := m.syncContainersToStore(ctx); err != nil {
|
||||||
|
return fmt.Errorf("sync containers to cluster store: %w", err)
|
||||||
|
}
|
||||||
|
case <-ticker.C:
|
||||||
|
slog.Debug("Syncing containers to cluster store triggered by a regular interval.",
|
||||||
|
"interval", SyncInterval)
|
||||||
|
if err := m.syncContainersToStore(ctx); err != nil {
|
||||||
|
return fmt.Errorf("sync containers to cluster store: %w", err)
|
||||||
|
}
|
||||||
|
case err := <-errCh:
|
||||||
|
if errors.Is(err, context.Canceled) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return fmt.Errorf("receive Docker event: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) syncContainersToStore(ctx context.Context) error {
|
||||||
|
storeContainers, err := m.store.ListContainers(ctx, store.ListOptions{MachineIDs: []string{m.machineID}})
|
||||||
|
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{
|
||||||
|
Filters: filters.NewArgs(
|
||||||
|
filters.Arg("label", api.LabelServiceID),
|
||||||
|
filters.Arg("label", api.LabelServiceName),
|
||||||
|
),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
// TODO: mark all containers as outdated in the store.
|
||||||
|
return fmt.Errorf("list Docker containers: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete containers that are not present in the Docker daemon from the store.
|
||||||
|
var deleteIDs []string
|
||||||
|
for _, sc := range storeContainers {
|
||||||
|
found := false
|
||||||
|
for i, _ := range containers {
|
||||||
|
if containers[i].ID == sc.Container.ID {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
deleteIDs = append(deleteIDs, sc.Container.ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var storeErr error
|
||||||
|
if len(deleteIDs) > 0 {
|
||||||
|
if err = m.store.DeleteContainers(ctx, store.DeleteOptions{IDs: deleteIDs}); err != nil {
|
||||||
|
storeErr = fmt.Errorf("delete containers from store: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create or update the current Docker containers in the store.
|
||||||
|
for _, dc := range containers {
|
||||||
|
c := &api.Container{Container: dc}
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return storeErr
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
//go:build darwin
|
||||||
|
|
||||||
|
package docker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/netip"
|
||||||
|
)
|
||||||
|
|
||||||
|
// EnsureUncloudNetwork is a stub for darwin.
|
||||||
|
func (d *Manager) EnsureUncloudNetwork(ctx context.Context, subnet netip.Prefix) error {
|
||||||
|
return fmt.Errorf("not supported on darwin")
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
package docker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
dnetwork "github.com/docker/docker/api/types/network"
|
||||||
|
"github.com/docker/docker/client"
|
||||||
|
"github.com/docker/docker/libnetwork/iptables"
|
||||||
|
"log/slog"
|
||||||
|
"net/netip"
|
||||||
|
"uncloud/internal/machine/network"
|
||||||
|
)
|
||||||
|
|
||||||
|
// EnsureUncloudNetwork creates the Docker bridge network NetworkName with the provided machine subnet
|
||||||
|
// if it doesn't exist. If the network exists but has a different subnet, it removes and recreates the network.
|
||||||
|
// It also configures iptables to allow container access from the WireGuard network.
|
||||||
|
func (d *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{})
|
||||||
|
if err != nil {
|
||||||
|
if !client.IsErrNotFound(err) {
|
||||||
|
return fmt.Errorf("inspect Docker network %q: %w", NetworkName, err)
|
||||||
|
}
|
||||||
|
needsCreation = true
|
||||||
|
} else if nw.IPAM.Config[0].Subnet != subnet.String() {
|
||||||
|
// Remove the Docker network if the subnet is different.
|
||||||
|
// It could be a leftover from a previous incomplete cleanup.
|
||||||
|
slog.Info(
|
||||||
|
"Removing Docker network with old subnet.", "name", NetworkName, "subnet", nw.IPAM.Config[0].Subnet,
|
||||||
|
)
|
||||||
|
if err = d.client.NetworkRemove(ctx, NetworkName); err != nil {
|
||||||
|
// It can still fail if the network is in use by a container. Leave it to the user to resolve the issue.
|
||||||
|
return fmt.Errorf("remove Docker network %q: %w", NetworkName, err)
|
||||||
|
}
|
||||||
|
needsCreation = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if needsCreation {
|
||||||
|
if _, err = d.client.NetworkCreate(
|
||||||
|
ctx, NetworkName, dnetwork.CreateOptions{
|
||||||
|
Driver: "bridge",
|
||||||
|
Scope: "local",
|
||||||
|
IPAM: &dnetwork.IPAM{
|
||||||
|
Config: []dnetwork.IPAMConfig{
|
||||||
|
{
|
||||||
|
Subnet: subnet.String(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
); err != nil {
|
||||||
|
return fmt.Errorf("create Docker network %q: %w", NetworkName, err)
|
||||||
|
}
|
||||||
|
slog.Info("Docker network created.", "name", NetworkName, "subnet", subnet.String())
|
||||||
|
|
||||||
|
if nw, err = d.client.NetworkInspect(ctx, NetworkName, dnetwork.InspectOptions{}); err != nil {
|
||||||
|
return fmt.Errorf("inspect Docker network %q: %w", NetworkName, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configure iptables to allow WireGuard network to access containers. The Docker daemon should have already
|
||||||
|
// created the DOCKER-USER chain at this point.
|
||||||
|
// TODO: check if this works when firewalld used instead of raw iptables. The Docker daemon has a different
|
||||||
|
// code path for firewalld.
|
||||||
|
|
||||||
|
// Bridge name doesn't seem to be documented but this is the source code where it is generated:
|
||||||
|
// https://github.com/moby/moby/blob/v27.2.1/libnetwork/drivers/bridge/bridge_linux.go#L664
|
||||||
|
bridgeName := "br-" + nw.ID[:12]
|
||||||
|
ipt := iptables.GetIptable(iptables.IPv4)
|
||||||
|
rule := []string{"--in-interface", network.WireGuardInterfaceName, "--out-interface", bridgeName, "-j", "ACCEPT"}
|
||||||
|
if err = ipt.ProgramRule(iptables.Filter, UserChain, iptables.Insert, rule); err != nil {
|
||||||
|
return fmt.Errorf("insert iptables rule: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,195 @@
|
|||||||
|
package docker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"github.com/docker/docker/api/types/container"
|
||||||
|
"github.com/docker/docker/api/types/filters"
|
||||||
|
"github.com/docker/docker/api/types/image"
|
||||||
|
"github.com/docker/docker/api/types/network"
|
||||||
|
"github.com/docker/docker/client"
|
||||||
|
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
"google.golang.org/grpc/codes"
|
||||||
|
"google.golang.org/grpc/status"
|
||||||
|
"google.golang.org/protobuf/types/known/emptypb"
|
||||||
|
"io"
|
||||||
|
"uncloud/internal/machine/api/pb"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Server implements the gRPC Docker service that proxies requests to the Docker daemon.
|
||||||
|
type Server struct {
|
||||||
|
pb.UnimplementedDockerServer
|
||||||
|
client *client.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewServer creates a new Docker gRPC server with the provided Docker client.
|
||||||
|
func NewServer(cli *client.Client) *Server {
|
||||||
|
return &Server{client: cli}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateContainer creates a new container based on the given configuration.
|
||||||
|
func (s *Server) CreateContainer(ctx context.Context, req *pb.CreateContainerRequest) (*pb.CreateContainerResponse, error) {
|
||||||
|
var config container.Config
|
||||||
|
var hostConfig container.HostConfig
|
||||||
|
var networkConfig network.NetworkingConfig
|
||||||
|
var platform ocispec.Platform
|
||||||
|
|
||||||
|
// Unmarshal configurations from the request.
|
||||||
|
if err := json.Unmarshal(req.Config, &config); err != nil {
|
||||||
|
return nil, status.Errorf(codes.InvalidArgument, "unmarshal container config: %v", err)
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(req.HostConfig, &hostConfig); err != nil {
|
||||||
|
return nil, status.Errorf(codes.InvalidArgument, "unmarshal host config: %v", err)
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(req.NetworkConfig, &networkConfig); err != nil {
|
||||||
|
return nil, status.Errorf(codes.InvalidArgument, "unmarshal network config: %v", err)
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(req.Platform, &platform); err != nil {
|
||||||
|
return nil, status.Errorf(codes.InvalidArgument, "unmarshal platform: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
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.Internal, "create container: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
respBytes, err := json.Marshal(resp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, status.Errorf(codes.Internal, "marshal response: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &pb.CreateContainerResponse{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
|
||||||
|
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.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.Internal, "start container: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &emptypb.Empty{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) ListContainers(ctx context.Context, req *pb.ListContainersRequest) (*pb.ListContainersResponse, error) {
|
||||||
|
var opts container.ListOptions
|
||||||
|
if len(req.Options) > 0 {
|
||||||
|
if err := json.Unmarshal(req.Options, &opts); err != nil {
|
||||||
|
return nil, status.Errorf(codes.InvalidArgument, "unmarshal options: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle filters separately because they implement custom JSON unmarshalling.
|
||||||
|
var raw map[string]json.RawMessage
|
||||||
|
if err := json.Unmarshal(req.Options, &raw); err != nil {
|
||||||
|
return nil, status.Errorf(codes.InvalidArgument, "unmarshal options to raw map: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if filtersBytes, ok := raw["Filters"]; ok {
|
||||||
|
args, err := filters.FromJSON(string(filtersBytes))
|
||||||
|
if err != nil {
|
||||||
|
return nil, status.Errorf(codes.InvalidArgument, "unmarshal filters: %v", err)
|
||||||
|
}
|
||||||
|
opts.Filters = args
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
containers, err := s.client.ContainerList(ctx, opts)
|
||||||
|
if err != nil {
|
||||||
|
return nil, status.Errorf(codes.Internal, "list container: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
containersBytes, err := json.Marshal(containers)
|
||||||
|
if err != nil {
|
||||||
|
return nil, status.Errorf(codes.Internal, "marshal containers: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &pb.ListContainersResponse{
|
||||||
|
Messages: []*pb.MachineContainers{
|
||||||
|
{
|
||||||
|
Containers: containersBytes,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveContainer stops (kills after grace period) and removes a container with the given ID.
|
||||||
|
func (s *Server) RemoveContainer(ctx context.Context, req *pb.RemoveContainerRequest) (*emptypb.Empty, error) {
|
||||||
|
var opts container.RemoveOptions
|
||||||
|
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.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.Internal, "remove container: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &emptypb.Empty{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) PullImage(req *pb.PullImageRequest, stream grpc.ServerStreamingServer[pb.JSONMessage]) error {
|
||||||
|
ctx := stream.Context()
|
||||||
|
|
||||||
|
// TODO: replace with another JSON serializable type (PullOptions.PrivilegeFunc is not serializable).
|
||||||
|
var opts image.PullOptions
|
||||||
|
if len(req.Options) > 0 {
|
||||||
|
if err := json.Unmarshal(req.Options, &opts); err != nil {
|
||||||
|
return status.Errorf(codes.InvalidArgument, "unmarshal options: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
respBody, err := s.client.ImagePull(ctx, req.Image, opts)
|
||||||
|
if err != nil {
|
||||||
|
return status.Errorf(codes.Internal, "pull image: %v", err)
|
||||||
|
}
|
||||||
|
defer respBody.Close()
|
||||||
|
|
||||||
|
decoder := json.NewDecoder(respBody)
|
||||||
|
errCh := make(chan error, 1)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
var raw json.RawMessage
|
||||||
|
for {
|
||||||
|
if err = decoder.Decode(&raw); err != nil {
|
||||||
|
if errors.Is(err, io.EOF) {
|
||||||
|
errCh <- nil
|
||||||
|
return
|
||||||
|
}
|
||||||
|
errCh <- status.Errorf(codes.Internal, "decode image pull message: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = stream.Send(&pb.JSONMessage{Message: raw}); err != nil {
|
||||||
|
errCh <- status.Errorf(codes.Internal, "send image pull message to stream: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case err = <-errCh:
|
||||||
|
return err
|
||||||
|
case <-ctx.Done():
|
||||||
|
return status.Errorf(codes.Canceled, "pull image: %v", ctx.Err())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+383
-95
@@ -2,9 +2,12 @@ package machine
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/docker/docker/client"
|
||||||
"github.com/docker/go-connections/sockets"
|
"github.com/docker/go-connections/sockets"
|
||||||
|
"github.com/siderolabs/grpc-proxy/proxy"
|
||||||
"golang.org/x/sync/errgroup"
|
"golang.org/x/sync/errgroup"
|
||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
"google.golang.org/grpc/codes"
|
"google.golang.org/grpc/codes"
|
||||||
@@ -15,21 +18,120 @@ import (
|
|||||||
"net/netip"
|
"net/netip"
|
||||||
"os"
|
"os"
|
||||||
"os/user"
|
"os/user"
|
||||||
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"uncloud/internal/corrosion"
|
||||||
|
"uncloud/internal/docker"
|
||||||
|
"uncloud/internal/fs"
|
||||||
"uncloud/internal/machine/api/pb"
|
"uncloud/internal/machine/api/pb"
|
||||||
|
apiproxy "uncloud/internal/machine/api/proxy"
|
||||||
|
"uncloud/internal/machine/caddyfile"
|
||||||
"uncloud/internal/machine/cluster"
|
"uncloud/internal/machine/cluster"
|
||||||
|
"uncloud/internal/machine/corroservice"
|
||||||
|
machinedocker "uncloud/internal/machine/docker"
|
||||||
"uncloud/internal/machine/network"
|
"uncloud/internal/machine/network"
|
||||||
|
"uncloud/internal/machine/store"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
DefaultAPISockPath = "/run/uncloud.sock"
|
DefaultMachineSockPath = "/run/uncloud/machine.sock"
|
||||||
DefaultAPISockGroup = "uncloud"
|
DefaultUncloudSockPath = "/run/uncloud/uncloud.sock"
|
||||||
|
DefaultSockGroup = "uncloud"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
// DataDir is the directory where the machine stores its persistent state.
|
// DataDir is the directory where the machine stores its persistent state. Default is /var/lib/uncloud.
|
||||||
DataDir string
|
DataDir string
|
||||||
APISockPath string
|
MachineSockPath string
|
||||||
|
UncloudSockPath string
|
||||||
|
|
||||||
|
CorrosionDir string
|
||||||
|
CorrosionAPIListenAddr netip.AddrPort
|
||||||
|
CorrosionAPIAddr netip.AddrPort
|
||||||
|
CorrosionAdminSockPath string
|
||||||
|
CorrosionService corroservice.Service
|
||||||
|
// CorrosionUser sets the Linux user for running the corrosion service.
|
||||||
|
CorrosionUser string
|
||||||
|
|
||||||
|
// DockerClient manages system and user containers using the local Docker daemon.
|
||||||
|
DockerClient *client.Client
|
||||||
|
|
||||||
|
// CaddyfilePath specifies where the machine generates the Caddy reverse proxy configuration file for routing
|
||||||
|
// external traffic to service containers across the internal network. Default is DataDir/caddy/Caddyfile.
|
||||||
|
CaddyfilePath string
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetDefaults returns a new Config with default values set where not provided.
|
||||||
|
func (c *Config) SetDefaults() (*Config, error) {
|
||||||
|
// Copy c into a new Config to avoid modifying the original.
|
||||||
|
cfg := *c
|
||||||
|
|
||||||
|
if cfg.DataDir == "" {
|
||||||
|
cfg.DataDir = "/var/lib/uncloud"
|
||||||
|
}
|
||||||
|
if cfg.MachineSockPath == "" {
|
||||||
|
cfg.MachineSockPath = DefaultMachineSockPath
|
||||||
|
}
|
||||||
|
if cfg.UncloudSockPath == "" {
|
||||||
|
cfg.UncloudSockPath = DefaultUncloudSockPath
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.DockerClient == nil {
|
||||||
|
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("create Docker client: %w", err)
|
||||||
|
}
|
||||||
|
cfg.DockerClient = cli
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.CorrosionDir == "" {
|
||||||
|
cfg.CorrosionDir = filepath.Join(cfg.DataDir, "corrosion")
|
||||||
|
}
|
||||||
|
if !cfg.CorrosionAPIListenAddr.IsValid() {
|
||||||
|
cfg.CorrosionAPIListenAddr = netip.AddrPortFrom(
|
||||||
|
netip.AddrFrom4([4]byte{127, 0, 0, 1}), corroservice.DefaultAPIPort)
|
||||||
|
}
|
||||||
|
if !cfg.CorrosionAPIAddr.IsValid() {
|
||||||
|
cfg.CorrosionAPIAddr = netip.AddrPortFrom(
|
||||||
|
netip.AddrFrom4([4]byte{127, 0, 0, 1}), corroservice.DefaultAPIPort)
|
||||||
|
}
|
||||||
|
if cfg.CorrosionAdminSockPath == "" {
|
||||||
|
cfg.CorrosionAdminSockPath = filepath.Join(cfg.CorrosionDir, "admin.sock")
|
||||||
|
}
|
||||||
|
if cfg.CorrosionUser == "" {
|
||||||
|
cfg.CorrosionUser = corroservice.DefaultUser
|
||||||
|
}
|
||||||
|
if cfg.CorrosionService == nil {
|
||||||
|
if isRunningInDocker() {
|
||||||
|
// Run corrosion in a nested Docker container if the machine is running in a container.
|
||||||
|
uid, gid, err := fs.LookupUIDGID(cfg.CorrosionUser)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("lookup corrosion user %q: %w", cfg.CorrosionUser, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.CorrosionService = &corroservice.DockerService{
|
||||||
|
Client: cfg.DockerClient,
|
||||||
|
Image: corroservice.LatestImage,
|
||||||
|
Name: "uncloud-corrosion",
|
||||||
|
DataDir: cfg.CorrosionDir,
|
||||||
|
User: fmt.Sprintf("%d:%d", uid, gid),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
cfg.CorrosionService = corroservice.DefaultSystemdService(cfg.CorrosionDir)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.CaddyfilePath == "" {
|
||||||
|
cfg.CaddyfilePath = filepath.Join(cfg.DataDir, "caddy", "caddy.json")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &cfg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// isRunningInDocker returns true if the current process is running in a Docker container.
|
||||||
|
func isRunningInDocker() bool {
|
||||||
|
_, err := os.Stat("/.dockerenv")
|
||||||
|
return err == nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type Machine struct {
|
type Machine struct {
|
||||||
@@ -42,12 +144,27 @@ type Machine struct {
|
|||||||
// initialised is signalled when the machine is configured as a member of a cluster.
|
// initialised is signalled when the machine is configured as a member of a cluster.
|
||||||
initialised chan struct{}
|
initialised chan struct{}
|
||||||
|
|
||||||
localServer *grpc.Server
|
// store is the cluster store backed by a distributed Corrosion database.
|
||||||
cluster *cluster.Cluster
|
store *store.Store
|
||||||
newMachinesCh <-chan *pb.MachineInfo
|
cluster *cluster.Cluster
|
||||||
|
docker *machinedocker.Server
|
||||||
|
// localMachineServer is the gRPC server for the machine API listening on the local Unix socket.
|
||||||
|
localMachineServer *grpc.Server
|
||||||
|
|
||||||
|
// proxyDirector manages routing of gRPC requests between local and remote machine API servers.
|
||||||
|
proxyDirector *apiproxy.Director
|
||||||
|
// localProxyServer is the gRPC proxy server for the machine API listening on the local Unix socket.
|
||||||
|
// It proxies requests to the local or remote machine API servers depending on the request targets
|
||||||
|
// and aggregates responses.
|
||||||
|
localProxyServer *grpc.Server
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewMachine(config *Config) (*Machine, error) {
|
func NewMachine(config *Config) (*Machine, error) {
|
||||||
|
config, err := config.SetDefaults()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("set default config values: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
// Load the existing machine state or create a new one.
|
// Load the existing machine state or create a new one.
|
||||||
statePath := StatePath(config.DataDir)
|
statePath := StatePath(config.DataDir)
|
||||||
state, err := ParseState(statePath)
|
state, err := ParseState(statePath)
|
||||||
@@ -75,41 +192,58 @@ func NewMachine(config *Config) (*Machine, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var c *cluster.Cluster
|
corro, err := corrosion.NewAPIClient(config.CorrosionAPIAddr)
|
||||||
clusterState := cluster.NewState(cluster.StatePath(config.DataDir))
|
if err != nil {
|
||||||
if err = clusterState.Load(); err != nil {
|
return nil, fmt.Errorf("create corrosion API client: %w", err)
|
||||||
if errors.Is(err, os.ErrNotExist) {
|
|
||||||
// Cluster state file does not exist, initialise the cluster without a state to fail cluster requests.
|
|
||||||
c = cluster.NewCluster(nil)
|
|
||||||
} else {
|
|
||||||
return nil, fmt.Errorf("load cluster state: %w", err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Cluster state is successfully loaded, initialise the cluster with it.
|
|
||||||
c = cluster.NewCluster(clusterState)
|
|
||||||
}
|
}
|
||||||
|
corroStore := store.New(corro)
|
||||||
|
corroAdmin, err := corrosion.NewAdminClient(config.CorrosionAdminSockPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("create corrosion admin client: %w", err)
|
||||||
|
}
|
||||||
|
c := cluster.NewCluster(corroStore, corroAdmin)
|
||||||
|
|
||||||
|
// Init a gRPC Docker server that proxies requests to the local Docker daemon.
|
||||||
|
dockerCli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("create Docker client: %w", err)
|
||||||
|
}
|
||||||
|
dockerServer := machinedocker.NewServer(dockerCli)
|
||||||
|
|
||||||
|
// Init a local gRPC proxy server that proxies requests to the local or remote machine API servers.
|
||||||
|
proxyDirector := apiproxy.NewDirector(config.MachineSockPath, APIPort)
|
||||||
|
localProxyServer := grpc.NewServer(
|
||||||
|
grpc.ForceServerCodecV2(proxy.Codec()),
|
||||||
|
grpc.UnknownServiceHandler(
|
||||||
|
proxy.TransparentHandler(proxyDirector.Director),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
m := &Machine{
|
m := &Machine{
|
||||||
config: *config,
|
config: *config,
|
||||||
state: state,
|
state: state,
|
||||||
started: make(chan struct{}),
|
started: make(chan struct{}),
|
||||||
initialised: make(chan struct{}, 1),
|
initialised: make(chan struct{}, 1),
|
||||||
cluster: c,
|
store: corroStore,
|
||||||
newMachinesCh: c.WatchNewMachines(),
|
cluster: c,
|
||||||
|
docker: dockerServer,
|
||||||
|
localProxyServer: localProxyServer,
|
||||||
|
proxyDirector: proxyDirector,
|
||||||
}
|
}
|
||||||
m.localServer = newGRPCServer(m, c)
|
m.localMachineServer = newGRPCServer(m, c, dockerServer)
|
||||||
|
|
||||||
if m.IsInitialised() {
|
if m.Initialised() {
|
||||||
m.initialised <- struct{}{}
|
m.initialised <- struct{}{}
|
||||||
}
|
}
|
||||||
|
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func newGRPCServer(m pb.MachineServer, c pb.ClusterServer) *grpc.Server {
|
func newGRPCServer(m pb.MachineServer, c pb.ClusterServer, d pb.DockerServer) *grpc.Server {
|
||||||
s := grpc.NewServer()
|
s := grpc.NewServer()
|
||||||
pb.RegisterMachineServer(s, m)
|
pb.RegisterMachineServer(s, m)
|
||||||
pb.RegisterClusterServer(s, c)
|
pb.RegisterClusterServer(s, c)
|
||||||
|
pb.RegisterDockerServer(s, d)
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,9 +252,9 @@ func (m *Machine) Started() <-chan struct{} {
|
|||||||
return m.started
|
return m.started
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsInitialised returns true if the machine has been configured as a member of a cluster,
|
// Initialised returns true if the machine has been configured as a member of a cluster,
|
||||||
// either by initialising a new cluster on it or joining an existing one.
|
// either by initialising a new cluster on it or joining an existing one.
|
||||||
func (m *Machine) IsInitialised() bool {
|
func (m *Machine) Initialised() bool {
|
||||||
m.state.mu.RLock()
|
m.state.mu.RLock()
|
||||||
defer m.state.mu.RUnlock()
|
defer m.state.mu.RUnlock()
|
||||||
|
|
||||||
@@ -128,33 +262,64 @@ func (m *Machine) IsInitialised() bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *Machine) Run(ctx context.Context) error {
|
func (m *Machine) Run(ctx context.Context) error {
|
||||||
|
// Docker dependency is essential for the machine to function. Block until it's ready.
|
||||||
|
if err := docker.WaitDaemonReady(ctx, m.config.DockerClient); err != nil {
|
||||||
|
return fmt.Errorf("wait for Docker daemon: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configure and start the corrosion service on the loopback if the machine is not initialised as a cluster
|
||||||
|
// member. This provides the store required for the machine to initialise a new cluster on it. Once the machine
|
||||||
|
// is initialised, the corrosion service is managed by the networkController.
|
||||||
|
if !m.Initialised() {
|
||||||
|
if err := m.configureCorrosion(); err != nil {
|
||||||
|
return fmt.Errorf("configure corrosion service: %w", err)
|
||||||
|
}
|
||||||
|
slog.Info("Configured corrosion service.", "dir", m.config.CorrosionDir)
|
||||||
|
|
||||||
|
if err := m.config.CorrosionService.Start(ctx); err != nil {
|
||||||
|
return fmt.Errorf("start corrosion service: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Use an errgroup to coordinate error handling and graceful shutdown of multiple machine components.
|
// Use an errgroup to coordinate error handling and graceful shutdown of multiple machine components.
|
||||||
errGroup, ctx := errgroup.WithContext(ctx)
|
errGroup, ctx := errgroup.WithContext(ctx)
|
||||||
|
|
||||||
// Start the machine local API server.
|
// Start the local machine API server.
|
||||||
apiSockPath := DefaultAPISockPath
|
machineListener, err := listenUnixSocket(m.config.MachineSockPath)
|
||||||
if m.config.APISockPath != "" {
|
|
||||||
apiSockPath = m.config.APISockPath
|
|
||||||
}
|
|
||||||
localListener, err := listenUnixSocket(apiSockPath)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("listen API unix socket %q: %w", apiSockPath, err)
|
return fmt.Errorf("listen machine API unix socket %q: %w", m.config.MachineSockPath, err)
|
||||||
}
|
}
|
||||||
errGroup.Go(
|
errGroup.Go(
|
||||||
func() error {
|
func() error {
|
||||||
slog.Info("Starting local API server.", "path", apiSockPath)
|
slog.Info("Starting local machine API server.", "path", m.config.MachineSockPath)
|
||||||
if err := m.localServer.Serve(localListener); err != nil {
|
if err := m.localMachineServer.Serve(machineListener); err != nil {
|
||||||
return fmt.Errorf("local API server failed: %w", err)
|
return fmt.Errorf("local machine API server failed: %w", err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
close(m.started)
|
|
||||||
|
|
||||||
// Control loop for managing the network controller.
|
// Start the local API proxy server.
|
||||||
|
proxyListener, err := listenUnixSocket(m.config.UncloudSockPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("listen API proxy unix socket %q: %w", m.config.UncloudSockPath, err)
|
||||||
|
}
|
||||||
errGroup.Go(
|
errGroup.Go(
|
||||||
func() error {
|
func() error {
|
||||||
if !m.IsInitialised() {
|
slog.Info("Starting local API proxy server.", "path", m.config.UncloudSockPath)
|
||||||
|
if err := m.localProxyServer.Serve(proxyListener); err != nil {
|
||||||
|
return fmt.Errorf("local API proxy server failed: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
)
|
||||||
|
// Signal that the machine is ready.
|
||||||
|
close(m.started)
|
||||||
|
|
||||||
|
// Control loop for managing components that depend on the machine being initialised as a cluster member.
|
||||||
|
errGroup.Go(
|
||||||
|
func() error {
|
||||||
|
if !m.Initialised() {
|
||||||
slog.Info(
|
slog.Info(
|
||||||
"Waiting for the machine to be initialised as a member of a cluster " +
|
"Waiting for the machine to be initialised as a member of a cluster " +
|
||||||
"to start the network controller.",
|
"to start the network controller.",
|
||||||
@@ -171,9 +336,40 @@ func (m *Machine) Run(ctx context.Context) error {
|
|||||||
// It can be reset when leaving the cluster and then re-initialised again with a new configuration.
|
// It can be reset when leaving the cluster and then re-initialised again with a new configuration.
|
||||||
case <-m.initialised:
|
case <-m.initialised:
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
|
m.cluster.UpdateMachineID(m.state.ID)
|
||||||
|
|
||||||
|
// Ensure the corrosion config is up to date, including a new gossip address if the machine
|
||||||
|
// has just joined a cluster.
|
||||||
|
if err = m.configureCorrosion(); err != nil {
|
||||||
|
return fmt.Errorf("configure corrosion service: %w", err)
|
||||||
|
}
|
||||||
|
slog.Info("Configured corrosion service.", "dir", m.config.CorrosionDir)
|
||||||
|
|
||||||
slog.Info("Starting network controller.")
|
slog.Info("Starting network controller.")
|
||||||
networkServer := newGRPCServer(m, m.cluster)
|
// Update the proxy director's local address to the machine's management IP address, allowing
|
||||||
ctrl, err = newNetworkController(m.state, networkServer, m.newMachinesCh)
|
// the proxy to identify which requests should be proxied to the local machine API server.
|
||||||
|
m.proxyDirector.UpdateLocalAddress(m.state.Network.ManagementIP.String())
|
||||||
|
proxyServer := grpc.NewServer(
|
||||||
|
grpc.ForceServerCodecV2(proxy.Codec()),
|
||||||
|
grpc.UnknownServiceHandler(
|
||||||
|
proxy.TransparentHandler(m.proxyDirector.Director),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
caddyfileCtrl, err := caddyfile.NewController(m.store, m.config.CaddyfilePath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("create Caddyfile controller: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctrl, err = newNetworkController(
|
||||||
|
m.state,
|
||||||
|
m.store,
|
||||||
|
proxyServer,
|
||||||
|
m.config.CorrosionService,
|
||||||
|
m.config.DockerClient,
|
||||||
|
caddyfileCtrl,
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("initialise network controller: %w", err)
|
return fmt.Errorf("initialise network controller: %w", err)
|
||||||
}
|
}
|
||||||
@@ -208,10 +404,19 @@ func (m *Machine) Run(ctx context.Context) error {
|
|||||||
errGroup.Go(
|
errGroup.Go(
|
||||||
func() error {
|
func() error {
|
||||||
<-ctx.Done()
|
<-ctx.Done()
|
||||||
slog.Info("Stopping local API server.")
|
slog.Info("Stopping local machine API server.")
|
||||||
// TODO: implement timeout for graceful shutdown.
|
// TODO: implement timeout for graceful shutdown.
|
||||||
m.localServer.GracefulStop()
|
m.localMachineServer.GracefulStop()
|
||||||
slog.Info("Local API server stopped.")
|
slog.Info("Local machine API server stopped.")
|
||||||
|
|
||||||
|
slog.Info("Stopping local API proxy server.")
|
||||||
|
// TODO: implement timeout for graceful shutdown.
|
||||||
|
m.localProxyServer.GracefulStop()
|
||||||
|
// Close the proxy director to close all backend connections.
|
||||||
|
m.proxyDirector.Close()
|
||||||
|
slog.Info("Local API proxy server stopped.")
|
||||||
|
|
||||||
|
m.config.DockerClient.Close()
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -223,50 +428,109 @@ func (m *Machine) Run(ctx context.Context) error {
|
|||||||
// access mode and uncloud group if the group is found, otherwise it falls back to the root group.
|
// access mode and uncloud group if the group is found, otherwise it falls back to the root group.
|
||||||
func listenUnixSocket(path string) (net.Listener, error) {
|
func listenUnixSocket(path string) (net.Listener, error) {
|
||||||
gid := 0 // Fall back to the root group if the uncloud group is not found.
|
gid := 0 // Fall back to the root group if the uncloud group is not found.
|
||||||
group, err := user.LookupGroup(DefaultAPISockGroup)
|
group, err := user.LookupGroup(DefaultSockGroup)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
//goland:noinspection GoTypeAssertionOnErrors
|
||||||
if _, ok := err.(user.UnknownGroupError); ok {
|
if _, ok := err.(user.UnknownGroupError); ok {
|
||||||
slog.Info(
|
slog.Info(
|
||||||
"Specified group not found, using root group for the API socket.", "group", DefaultAPISockGroup, "path",
|
"Specified group not found, using root group for the API socket.",
|
||||||
path,
|
"group", DefaultSockGroup, "path", path,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
return nil, fmt.Errorf("lookup %q group ID (GID): %w", DefaultAPISockGroup, err)
|
return nil, fmt.Errorf("lookup %q group ID (GID): %w", DefaultSockGroup, err)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
gid, err = strconv.Atoi(group.Gid)
|
gid, err = strconv.Atoi(group.Gid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("parse %q group ID (GID) %q: %w", DefaultAPISockGroup, group.Gid, err)
|
return nil, fmt.Errorf("parse %q group ID (GID) %q: %w", DefaultSockGroup, group.Gid, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ensure the parent directory exists and has the correct group permissions.
|
||||||
|
parent, _ := filepath.Split(path)
|
||||||
|
if err = os.MkdirAll(parent, 0750); err != nil {
|
||||||
|
return nil, fmt.Errorf("create directory %q: %w", parent, err)
|
||||||
|
}
|
||||||
|
if err = os.Chown(parent, -1, gid); err != nil {
|
||||||
|
return nil, fmt.Errorf("chown directory %q: %w", parent, err)
|
||||||
|
}
|
||||||
|
|
||||||
return sockets.NewUnixSocket(path, gid)
|
return sockets.NewUnixSocket(path, gid)
|
||||||
}
|
}
|
||||||
|
|
||||||
// InitCluster resets the local machine and initialises a new cluster with it.
|
func (m *Machine) configureCorrosion() error {
|
||||||
|
if err := corroservice.MkDataDir(m.config.CorrosionDir, m.config.CorrosionUser); err != nil {
|
||||||
|
return fmt.Errorf("create corrosion data directory: %w", err)
|
||||||
|
}
|
||||||
|
configPath := filepath.Join(m.config.CorrosionDir, "config.toml")
|
||||||
|
schemaPath := filepath.Join(m.config.CorrosionDir, "schema.sql")
|
||||||
|
|
||||||
|
// Use a loopback address as the gossip address (required) unless the machine has joined a cluster
|
||||||
|
// and has a management IP.
|
||||||
|
gossipAddr := netip.AddrPortFrom(netip.AddrFrom4([4]byte{127, 0, 0, 1}), corroservice.DefaultGossipPort)
|
||||||
|
if m.state.Network.ManagementIP.IsValid() {
|
||||||
|
gossipAddr = netip.AddrPortFrom(m.state.Network.ManagementIP, corroservice.DefaultGossipPort)
|
||||||
|
}
|
||||||
|
// TODO: use a partial list of machine peers for bootstrapping if the cluster is large.
|
||||||
|
var bootstrap []string
|
||||||
|
for _, peer := range m.state.Network.Peers {
|
||||||
|
if peer.Subnet == nil {
|
||||||
|
// Skip non-machine peers.
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
bootstrap = append(bootstrap, netip.AddrPortFrom(peer.ManagementIP, corroservice.DefaultGossipPort).String())
|
||||||
|
}
|
||||||
|
cfg := corroservice.Config{
|
||||||
|
DB: corroservice.DBConfig{
|
||||||
|
Path: filepath.Join(m.config.CorrosionDir, "store.db"),
|
||||||
|
SchemaPaths: []string{schemaPath},
|
||||||
|
},
|
||||||
|
Gossip: corroservice.GossipConfig{
|
||||||
|
Addr: gossipAddr,
|
||||||
|
Bootstrap: bootstrap,
|
||||||
|
Plaintext: true,
|
||||||
|
},
|
||||||
|
API: corroservice.APIConfig{
|
||||||
|
Addr: m.config.CorrosionAPIAddr,
|
||||||
|
},
|
||||||
|
Admin: corroservice.AdminConfig{
|
||||||
|
Path: filepath.Join(m.config.CorrosionDir, "admin.sock"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
// TODO: change file permissions to 0640 root:uncloud to emphasize the owner is the machine, not corrosion.
|
||||||
|
if err := cfg.Write(configPath, m.config.CorrosionUser); err != nil {
|
||||||
|
return fmt.Errorf("write corrosion config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.WriteFile(schemaPath, []byte(store.Schema), 0644); err != nil {
|
||||||
|
return fmt.Errorf("write corrosion schema: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// InitCluster initialises a new cluster on the local machine with the provided network configuration.
|
||||||
func (m *Machine) InitCluster(ctx context.Context, req *pb.InitClusterRequest) (*pb.InitClusterResponse, error) {
|
func (m *Machine) InitCluster(ctx context.Context, req *pb.InitClusterRequest) (*pb.InitClusterResponse, error) {
|
||||||
var err error
|
if m.Initialised() {
|
||||||
|
return nil, status.Error(codes.FailedPrecondition, "machine is already configured as a cluster member")
|
||||||
|
}
|
||||||
|
|
||||||
|
clusterNetwork, err := req.Network.ToPrefix()
|
||||||
|
if err != nil {
|
||||||
|
return nil, status.Errorf(codes.InvalidArgument, "invalid network: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = m.cluster.Init(ctx, clusterNetwork); err != nil {
|
||||||
|
return nil, status.Errorf(codes.Internal, "init cluster: %v", err)
|
||||||
|
}
|
||||||
|
slog.Info("Cluster state initialised.", "network", clusterNetwork.String())
|
||||||
|
|
||||||
machineName := req.MachineName
|
machineName := req.MachineName
|
||||||
if machineName == "" {
|
if machineName == "" {
|
||||||
machineName, err = cluster.NewRandomMachineName()
|
if machineName, err = cluster.NewRandomMachineName(); err != nil {
|
||||||
if err != nil {
|
|
||||||
return nil, status.Errorf(codes.Internal, "generate machine name: %v", err)
|
return nil, status.Errorf(codes.Internal, "generate machine name: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: a proper cluster leave mechanism and machine reset should be implemented later.
|
|
||||||
// For now just reset the machine state and cluster state.
|
|
||||||
clusterStatePath := cluster.StatePath(m.config.DataDir)
|
|
||||||
clusterState := cluster.NewState(clusterStatePath)
|
|
||||||
if err = clusterState.Save(); err != nil {
|
|
||||||
return nil, status.Errorf(codes.Internal, "save cluster state: %v", err)
|
|
||||||
}
|
|
||||||
m.cluster.SetState(clusterState)
|
|
||||||
slog.Info("Cluster state initialised.", "path", clusterStatePath)
|
|
||||||
if err = m.cluster.SetNetwork(req.Network); err != nil {
|
|
||||||
return nil, status.Errorf(codes.InvalidArgument, "set cluster network: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use the public and all routable IPs as endpoints.
|
// Use the public and all routable IPs as endpoints.
|
||||||
ips, err := network.ListRoutableIPs()
|
ips, err := network.ListRoutableIPs()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -314,28 +578,10 @@ func (m *Machine) InitCluster(ctx context.Context, req *pb.InitClusterRequest) (
|
|||||||
PrivateKey: m.state.Network.PrivateKey,
|
PrivateKey: m.state.Network.PrivateKey,
|
||||||
PublicKey: m.state.Network.PublicKey,
|
PublicKey: m.state.Network.PublicKey,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add a user to the cluster and build a peers config from it if provided.
|
|
||||||
if req.User != nil {
|
|
||||||
if err = m.cluster.AddUser(req.User); err != nil {
|
|
||||||
return nil, status.Errorf(codes.Internal, "add user to cluster: %v", err)
|
|
||||||
}
|
|
||||||
userManageIP, uErr := req.User.Network.ManagementIp.ToAddr()
|
|
||||||
if uErr != nil {
|
|
||||||
return nil, status.Error(codes.Internal, uErr.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
m.state.Network.Peers = make([]network.PeerConfig, 1)
|
|
||||||
m.state.Network.Peers[0] = network.PeerConfig{
|
|
||||||
ManagementIP: userManageIP,
|
|
||||||
PublicKey: req.User.Network.PublicKey,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = m.state.Save(); err != nil {
|
if err = m.state.Save(); err != nil {
|
||||||
return nil, status.Errorf(codes.Internal, "save machine state: %v", err)
|
return nil, status.Errorf(codes.Internal, "save machine state: %v", err)
|
||||||
}
|
}
|
||||||
slog.Info("Cluster initialised with machine.", "machine", m.state.Name)
|
slog.Info("Cluster initialised with machine.", "id", m.state.ID, "machine", m.state.Name)
|
||||||
// Signal that the machine is initialised as a member of a cluster.
|
// Signal that the machine is initialised as a member of a cluster.
|
||||||
m.initialised <- struct{}{}
|
m.initialised <- struct{}{}
|
||||||
|
|
||||||
@@ -345,10 +591,11 @@ func (m *Machine) InitCluster(ctx context.Context, req *pb.InitClusterRequest) (
|
|||||||
return resp, nil
|
return resp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// JoinCluster resets the local machine and configures it to join an existing cluster.
|
// JoinCluster configures the local machine to join an existing cluster.
|
||||||
func (m *Machine) JoinCluster(ctx context.Context, req *pb.JoinClusterRequest) (*emptypb.Empty, error) {
|
func (m *Machine) JoinCluster(_ context.Context, req *pb.JoinClusterRequest) (*emptypb.Empty, error) {
|
||||||
// TODO: a proper cluster leave mechanism and machine reset should be implemented later.
|
if m.Initialised() {
|
||||||
// For now assume the machine wasn't part of a cluster.
|
return nil, status.Error(codes.FailedPrecondition, "machine is already configured as a cluster member")
|
||||||
|
}
|
||||||
|
|
||||||
if req.Machine.Id == "" {
|
if req.Machine.Id == "" {
|
||||||
return nil, status.Error(codes.InvalidArgument, "machine ID not set")
|
return nil, status.Error(codes.InvalidArgument, "machine ID not set")
|
||||||
@@ -455,3 +702,44 @@ func (m *Machine) Inspect(_ context.Context, _ *emptypb.Empty) (*pb.MachineInfo,
|
|||||||
},
|
},
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// InspectService returns detailed information about a service and its containers.
|
||||||
|
func (m *Machine) InspectService(
|
||||||
|
ctx context.Context, req *pb.InspectServiceRequest,
|
||||||
|
) (*pb.InspectServiceResponse, error) {
|
||||||
|
opts := store.ListOptions{ServiceIDOrName: store.ServiceIDOrNameOptions{
|
||||||
|
ID: req.Id,
|
||||||
|
Name: req.Id,
|
||||||
|
}}
|
||||||
|
|
||||||
|
records, err := m.store.ListContainers(ctx, opts)
|
||||||
|
if err != nil {
|
||||||
|
return nil, status.Errorf(codes.Internal, "list containers: %v", err)
|
||||||
|
}
|
||||||
|
if len(records) == 0 {
|
||||||
|
return nil, status.Error(codes.NotFound, "service not found")
|
||||||
|
}
|
||||||
|
// TODO: handle SyncStatus to return only trusted container statuses.
|
||||||
|
// TODO: handle multiple services with the same name but different IDs. This can happen when two services
|
||||||
|
// with the same name are created concurrently on different machines.
|
||||||
|
|
||||||
|
containers := make([]*pb.Service_Container, len(records))
|
||||||
|
for i, r := range records {
|
||||||
|
containerJSON, err := json.Marshal(r.Container)
|
||||||
|
if err != nil {
|
||||||
|
return nil, status.Errorf(codes.Internal, "marshal container: %v", err)
|
||||||
|
}
|
||||||
|
containers[i] = &pb.Service_Container{
|
||||||
|
MachineId: r.MachineID,
|
||||||
|
Container: containerJSON,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
svc := &pb.Service{
|
||||||
|
Id: records[0].Container.ServiceID(),
|
||||||
|
Name: records[0].Container.ServiceName(),
|
||||||
|
Mode: records[0].Container.ServiceMode(),
|
||||||
|
Containers: containers,
|
||||||
|
}
|
||||||
|
return &pb.InspectServiceResponse{Service: svc}, nil
|
||||||
|
}
|
||||||
|
|||||||
+254
-70
@@ -2,33 +2,54 @@ package machine
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/cenkalti/backoff/v4"
|
||||||
|
"github.com/docker/docker/client"
|
||||||
"golang.org/x/sync/errgroup"
|
"golang.org/x/sync/errgroup"
|
||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net"
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"time"
|
||||||
"uncloud/internal/machine/api/pb"
|
"uncloud/internal/machine/api/pb"
|
||||||
|
"uncloud/internal/machine/caddyfile"
|
||||||
|
"uncloud/internal/machine/corroservice"
|
||||||
|
"uncloud/internal/machine/docker"
|
||||||
"uncloud/internal/machine/network"
|
"uncloud/internal/machine/network"
|
||||||
|
"uncloud/internal/machine/store"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
APIPort = 51000
|
APIPort = 51000
|
||||||
DockerNetworkName = "uncloud"
|
|
||||||
DockerUserChain = "DOCKER-USER"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type networkController struct {
|
type networkController struct {
|
||||||
state *State
|
state *State
|
||||||
wgnet *network.WireGuardNetwork
|
store *store.Store
|
||||||
|
|
||||||
|
wgnet *network.WireGuardNetwork
|
||||||
|
endpointChanges <-chan network.EndpointChangeEvent
|
||||||
|
|
||||||
server *grpc.Server
|
server *grpc.Server
|
||||||
newMachinesCh <-chan *pb.MachineInfo
|
corroService corroservice.Service
|
||||||
|
dockerCli *client.Client
|
||||||
|
caddyfileCtrl *caddyfile.Controller
|
||||||
|
|
||||||
// TODO: DNS server/resolver listening on the machine IP, e.g. 10.210.0.1:53. It can't listen on 127.0.X.X
|
// TODO: DNS server/resolver listening on the machine IP, e.g. 10.210.0.1:53. It can't listen on 127.0.X.X
|
||||||
// like resolved does because it needs to be reachable from both the host and the containers.
|
// like resolved does because it needs to be reachable from both the host and the containers.
|
||||||
}
|
}
|
||||||
|
|
||||||
func newNetworkController(state *State, server *grpc.Server, newMachCh <-chan *pb.MachineInfo) (
|
func newNetworkController(
|
||||||
|
state *State,
|
||||||
|
store *store.Store,
|
||||||
|
server *grpc.Server,
|
||||||
|
corroService corroservice.Service,
|
||||||
|
dockerCli *client.Client,
|
||||||
|
caddyfileCtrl *caddyfile.Controller,
|
||||||
|
) (
|
||||||
*networkController, error,
|
*networkController, error,
|
||||||
) {
|
) {
|
||||||
slog.Info("Starting WireGuard network.")
|
slog.Info("Starting WireGuard network.")
|
||||||
@@ -36,12 +57,17 @@ func newNetworkController(state *State, server *grpc.Server, newMachCh <-chan *p
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("create WireGuard network: %w", err)
|
return nil, fmt.Errorf("create WireGuard network: %w", err)
|
||||||
}
|
}
|
||||||
|
endpointChanges := wgnet.WatchEndpoints()
|
||||||
|
|
||||||
return &networkController{
|
return &networkController{
|
||||||
state: state,
|
state: state,
|
||||||
wgnet: wgnet,
|
store: store,
|
||||||
server: server,
|
wgnet: wgnet,
|
||||||
newMachinesCh: newMachCh,
|
endpointChanges: endpointChanges,
|
||||||
|
server: server,
|
||||||
|
corroService: corroService,
|
||||||
|
dockerCli: dockerCli,
|
||||||
|
caddyfileCtrl: caddyfileCtrl,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,6 +77,21 @@ func (nc *networkController) Run(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
slog.Info("WireGuard network configured.")
|
slog.Info("WireGuard network configured.")
|
||||||
|
|
||||||
|
if nc.corroService.Running() {
|
||||||
|
// Corrosion service was running before the WireGuard network was configured so we need to restart it.
|
||||||
|
slog.Info("Restarting corrosion service to apply new configuration with WireGuard network.")
|
||||||
|
if err := nc.corroService.Restart(ctx); err != nil {
|
||||||
|
return fmt.Errorf("restart corrosion service: %w", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
slog.Info("Starting corrosion service.")
|
||||||
|
if err := nc.corroService.Start(ctx); err != nil {
|
||||||
|
return fmt.Errorf("start corrosion service: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// TODO: Figure out if we need to manually stop the corrosion service when the context is done or just
|
||||||
|
// rely on systemd to handle service dependencies on its own.
|
||||||
|
|
||||||
errGroup, ctx := errgroup.WithContext(ctx)
|
errGroup, ctx := errgroup.WithContext(ctx)
|
||||||
|
|
||||||
// Start the network API server. Assume the management IP can't be changed when the network is running.
|
// Start the network API server. Assume the management IP can't be changed when the network is running.
|
||||||
@@ -69,40 +110,75 @@ func (nc *networkController) Run(ctx context.Context) error {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
// Setup Docker network and iptables rules in a goroutine because it may block until the Docker daemon is ready.
|
// Setup Docker network and synchronise containers to the cluster store.
|
||||||
errGroup.Go(
|
errGroup.Go(
|
||||||
func() error {
|
func() error {
|
||||||
if err := nc.setupDockerNetwork(ctx); err != nil {
|
return nc.prepareAndWatchDocker(ctx)
|
||||||
return fmt.Errorf("setup Docker network: %w", err)
|
|
||||||
}
|
|
||||||
slog.Info("Docker network configured.")
|
|
||||||
return nil
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
// Handle new machines added to the cluster. Handling new machines and endpoint changes should be done
|
// Handle machine changes in the cluster. Handling machine and endpoint changes should be done
|
||||||
// in separate goroutines to avoid a deadlock when reconfiguring the network.
|
// in separate goroutines to avoid a deadlock when reconfiguring the network.
|
||||||
errGroup.Go(
|
errGroup.Go(
|
||||||
func() error {
|
func() error {
|
||||||
if err := nc.handleNewMachines(ctx); err != nil {
|
if err := nc.handleMachineChanges(ctx); err != nil {
|
||||||
return fmt.Errorf("handle new machines: %w", err)
|
return fmt.Errorf("handle new machines: %w", err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
// TODO: run another goroutine to watch WG network endpoint changes and update the state accordingly.
|
// Watch for endpoint changes and update the machine state accordingly.
|
||||||
// Network updates in the state should not occur outside of this controller.
|
errGroup.Go(
|
||||||
|
func() error {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case e, ok := <-nc.endpointChanges:
|
||||||
|
if !ok {
|
||||||
|
// The channel was closed, stop watching for changes.
|
||||||
|
nc.endpointChanges = nil
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
nc.state.mu.Lock()
|
||||||
|
for i := range nc.state.Network.Peers {
|
||||||
|
if nc.state.Network.Peers[i].PublicKey.Equal(e.PublicKey) {
|
||||||
|
nc.state.Network.Peers[i].Endpoint = &e.Endpoint
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := nc.state.Save(); err != nil {
|
||||||
|
slog.Error("Failed to save machine state.", "err", err)
|
||||||
|
}
|
||||||
|
nc.state.mu.Unlock()
|
||||||
|
|
||||||
|
slog.Debug("Preserved endpoint change in the machine state.",
|
||||||
|
"public_key", e.PublicKey, "endpoint", e.Endpoint)
|
||||||
|
case <-ctx.Done():
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
errGroup.Go(
|
errGroup.Go(
|
||||||
func() error {
|
func() error {
|
||||||
if err = nc.wgnet.Run(ctx); err != nil {
|
if err := nc.wgnet.Run(ctx); err != nil {
|
||||||
return fmt.Errorf("WireGuard network failed: %w", err)
|
return fmt.Errorf("WireGuard network failed: %w", err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
errGroup.Go(func() error {
|
||||||
|
slog.Info("Starting Caddyfile controller.")
|
||||||
|
if err := nc.caddyfileCtrl.Run(ctx); err != nil {
|
||||||
|
//goland:noinspection GoErrorStringFormat
|
||||||
|
return fmt.Errorf("Caddyfile controller failed: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
// Wait for the context to be done and stop the network API server.
|
// Wait for the context to be done and stop the network API server.
|
||||||
errGroup.Go(
|
errGroup.Go(
|
||||||
func() error {
|
func() error {
|
||||||
@@ -118,59 +194,167 @@ func (nc *networkController) Run(ctx context.Context) error {
|
|||||||
return errGroup.Wait()
|
return errGroup.Wait()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (nc *networkController) handleNewMachines(ctx context.Context) error {
|
// prepareAndWatchDocker configures the Docker network and watches local Docker containers to sync them
|
||||||
for {
|
// to the cluster store.
|
||||||
select {
|
func (nc *networkController) prepareAndWatchDocker(ctx context.Context) error {
|
||||||
case minfo := <-nc.newMachinesCh:
|
manager := docker.NewManager(nc.dockerCli, nc.state.ID, nc.store)
|
||||||
slog.Info("Handling new machine added to the cluster.", "name", minfo.Name)
|
if err := manager.WaitDaemonReady(ctx); err != nil {
|
||||||
|
return fmt.Errorf("wait for Docker daemon: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
// Skip the current machine.
|
if err := manager.EnsureUncloudNetwork(ctx, nc.state.Network.Subnet); err != nil {
|
||||||
nc.state.mu.RLock()
|
return fmt.Errorf("ensure Docker network: %w", err)
|
||||||
currentMachID := nc.state.ID
|
}
|
||||||
nc.state.mu.RUnlock()
|
slog.Info("Docker network configured.")
|
||||||
if minfo.Id == currentMachID {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := minfo.Network.Validate(); err != nil {
|
slog.Info("Watching Docker containers and syncing them to cluster store.")
|
||||||
slog.Error("Invalid machine network configuration.", "err", err)
|
// Retry to watch and sync containers until the context is done.
|
||||||
continue
|
boff := backoff.WithContext(backoff.NewExponentialBackOff(
|
||||||
}
|
backoff.WithInitialInterval(100*time.Millisecond),
|
||||||
// Ignore errors as they are already validated.
|
backoff.WithMaxInterval(5*time.Second),
|
||||||
subnet, _ := minfo.Network.Subnet.ToPrefix()
|
backoff.WithMaxElapsedTime(0),
|
||||||
manageIP, _ := minfo.Network.ManagementIp.ToAddr()
|
), ctx)
|
||||||
endpoints := make([]netip.AddrPort, len(minfo.Network.Endpoints))
|
watchAndSync := func() error {
|
||||||
for i, ep := range minfo.Network.Endpoints {
|
if wErr := manager.WatchAndSyncContainers(ctx); wErr != nil {
|
||||||
addrPort, _ := ep.ToAddrPort()
|
slog.Error("Failed to watch and sync containers to cluster store, retrying.", "err", wErr)
|
||||||
endpoints[i] = addrPort
|
return wErr
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
peer := network.PeerConfig{
|
}
|
||||||
Subnet: &subnet,
|
if err := backoff.Retry(watchAndSync, boff); err != nil {
|
||||||
ManagementIP: manageIP,
|
if errors.Is(err, context.Canceled) {
|
||||||
AllEndpoints: endpoints,
|
|
||||||
PublicKey: minfo.Network.PublicKey,
|
|
||||||
}
|
|
||||||
if len(endpoints) > 0 {
|
|
||||||
peer.Endpoint = &endpoints[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
nc.state.mu.Lock()
|
|
||||||
// TODO: deduplicate peers by public key, maybe implement addNetworkPeer method in the state.
|
|
||||||
nc.state.Network.Peers = append(nc.state.Network.Peers, peer)
|
|
||||||
err := nc.state.Save()
|
|
||||||
nc.state.mu.Unlock()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("save machine state: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := nc.wgnet.Configure(*nc.state.Network); err != nil {
|
|
||||||
return fmt.Errorf("configure network with new peer: %w", err)
|
|
||||||
}
|
|
||||||
case <-ctx.Done():
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
return fmt.Errorf("watch and sync containers to cluster store: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleMachineChanges subscribes to machine changes in the cluster and reconfigures the network peers accordingly
|
||||||
|
// when changes occur.
|
||||||
|
func (nc *networkController) handleMachineChanges(ctx context.Context) error {
|
||||||
|
for {
|
||||||
|
// Retry to subscribe to machine changes indefinitely until the context is done.
|
||||||
|
boff := backoff.WithContext(backoff.NewExponentialBackOff(
|
||||||
|
backoff.WithInitialInterval(1*time.Second),
|
||||||
|
backoff.WithMaxInterval(60*time.Second),
|
||||||
|
backoff.WithMaxElapsedTime(0),
|
||||||
|
), ctx)
|
||||||
|
|
||||||
|
var (
|
||||||
|
machines []*pb.MachineInfo
|
||||||
|
changes <-chan struct{}
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
subscribe := func() error {
|
||||||
|
if machines, changes, err = nc.store.SubscribeMachines(ctx); err != nil {
|
||||||
|
slog.Info("Failed to subscribe to machine changes, retrying.", "err", err)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err = backoff.Retry(subscribe, boff); err != nil {
|
||||||
|
if errors.Is(err, context.Canceled) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
slog.Error("Unexpected error while retrying to subscribe to machine changes.", "err", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
slog.Info("Subscribed to machine changes in the cluster to reconfigure network peers.")
|
||||||
|
|
||||||
|
// The machine store may be empty when a machine first joins the cluster, before store synchronization
|
||||||
|
// completes. Skip configuration now and apply it when the store changes are received.
|
||||||
|
if len(machines) > 0 {
|
||||||
|
slog.Info("Reconfiguring network peers with the current machines.", "machines", len(machines))
|
||||||
|
if err = nc.configurePeers(machines); err != nil {
|
||||||
|
slog.Error("Failed to configure peers.", "err", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// For simplicity, reconfigure all peers on any change.
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
// TODO: test when Corrosion fails and the subscription fails to resubscribe (after 1 minute). It seems
|
||||||
|
// the changes channel will be closed and this will become a busy loop. Perhaps, the outer for loop should
|
||||||
|
// be reworked as well.
|
||||||
|
case <-changes:
|
||||||
|
slog.Info("Cluster machines changed, reconfiguring network peers.")
|
||||||
|
if machines, err = nc.store.ListMachines(ctx); err != nil {
|
||||||
|
slog.Error("Failed to list machines.", "err", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err = nc.configurePeers(machines); err != nil {
|
||||||
|
slog.Error("Failed to configure peers.", "err", err)
|
||||||
|
}
|
||||||
|
case <-ctx.Done():
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (nc *networkController) configurePeers(machines []*pb.MachineInfo) error {
|
||||||
|
if len(machines) == 0 {
|
||||||
|
return fmt.Errorf("no machines to configure peers")
|
||||||
|
}
|
||||||
|
|
||||||
|
nc.state.mu.RLock()
|
||||||
|
currentPeerEndpoints := make(map[string]*netip.AddrPort, len(nc.state.Network.Peers))
|
||||||
|
for _, p := range nc.state.Network.Peers {
|
||||||
|
currentPeerEndpoints[p.PublicKey.String()] = p.Endpoint
|
||||||
|
}
|
||||||
|
nc.state.mu.RUnlock()
|
||||||
|
|
||||||
|
// Construct the list of peers from the machine configurations ensuring that the current endpoint is preserved.
|
||||||
|
peers := make([]network.PeerConfig, 0, len(machines)-1)
|
||||||
|
for _, m := range machines {
|
||||||
|
// Skip the current machine.
|
||||||
|
if m.Id == nc.state.ID {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := m.Network.Validate(); err != nil {
|
||||||
|
slog.Error("Invalid machine network configuration.", "machine", m.Name, "err", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Ignore errors as they are already validated.
|
||||||
|
subnet, _ := m.Network.Subnet.ToPrefix()
|
||||||
|
manageIP, _ := m.Network.ManagementIp.ToAddr()
|
||||||
|
endpoints := make([]netip.AddrPort, len(m.Network.Endpoints))
|
||||||
|
for i, ep := range m.Network.Endpoints {
|
||||||
|
addrPort, _ := ep.ToAddrPort()
|
||||||
|
endpoints[i] = addrPort
|
||||||
|
}
|
||||||
|
peer := network.PeerConfig{
|
||||||
|
Subnet: &subnet,
|
||||||
|
ManagementIP: manageIP,
|
||||||
|
AllEndpoints: endpoints,
|
||||||
|
PublicKey: m.Network.PublicKey,
|
||||||
|
}
|
||||||
|
|
||||||
|
currentEndpoint := currentPeerEndpoints[peer.PublicKey.String()]
|
||||||
|
if currentEndpoint != nil && slices.Contains(endpoints, *currentEndpoint) {
|
||||||
|
peer.Endpoint = currentEndpoint
|
||||||
|
} else if len(endpoints) > 0 {
|
||||||
|
peer.Endpoint = &endpoints[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
peers = append(peers, peer)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Preserve the new list of peers in the machine state.
|
||||||
|
nc.state.mu.Lock()
|
||||||
|
nc.state.Network.Peers = peers
|
||||||
|
err := nc.state.Save()
|
||||||
|
nc.state.mu.Unlock()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("save machine state: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
nc.state.mu.RLock()
|
||||||
|
defer nc.state.mu.RUnlock()
|
||||||
|
if err = nc.wgnet.Configure(*nc.state.Network); err != nil {
|
||||||
|
return fmt.Errorf("configure network peers: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: method to shutdown network when leaving a cluster. Regular context cancellation shouldn't bring it down.
|
// TODO: method to shutdown network when leaving a cluster. Regular context cancellation shouldn't bring it down.
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@@ -24,6 +25,13 @@ func ListRoutableIPs() ([]netip.Addr, error) {
|
|||||||
// Skip the Uncloud WireGuard and Docker interfaces.
|
// Skip the Uncloud WireGuard and Docker interfaces.
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
// Docker bridge name doesn't seem to be documented but this is the source code where it is generated:
|
||||||
|
// https://github.com/moby/moby/blob/v27.2.1/libnetwork/drivers/bridge/bridge_linux.go#L664
|
||||||
|
if match, _ := regexp.MatchString(`br-[0-9a-f]{12}`, iface.Name); match {
|
||||||
|
// Skip Docker bridge interfaces.
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
if iface.Flags&net.FlagUp == 0 || iface.Flags&net.FlagRunning == 0 || iface.Flags&net.FlagLoopback != 0 {
|
if iface.Flags&net.FlagUp == 0 || iface.Flags&net.FlagRunning == 0 || iface.Flags&net.FlagLoopback != 0 {
|
||||||
// Skip interfaces:
|
// Skip interfaces:
|
||||||
// * Not administratively UP.
|
// * Not administratively UP.
|
||||||
|
|||||||
@@ -8,11 +8,6 @@ import (
|
|||||||
"uncloud/internal/secret"
|
"uncloud/internal/secret"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
|
||||||
DefaultNetwork = netip.MustParsePrefix("10.210.0.0/16")
|
|
||||||
DefaultSubnetBits = 24
|
|
||||||
)
|
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
// Subnet is the IPv4 address range allocated to the machine. The machine's IP address is the first address
|
// Subnet is the IPv4 address range allocated to the machine. The machine's IP address is the first address
|
||||||
// in the subnet. Other IP addresses are allocated to containers running on the machine.
|
// in the subnet. Other IP addresses are allocated to containers running on the machine.
|
||||||
@@ -41,7 +36,9 @@ func (c Config) IsConfigured() bool {
|
|||||||
c.PrivateKey != nil && c.PublicKey != nil
|
c.PrivateKey != nil && c.PublicKey != nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c Config) toDeviceConfig() (wgtypes.Config, error) {
|
// toDeviceConfig converts the configuration to a WireGuard device configuration. It updates the existing peers
|
||||||
|
// without replacing them to not disrupt existing connections and to not lose track of the last handshake time.
|
||||||
|
func (c Config) toDeviceConfig(currentPeers []wgtypes.Peer) (wgtypes.Config, error) {
|
||||||
privateKey, err := wgtypes.NewKey(c.PrivateKey)
|
privateKey, err := wgtypes.NewKey(c.PrivateKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return wgtypes.Config{}, fmt.Errorf("parse private key: %w", err)
|
return wgtypes.Config{}, fmt.Errorf("parse private key: %w", err)
|
||||||
@@ -50,6 +47,8 @@ func (c Config) toDeviceConfig() (wgtypes.Config, error) {
|
|||||||
|
|
||||||
persistentKeepalive := WireGuardKeepaliveInterval
|
persistentKeepalive := WireGuardKeepaliveInterval
|
||||||
wgPeerConfigs := make([]wgtypes.PeerConfig, len(c.Peers))
|
wgPeerConfigs := make([]wgtypes.PeerConfig, len(c.Peers))
|
||||||
|
// A set of new peer public keys for checking which current peers should be removed.
|
||||||
|
newPeersSet := make(map[string]struct{}, len(c.Peers))
|
||||||
for i, peerConfig := range c.Peers {
|
for i, peerConfig := range c.Peers {
|
||||||
peerPublicKey, kErr := wgtypes.NewKey(peerConfig.PublicKey)
|
peerPublicKey, kErr := wgtypes.NewKey(peerConfig.PublicKey)
|
||||||
if kErr != nil {
|
if kErr != nil {
|
||||||
@@ -75,12 +74,36 @@ func (c Config) toDeviceConfig() (wgtypes.Config, error) {
|
|||||||
Port: int(peerConfig.Endpoint.Port()),
|
Port: int(peerConfig.Endpoint.Port()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
newPeersSet[wgPeerConfigs[i].PublicKey.String()] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove peers that are not in the configuration.
|
||||||
|
for _, p := range currentPeers {
|
||||||
|
if _, ok := newPeersSet[p.PublicKey.String()]; !ok {
|
||||||
|
wgPeerConfigs = append(wgPeerConfigs, wgtypes.PeerConfig{
|
||||||
|
PublicKey: p.PublicKey,
|
||||||
|
Remove: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return wgtypes.Config{
|
return wgtypes.Config{
|
||||||
PrivateKey: &privateKey,
|
PrivateKey: &privateKey,
|
||||||
ListenPort: &listenPort,
|
ListenPort: &listenPort,
|
||||||
ReplacePeers: true,
|
ReplacePeers: false,
|
||||||
Peers: wgPeerConfigs,
|
Peers: wgPeerConfigs,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *PeerConfig) prefixes() ([]netip.Prefix, error) {
|
||||||
|
managePrefix, err := addrToSingleIPPrefix(p.ManagementIP)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("parse management IP: %w", err)
|
||||||
|
}
|
||||||
|
prefixes := []netip.Prefix{managePrefix}
|
||||||
|
if p.Subnet != nil {
|
||||||
|
prefixes = append(prefixes, *p.Subnet)
|
||||||
|
}
|
||||||
|
return prefixes, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,172 @@
|
|||||||
|
package network
|
||||||
|
|
||||||
|
import (
|
||||||
|
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
||||||
|
"log/slog"
|
||||||
|
"net/netip"
|
||||||
|
"slices"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
PeerStatusUnknown = "unknown"
|
||||||
|
PeerStatusUp = "up"
|
||||||
|
PeerStatusDown = "down"
|
||||||
|
)
|
||||||
|
|
||||||
|
type peer struct {
|
||||||
|
config PeerConfig
|
||||||
|
lastEndpointChangeTime time.Time
|
||||||
|
lastHandshakeTime time.Time
|
||||||
|
receiveBytes int64
|
||||||
|
transmitBytes int64
|
||||||
|
status string
|
||||||
|
}
|
||||||
|
|
||||||
|
func newPeer(config PeerConfig, wgPeer *wgtypes.Peer) *peer {
|
||||||
|
p := &peer{
|
||||||
|
config: config,
|
||||||
|
status: PeerStatusUnknown,
|
||||||
|
}
|
||||||
|
if p.config.Endpoint != nil {
|
||||||
|
p.lastEndpointChangeTime = time.Now()
|
||||||
|
// Reset the endpoint change time if the endpoint is the same as the one in the current WireGuard peer.
|
||||||
|
// This is to avoid unnecessary endpoint rotation for the already connected peer.
|
||||||
|
if wgPeer != nil && wgPeer.Endpoint != nil {
|
||||||
|
wgEndpoint := wgPeer.Endpoint.AddrPort()
|
||||||
|
if *p.config.Endpoint == wgEndpoint {
|
||||||
|
p.lastEndpointChangeTime = time.Time{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *peer) updateConfig(config PeerConfig) {
|
||||||
|
if p.config.Endpoint != config.Endpoint {
|
||||||
|
p.lastEndpointChangeTime = time.Now()
|
||||||
|
p.status = PeerStatusUnknown
|
||||||
|
}
|
||||||
|
p.config = config
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *peer) updateFromDevice(wgPeer wgtypes.Peer) (endpointChanged bool) {
|
||||||
|
if wgPeer.Endpoint != nil {
|
||||||
|
wgEndpoint := wgPeer.Endpoint.AddrPort()
|
||||||
|
if p.config.Endpoint == nil || *p.config.Endpoint != wgEndpoint {
|
||||||
|
// The peer endpoint has been automatically updated on the WireGuard device which normally happens
|
||||||
|
// when the peer establishes a reverse connection to this machine.
|
||||||
|
p.config.Endpoint = &wgEndpoint
|
||||||
|
// Reset the endpoint change time to not attempt to rotate the endpoint and correctly calculate the status.
|
||||||
|
p.lastEndpointChangeTime = time.Time{}
|
||||||
|
endpointChanged = true
|
||||||
|
slog.Info("Peer endpoint automatically updated on WireGuard interface by establishing a reverse "+
|
||||||
|
"connection to this machine.", "public_key", p.config.PublicKey, "endpoint", wgEndpoint)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
p.lastHandshakeTime = wgPeer.LastHandshakeTime
|
||||||
|
p.receiveBytes = wgPeer.ReceiveBytes
|
||||||
|
p.transmitBytes = wgPeer.TransmitBytes
|
||||||
|
p.calculateStatus()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Peer status calculation is based on Talos Kubespan implementation:
|
||||||
|
// https://github.com/siderolabs/talos/blob/v1.8.0/internal/app/machined/pkg/adapters/kubespan/peer_status.go
|
||||||
|
|
||||||
|
// endpointConnectionTimeout is time to wait for initial handshake when the endpoint is just set.
|
||||||
|
const endpointConnectionTimeout = 15 * time.Second
|
||||||
|
|
||||||
|
// peerDownInterval is the time since last handshake when established peer is considered to be down.
|
||||||
|
//
|
||||||
|
// WG whitepaper defines a downed peer as being:
|
||||||
|
// Handshake Timeout (180s) + Rekey Timeout (5s) + Rekey Attempt Timeout (90s)
|
||||||
|
//
|
||||||
|
// This interval is applied when the link is already established.
|
||||||
|
const peerDownInterval = (180 + 5 + 90) * time.Second
|
||||||
|
|
||||||
|
// calculateStatus updates the peer's connection status based on other field values.
|
||||||
|
//
|
||||||
|
// Goal: endpoint is ultimately down if we haven't seen handshake for more than peerDownInterval,
|
||||||
|
// but as the endpoints get updated we want faster feedback, so we start checking more aggressively
|
||||||
|
// that the handshake happened within endpointConnectionTimeout since last endpoint change.
|
||||||
|
//
|
||||||
|
// Timeline:
|
||||||
|
//
|
||||||
|
// ---------------------------------------------------------------------->
|
||||||
|
// ^ ^ ^
|
||||||
|
// | | |
|
||||||
|
// T0 T0+endpointConnectionTimeout T0+peerDownInterval
|
||||||
|
//
|
||||||
|
// Where T0 = lastEndpointChangeTime
|
||||||
|
//
|
||||||
|
// The question is where is LastHandshakeTimeout vs. those points above:
|
||||||
|
//
|
||||||
|
// - if we're past (T0+peerDownInterval), simply check that time since last handshake < peerDownInterval
|
||||||
|
// - if we're between (T0+endpointConnectionTimeout) and (T0+peerDownInterval), and there's no handshake
|
||||||
|
// after the endpoint change, assume that the endpoint is down
|
||||||
|
// - if we're between (T0) and (T0+endpointConnectionTimeout), and there's no handshake since the endpoint change,
|
||||||
|
// consider the state to be unknown
|
||||||
|
func (p *peer) calculateStatus() {
|
||||||
|
lastStatus := p.status
|
||||||
|
sinceLastHandshake := time.Since(p.lastHandshakeTime)
|
||||||
|
sinceEndpointChange := time.Since(p.lastEndpointChangeTime)
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case sinceEndpointChange > peerDownInterval: // past T0+peerDownInterval
|
||||||
|
// If we got handshake in the last peerDownInterval, endpoint is up.
|
||||||
|
if sinceLastHandshake < peerDownInterval {
|
||||||
|
p.status = PeerStatusUp
|
||||||
|
} else {
|
||||||
|
p.status = PeerStatusDown
|
||||||
|
}
|
||||||
|
case sinceEndpointChange < endpointConnectionTimeout: // between (T0) and (T0+endpointConnectionTimeout)
|
||||||
|
// Endpoint got recently updated, consider no handshake as 'unknown'.
|
||||||
|
if p.lastHandshakeTime.After(p.lastEndpointChangeTime) {
|
||||||
|
p.status = PeerStatusUp
|
||||||
|
} else {
|
||||||
|
p.status = PeerStatusUnknown
|
||||||
|
}
|
||||||
|
default: // otherwise, we're between (T0+endpointConnectionTimeout) and (T0+peerDownInterval)
|
||||||
|
// If we haven't had the handshake yet, consider the endpoint to be down.
|
||||||
|
if p.lastHandshakeTime.After(p.lastEndpointChangeTime) {
|
||||||
|
p.status = PeerStatusUp
|
||||||
|
} else {
|
||||||
|
p.status = PeerStatusDown
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.status == PeerStatusDown && p.config.Endpoint == nil {
|
||||||
|
// No endpoint, so unknown.
|
||||||
|
p.status = PeerStatusUnknown
|
||||||
|
}
|
||||||
|
if p.status != lastStatus {
|
||||||
|
slog.Info("Peer status changed.", "public_key", p.config.PublicKey,
|
||||||
|
"status", p.status, "previous_status", lastStatus)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// shouldChangeEndpoint returns the next endpoint to use and a boolean indicating if the endpoint should be changed.
|
||||||
|
func (p *peer) shouldChangeEndpoint() (netip.AddrPort, bool) {
|
||||||
|
if p.config.Endpoint != nil && p.status != PeerStatusDown {
|
||||||
|
// Shouldn't change the endpoint if it's set and the status is 'up' or 'unknown'.
|
||||||
|
return netip.AddrPort{}, false
|
||||||
|
}
|
||||||
|
if len(p.config.AllEndpoints) == 0 {
|
||||||
|
// No endpoints to choose from.
|
||||||
|
return netip.AddrPort{}, false
|
||||||
|
}
|
||||||
|
if p.config.Endpoint == nil {
|
||||||
|
// No endpoint set, so choose the first one.
|
||||||
|
return p.config.AllEndpoints[0], true
|
||||||
|
}
|
||||||
|
if len(p.config.AllEndpoints) == 1 && p.config.Endpoint == &p.config.AllEndpoints[0] {
|
||||||
|
// Only one endpoint and it's the current one, can't rotate.
|
||||||
|
return netip.AddrPort{}, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// The endpoint is set and the status is 'down', so rotate to the next one.
|
||||||
|
idx := slices.Index(p.config.AllEndpoints, *p.config.Endpoint)
|
||||||
|
endpoint := p.config.AllEndpoints[(idx+1)%len(p.config.AllEndpoints)]
|
||||||
|
return endpoint, true
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ package network
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
||||||
|
"net/netip"
|
||||||
"time"
|
"time"
|
||||||
"uncloud/internal/secret"
|
"uncloud/internal/secret"
|
||||||
)
|
)
|
||||||
@@ -14,6 +15,12 @@ const (
|
|||||||
WireGuardKeepaliveInterval = 25 * time.Second
|
WireGuardKeepaliveInterval = 25 * time.Second
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type EndpointChangeEvent struct {
|
||||||
|
PublicKey secret.Secret
|
||||||
|
// Endpoint is the new endpoint of the peer.
|
||||||
|
Endpoint netip.AddrPort
|
||||||
|
}
|
||||||
|
|
||||||
// NewMachineKeys generates a new WireGuard private and public key pair.
|
// NewMachineKeys generates a new WireGuard private and public key pair.
|
||||||
func NewMachineKeys() (privKey, pubKey secret.Secret, err error) {
|
func NewMachineKeys() (privKey, pubKey secret.Secret, err error) {
|
||||||
wgPrivKey, err := wgtypes.GeneratePrivateKey()
|
wgPrivKey, err := wgtypes.GeneratePrivateKey()
|
||||||
|
|||||||
@@ -20,3 +20,7 @@ func (n *WireGuardNetwork) Configure(config Config) error {
|
|||||||
func (n *WireGuardNetwork) Run(ctx context.Context) error {
|
func (n *WireGuardNetwork) Run(ctx context.Context) error {
|
||||||
return errors.New("not implemented on darwin")
|
return errors.New("not implemented on darwin")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (n *WireGuardNetwork) WatchEndpoints() <-chan EndpointChangeEvent {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -10,33 +10,32 @@ import (
|
|||||||
"go4.org/netipx"
|
"go4.org/netipx"
|
||||||
"golang.org/x/sys/unix"
|
"golang.org/x/sys/unix"
|
||||||
"golang.zx2c4.com/wireguard/wgctrl"
|
"golang.zx2c4.com/wireguard/wgctrl"
|
||||||
|
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"slices"
|
"slices"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
"uncloud/internal/secret"
|
||||||
)
|
)
|
||||||
|
|
||||||
type WireGuardNetwork struct {
|
type WireGuardNetwork struct {
|
||||||
link netlink.Link
|
link netlink.Link
|
||||||
peers []peer
|
// peers is a map of peers indexed by their public key.
|
||||||
|
peers map[string]*peer
|
||||||
|
// watchers is a list of channels that are notified when the endpoints of the peers change.
|
||||||
|
watchers []chan EndpointChangeEvent
|
||||||
// mu synchronises concurrent network configuration changes.
|
// mu synchronises concurrent network configuration changes.
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
type peer struct {
|
|
||||||
config PeerConfig
|
|
||||||
lastEndpointChangeTime time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewWireGuardNetwork() (*WireGuardNetwork, error) {
|
func NewWireGuardNetwork() (*WireGuardNetwork, error) {
|
||||||
link, err := createOrGetLink(WireGuardInterfaceName)
|
link, err := createOrGetLink(WireGuardInterfaceName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("create or get WireGuard link %q: %v", WireGuardInterfaceName, err)
|
return nil, fmt.Errorf("create or get WireGuard link %q: %v", WireGuardInterfaceName, err)
|
||||||
}
|
}
|
||||||
return &WireGuardNetwork{
|
return &WireGuardNetwork{link: link}, nil
|
||||||
link: link,
|
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// createOrGetLink creates a new WireGuard link with the given name if it doesn't already exist, otherwise it returns the existing link.
|
// createOrGetLink creates a new WireGuard link with the given name if it doesn't already exist, otherwise it returns the existing link.
|
||||||
@@ -74,39 +73,9 @@ func (n *WireGuardNetwork) Configure(config Config) error {
|
|||||||
n.mu.Lock()
|
n.mu.Lock()
|
||||||
defer n.mu.Unlock()
|
defer n.mu.Unlock()
|
||||||
|
|
||||||
// Reconstruct the list of peers, ensuring that the last endpoint change time is preserved for any existing peers.
|
if err := n.configureDevice(config); err != nil {
|
||||||
existingPeersByPublicKey := map[string]peer{}
|
|
||||||
for _, p := range n.peers {
|
|
||||||
existingPeersByPublicKey[p.config.PublicKey.String()] = p
|
|
||||||
}
|
|
||||||
n.peers = make([]peer, len(config.Peers))
|
|
||||||
for i, peerConfig := range config.Peers {
|
|
||||||
n.peers[i] = peer{
|
|
||||||
config: peerConfig,
|
|
||||||
}
|
|
||||||
existingPeer, ok := existingPeersByPublicKey[peerConfig.PublicKey.String()]
|
|
||||||
if ok && existingPeer.config.Endpoint == peerConfig.Endpoint {
|
|
||||||
n.peers[i].lastEndpointChangeTime = existingPeer.lastEndpointChangeTime
|
|
||||||
} else {
|
|
||||||
n.peers[i].lastEndpointChangeTime = time.Now()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
wg, err := wgctrl.New()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("create WireGuard client: %w", err)
|
|
||||||
}
|
|
||||||
//goland:noinspection GoUnhandledErrorResult
|
|
||||||
defer wg.Close()
|
|
||||||
|
|
||||||
wgConfig, err := config.toDeviceConfig()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
// Apply the new configuration to the WireGuard device.
|
|
||||||
if err = wg.ConfigureDevice(n.link.Attrs().Name, wgConfig); err != nil {
|
|
||||||
return fmt.Errorf("configure WireGuard device %q: %w", n.link.Attrs().Name, err)
|
|
||||||
}
|
|
||||||
slog.Info("Configured WireGuard interface.", "name", n.link.Attrs().Name)
|
slog.Info("Configured WireGuard interface.", "name", n.link.Attrs().Name)
|
||||||
|
|
||||||
machinePrefix := netip.PrefixFrom(MachineIP(config.Subnet), config.Subnet.Bits())
|
machinePrefix := netip.PrefixFrom(MachineIP(config.Subnet), config.Subnet.Bits())
|
||||||
@@ -141,6 +110,65 @@ func (n *WireGuardNetwork) Configure(config Config) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (n *WireGuardNetwork) configureDevice(config Config) error {
|
||||||
|
wg, err := wgctrl.New()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("create WireGuard client: %w", err)
|
||||||
|
}
|
||||||
|
defer wg.Close()
|
||||||
|
|
||||||
|
// Get the current WireGuard peers from the device which are required to reconstruct the local peers structs
|
||||||
|
// and build a new config for the device.
|
||||||
|
dev, err := wg.Device(n.link.Attrs().Name)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("get WireGuard device %q: %w", n.link.Attrs().Name, err)
|
||||||
|
}
|
||||||
|
if n.peers == nil {
|
||||||
|
// This is the first time we configure this instance of the network. If the device has been configured earlier
|
||||||
|
// and the daemon restarted, attempt to reconstruct the peers from the device to avoid unnecessary endpoint
|
||||||
|
// rotations and connection disruptions.
|
||||||
|
n.peers = make(map[string]*peer, len(config.Peers))
|
||||||
|
wgPeers := make(map[string]*wgtypes.Peer, len(dev.Peers))
|
||||||
|
for i, _ := range dev.Peers {
|
||||||
|
wgPeers[secret.Secret(dev.Peers[i].PublicKey[:]).String()] = &dev.Peers[i]
|
||||||
|
}
|
||||||
|
for _, pc := range config.Peers {
|
||||||
|
wgPeer := wgPeers[pc.PublicKey.String()]
|
||||||
|
n.peers[pc.PublicKey.String()] = newPeer(pc, wgPeer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create or update peer structs based on the config.
|
||||||
|
newPeersSet := make(map[string]struct{}, len(config.Peers))
|
||||||
|
for _, pc := range config.Peers {
|
||||||
|
if p, ok := n.peers[pc.PublicKey.String()]; ok {
|
||||||
|
p.updateConfig(pc)
|
||||||
|
} else {
|
||||||
|
// WireGuard peers should not be configured out of band so the current WG peer is provided only
|
||||||
|
// for the first time configuration above.
|
||||||
|
n.peers[pc.PublicKey.String()] = newPeer(pc, nil)
|
||||||
|
}
|
||||||
|
newPeersSet[pc.PublicKey.String()] = struct{}{}
|
||||||
|
}
|
||||||
|
// Delete peers that are no longer in the config.
|
||||||
|
for k := range n.peers {
|
||||||
|
if _, ok := newPeersSet[k]; !ok {
|
||||||
|
delete(n.peers, k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
wgConfig, err := config.toDeviceConfig(dev.Peers)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Apply the new configuration to the WireGuard device.
|
||||||
|
if err = wg.ConfigureDevice(n.link.Attrs().Name, wgConfig); err != nil {
|
||||||
|
return fmt.Errorf("configure WireGuard device %q: %w", n.link.Attrs().Name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// updateAddresses assigns addresses to the WireGuard interface and removes old ones.
|
// updateAddresses assigns addresses to the WireGuard interface and removes old ones.
|
||||||
// It also removes any other addresses that have been added out of band.
|
// It also removes any other addresses that have been added out of band.
|
||||||
func (n *WireGuardNetwork) updateAddresses(addrs []netip.Prefix) error {
|
func (n *WireGuardNetwork) updateAddresses(addrs []netip.Prefix) error {
|
||||||
@@ -178,7 +206,7 @@ func (n *WireGuardNetwork) updatePeerRoutes() error {
|
|||||||
// Build a set of compacted IP ranges for all peers.
|
// Build a set of compacted IP ranges for all peers.
|
||||||
var ipsetBuilder netipx.IPSetBuilder
|
var ipsetBuilder netipx.IPSetBuilder
|
||||||
for _, p := range n.peers {
|
for _, p := range n.peers {
|
||||||
prefixes, err := p.prefixes()
|
prefixes, err := p.config.prefixes()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("get peer addresses: %w", err)
|
return fmt.Errorf("get peer addresses: %w", err)
|
||||||
}
|
}
|
||||||
@@ -200,13 +228,16 @@ func (n *WireGuardNetwork) updatePeerRoutes() error {
|
|||||||
Scope: netlink.SCOPE_LINK,
|
Scope: netlink.SCOPE_LINK,
|
||||||
Dst: &dst,
|
Dst: &dst,
|
||||||
},
|
},
|
||||||
); err != nil && !errors.Is(err, unix.EEXIST) {
|
); err != nil {
|
||||||
return fmt.Errorf("add route to WireGuard link %q: %w", n.link.Attrs().Name, err)
|
if !errors.Is(err, unix.EEXIST) {
|
||||||
|
return fmt.Errorf("add route to WireGuard link %q: %w", n.link.Attrs().Name, err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
slog.Debug(
|
||||||
|
"Added route to peer(s) via WireGuard interface.",
|
||||||
|
"name", n.link.Attrs().Name, "dst", prefix,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
slog.Debug(
|
|
||||||
"Added route to peer(s) via WireGuard interface.",
|
|
||||||
"name", n.link.Attrs().Name, "dst", prefix,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove old routes to IP ranges that are no longer in the configuration.
|
// Remove old routes to IP ranges that are no longer in the configuration.
|
||||||
@@ -235,18 +266,164 @@ func (n *WireGuardNetwork) updatePeerRoutes() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (n *WireGuardNetwork) Run(ctx context.Context) error {
|
func (n *WireGuardNetwork) Run(ctx context.Context) error {
|
||||||
<-ctx.Done()
|
wg, err := wgctrl.New()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("create WireGuard client: %w", err)
|
||||||
|
}
|
||||||
|
defer wg.Close()
|
||||||
|
|
||||||
|
ticker := time.NewTicker(1 * time.Second)
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ticker.C:
|
||||||
|
n.mu.Lock()
|
||||||
|
if err = n.updatePeersFromDevice(ctx); err != nil {
|
||||||
|
slog.Error("Failed to update peers status from WireGuard interface.",
|
||||||
|
"name", n.link.Attrs().Name, "err", err)
|
||||||
|
}
|
||||||
|
if err = n.changeWireGuardEndpoints(ctx); err != nil {
|
||||||
|
slog.Error("Failed to update peer endpoints on WireGuard interface.",
|
||||||
|
"name", n.link.Attrs().Name, "err", err)
|
||||||
|
}
|
||||||
|
n.mu.Unlock()
|
||||||
|
case <-ctx.Done():
|
||||||
|
for _, ch := range n.watchers {
|
||||||
|
close(ch)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WatchEndpoints returns a channel that receives endpoint change events for the WireGuard peers.
|
||||||
|
func (n *WireGuardNetwork) WatchEndpoints() <-chan EndpointChangeEvent {
|
||||||
|
ch := make(chan EndpointChangeEvent)
|
||||||
|
n.watchers = append(n.watchers, ch)
|
||||||
|
return ch
|
||||||
|
}
|
||||||
|
|
||||||
|
// updatePeersFromDevice updates the peers status from the WireGuard device peers.
|
||||||
|
// mu lock must be held before calling this method.
|
||||||
|
func (n *WireGuardNetwork) updatePeersFromDevice(ctx context.Context) error {
|
||||||
|
wg, err := wgctrl.New()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("create WireGuard client: %w", err)
|
||||||
|
}
|
||||||
|
defer wg.Close()
|
||||||
|
|
||||||
|
dev, err := wg.Device(n.link.Attrs().Name)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("get WireGuard device %q: %w", n.link.Attrs().Name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var events []EndpointChangeEvent
|
||||||
|
for _, wgPeer := range dev.Peers {
|
||||||
|
publicKey := secret.Secret(wgPeer.PublicKey[:])
|
||||||
|
if p, ok := n.peers[publicKey.String()]; ok {
|
||||||
|
endpointChanged := p.updateFromDevice(wgPeer)
|
||||||
|
if endpointChanged {
|
||||||
|
events = append(events, EndpointChangeEvent{
|
||||||
|
PublicKey: publicKey,
|
||||||
|
Endpoint: *p.config.Endpoint,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Assume that WG peers are not updated out of band so they should always be in sync with the config.
|
||||||
|
slog.Warn("Found WireGuard peer that is not in the configuration.", "public_key", publicKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(events) > 0 {
|
||||||
|
if err = n.notifyWatchers(ctx, events); err != nil {
|
||||||
|
// As of October 2024, the machine is the only watcher to persist changed endpoints in the machine
|
||||||
|
// state which can tolerate missed events. Therefore, we can ignore the error here.
|
||||||
|
slog.Error("Failed to notify watchers about a peer endpoint change.", "err", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p peer) prefixes() ([]netip.Prefix, error) {
|
// changeWireGuardEndpoints rotates the endpoints of the WireGuard peers with 'down' status
|
||||||
managePrefix, err := addrToSingleIPPrefix(p.config.ManagementIP)
|
// in an attempt to find a working one.
|
||||||
|
// mu lock must be held before calling this method.
|
||||||
|
func (n *WireGuardNetwork) changeWireGuardEndpoints(ctx context.Context) error {
|
||||||
|
var wgPeerConfigs []wgtypes.PeerConfig
|
||||||
|
var events []EndpointChangeEvent
|
||||||
|
for _, p := range n.peers {
|
||||||
|
newEndpoint, ok := p.shouldChangeEndpoint()
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
newConfig := p.config
|
||||||
|
newConfig.Endpoint = &newEndpoint
|
||||||
|
p.updateConfig(newConfig)
|
||||||
|
|
||||||
|
publicKey, err := wgtypes.NewKey(p.config.PublicKey)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("parse peer public key: %w", err)
|
||||||
|
}
|
||||||
|
wgPeerConfigs = append(wgPeerConfigs, wgtypes.PeerConfig{
|
||||||
|
PublicKey: publicKey,
|
||||||
|
UpdateOnly: true,
|
||||||
|
Endpoint: &net.UDPAddr{
|
||||||
|
IP: p.config.Endpoint.Addr().AsSlice(),
|
||||||
|
Port: int(p.config.Endpoint.Port()),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
events = append(events, EndpointChangeEvent{
|
||||||
|
PublicKey: p.config.PublicKey,
|
||||||
|
Endpoint: *p.config.Endpoint,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if len(wgPeerConfigs) == 0 {
|
||||||
|
// No changes to the endpoints.
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
wg, err := wgctrl.New()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("parse management IP: %w", err)
|
return fmt.Errorf("create WireGuard client: %w", err)
|
||||||
}
|
}
|
||||||
prefixes := []netip.Prefix{managePrefix}
|
defer wg.Close()
|
||||||
if p.config.Subnet != nil {
|
|
||||||
prefixes = append(prefixes, *p.config.Subnet)
|
wgConfigPatch := wgtypes.Config{
|
||||||
|
ReplacePeers: false,
|
||||||
|
Peers: wgPeerConfigs,
|
||||||
}
|
}
|
||||||
return prefixes, nil
|
// Apply the configuration patch to the WireGuard device.
|
||||||
|
if err = wg.ConfigureDevice(n.link.Attrs().Name, wgConfigPatch); err != nil {
|
||||||
|
return fmt.Errorf("configure WireGuard device %q with endpoint changes: %w", n.link.Attrs().Name, err)
|
||||||
|
}
|
||||||
|
for _, pc := range wgPeerConfigs {
|
||||||
|
publicKey := secret.Secret(pc.PublicKey[:])
|
||||||
|
slog.Info("Changed peer endpoint on WireGuard interface.",
|
||||||
|
"name", n.link.Attrs().Name, "public_key", publicKey, "endpoint", pc.Endpoint,
|
||||||
|
"status", n.peers[publicKey.String()].status)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = n.notifyWatchers(ctx, events); err != nil {
|
||||||
|
// As of October 2024, the machine is the only watcher to persist changed endpoints in the machine
|
||||||
|
// state which can tolerate missed events. Therefore, we can ignore the error here.
|
||||||
|
slog.Error("Failed to notify watchers about a peer endpoint change.", "err", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// notifyWatchers notifies the watchers about peer endpoint changes.
|
||||||
|
func (n *WireGuardNetwork) notifyWatchers(ctx context.Context, events []EndpointChangeEvent) error {
|
||||||
|
for _, ch := range n.watchers {
|
||||||
|
for _, e := range events {
|
||||||
|
select {
|
||||||
|
case ch <- e:
|
||||||
|
// Use a timeout to avoid blocking the network control loop.
|
||||||
|
case <-time.After(1 * time.Second):
|
||||||
|
return errors.New("timeout 1 second")
|
||||||
|
case <-ctx.Done():
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
//go:build darwin
|
|
||||||
|
|
||||||
package machine
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
)
|
|
||||||
|
|
||||||
// setupDockerNetwork is a stub for darwin.
|
|
||||||
func (nc *networkController) setupDockerNetwork(ctx context.Context) error {
|
|
||||||
return fmt.Errorf("not supported on darwin")
|
|
||||||
}
|
|
||||||
@@ -1,108 +0,0 @@
|
|||||||
//go:build linux
|
|
||||||
|
|
||||||
package machine
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
dnetwork "github.com/docker/docker/api/types/network"
|
|
||||||
"github.com/docker/docker/client"
|
|
||||||
"github.com/docker/docker/libnetwork/iptables"
|
|
||||||
"log/slog"
|
|
||||||
"time"
|
|
||||||
"uncloud/internal/machine/network"
|
|
||||||
)
|
|
||||||
|
|
||||||
// setupDockerNetwork creates the Docker bridge network DockerNetworkName with the machine subnet and configures
|
|
||||||
// iptables to allow WireGuard network to access containers.
|
|
||||||
func (nc *networkController) setupDockerNetwork(ctx context.Context) error {
|
|
||||||
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("init Docker client: %w", err)
|
|
||||||
}
|
|
||||||
defer cli.Close()
|
|
||||||
|
|
||||||
// Wait for the Docker daemon to start and be ready by sending a ping request in a loop.
|
|
||||||
ticker := time.NewTicker(1 * time.Second)
|
|
||||||
defer ticker.Stop()
|
|
||||||
ready, waitingLogged := false, false
|
|
||||||
for !ready {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return ctx.Err()
|
|
||||||
case <-ticker.C:
|
|
||||||
_, err = cli.Ping(ctx)
|
|
||||||
if err == nil {
|
|
||||||
ready = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if !client.IsErrConnectionFailed(err) {
|
|
||||||
return fmt.Errorf("connect to Docker daemon: %w", err)
|
|
||||||
}
|
|
||||||
if !waitingLogged {
|
|
||||||
slog.Info("Waiting for Docker daemon to start and be ready to setup Docker network.")
|
|
||||||
waitingLogged = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure the Docker network 'uncloud' is created with the correct subnet.
|
|
||||||
needsCreation := false
|
|
||||||
nw, err := cli.NetworkInspect(ctx, DockerNetworkName, dnetwork.InspectOptions{})
|
|
||||||
if err != nil {
|
|
||||||
if !client.IsErrNotFound(err) {
|
|
||||||
return fmt.Errorf("inspect Docker network %q: %w", DockerNetworkName, err)
|
|
||||||
}
|
|
||||||
needsCreation = true
|
|
||||||
} else if nw.IPAM.Config[0].Subnet != nc.state.Network.Subnet.String() {
|
|
||||||
// Remove the Docker network if the subnet is different.
|
|
||||||
// It could be a leftover from a previous incomplete cleanup.
|
|
||||||
slog.Info(
|
|
||||||
"Removing Docker network with old subnet.", "name", DockerNetworkName, "subnet", nw.IPAM.Config[0].Subnet,
|
|
||||||
)
|
|
||||||
if err = cli.NetworkRemove(ctx, DockerNetworkName); 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", DockerNetworkName, err)
|
|
||||||
}
|
|
||||||
needsCreation = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if needsCreation {
|
|
||||||
if _, err = cli.NetworkCreate(
|
|
||||||
ctx, DockerNetworkName, dnetwork.CreateOptions{
|
|
||||||
Driver: "bridge",
|
|
||||||
Scope: "local",
|
|
||||||
IPAM: &dnetwork.IPAM{
|
|
||||||
Config: []dnetwork.IPAMConfig{
|
|
||||||
{
|
|
||||||
Subnet: nc.state.Network.Subnet.String(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
); err != nil {
|
|
||||||
return fmt.Errorf("create Docker network %q: %w", DockerNetworkName, err)
|
|
||||||
}
|
|
||||||
slog.Info("Docker network created.", "name", DockerNetworkName, "subnet", nc.state.Network.Subnet.String())
|
|
||||||
|
|
||||||
if nw, err = cli.NetworkInspect(ctx, DockerNetworkName, dnetwork.InspectOptions{}); err != nil {
|
|
||||||
return fmt.Errorf("inspect Docker network %q: %w", DockerNetworkName, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Configure iptables to allow WireGuard network to access containers. The Docker daemon should have already
|
|
||||||
// created the DOCKER-USER chain at this point.
|
|
||||||
// TODO: check if this works when firewalld used instead of raw iptables. The Docker daemon has a different
|
|
||||||
// code path for firewalld.
|
|
||||||
|
|
||||||
// Bridge name doesn't seem to be documented but this is the source code where it is generated:
|
|
||||||
// https://github.com/moby/moby/blob/v27.2.1/libnetwork/drivers/bridge/bridge_linux.go#L664
|
|
||||||
bridgeName := "br-" + nw.ID[:12]
|
|
||||||
ipt := iptables.GetIptable(iptables.IPv4)
|
|
||||||
rule := []string{"--in-interface", network.WireGuardInterfaceName, "--out-interface", bridgeName, "-j", "ACCEPT"}
|
|
||||||
if err = ipt.ProgramRule(iptables.Filter, DockerUserChain, iptables.Insert, rule); err != nil {
|
|
||||||
return fmt.Errorf("insert iptables rule: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -3,11 +3,9 @@ package machine
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/netip"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sync"
|
"sync"
|
||||||
"uncloud/internal/machine/cluster"
|
|
||||||
"uncloud/internal/machine/network"
|
"uncloud/internal/machine/network"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -73,7 +71,7 @@ func (c *State) Save() error {
|
|||||||
return fmt.Errorf("state path not set")
|
return fmt.Errorf("state path not set")
|
||||||
}
|
}
|
||||||
dir, _ := filepath.Split(c.path)
|
dir, _ := filepath.Split(c.path)
|
||||||
if err := os.MkdirAll(dir, 0700); err != nil {
|
if err := os.MkdirAll(dir, 0711); err != nil {
|
||||||
return fmt.Errorf("create state directory %q: %w", dir, err)
|
return fmt.Errorf("create state directory %q: %w", dir, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,37 +81,3 @@ func (c *State) Save() error {
|
|||||||
}
|
}
|
||||||
return os.WriteFile(c.path, data, 0600)
|
return os.WriteFile(c.path, data, 0600)
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewBootstrapConfig returns a new machine configuration that should be applied to the first machine in a cluster.
|
|
||||||
func NewBootstrapConfig(name string, subnet netip.Prefix, peers ...network.PeerConfig) (*State, error) {
|
|
||||||
mid, err := cluster.NewMachineID()
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("generate machine ID: %w", err)
|
|
||||||
}
|
|
||||||
if name == "" {
|
|
||||||
name, err = cluster.NewRandomMachineName()
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("generate machine name: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
privKey, pubKey, err := network.NewMachineKeys()
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("generate machine keys: %w", err)
|
|
||||||
}
|
|
||||||
if subnet == (netip.Prefix{}) {
|
|
||||||
// Use the first /24 subnet in the default network.
|
|
||||||
subnet = netip.PrefixFrom(network.DefaultNetwork.Addr(), network.DefaultSubnetBits)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &State{
|
|
||||||
ID: mid,
|
|
||||||
Name: name,
|
|
||||||
Network: &network.Config{
|
|
||||||
Subnet: subnet,
|
|
||||||
ManagementIP: network.ManagementIP(pubKey),
|
|
||||||
PrivateKey: privKey,
|
|
||||||
PublicKey: pubKey,
|
|
||||||
Peers: peers,
|
|
||||||
},
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,220 @@
|
|||||||
|
package store
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
sq "github.com/Masterminds/squirrel"
|
||||||
|
"log/slog"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
"uncloud/internal/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// SyncStatusSynced indicates that a container record is synchronised with the Docker daemon. The record may
|
||||||
|
// become outdated even when the status is "synced" if the machine crashes or a network partition occurs.
|
||||||
|
// The cluster membership state of the machine should also be checked to determine if the record can be trusted.
|
||||||
|
SyncStatusSynced = "synced"
|
||||||
|
// SyncStatusOutdated indicates that a container record may be outdated, for example, due to being unable
|
||||||
|
// to retrieve the container's state from the Docker daemon or when the machine is being stopped or restarted.
|
||||||
|
SyncStatusOutdated = "outdated"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ContainerRecord struct {
|
||||||
|
Container *api.Container
|
||||||
|
MachineID string
|
||||||
|
SyncStatus string
|
||||||
|
UpdatedAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListOptions struct {
|
||||||
|
// MachineIDs filters containers by the machine IDs they are running on.
|
||||||
|
MachineIDs []string
|
||||||
|
ServiceIDOrName ServiceIDOrNameOptions
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServiceIDOrNameOptions filters containers by the service ID or name they are part of. If both ID and Name are
|
||||||
|
// provided, they are combined with an OR operator.
|
||||||
|
type ServiceIDOrNameOptions struct {
|
||||||
|
ID string
|
||||||
|
Name string
|
||||||
|
}
|
||||||
|
|
||||||
|
type DeleteOptions struct {
|
||||||
|
IDs []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("marshal container: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert or update the container record if the container or machine ID has changed.
|
||||||
|
res, err := s.corro.ExecContext(ctx, `
|
||||||
|
INSERT INTO containers (id, container, machine_id, sync_status, updated_at)
|
||||||
|
VALUES (?, ?, ?, ?, datetime('now'))
|
||||||
|
ON CONFLICT (id) DO UPDATE SET container = excluded.container,
|
||||||
|
machine_id = excluded.machine_id,
|
||||||
|
sync_status = excluded.sync_status,
|
||||||
|
updated_at = excluded.updated_at
|
||||||
|
WHERE containers.container != excluded.container
|
||||||
|
OR containers.machine_id != excluded.machine_id`,
|
||||||
|
c.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)
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
q := sq.Select("container", "machine_id", "sync_status", "updated_at").From("containers").
|
||||||
|
Where(sq.Eq{"sync_status": SyncStatusSynced})
|
||||||
|
|
||||||
|
if len(opts.MachineIDs) > 0 {
|
||||||
|
q = q.Where(sq.Eq{"machine_id": opts.MachineIDs})
|
||||||
|
}
|
||||||
|
|
||||||
|
if opts.ServiceIDOrName.ID != "" || opts.ServiceIDOrName.Name != "" {
|
||||||
|
var conditions []sq.Sqlizer
|
||||||
|
if opts.ServiceIDOrName.ID != "" {
|
||||||
|
conditions = append(conditions, sq.Eq{"service_id": opts.ServiceIDOrName.ID})
|
||||||
|
}
|
||||||
|
if opts.ServiceIDOrName.Name != "" {
|
||||||
|
conditions = append(conditions, sq.Eq{"service_name": opts.ServiceIDOrName.Name})
|
||||||
|
}
|
||||||
|
q = q.Where(sq.Or(conditions))
|
||||||
|
}
|
||||||
|
|
||||||
|
query, args, err := q.ToSql()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("build query: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
rows, err := s.corro.QueryContext(ctx, query, args...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("select query: %w", err)
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var containers []*ContainerRecord
|
||||||
|
var cJSON, machineID, syncStatus, updatedAtStr string
|
||||||
|
var updatedAt time.Time
|
||||||
|
|
||||||
|
for rows.Next() {
|
||||||
|
if err = rows.Scan(&cJSON, &machineID, &syncStatus, &updatedAtStr); err != nil {
|
||||||
|
return nil, fmt.Errorf("scan container record: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var c api.Container
|
||||||
|
if err = json.Unmarshal([]byte(cJSON), &c); err != nil {
|
||||||
|
return nil, fmt.Errorf("unmarshal container: %w", err)
|
||||||
|
}
|
||||||
|
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,
|
||||||
|
MachineID: machineID,
|
||||||
|
SyncStatus: syncStatus,
|
||||||
|
UpdatedAt: updatedAt,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return containers, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteContainers deletes container records from the store database that match the given options.
|
||||||
|
func (s *Store) DeleteContainers(ctx context.Context, opts DeleteOptions) error {
|
||||||
|
query := "DELETE FROM containers"
|
||||||
|
var args []any
|
||||||
|
|
||||||
|
if len(opts.IDs) > 0 {
|
||||||
|
query += " WHERE id IN (?" + strings.Repeat(", ?", len(opts.IDs)-1) + ")"
|
||||||
|
args = make([]any, len(opts.IDs))
|
||||||
|
for i, id := range opts.IDs {
|
||||||
|
args[i] = id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := s.corro.ExecContext(ctx, query, args...)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("delete query: %w", err)
|
||||||
|
}
|
||||||
|
if res.RowsAffected > 0 {
|
||||||
|
slog.Debug("Container records deleted from store DB.", "ids", opts.IDs, "count", res.RowsAffected)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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) {
|
||||||
|
// 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})
|
||||||
|
query, args, err := q.ToSql()
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("build query: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
sub, err := s.corro.SubscribeContext(ctx, query, args, false)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var containers []*ContainerRecord
|
||||||
|
var cJSON, updatedAtStr string
|
||||||
|
|
||||||
|
rows := sub.Rows()
|
||||||
|
for rows.Next() {
|
||||||
|
var cr ContainerRecord
|
||||||
|
if err = rows.Scan(&cJSON, &cr.MachineID, &cr.SyncStatus, &updatedAtStr); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = json.Unmarshal([]byte(cJSON), &cr.Container); err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("unmarshal container: %w", err)
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
events, err := sub.Changes()
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("get subscription changes: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
changes := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
defer close(changes)
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
case _, ok := <-events:
|
||||||
|
if !ok {
|
||||||
|
// events channel has been closed.
|
||||||
|
if sub.Err() != nil {
|
||||||
|
slog.Error("Containers subscription failed.", "id", sub.ID(), "err", sub.Err())
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Just signal that there is a change in the containers list.
|
||||||
|
changes <- struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return containers, changes, nil
|
||||||
|
}
|
||||||
Binary file not shown.
@@ -0,0 +1,37 @@
|
|||||||
|
-- cluster table stores the key-value pairs of the cluster configuration.
|
||||||
|
CREATE TABLE cluster
|
||||||
|
(
|
||||||
|
key TEXT NOT NULL PRIMARY KEY,
|
||||||
|
value ANY
|
||||||
|
);
|
||||||
|
|
||||||
|
-- machines table stores the basic information of the machines in the cluster.
|
||||||
|
CREATE TABLE machines
|
||||||
|
(
|
||||||
|
id TEXT NOT NULL PRIMARY KEY,
|
||||||
|
name TEXT AS (json_extract(info, '$.name')),
|
||||||
|
-- info is a JSON-serialized MachineInfo protobuf message.
|
||||||
|
info TEXT NOT NULL DEFAULT '{}' CHECK (json_valid(info))
|
||||||
|
-- TODO: add created_at and updated_at fields to track machine age and last update time.
|
||||||
|
);
|
||||||
|
|
||||||
|
-- containers table stores the Uncloud-managed Docker containers created in the cluster.
|
||||||
|
CREATE TABLE containers
|
||||||
|
(
|
||||||
|
id TEXT NOT NULL PRIMARY KEY,
|
||||||
|
-- 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"')),
|
||||||
|
-- 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.
|
||||||
|
updated_at TIMESTAMP NOT NULL DEFAULT '1970-01-01 00:00:00'
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_machines_name ON machines (name);
|
||||||
|
|
||||||
|
CREATE INDEX idx_containers_machine_id ON containers (machine_id);
|
||||||
|
CREATE INDEX idx_containers_service_id ON containers (service_id);
|
||||||
|
CREATE INDEX idx_containers_service_name ON containers (service_name);
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
package store
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
_ "embed"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"google.golang.org/protobuf/encoding/protojson"
|
||||||
|
"log/slog"
|
||||||
|
"uncloud/internal/corrosion"
|
||||||
|
"uncloud/internal/machine/api/pb"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
//go:embed schema.sql
|
||||||
|
Schema string
|
||||||
|
|
||||||
|
ErrKeyNotFound = errors.New("key not found")
|
||||||
|
)
|
||||||
|
|
||||||
|
// Store is a cluster store backed by a distributed Corrosion database.
|
||||||
|
type Store struct {
|
||||||
|
corro *corrosion.APIClient
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(corro *corrosion.APIClient) *Store {
|
||||||
|
return &Store{corro: corro}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) Get(ctx context.Context, key string, value any) error {
|
||||||
|
rows, err := s.corro.QueryContext(ctx, "SELECT value FROM cluster WHERE key = ?", key)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !rows.Next() {
|
||||||
|
if rows.Err() != nil {
|
||||||
|
return rows.Err()
|
||||||
|
}
|
||||||
|
return ErrKeyNotFound
|
||||||
|
}
|
||||||
|
if err = rows.Scan(value); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) Put(ctx context.Context, key string, value any) error {
|
||||||
|
_, err := s.corro.ExecContext(ctx, "INSERT OR REPLACE INTO cluster (key, value) VALUES (?, ?)", key, value)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) CreateMachine(ctx context.Context, m *pb.MachineInfo) error {
|
||||||
|
mJSON, err := protojson.Marshal(m)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("marshal machine info: %w", err)
|
||||||
|
}
|
||||||
|
_, err = s.corro.ExecContext(ctx, "INSERT INTO machines (id, info) VALUES (?, ?)", m.Id, string(mJSON))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("insert query: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) ListMachines(ctx context.Context) ([]*pb.MachineInfo, error) {
|
||||||
|
rows, err := s.corro.QueryContext(ctx, "SELECT info FROM machines ORDER BY name")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var machines []*pb.MachineInfo
|
||||||
|
for rows.Next() {
|
||||||
|
var mJSON string
|
||||||
|
if err = rows.Scan(&mJSON); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var m pb.MachineInfo
|
||||||
|
if err = protojson.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
|
||||||
|
}
|
||||||
|
machines = append(machines, &m)
|
||||||
|
}
|
||||||
|
return machines, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SubscribeMachines returns a list of machines and a channel that signals changes to the list. The channel doesn't
|
||||||
|
// receive any values, it just signals when a machine has been added, updated, or deleted in the database.
|
||||||
|
func (s *Store) SubscribeMachines(ctx context.Context) ([]*pb.MachineInfo, <-chan struct{}, error) {
|
||||||
|
sub, err := s.corro.SubscribeContext(ctx, "SELECT info FROM machines ORDER BY name", nil, false)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
rows := sub.Rows()
|
||||||
|
var machines []*pb.MachineInfo
|
||||||
|
for rows.Next() {
|
||||||
|
var mJSON string
|
||||||
|
if err = rows.Scan(&mJSON); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
var m pb.MachineInfo
|
||||||
|
if err = protojson.Unmarshal([]byte(mJSON), &m); err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("unmarshal machine info: %w", err)
|
||||||
|
}
|
||||||
|
machines = append(machines, &m)
|
||||||
|
}
|
||||||
|
events, err := sub.Changes()
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("get subscription changes: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
changes := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
defer close(changes)
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
case _, ok := <-events:
|
||||||
|
if !ok {
|
||||||
|
// events channel has been closed.
|
||||||
|
if sub.Err() != nil {
|
||||||
|
slog.Error("Machines subscription failed.", "id", sub.ID(), "err", sub.Err())
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Just signal that there is a change in the machines list.
|
||||||
|
changes <- struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return machines, changes, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package secret
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"fmt"
|
||||||
|
"math/big"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewID generates a unique 128-bit random ID as a hex-encoded string.
|
||||||
|
func NewID() (string, error) {
|
||||||
|
if s, err := New(16); err != nil {
|
||||||
|
return "", err
|
||||||
|
} else {
|
||||||
|
return s.String(), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RandomAlphaNumeric generates a random string of the specified length using the characters [a-z0-9].
|
||||||
|
func RandomAlphaNumeric(length int) (string, error) {
|
||||||
|
const charSet = "abcdefghijklmnopqrstuvwxyz0123456789"
|
||||||
|
s := make([]byte, length)
|
||||||
|
for i := range s {
|
||||||
|
randIdx, err := rand.Int(rand.Reader, big.NewInt(int64(len(charSet))))
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("get random number: %w", err)
|
||||||
|
}
|
||||||
|
s[i] = charSet[randIdx.Int64()]
|
||||||
|
}
|
||||||
|
return string(s), nil
|
||||||
|
}
|
||||||
@@ -54,11 +54,3 @@ func New(len int) (Secret, error) {
|
|||||||
}
|
}
|
||||||
return s, nil
|
return s, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewID() (string, error) {
|
|
||||||
if s, err := New(16); err != nil {
|
|
||||||
return "", err
|
|
||||||
} else {
|
|
||||||
return s.String(), nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,298 @@
|
|||||||
|
package ucind
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"github.com/cenkalti/backoff/v4"
|
||||||
|
"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"
|
||||||
|
"google.golang.org/protobuf/types/known/emptypb"
|
||||||
|
"net/netip"
|
||||||
|
"time"
|
||||||
|
"uncloud/internal/machine"
|
||||||
|
"uncloud/internal/machine/api/pb"
|
||||||
|
"uncloud/internal/machine/cluster"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
ClusterNameLabel = "ucind.cluster.name"
|
||||||
|
ManagedLabel = "ucind.managed"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Cluster struct {
|
||||||
|
Name string
|
||||||
|
Machines []Machine
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateClusterOptions struct {
|
||||||
|
Machines int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Provisioner) CreateCluster(ctx context.Context, name string, opts CreateClusterOptions) (Cluster, error) {
|
||||||
|
var c Cluster
|
||||||
|
|
||||||
|
_, err := p.InspectCluster(ctx, name)
|
||||||
|
if err == nil {
|
||||||
|
return c, fmt.Errorf("cluster with name '%s' already exists", name)
|
||||||
|
}
|
||||||
|
if !errors.Is(err, ErrNotFound) {
|
||||||
|
return c, fmt.Errorf("inspect cluster '%s': %w", name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
netOpts := network.CreateOptions{
|
||||||
|
Labels: map[string]string{
|
||||||
|
ClusterNameLabel: name,
|
||||||
|
ManagedLabel: "",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
// Create a Docker network with the same as the cluster name.
|
||||||
|
if _, err = p.dockerCli.NetworkCreate(ctx, name, netOpts); err != nil {
|
||||||
|
return c, fmt.Errorf("create Docker network '%s': %w", name, err)
|
||||||
|
}
|
||||||
|
c.Name = name
|
||||||
|
|
||||||
|
// Create machines (containers) in the created cluster network.
|
||||||
|
for i := 1; i < opts.Machines+1; i++ {
|
||||||
|
mopts := CreateMachineOptions{
|
||||||
|
Name: fmt.Sprintf("machine-%d", i),
|
||||||
|
}
|
||||||
|
m, err := p.CreateMachine(ctx, name, mopts)
|
||||||
|
if err != nil {
|
||||||
|
return c, fmt.Errorf("create machine '%s': %w", mopts.Name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Machines = append(c.Machines, m)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(c.Machines) == 0 {
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = p.initCluster(ctx, c.Machines); err != nil {
|
||||||
|
return c, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.configUpdater != nil {
|
||||||
|
if err = p.configUpdater.AddCluster(c); err != nil {
|
||||||
|
return c, fmt.Errorf("add cluster to Uncloud config: %w", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("Cluster '%s' added to Uncloud config.\n", c.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
return fmt.Errorf("wait for machine %q to be ready: %w", initMachine.Name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
initClient, err := initMachine.Connect(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("create machine client over TCP '%s': %w", initMachine.APIAddress, err)
|
||||||
|
}
|
||||||
|
defer initClient.Close()
|
||||||
|
|
||||||
|
req := &pb.InitClusterRequest{
|
||||||
|
MachineName: initMachine.Name,
|
||||||
|
Network: pb.NewIPPrefix(cluster.DefaultNetwork),
|
||||||
|
}
|
||||||
|
initResp, err := initClient.InitCluster(ctx, req)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("init cluster: %w", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("Cluster %q initialised with machine %q\n", initMachine.ClusterName, initResp.Machine.Name)
|
||||||
|
|
||||||
|
// Join the rest of the machines to the cluster.
|
||||||
|
for _, m := range machines[1:] {
|
||||||
|
if err = p.WaitMachineReady(ctx, m, 5*time.Second); err != nil {
|
||||||
|
return fmt.Errorf("wait for machine %q to be ready: %w", m.Name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cli, err := m.Connect(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("create machine client over TCP '%s': %w", m.APIAddress, err)
|
||||||
|
}
|
||||||
|
//goland:noinspection GoDeferInLoop
|
||||||
|
defer cli.Close()
|
||||||
|
|
||||||
|
tokenResp, err := cli.Token(ctx, &emptypb.Empty{})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("get machine token: %w", err)
|
||||||
|
}
|
||||||
|
token, err := machine.ParseToken(tokenResp.Token)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("parse machine token: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register the machine in the cluster using its public key and endpoints from the token.
|
||||||
|
endpoints := make([]*pb.IPPort, len(token.Endpoints))
|
||||||
|
for i, addrPort := range token.Endpoints {
|
||||||
|
endpoints[i] = pb.NewIPPort(addrPort)
|
||||||
|
}
|
||||||
|
addReq := &pb.AddMachineRequest{
|
||||||
|
Name: m.Name,
|
||||||
|
Network: &pb.NetworkConfig{
|
||||||
|
Endpoints: endpoints,
|
||||||
|
PublicKey: token.PublicKey,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
addResp, err := initClient.AddMachine(ctx, addReq)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("add machine to cluster: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configure the machine to join the cluster.
|
||||||
|
joinReq := &pb.JoinClusterRequest{
|
||||||
|
Machine: addResp.Machine,
|
||||||
|
OtherMachines: []*pb.MachineInfo{initResp.Machine},
|
||||||
|
}
|
||||||
|
if _, err = cli.JoinCluster(ctx, joinReq); err != nil {
|
||||||
|
return fmt.Errorf("join cluster: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Machine %q added to cluster\n", addResp.Machine.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Provisioner) InspectCluster(ctx context.Context, name string) (Cluster, error) {
|
||||||
|
var c Cluster
|
||||||
|
|
||||||
|
// Docker network name is the same as the cluster name.
|
||||||
|
net, err := p.dockerCli.NetworkInspect(ctx, name, network.InspectOptions{})
|
||||||
|
if err != nil {
|
||||||
|
if dockerclient.IsErrNotFound(err) {
|
||||||
|
return c, ErrNotFound
|
||||||
|
}
|
||||||
|
return c, fmt.Errorf("inspect Docker network '%s': %w", name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := net.Labels[ManagedLabel]; !ok {
|
||||||
|
// The network with the cluster name exists, but it's not managed by ucind.
|
||||||
|
return c, ErrNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Name = name
|
||||||
|
// Include all containers (machines) with the cluster name label.
|
||||||
|
opts := container.ListOptions{
|
||||||
|
All: true,
|
||||||
|
Filters: filters.NewArgs(
|
||||||
|
filters.Arg("label", ClusterNameLabel+"="+name),
|
||||||
|
filters.Arg("label", ManagedLabel),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
containers, err := p.dockerCli.ContainerList(ctx, opts)
|
||||||
|
if err != nil {
|
||||||
|
return c, fmt.Errorf("list Docker containers with cluster name '%s': %w", name, err)
|
||||||
|
}
|
||||||
|
for _, ctr := range containers {
|
||||||
|
m := Machine{
|
||||||
|
ClusterName: name,
|
||||||
|
ContainerName: ctr.Names[0],
|
||||||
|
Name: ctr.Labels[MachineNameLabel],
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, port := range ctr.Ports {
|
||||||
|
if port.PrivatePort == UncloudAPIPort {
|
||||||
|
if ip, err := netip.ParseAddr(port.IP); err == nil {
|
||||||
|
m.APIAddress = netip.AddrPortFrom(ip, port.PublicPort)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !m.APIAddress.IsValid() {
|
||||||
|
return c, fmt.Errorf("API binding not found for container '%s'", m.ContainerName)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Machines = append(c.Machines, m)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// WaitClusterReady waits for all machines in the cluster to be ready and UP.
|
||||||
|
func (p *Provisioner) WaitClusterReady(ctx context.Context, c Cluster, timeout time.Duration) error {
|
||||||
|
firstMachine := c.Machines[0]
|
||||||
|
if err := p.WaitMachineReady(ctx, firstMachine, timeout); err != nil {
|
||||||
|
return fmt.Errorf("wait for machine '%s' to be ready: %w", firstMachine.Name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cli, err := firstMachine.Connect(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("connect to machine over TCP '%s': %w", firstMachine.APIAddress, err)
|
||||||
|
}
|
||||||
|
defer cli.Close()
|
||||||
|
|
||||||
|
boff := backoff.WithContext(backoff.NewExponentialBackOff(
|
||||||
|
backoff.WithInitialInterval(100*time.Millisecond),
|
||||||
|
backoff.WithMaxInterval(1*time.Second),
|
||||||
|
backoff.WithMaxElapsedTime(timeout),
|
||||||
|
), ctx)
|
||||||
|
|
||||||
|
checkMachinesUp := func() error {
|
||||||
|
machines, err := cli.ListMachines(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("list machines: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(machines) != len(c.Machines) {
|
||||||
|
return fmt.Errorf("expected %d machines, got %d", len(c.Machines), len(machines))
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, m := range machines {
|
||||||
|
if m.State != pb.MachineMember_UP {
|
||||||
|
return fmt.Errorf("machine '%s' state is not UP: %s", m.Machine.Name, m.State)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return backoff.Retry(checkMachinesUp, boff)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Provisioner) RemoveCluster(ctx context.Context, name string) error {
|
||||||
|
if _, err := p.InspectCluster(ctx, name); err != nil {
|
||||||
|
if errors.Is(err, ErrNotFound) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove all containers (machines) with the cluster name label.
|
||||||
|
opts := container.ListOptions{
|
||||||
|
All: true,
|
||||||
|
Filters: filters.NewArgs(
|
||||||
|
filters.Arg("label", ClusterNameLabel+"="+name),
|
||||||
|
filters.Arg("label", ManagedLabel),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
containers, err := p.dockerCli.ContainerList(ctx, opts)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("list Docker containers with cluster name '%s': %w", name, err)
|
||||||
|
}
|
||||||
|
for _, c := range containers {
|
||||||
|
if err = p.dockerCli.ContainerRemove(ctx, c.ID, container.RemoveOptions{Force: true}); err != nil {
|
||||||
|
return fmt.Errorf("remove Docker container '%s': %w", c.ID, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = p.dockerCli.NetworkRemove(ctx, name); err != nil {
|
||||||
|
return fmt.Errorf("remove Docker network '%s': %w", name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.configUpdater != nil {
|
||||||
|
if err = p.configUpdater.RemoveCluster(name); err != nil {
|
||||||
|
return fmt.Errorf("remove cluster from Uncloud config: %w", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("Cluster '%s' removed from Uncloud config.\n", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
package ucind
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"uncloud/internal/cli/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ConfigUpdater struct {
|
||||||
|
path string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewConfigUpdater(path string) *ConfigUpdater {
|
||||||
|
return &ConfigUpdater{path: path}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *ConfigUpdater) AddCluster(c Cluster) error {
|
||||||
|
cfg, err := config.NewFromFile(u.path)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("read Uncloud config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := cfg.Clusters[c.Name]; ok {
|
||||||
|
return fmt.Errorf("cluster '%s' already exists", c.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
clusterCfg := &config.Cluster{
|
||||||
|
Name: c.Name,
|
||||||
|
Connections: make([]config.MachineConnection, len(c.Machines)),
|
||||||
|
}
|
||||||
|
for i, m := range c.Machines {
|
||||||
|
clusterCfg.Connections[i] = config.MachineConnection{
|
||||||
|
TCP: m.APIAddress,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.Clusters[c.Name] = clusterCfg
|
||||||
|
cfg.CurrentCluster = c.Name
|
||||||
|
|
||||||
|
if err = cfg.Save(); err != nil {
|
||||||
|
return fmt.Errorf("save config: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *ConfigUpdater) RemoveCluster(name string) error {
|
||||||
|
cfg, err := config.NewFromFile(u.path)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("read Uncloud config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := cfg.Clusters[name]; !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(cfg.Clusters, name)
|
||||||
|
|
||||||
|
if cfg.CurrentCluster == name {
|
||||||
|
cfg.CurrentCluster = ""
|
||||||
|
}
|
||||||
|
if _, ok := cfg.Clusters["default"]; ok {
|
||||||
|
cfg.CurrentCluster = "default"
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = cfg.Save(); err != nil {
|
||||||
|
return fmt.Errorf("save config: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,208 @@
|
|||||||
|
package ucind
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"github.com/cenkalti/backoff/v4"
|
||||||
|
"github.com/docker/docker/api/types/container"
|
||||||
|
"github.com/docker/docker/api/types/image"
|
||||||
|
dockerclient "github.com/docker/docker/client"
|
||||||
|
"github.com/docker/go-connections/nat"
|
||||||
|
"google.golang.org/protobuf/types/known/emptypb"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"net/netip"
|
||||||
|
"time"
|
||||||
|
"uncloud/internal/cli/client"
|
||||||
|
"uncloud/internal/cli/client/connector"
|
||||||
|
"uncloud/internal/secret"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
DefaultImage = "ghcr.io/psviderski/ucind:latest"
|
||||||
|
UncloudAPIPort = 51000
|
||||||
|
|
||||||
|
MachineNameLabel = "ucind.machine.name"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Machine struct {
|
||||||
|
ClusterName string
|
||||||
|
ContainerName string
|
||||||
|
Name string
|
||||||
|
APIAddress netip.AddrPort
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Machine) Connect(ctx context.Context) (*client.Client, error) {
|
||||||
|
return client.New(ctx, connector.NewTCPConnector(m.APIAddress))
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateMachineOptions struct {
|
||||||
|
Name string
|
||||||
|
Image string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Provisioner) CreateMachine(ctx context.Context, clusterName string, opts CreateMachineOptions) (Machine, error) {
|
||||||
|
var m Machine
|
||||||
|
|
||||||
|
machineName := opts.Name
|
||||||
|
if machineName == "" {
|
||||||
|
var err error
|
||||||
|
if machineName, err = randomMachineName(); err != nil {
|
||||||
|
return m, fmt.Errorf("generate random machine name: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
containerName := clusterName + "-" + machineName
|
||||||
|
|
||||||
|
img := DefaultImage
|
||||||
|
if opts.Image != "" {
|
||||||
|
img = opts.Image
|
||||||
|
}
|
||||||
|
|
||||||
|
apiPort := nat.Port(fmt.Sprintf("%d/tcp", UncloudAPIPort))
|
||||||
|
config := &container.Config{
|
||||||
|
Image: img,
|
||||||
|
Labels: map[string]string{
|
||||||
|
ClusterNameLabel: clusterName,
|
||||||
|
MachineNameLabel: machineName,
|
||||||
|
ManagedLabel: "",
|
||||||
|
},
|
||||||
|
ExposedPorts: nat.PortSet{
|
||||||
|
apiPort: struct{}{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
hostConfig := &container.HostConfig{
|
||||||
|
NetworkMode: container.NetworkMode(clusterName),
|
||||||
|
PortBindings: nat.PortMap{
|
||||||
|
apiPort: []nat.PortBinding{
|
||||||
|
{
|
||||||
|
HostIP: "127.0.0.1",
|
||||||
|
// Host port is a random available port.
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Privileged: true,
|
||||||
|
RestartPolicy: container.RestartPolicy{
|
||||||
|
Name: container.RestartPolicyAlways,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := p.createContainerWithImagePull(ctx, containerName, config, hostConfig); err != nil {
|
||||||
|
return m, err
|
||||||
|
}
|
||||||
|
if err := p.dockerCli.ContainerStart(ctx, containerName, container.StartOptions{}); err != nil {
|
||||||
|
return m, fmt.Errorf("start Docker container: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
apiPortBindings, err := p.waitPortPublished(ctx, containerName, apiPort)
|
||||||
|
if err != nil {
|
||||||
|
return m, fmt.Errorf("wait for machine API port '%s' to be published: %w", apiPort, err)
|
||||||
|
}
|
||||||
|
apiAddr, err := netip.ParseAddrPort(net.JoinHostPort(apiPortBindings[0].HostIP, apiPortBindings[0].HostPort))
|
||||||
|
if err != nil {
|
||||||
|
return m, fmt.Errorf("parse machine API port binding: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
m = Machine{
|
||||||
|
ClusterName: clusterName,
|
||||||
|
ContainerName: containerName,
|
||||||
|
Name: machineName,
|
||||||
|
APIAddress: apiAddr,
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// createContainerWithImagePull creates a Docker container. If the image is missing, it pulls the image first.
|
||||||
|
func (p *Provisioner) createContainerWithImagePull(
|
||||||
|
ctx context.Context, name string, config *container.Config, hostConfig *container.HostConfig,
|
||||||
|
) (container.CreateResponse, error) {
|
||||||
|
var resp container.CreateResponse
|
||||||
|
|
||||||
|
_, err := p.dockerCli.ContainerCreate(ctx, config, hostConfig, nil, nil, name)
|
||||||
|
if err == nil {
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if !dockerclient.IsErrNotFound(err) {
|
||||||
|
return resp, fmt.Errorf("create Docker container: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
respBody, err := p.dockerCli.ImagePull(ctx, config.Image, image.PullOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return resp, fmt.Errorf("pull Docker image: %w", err)
|
||||||
|
}
|
||||||
|
defer respBody.Close()
|
||||||
|
|
||||||
|
// Wait for pull to complete.
|
||||||
|
if _, err = io.Copy(io.Discard, respBody); err != nil {
|
||||||
|
return resp, fmt.Errorf("read Docker pull response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create container again after image pull.
|
||||||
|
if resp, err = p.dockerCli.ContainerCreate(ctx, config, hostConfig, nil, nil, name); err != nil {
|
||||||
|
return resp, fmt.Errorf("create Docker container: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// waitPortPublished waits for a Docker container port to be published on the host which happens asynchronously.
|
||||||
|
func (p *Provisioner) waitPortPublished(ctx context.Context, containerID string, port nat.Port) ([]nat.PortBinding, error) {
|
||||||
|
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
for {
|
||||||
|
c, err := p.dockerCli.ContainerInspect(ctx, containerID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("inspect container: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
binding, ok := c.NetworkSettings.Ports[port]
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("port '%s' not published", port)
|
||||||
|
}
|
||||||
|
if len(binding) > 0 {
|
||||||
|
return binding, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-time.After(10 * time.Millisecond):
|
||||||
|
case <-ctx.Done():
|
||||||
|
if errors.Is(ctx.Err(), context.DeadlineExceeded) {
|
||||||
|
return nil, errors.New("timeout")
|
||||||
|
}
|
||||||
|
return nil, ctx.Err()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func randomMachineName() (string, error) {
|
||||||
|
suffix, err := secret.RandomAlphaNumeric(4)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("generate random suffix: %w", err)
|
||||||
|
}
|
||||||
|
return "machine-" + suffix, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// WaitMachineReady waits for the machine API to respond.
|
||||||
|
func (p *Provisioner) WaitMachineReady(ctx context.Context, m Machine, timeout time.Duration) error {
|
||||||
|
cli, err := m.Connect(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("connect to machine over TCP '%s': %w", m.APIAddress, err)
|
||||||
|
}
|
||||||
|
defer cli.Close()
|
||||||
|
|
||||||
|
boff := backoff.WithContext(backoff.NewExponentialBackOff(
|
||||||
|
backoff.WithInitialInterval(100*time.Millisecond),
|
||||||
|
backoff.WithMaxInterval(1*time.Second),
|
||||||
|
backoff.WithMaxElapsedTime(timeout),
|
||||||
|
), ctx)
|
||||||
|
|
||||||
|
inspect := func() error {
|
||||||
|
if _, err := cli.Inspect(ctx, &emptypb.Empty{}); err != nil {
|
||||||
|
return fmt.Errorf("inspect machine: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return backoff.Retry(inspect, boff)
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package ucind
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"github.com/docker/docker/client"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ErrNotFound = errors.New("not found")
|
||||||
|
|
||||||
|
type Provisioner struct {
|
||||||
|
dockerCli *client.Client
|
||||||
|
configUpdater *ConfigUpdater
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewProvisioner(cli *client.Client, configUpdater *ConfigUpdater) *Provisioner {
|
||||||
|
return &Provisioner{
|
||||||
|
dockerCli: cli,
|
||||||
|
configUpdater: configUpdater,
|
||||||
|
}
|
||||||
|
}
|
||||||
Executable
+83
@@ -0,0 +1,83 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# This is a fork of https://github.com/moby/moby/blob/65cfcc28ab37cb75e1560e4b4738719c07c6618e/hack/dind
|
||||||
|
# with a fix for cgroup v2 initialization races.
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# DinD: a wrapper script which allows docker to be run inside a docker container.
|
||||||
|
# Original version by Jerome Petazzoni <jerome@docker.com>
|
||||||
|
# See the blog post: https://www.docker.com/blog/docker-can-now-run-within-docker/
|
||||||
|
#
|
||||||
|
# This script should be executed inside a docker container in privileged mode
|
||||||
|
# ('docker run --privileged', introduced in docker 0.6).
|
||||||
|
|
||||||
|
# Usage: dind CMD [ARG...]
|
||||||
|
|
||||||
|
# apparmor sucks and Docker needs to know that it's in a container (c) @tianon
|
||||||
|
#
|
||||||
|
# Set the container env-var, so that AppArmor is enabled in the daemon and
|
||||||
|
# containerd when running docker-in-docker.
|
||||||
|
#
|
||||||
|
# see: https://github.com/containerd/containerd/blob/787943dc1027a67f3b52631e084db0d4a6be2ccc/pkg/apparmor/apparmor_linux.go#L29-L45
|
||||||
|
# see: https://github.com/moby/moby/commit/de191e86321f7d3136ff42ff75826b8107399497
|
||||||
|
export container=docker
|
||||||
|
|
||||||
|
# Allow AppArmor to work inside the container;
|
||||||
|
#
|
||||||
|
# aa-status
|
||||||
|
# apparmor filesystem is not mounted.
|
||||||
|
# apparmor module is loaded.
|
||||||
|
#
|
||||||
|
# mount -t securityfs none /sys/kernel/security
|
||||||
|
#
|
||||||
|
# aa-status
|
||||||
|
# apparmor module is loaded.
|
||||||
|
# 30 profiles are loaded.
|
||||||
|
# 30 profiles are in enforce mode.
|
||||||
|
# /snap/snapd/18357/usr/lib/snapd/snap-confine
|
||||||
|
# ...
|
||||||
|
#
|
||||||
|
# Note: https://0xn3va.gitbook.io/cheat-sheets/container/escaping/sensitive-mounts#sys-kernel-security
|
||||||
|
#
|
||||||
|
# ## /sys/kernel/security
|
||||||
|
#
|
||||||
|
# In /sys/kernel/security mounted the securityfs interface, which allows
|
||||||
|
# configuration of Linux Security Modules. This allows configuration of
|
||||||
|
# AppArmor policies, and so access to this may allow a container to disable
|
||||||
|
# its MAC system.
|
||||||
|
#
|
||||||
|
# Given that we're running privileged already, this should not be an issue.
|
||||||
|
if [ -d /sys/kernel/security ] && ! mountpoint -q /sys/kernel/security; then
|
||||||
|
mount -t securityfs none /sys/kernel/security || {
|
||||||
|
echo >&2 'Could not mount /sys/kernel/security.'
|
||||||
|
echo >&2 'AppArmor detection and --privileged mode might break.'
|
||||||
|
}
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Mount /tmp (conditionally)
|
||||||
|
if ! mountpoint -q /tmp; then
|
||||||
|
mount -t tmpfs none /tmp
|
||||||
|
fi
|
||||||
|
|
||||||
|
# cgroup v2: enable nesting
|
||||||
|
if [ -f /sys/fs/cgroup/cgroup.controllers ]; then
|
||||||
|
# move the processes from the root group to the /init group,
|
||||||
|
# otherwise writing subtree_control fails with EBUSY.
|
||||||
|
# An error during moving non-existent process (i.e., "cat") is ignored.
|
||||||
|
mkdir -p /sys/fs/cgroup/init
|
||||||
|
xargs -rn1 < /sys/fs/cgroup/cgroup.procs > /sys/fs/cgroup/init/cgroup.procs || :
|
||||||
|
# enable controllers with retry on cgroup initialization races
|
||||||
|
timeout 5s sh -c "until sed -e 's/ / +/g' -e 's/^/+/' < /sys/fs/cgroup/cgroup.controllers \
|
||||||
|
> /sys/fs/cgroup/cgroup.subtree_control 2>/dev/null; do sleep 0.1; done"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Change mount propagation to shared to make the environment more similar to a
|
||||||
|
# modern Linux system, e.g. with SystemD as PID 1.
|
||||||
|
mount --make-rshared /
|
||||||
|
|
||||||
|
if [ $# -gt 0 ]; then
|
||||||
|
exec "$@"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo >&2 'ERROR: No command specified.'
|
||||||
|
echo >&2 'You probably want to run hack/make.sh, or maybe a shell?'
|
||||||
Executable
+41
@@ -0,0 +1,41 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
# Cleanup function to properly terminate background processes.
|
||||||
|
cleanup() {
|
||||||
|
echo "Terminating container processes..."
|
||||||
|
|
||||||
|
# Terminate the main process if it has been started.
|
||||||
|
if [ -n "$MAIN_PID" ]; then
|
||||||
|
kill "$MAIN_PID" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Terminate Docker daemon if PID file exists.
|
||||||
|
if [ -f /run/docker.pid ]; then
|
||||||
|
kill "$(cat /run/docker.pid)" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Terminate socat proxy if running.
|
||||||
|
pkill socat 2>/dev/null || true
|
||||||
|
|
||||||
|
# Wait for processes to terminate.
|
||||||
|
wait
|
||||||
|
}
|
||||||
|
trap cleanup INT TERM EXIT
|
||||||
|
|
||||||
|
dind dockerd &
|
||||||
|
echo "Waiting for Docker in Docker to be ready..."
|
||||||
|
timeout 5s sh -c "until docker info &> /dev/null; do sleep 0.1; done"
|
||||||
|
echo "Docker in Docker is ready."
|
||||||
|
|
||||||
|
echo "Loading corrosion image from /images/corrosion.tar..."
|
||||||
|
docker load < /images/corrosion.tar
|
||||||
|
|
||||||
|
# Make machine API accessible from the host via port publishing.
|
||||||
|
echo "Proxying Uncloud API port 51000/tcp to Unix socket /run/uncloud/uncloud.sock..."
|
||||||
|
socat TCP-LISTEN:51000,reuseaddr,fork,bind="$(hostname -i)" UNIX-CONNECT:/run/uncloud/uncloud.sock &
|
||||||
|
|
||||||
|
# Execute the passed command and wait for it while maintaining signal handling.
|
||||||
|
"$@" &
|
||||||
|
MAIN_PID=$!
|
||||||
|
wait $MAIN_PID
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user