mirror of
https://github.com/psviderski/uncloud.git
synced 2026-08-26 19:13:34 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1caa5ccf0e | ||
|
|
76affeb6b2 | ||
|
|
a945291371 | ||
|
|
06c3fba96e | ||
|
|
7e739dd2f0 | ||
|
|
6c2f85d38a | ||
|
|
1ea1d15845 | ||
|
|
5e0d4d0f05 | ||
|
|
9a1d2e0d46 | ||
|
|
878f96a4b7 | ||
|
|
9efaab2beb | ||
|
|
2c34ba7eff | ||
|
|
57e2e22266 | ||
|
|
4ec5c15254 | ||
|
|
33a3683196 | ||
|
|
4be1ea3506 | ||
|
|
f86e63c851 | ||
|
|
f6cf73190f | ||
|
|
5308c87651 | ||
|
|
9bd5ffcd2d | ||
|
|
8d8acd5410 | ||
|
|
4d97c30a9c | ||
|
|
aa71ab0220 | ||
|
|
2345ee9856 | ||
|
|
65e9cdd444 | ||
|
|
85a2615db1 | ||
|
|
777615bc56 | ||
|
|
bac34ebd0c | ||
|
|
b2322e1c49 | ||
|
|
436671457d | ||
|
|
fc847093d2 | ||
|
|
d2a7af744d | ||
|
|
5acf557675 | ||
|
|
0b32cd2095 | ||
|
|
425b974331 | ||
|
|
db09dec855 | ||
|
|
20cdcdf917 | ||
|
|
796363f23c | ||
|
|
cb0df2169f | ||
|
|
95afc7f289 | ||
|
|
bc65c73c51 | ||
|
|
596bc561b8 | ||
|
|
f215fae6c7 | ||
|
|
5707c37b63 | ||
|
|
5780d3a8a4 | ||
|
|
ac18ff3e8b | ||
|
|
a9a720f3c3 | ||
|
|
4b4b721d49 | ||
|
|
6373d4097c | ||
|
|
dfe12ca644 | ||
|
|
2ef8a6fd20 | ||
|
|
27cfe1c4f3 | ||
|
|
ebd4622592 | ||
|
|
4fa30ab8cb | ||
|
|
1e6aaf451e | ||
|
|
bf65fbaca5 | ||
|
|
79dc05cb66 | ||
|
|
234985b57d | ||
|
|
d89bfb8e03 | ||
|
|
91a09d5d37 | ||
|
|
e87207eae3 | ||
|
|
3c33973269 | ||
|
|
3019377310 | ||
|
|
53def3a624 | ||
|
|
eb23e956cb | ||
|
|
d029d4d0bf | ||
|
|
4433095045 | ||
|
|
05c8117f7b | ||
|
|
3fbba6d540 | ||
|
|
439d956ac2 | ||
|
|
055cfae272 | ||
|
|
76b4369aa6 | ||
|
|
74c7738bc3 | ||
|
|
f6d75d507c | ||
|
|
6a1cd7bf87 | ||
|
|
57205e1149 | ||
|
|
ce4a8cce72 | ||
|
|
75085b3929 | ||
|
|
c489fcc63c | ||
|
|
e30ea89990 | ||
|
|
9ddcbd6de0 | ||
|
|
939645e144 | ||
|
|
1d413734d8 | ||
|
|
2e655d870c | ||
|
|
eea81723b8 |
@@ -36,6 +36,12 @@ jobs:
|
|||||||
git diff --exit-code ||
|
git diff --exit-code ||
|
||||||
(echo "go.mod or go.sum has changed. Please run 'go mod tidy' and commit the changes." && exit 1)
|
(echo "go.mod or go.sum has changed. Please run 'go mod tidy' and commit the changes." && exit 1)
|
||||||
|
|
||||||
|
- name: Generate docs
|
||||||
|
run: |
|
||||||
|
make cli-docs
|
||||||
|
git diff --exit-code ||
|
||||||
|
(echo "Documentation is out of date. Please run 'make cli-docs' and commit the changes." && exit 1)
|
||||||
|
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: |
|
run: |
|
||||||
make ucind-image
|
make ucind-image
|
||||||
|
|||||||
@@ -7,6 +7,8 @@
|
|||||||
*.dll
|
*.dll
|
||||||
*.so
|
*.so
|
||||||
*.dylib
|
*.dylib
|
||||||
|
/uncloud
|
||||||
|
/uncloudd
|
||||||
|
|
||||||
# OS X
|
# OS X
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|||||||
+2
-1
@@ -16,7 +16,8 @@ builds:
|
|||||||
main: ./cmd/uncloud
|
main: ./cmd/uncloud
|
||||||
binary: uncloud
|
binary: uncloud
|
||||||
env:
|
env:
|
||||||
- CGO_ENABLED=0
|
# Use CGO to be able to build fsevent required by compose on macOS.
|
||||||
|
- CGO_ENABLED={{ if eq .Os "darwin" }}1{{ else }}0{{ end }}
|
||||||
goos:
|
goos:
|
||||||
- darwin
|
- darwin
|
||||||
- linux
|
- linux
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ This document provides comprehensive information about the Uncloud project for A
|
|||||||
- **Docker Compose compatibility**: Uses familiar Docker Compose format
|
- **Docker Compose compatibility**: Uses familiar Docker Compose format
|
||||||
- **Zero-downtime deployments**: Rolling updates without service interruption
|
- **Zero-downtime deployments**: Rolling updates without service interruption
|
||||||
- **Automatic HTTPS**: Caddy reverse proxy with Let's Encrypt integration
|
- **Automatic HTTPS**: Caddy reverse proxy with Let's Encrypt integration
|
||||||
- **Managed DNS**: Free `*.cluster.uncloud.run` subdomains via Uncloud DNS service
|
- **Managed DNS**: Free `*.xxxxxx.uncld.dev` subdomains via Uncloud DNS service
|
||||||
- **Cross-machine scaling**: Run containers across multiple machines
|
- **Cross-machine scaling**: Run containers across multiple machines
|
||||||
|
|
||||||
### 🔧 Developer Experience
|
### 🔧 Developer Experience
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<div align="center">
|
<div align="center">
|
||||||
<img src="./website/landing/images/logo.svg" height="100" width="100" alt="Uncloud logo"/>
|
<img src="./website/landing/images/logo-title.svg#gh-light-mode-only" alt="Uncloud logo"/>
|
||||||
<h1>Uncloud</h1>
|
<img src="./website/landing/images/logo-title-dark.svg#gh-dark-mode-only" alt="Uncloud logo"/>
|
||||||
<p><strong>Docker simplicity. Multi-machine power.</strong></p>
|
<p><strong>▸ Deploy and scale containerised apps across servers without Swarm or Kubernetes overhead ◂</strong></p>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
<a href="https://uncloud.run/docs"><img src="https://img.shields.io/badge/Docs-blue.svg?style=for-the-badge&logo=gitbook&logoColor=white" alt="Documentation"></a>
|
<a href="https://uncloud.run/docs"><img src="https://img.shields.io/badge/Docs-blue.svg?style=for-the-badge&logo=gitbook&logoColor=white" alt="Documentation"></a>
|
||||||
@@ -40,7 +40,7 @@ complexity of Kubernetes.
|
|||||||
* **No control plane**: Fully decentralised design eliminates single points of failure and reduces operational overhead.
|
* **No control plane**: Fully decentralised design eliminates single points of failure and reduces operational overhead.
|
||||||
* **Imperative over declarative**: Favoring imperative operations over state reconciliation simplifies both the mental
|
* **Imperative over declarative**: Favoring imperative operations over state reconciliation simplifies both the mental
|
||||||
model and troubleshooting.
|
model and troubleshooting.
|
||||||
* **Managed DNS**: Automatic DNS records `*.<id>.cluster.uncloud.run` for services with public access via managed
|
* **Managed DNS**: Automatic DNS records `*.xxxxxx.uncld.dev` for services with public access via managed
|
||||||
[Uncloud DNS](https://github.com/psviderski/uncloud-dns) service.
|
[Uncloud DNS](https://github.com/psviderski/uncloud-dns) service.
|
||||||
* **Automatic HTTPS**: Built-in Caddy reverse proxy handles TLS certificate provisioning and renewal using Let's
|
* **Automatic HTTPS**: Built-in Caddy reverse proxy handles TLS certificate provisioning and renewal using Let's
|
||||||
Encrypt.
|
Encrypt.
|
||||||
@@ -281,6 +281,16 @@ using the CLI or directly over SSH. They all have the complete cluster state and
|
|||||||
machine is a full backup of your control plane.
|
machine is a full backup of your control plane.
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
|
## 🧪 Interactive tutorials
|
||||||
|
|
||||||
|
To give you a chance to play with Uncloud without even leaving your browser, we're providing interactive tutorials and playgrounds on the [iximiuz Labs](https://labs.iximiuz.com/) platform.
|
||||||
|
|
||||||
|
Available tutorials:
|
||||||
|
|
||||||
|
1. [Setting up a new Uncloud cluster](https://labs.iximiuz.com/tutorials/uncloud-create-cluster-ebebf72b) - the tutorial walks you through creating a new cluster with two machines and then deploying a simple web service to it.
|
||||||
|
|
||||||
|
You can also launch the [Uncloud playground](https://labs.iximiuz.com/playgrounds/uncloud-cluster-64523f7c) where you can play with an already initialized Uncloud cluster.
|
||||||
|
|
||||||
## 🏗 Project status
|
## 🏗 Project status
|
||||||
|
|
||||||
Uncloud is currently in active development and is **not ready for production use**. Features may change significantly
|
Uncloud is currently in active development and is **not ready for production use**. Features may change significantly
|
||||||
@@ -337,6 +347,10 @@ These companies and projects are helping Uncloud with their generous sponsorship
|
|||||||
|
|
||||||
## ❤️ Contributors
|
## ❤️ Contributors
|
||||||
|
|
||||||
|
<a href="https://trendshift.io/repositories/14069">
|
||||||
|
<img alt="Trendshift" src="https://trendshift.io/api/badge/repositories/14069" />
|
||||||
|
</a>
|
||||||
|
|
||||||
Thank you [@cedws](https://github.com/cedws) for being the first contributor to Uncloud! 🎉
|
Thank you [@cedws](https://github.com/cedws) for being the first contributor to Uncloud! 🎉
|
||||||
|
|
||||||
<a href="https://github.com/psviderski/uncloud/graphs/contributors">
|
<a href="https://github.com/psviderski/uncloud/graphs/contributors">
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ to cluster machines or --push-registry to upload them to external registries.`,
|
|||||||
|
|
||||||
return runBuild(cmd.Context(), uncli, opts)
|
return runBuild(cmd.Context(), uncli, opts)
|
||||||
},
|
},
|
||||||
|
GroupID: "service",
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd.Flags().StringArrayVar(&opts.BuildArgs, "build-arg", nil,
|
cmd.Flags().StringArrayVar(&opts.BuildArgs, "build-arg", nil,
|
||||||
@@ -74,8 +75,6 @@ to cluster machines or --push-registry to upload them to external registries.`,
|
|||||||
"Use --machine to specify which machines. (default is all machines)")
|
"Use --machine to specify which machines. (default is all machines)")
|
||||||
cmd.Flags().BoolVar(&opts.PushRegistry, "push-registry", false,
|
cmd.Flags().BoolVar(&opts.PushRegistry, "push-registry", false,
|
||||||
"Upload the built images to external registries (e.g., Docker Hub) after building.")
|
"Upload the built images to external registries (e.g., Docker Hub) after building.")
|
||||||
cmd.Flags().StringVarP(&opts.Context, "context", "c", "",
|
|
||||||
"Name of the cluster context. (default is the current context)")
|
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,14 +7,12 @@ import (
|
|||||||
|
|
||||||
"github.com/alecthomas/chroma/v2/quick"
|
"github.com/alecthomas/chroma/v2/quick"
|
||||||
"github.com/psviderski/uncloud/internal/cli"
|
"github.com/psviderski/uncloud/internal/cli"
|
||||||
"github.com/psviderski/uncloud/pkg/api"
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
type configOptions struct {
|
type configOptions struct {
|
||||||
machine string
|
machine string
|
||||||
noColor bool
|
noColor bool
|
||||||
context string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewConfigCommand() *cobra.Command {
|
func NewConfigCommand() *cobra.Command {
|
||||||
@@ -34,16 +32,12 @@ func NewConfigCommand() *cobra.Command {
|
|||||||
"Name or ID of the machine to get the configuration from. (default is connected machine)")
|
"Name or ID of the machine to get the configuration from. (default is connected machine)")
|
||||||
cmd.Flags().BoolVar(&opts.noColor, "no-color", false,
|
cmd.Flags().BoolVar(&opts.noColor, "no-color", false,
|
||||||
"Disable syntax highlighting for the output.")
|
"Disable syntax highlighting for the output.")
|
||||||
cmd.Flags().StringVarP(
|
|
||||||
&opts.context, "context", "c", "",
|
|
||||||
"Name of the cluster context. (default is the current context)",
|
|
||||||
)
|
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
func runConfig(ctx context.Context, uncli *cli.CLI, opts configOptions) error {
|
func runConfig(ctx context.Context, uncli *cli.CLI, opts configOptions) error {
|
||||||
clusterClient, err := uncli.ConnectCluster(ctx, opts.context)
|
clusterClient, err := uncli.ConnectCluster(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("connect to cluster: %w", err)
|
return fmt.Errorf("connect to cluster: %w", err)
|
||||||
}
|
}
|
||||||
@@ -51,7 +45,7 @@ func runConfig(ctx context.Context, uncli *cli.CLI, opts configOptions) error {
|
|||||||
|
|
||||||
if opts.machine != "" {
|
if opts.machine != "" {
|
||||||
// If a specific machine is requested, use it to get the Caddy configuration.
|
// If a specific machine is requested, use it to get the Caddy configuration.
|
||||||
ctx, _, err = api.ProxyMachinesContext(ctx, clusterClient, []string{opts.machine})
|
ctx, _, err = clusterClient.ProxyMachinesContext(ctx, []string{opts.machine})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ type deployOptions struct {
|
|||||||
caddyfile string
|
caddyfile string
|
||||||
image string
|
image string
|
||||||
machines []string
|
machines []string
|
||||||
context string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewDeployCommand() *cobra.Command {
|
func NewDeployCommand() *cobra.Command {
|
||||||
@@ -46,10 +45,6 @@ func NewDeployCommand() *cobra.Command {
|
|||||||
cmd.Flags().StringSliceVarP(&opts.machines, "machine", "m", nil,
|
cmd.Flags().StringSliceVarP(&opts.machines, "machine", "m", nil,
|
||||||
"Machine names or IDs to deploy to. Can be specified multiple times or as a comma-separated "+
|
"Machine names or IDs to deploy to. Can be specified multiple times or as a comma-separated "+
|
||||||
"list. (default is all machines)")
|
"list. (default is all machines)")
|
||||||
cmd.Flags().StringVarP(
|
|
||||||
&opts.context, "context", "c", "",
|
|
||||||
"Name of the cluster context to deploy to. (default is the current context)",
|
|
||||||
)
|
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
@@ -64,7 +59,7 @@ func runDeploy(ctx context.Context, uncli *cli.CLI, opts deployOptions) error {
|
|||||||
caddyfile = strings.TrimSpace(string(data))
|
caddyfile = strings.TrimSpace(string(data))
|
||||||
}
|
}
|
||||||
|
|
||||||
clusterClient, err := uncli.ConnectCluster(ctx, opts.context)
|
clusterClient, err := uncli.ConnectCluster(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("connect to cluster: %w", err)
|
return fmt.Errorf("connect to cluster: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
package context
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/huh"
|
||||||
|
"github.com/psviderski/uncloud/internal/cli"
|
||||||
|
"github.com/psviderski/uncloud/internal/cli/config"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewConnectionCommand() *cobra.Command {
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "connection",
|
||||||
|
Aliases: []string{"conn"},
|
||||||
|
Short: "Choose a new default connection for the current context.",
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
uncli := cmd.Context().Value("cli").(*cli.CLI)
|
||||||
|
return selectConnection(uncli)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func selectConnection(uncli *cli.CLI) error {
|
||||||
|
if uncli.Config == nil {
|
||||||
|
return fmt.Errorf("connection management is not available: Uncloud configuration file is not being used")
|
||||||
|
}
|
||||||
|
if len(uncli.Config.Contexts) == 0 {
|
||||||
|
return fmt.Errorf("no contexts found in Uncloud config (%s)", uncli.Config.Path())
|
||||||
|
}
|
||||||
|
|
||||||
|
currentCtxName := uncli.Config.CurrentContext
|
||||||
|
currentCtx, ok := uncli.Config.Contexts[currentCtxName]
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("current context '%s' not found", currentCtxName)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(currentCtx.Connections) == 0 {
|
||||||
|
return fmt.Errorf("no connections found in context '%s'", currentCtxName)
|
||||||
|
}
|
||||||
|
|
||||||
|
var selectedConnection int
|
||||||
|
form := huh.NewForm(
|
||||||
|
huh.NewGroup(
|
||||||
|
huh.NewSelect[int]().
|
||||||
|
Title("Select a default connection").
|
||||||
|
Options(buildConnectionOptions(currentCtx.Connections)...).
|
||||||
|
Value(&selectedConnection),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
if err := form.Run(); err != nil {
|
||||||
|
return fmt.Errorf("select connection: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
currentCtx.SetDefaultConnection(selectedConnection)
|
||||||
|
selected := currentCtx.Connections[0]
|
||||||
|
|
||||||
|
if err := uncli.Config.Save(); err != nil {
|
||||||
|
return fmt.Errorf("save config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Default connection for context '%s' is now '%s'.\n", currentCtxName, selected)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildConnectionOptions(connections []config.MachineConnection) []huh.Option[int] {
|
||||||
|
options := make([]huh.Option[int], len(connections))
|
||||||
|
for i, conn := range connections {
|
||||||
|
key := conn.String()
|
||||||
|
opt := huh.NewOption(key, i)
|
||||||
|
if i == 0 {
|
||||||
|
opt.Key += " (default)"
|
||||||
|
opt = opt.Selected(true)
|
||||||
|
}
|
||||||
|
options[i] = opt
|
||||||
|
}
|
||||||
|
return options
|
||||||
|
}
|
||||||
@@ -19,6 +19,7 @@ func NewRootCommand() *cobra.Command {
|
|||||||
cmd.AddCommand(
|
cmd.AddCommand(
|
||||||
NewListCommand(),
|
NewListCommand(),
|
||||||
NewUseCommand(),
|
NewUseCommand(),
|
||||||
|
NewConnectionCommand(),
|
||||||
)
|
)
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
|
|||||||
@@ -26,8 +26,6 @@ type deployOptions struct {
|
|||||||
noBuild bool
|
noBuild bool
|
||||||
recreate bool
|
recreate bool
|
||||||
yes bool
|
yes bool
|
||||||
|
|
||||||
context string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewDeployCommand creates a new command to deploy services from a Compose file.
|
// NewDeployCommand creates a new command to deploy services from a Compose file.
|
||||||
@@ -44,6 +42,7 @@ func NewDeployCommand() *cobra.Command {
|
|||||||
|
|
||||||
return runDeploy(cmd.Context(), uncli, opts)
|
return runDeploy(cmd.Context(), uncli, opts)
|
||||||
},
|
},
|
||||||
|
GroupID: "service",
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd.Flags().StringArrayVar(&opts.BuildServicesOptions.BuildArgs, "build-arg", nil,
|
cmd.Flags().StringArrayVar(&opts.BuildServicesOptions.BuildArgs, "build-arg", nil,
|
||||||
@@ -51,8 +50,6 @@ func NewDeployCommand() *cobra.Command {
|
|||||||
"Can be specified multiple times. Format: --build-arg VAR=VALUE")
|
"Can be specified multiple times. Format: --build-arg VAR=VALUE")
|
||||||
cmd.Flags().BoolVar(&opts.BuildServicesOptions.Pull, "build-pull", false,
|
cmd.Flags().BoolVar(&opts.BuildServicesOptions.Pull, "build-pull", false,
|
||||||
"Always attempt to pull newer versions of base images before building service images.")
|
"Always attempt to pull newer versions of base images before building service images.")
|
||||||
cmd.Flags().StringVarP(&opts.context, "context", "c", "",
|
|
||||||
"Name of the cluster context to deploy to (default is the current context)")
|
|
||||||
cmd.Flags().StringSliceVarP(&opts.files, "file", "f", nil,
|
cmd.Flags().StringSliceVarP(&opts.files, "file", "f", nil,
|
||||||
"One or more Compose files to deploy services from. (default compose.yaml)")
|
"One or more Compose files to deploy services from. (default compose.yaml)")
|
||||||
cmd.Flags().BoolVar(&opts.noBuild, "no-build", false,
|
cmd.Flags().BoolVar(&opts.noBuild, "no-build", false,
|
||||||
@@ -108,7 +105,7 @@ func runDeploy(ctx context.Context, uncli *cli.CLI, opts deployOptions) error {
|
|||||||
fmt.Println()
|
fmt.Println()
|
||||||
}
|
}
|
||||||
|
|
||||||
clusterClient, err := uncli.ConnectCluster(ctx, opts.context)
|
clusterClient, err := uncli.ConnectCluster(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("connect to cluster: %w", err)
|
return fmt.Errorf("connect to cluster: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,32 +12,21 @@ import (
|
|||||||
"google.golang.org/protobuf/types/known/emptypb"
|
"google.golang.org/protobuf/types/known/emptypb"
|
||||||
)
|
)
|
||||||
|
|
||||||
type releaseOptions struct {
|
|
||||||
context string
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewReleaseCommand() *cobra.Command {
|
func NewReleaseCommand() *cobra.Command {
|
||||||
opts := releaseOptions{}
|
|
||||||
|
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "release",
|
Use: "release",
|
||||||
Short: "Release the reserved cluster domain.",
|
Short: "Release the reserved cluster domain.",
|
||||||
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 release(cmd.Context(), uncli, opts)
|
return release(cmd.Context(), uncli)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd.Flags().StringVarP(
|
|
||||||
&opts.context, "context", "c", "",
|
|
||||||
"Name of the cluster context. (default is the current context)",
|
|
||||||
)
|
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
func release(ctx context.Context, uncli *cli.CLI, opts releaseOptions) error {
|
func release(ctx context.Context, uncli *cli.CLI) error {
|
||||||
client, err := uncli.ConnectCluster(ctx, opts.context)
|
client, err := uncli.ConnectCluster(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("connect to cluster: %w", err)
|
return fmt.Errorf("connect to cluster: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ const DefaultUncloudDNSAPIEndpoint = "https://dns.uncloud.run/v1"
|
|||||||
|
|
||||||
type reserveOptions struct {
|
type reserveOptions struct {
|
||||||
endpoint string
|
endpoint string
|
||||||
context string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewReserveCommand() *cobra.Command {
|
func NewReserveCommand() *cobra.Command {
|
||||||
@@ -36,16 +35,12 @@ func NewReserveCommand() *cobra.Command {
|
|||||||
|
|
||||||
cmd.Flags().StringVar(&opts.endpoint, "endpoint", DefaultUncloudDNSAPIEndpoint,
|
cmd.Flags().StringVar(&opts.endpoint, "endpoint", DefaultUncloudDNSAPIEndpoint,
|
||||||
"API endpoint for the Uncloud DNS service.")
|
"API endpoint for the Uncloud DNS service.")
|
||||||
cmd.Flags().StringVarP(
|
|
||||||
&opts.context, "context", "c", "",
|
|
||||||
"Name of the cluster context. (default is the current context)",
|
|
||||||
)
|
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
func reserve(ctx context.Context, uncli *cli.CLI, opts reserveOptions) error {
|
func reserve(ctx context.Context, uncli *cli.CLI, opts reserveOptions) error {
|
||||||
clusterClient, err := uncli.ConnectCluster(ctx, opts.context)
|
clusterClient, err := uncli.ConnectCluster(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("connect to cluster: %w", err)
|
return fmt.Errorf("connect to cluster: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ func NewRootCommand() *cobra.Command {
|
|||||||
Use: "dns",
|
Use: "dns",
|
||||||
Short: "Manage cluster domain in Uncloud DNS.",
|
Short: "Manage cluster domain in Uncloud DNS.",
|
||||||
Long: "Manage cluster domain in Uncloud DNS.\n" +
|
Long: "Manage cluster domain in Uncloud DNS.\n" +
|
||||||
"DNS commands allow you to reserve or release a unique '<id>.cluster.uncloud.run' domain for your " +
|
"DNS commands allow you to reserve or release a unique 'xxxxxx.uncld.dev' domain for your " +
|
||||||
"cluster. When reserved, Caddy service deployments will automatically update DNS records to route " +
|
"cluster. When reserved, Caddy service deployments will automatically update DNS records to route " +
|
||||||
"traffic to the services in the cluster.",
|
"traffic to the services in the cluster.",
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-14
@@ -10,32 +10,21 @@ import (
|
|||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
type showOptions struct {
|
|
||||||
context string
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewShowCommand() *cobra.Command {
|
func NewShowCommand() *cobra.Command {
|
||||||
opts := showOptions{}
|
|
||||||
|
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "show",
|
Use: "show",
|
||||||
Short: "Print the cluster domain name.",
|
Short: "Print the cluster domain name.",
|
||||||
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 show(cmd.Context(), uncli, opts)
|
return show(cmd.Context(), uncli)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd.Flags().StringVarP(
|
|
||||||
&opts.context, "context", "c", "",
|
|
||||||
"Name of the cluster context. (default is the current context)",
|
|
||||||
)
|
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
func show(ctx context.Context, uncli *cli.CLI, opts showOptions) error {
|
func show(ctx context.Context, uncli *cli.CLI) error {
|
||||||
clusterClient, err := uncli.ConnectCluster(ctx, opts.context)
|
clusterClient, err := uncli.ConnectCluster(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("connect to cluster: %w", err)
|
return fmt.Errorf("connect to cluster: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ import (
|
|||||||
type listOptions struct {
|
type listOptions struct {
|
||||||
machines []string
|
machines []string
|
||||||
nameFilter string
|
nameFilter string
|
||||||
context string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewListCommand() *cobra.Command {
|
func NewListCommand() *cobra.Command {
|
||||||
@@ -60,10 +59,6 @@ func NewListCommand() *cobra.Command {
|
|||||||
cmd.Flags().StringSliceVarP(&opts.machines, "machine", "m", nil,
|
cmd.Flags().StringSliceVarP(&opts.machines, "machine", "m", nil,
|
||||||
"Filter images by machine name or ID. Can be specified multiple times or as a comma-separated list. "+
|
"Filter images by machine name or ID. Can be specified multiple times or as a comma-separated list. "+
|
||||||
"(default is include all machines)")
|
"(default is include all machines)")
|
||||||
cmd.Flags().StringVarP(
|
|
||||||
&opts.context, "context", "c", "",
|
|
||||||
"Name of the cluster context. (default is the current context)",
|
|
||||||
)
|
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
@@ -82,7 +77,7 @@ type imageRow struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func list(ctx context.Context, uncli *cli.CLI, opts listOptions) error {
|
func list(ctx context.Context, uncli *cli.CLI, opts listOptions) error {
|
||||||
clusterClient, err := uncli.ConnectCluster(ctx, opts.context)
|
clusterClient, err := uncli.ConnectCluster(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("connect to cluster: %w", err)
|
return fmt.Errorf("connect to cluster: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ type pushOptions struct {
|
|||||||
image string
|
image string
|
||||||
machines []string
|
machines []string
|
||||||
platform string
|
platform string
|
||||||
context string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewPushCommand() *cobra.Command {
|
func NewPushCommand() *cobra.Command {
|
||||||
@@ -54,16 +53,12 @@ The image is uploaded to all cluster machines (default) or the specified machine
|
|||||||
"Push a specific platform of a multi-platform image (e.g., linux/amd64, linux/arm64).\n"+
|
"Push a specific platform of a multi-platform image (e.g., linux/amd64, linux/arm64).\n"+
|
||||||
"Local Docker must be configured to use containerd image store to support multi-platform images.",
|
"Local Docker must be configured to use containerd image store to support multi-platform images.",
|
||||||
)
|
)
|
||||||
cmd.Flags().StringVarP(
|
|
||||||
&opts.context, "context", "c", "",
|
|
||||||
"Name of the cluster context. (default is the current context)",
|
|
||||||
)
|
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
func push(ctx context.Context, uncli *cli.CLI, opts pushOptions) error {
|
func push(ctx context.Context, uncli *cli.CLI, opts pushOptions) error {
|
||||||
clusterClient, err := uncli.ConnectCluster(ctx, opts.context)
|
clusterClient, err := uncli.ConnectCluster(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("connect to cluster: %w", err)
|
return fmt.Errorf("connect to cluster: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
+98
-67
@@ -5,9 +5,11 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/cenkalti/backoff/v4"
|
"github.com/charmbracelet/huh/spinner"
|
||||||
|
"github.com/charmbracelet/lipgloss"
|
||||||
"github.com/docker/compose/v2/pkg/progress"
|
"github.com/docker/compose/v2/pkg/progress"
|
||||||
"github.com/psviderski/uncloud/cmd/uncloud/caddy"
|
"github.com/psviderski/uncloud/cmd/uncloud/caddy"
|
||||||
"github.com/psviderski/uncloud/internal/cli"
|
"github.com/psviderski/uncloud/internal/cli"
|
||||||
@@ -15,8 +17,6 @@ import (
|
|||||||
"github.com/psviderski/uncloud/pkg/api"
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
"github.com/psviderski/uncloud/pkg/client"
|
"github.com/psviderski/uncloud/pkg/client"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"google.golang.org/grpc/codes"
|
|
||||||
"google.golang.org/grpc/status"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type addOptions struct {
|
type addOptions struct {
|
||||||
@@ -25,8 +25,8 @@ type addOptions struct {
|
|||||||
noInstall bool
|
noInstall bool
|
||||||
publicIP string
|
publicIP string
|
||||||
sshKey string
|
sshKey string
|
||||||
context string
|
|
||||||
version string
|
version string
|
||||||
|
yes bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAddCommand() *cobra.Command {
|
func NewAddCommand() *cobra.Command {
|
||||||
@@ -34,19 +34,33 @@ func NewAddCommand() *cobra.Command {
|
|||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "add [USER@]HOST[:PORT]",
|
Use: "add [USER@]HOST[:PORT]",
|
||||||
Short: "Add a remote machine to a cluster.",
|
Short: "Add a remote machine to a cluster.",
|
||||||
Args: cobra.ExactArgs(1),
|
Long: `Add a new machine to an existing Uncloud cluster.
|
||||||
|
|
||||||
|
Connection methods:
|
||||||
|
ssh://user@host - Use built-in SSH library (default, no prefix required)
|
||||||
|
ssh+cli://user@host - Use system SSH command (supports ProxyJump, SSH config)`,
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
cli.BindEnvToFlag(cmd, "yes", "UNCLOUD_AUTO_CONFIRM")
|
||||||
|
|
||||||
uncli := cmd.Context().Value("cli").(*cli.CLI)
|
uncli := cmd.Context().Value("cli").(*cli.CLI)
|
||||||
|
|
||||||
user, host, port, err := config.SSHDestination(args[0]).Parse()
|
// Determine if SSH CLI needs to be used and strip scheme
|
||||||
|
destination := args[0]
|
||||||
|
useSSHCLI := strings.HasPrefix(destination, "ssh+cli://")
|
||||||
|
destination = strings.TrimPrefix(destination, "ssh+cli://")
|
||||||
|
destination = strings.TrimPrefix(destination, "ssh://")
|
||||||
|
|
||||||
|
user, host, port, err := config.SSHDestination(destination).Parse()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("parse remote machine: %w", err)
|
return fmt.Errorf("parse remote machine: %w", err)
|
||||||
}
|
}
|
||||||
remoteMachine := &cli.RemoteMachine{
|
remoteMachine := &cli.RemoteMachine{
|
||||||
User: user,
|
User: user,
|
||||||
Host: host,
|
Host: host,
|
||||||
Port: port,
|
Port: port,
|
||||||
KeyPath: opts.sshKey,
|
KeyPath: opts.sshKey,
|
||||||
|
UseSSHCLI: useSSHCLI,
|
||||||
}
|
}
|
||||||
|
|
||||||
return add(cmd.Context(), uncli, remoteMachine, opts)
|
return add(cmd.Context(), uncli, remoteMachine, opts)
|
||||||
@@ -76,10 +90,9 @@ func NewAddCommand() *cobra.Command {
|
|||||||
&opts.version, "version", "latest",
|
&opts.version, "version", "latest",
|
||||||
"Version of the Uncloud daemon to install on the machine.",
|
"Version of the Uncloud daemon to install on the machine.",
|
||||||
)
|
)
|
||||||
cmd.Flags().StringVarP(
|
cmd.Flags().BoolVarP(&opts.yes, "yes", "y", false,
|
||||||
&opts.context, "context", "c", "",
|
"Auto-confirm prompts (e.g., resetting an already initialised machine).\n"+
|
||||||
"Name of the cluster context to add the machine to. (default is the current context)",
|
"Should be explicitly set when running non-interactively, e.g., in CI/CD pipelines. [$UNCLOUD_AUTO_CONFIRM]")
|
||||||
)
|
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
@@ -100,12 +113,12 @@ func add(ctx context.Context, uncli *cli.CLI, remoteMachine *cli.RemoteMachine,
|
|||||||
}
|
}
|
||||||
|
|
||||||
clusterClient, machineClient, err := uncli.AddMachine(ctx, cli.AddMachineOptions{
|
clusterClient, machineClient, err := uncli.AddMachine(ctx, cli.AddMachineOptions{
|
||||||
Context: opts.context,
|
|
||||||
MachineName: opts.name,
|
MachineName: opts.name,
|
||||||
PublicIP: publicIP,
|
PublicIP: publicIP,
|
||||||
RemoteMachine: remoteMachine,
|
RemoteMachine: remoteMachine,
|
||||||
SkipInstall: opts.noInstall,
|
SkipInstall: opts.noInstall,
|
||||||
Version: opts.version,
|
Version: opts.version,
|
||||||
|
AutoConfirm: opts.yes,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -117,79 +130,97 @@ func add(ctx context.Context, uncli *cli.CLI, remoteMachine *cli.RemoteMachine,
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait for the cluster to be initialised to be able to deploy the Caddy service.
|
// Wait for the cluster to be initialised on the machine to be able to deploy the Caddy service.
|
||||||
fmt.Println("Waiting for the machine to be ready...")
|
err = spinner.New().
|
||||||
fmt.Println()
|
Title(" Waiting for the machine to join the cluster...").
|
||||||
if err = waitClusterInitialised(ctx, machineClient); err != nil {
|
Type(spinner.MiniDot).
|
||||||
return fmt.Errorf("wait for cluster to be initialised on machine: %w", err)
|
Style(lipgloss.NewStyle().Foreground(lipgloss.Color("3"))).
|
||||||
|
TitleStyle(lipgloss.NewStyle()).
|
||||||
|
ActionWithErr(func(ctx context.Context) error {
|
||||||
|
return machineClient.WaitClusterReady(ctx, 5*time.Minute)
|
||||||
|
}).
|
||||||
|
Run()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("wait for machine to join the cluster: %w", err)
|
||||||
}
|
}
|
||||||
|
fmt.Println("Machine joined the cluster.")
|
||||||
|
|
||||||
|
// TODO: scale the existing Caddy service to the new machine instead of running a new deployment
|
||||||
|
// that may cause a small downtime.
|
||||||
// Deploy a Caddy service container to the added machine. If caddy service is already deployed on other machines,
|
// Deploy a Caddy service container to the added machine. If caddy service is already deployed on other machines,
|
||||||
// use the deployed image version. Otherwise, use the latest version.
|
// use the deployed image version.
|
||||||
// NOTE: We use the cluster client to inspect and scale the Caddy service because the newly added machine may have
|
// NOTE: We use the cluster client to inspect and scale the Caddy service because the newly added machine may have
|
||||||
// issues accessing the Machine API of existing machines in the cluster.
|
// issues accessing the Machine API of existing machines in the cluster.
|
||||||
// See the issue for more details: https://github.com/psviderski/uncloud/issues/65.
|
// See the issue for more details: https://github.com/psviderski/uncloud/issues/65.
|
||||||
caddyImage := ""
|
caddyImage := ""
|
||||||
caddySvc, err := clusterClient.InspectService(ctx, client.CaddyServiceName)
|
caddySvc, err := clusterClient.InspectService(ctx, client.CaddyServiceName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !errors.Is(err, api.ErrNotFound) {
|
if errors.Is(err, api.ErrNotFound) {
|
||||||
return fmt.Errorf("inspect caddy service: %w", err)
|
// Caddy service is not deployed.
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
} else {
|
return fmt.Errorf("inspect caddy service: %w", err)
|
||||||
caddyImage = caddySvc.Containers[0].Container.Config.Image
|
}
|
||||||
// Find the latest created container and use its image.
|
caddyImage = caddySvc.Containers[0].Container.Config.Image
|
||||||
var latestCreated time.Time
|
// Find the latest created container and use its image.
|
||||||
for _, c := range caddySvc.Containers[1:] {
|
var latestCreated time.Time
|
||||||
created, err := time.Parse(time.RFC3339Nano, c.Container.Created)
|
for _, c := range caddySvc.Containers[1:] {
|
||||||
if err != nil {
|
created, err := time.Parse(time.RFC3339Nano, c.Container.Created)
|
||||||
continue
|
if err != nil {
|
||||||
}
|
continue
|
||||||
if created.After(latestCreated) {
|
}
|
||||||
latestCreated = created
|
if created.After(latestCreated) {
|
||||||
caddyImage = c.Container.Config.Image
|
latestCreated = created
|
||||||
}
|
caddyImage = c.Container.Config.Image
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: scale the existing Caddy service to the new machine instead of running a new deployment
|
|
||||||
// that may cause a small downtime.
|
|
||||||
d, err := clusterClient.NewCaddyDeployment(caddyImage, "", api.Placement{})
|
d, err := clusterClient.NewCaddyDeployment(caddyImage, "", api.Placement{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("create caddy deployment: %w", err)
|
return fmt.Errorf("create caddy deployment: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = progress.RunWithTitle(ctx, func(ctx context.Context) error {
|
plan, err := d.Plan(ctx)
|
||||||
if _, err = d.Run(ctx); err != nil {
|
|
||||||
return fmt.Errorf("deploy caddy: %w", err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}, uncli.ProgressOut(), fmt.Sprintf("Deploying service %s", d.Spec.Name))
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return fmt.Errorf("plan caddy deployment: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println()
|
||||||
|
if len(plan.Operations) == 0 {
|
||||||
|
fmt.Printf("%s service is up to date.\n", client.CaddyServiceName)
|
||||||
|
} else {
|
||||||
|
// Initialise a machine and container name resolver to properly format the plan output.
|
||||||
|
resolver, err := clusterClient.ServiceOperationNameResolver(ctx, caddySvc)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("create machine and container name resolver for service operations: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("caddy deployment plan:")
|
||||||
|
fmt.Println(plan.Format(resolver))
|
||||||
|
fmt.Println()
|
||||||
|
|
||||||
|
if !opts.yes {
|
||||||
|
confirmed, err := cli.Confirm()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("confirm deployment: %w", err)
|
||||||
|
}
|
||||||
|
if !confirmed {
|
||||||
|
fmt.Println("Cancelled. No changes were made.")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = progress.RunWithTitle(ctx, func(ctx context.Context) error {
|
||||||
|
if _, err = d.Run(ctx); err != nil {
|
||||||
|
return fmt.Errorf("deploy caddy: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}, uncli.ProgressOut(), fmt.Sprintf("Deploying service %s (%s mode)", d.Spec.Name, d.Spec.Mode))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
return caddy.UpdateDomainRecords(ctx, machineClient, uncli.ProgressOut())
|
return caddy.UpdateDomainRecords(ctx, machineClient, uncli.ProgressOut())
|
||||||
}
|
}
|
||||||
|
|
||||||
func waitClusterInitialised(ctx context.Context, client *client.Client) error {
|
|
||||||
boff := backoff.WithContext(backoff.NewExponentialBackOff(
|
|
||||||
backoff.WithMaxInterval(1*time.Second),
|
|
||||||
backoff.WithMaxElapsedTime(5*time.Minute),
|
|
||||||
), ctx)
|
|
||||||
|
|
||||||
check := func() error {
|
|
||||||
_, err := client.ListMachines(ctx, nil)
|
|
||||||
if err == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
statusErr := status.Convert(err)
|
|
||||||
if statusErr.Code() == codes.FailedPrecondition {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return backoff.Permanent(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return backoff.Retry(check, boff)
|
|
||||||
}
|
|
||||||
|
|||||||
+58
-17
@@ -4,7 +4,11 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/huh/spinner"
|
||||||
|
"github.com/charmbracelet/lipgloss"
|
||||||
"github.com/docker/compose/v2/pkg/progress"
|
"github.com/docker/compose/v2/pkg/progress"
|
||||||
"github.com/psviderski/uncloud/cmd/uncloud/caddy"
|
"github.com/psviderski/uncloud/cmd/uncloud/caddy"
|
||||||
"github.com/psviderski/uncloud/cmd/uncloud/dns"
|
"github.com/psviderski/uncloud/cmd/uncloud/dns"
|
||||||
@@ -27,15 +31,20 @@ type initOptions struct {
|
|||||||
sshKey string
|
sshKey string
|
||||||
version string
|
version string
|
||||||
context string
|
context string
|
||||||
|
yes bool
|
||||||
}
|
}
|
||||||
|
|
||||||
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 [schema://]USER@HOST[:PORT]",
|
||||||
Short: "Initialise a new cluster with a remote machine as the first member.",
|
Short: "Initialise a new cluster with a remote machine as the first member.",
|
||||||
Long: "Initialise a new cluster by setting up a remote machine as the first member.\n" +
|
Long: `Initialise a new cluster by setting up a remote machine as the first member.
|
||||||
"This command creates a new context in your Uncloud config to manage the cluster.",
|
This command creates a new context in your Uncloud config to manage the cluster.
|
||||||
|
|
||||||
|
Connection methods:
|
||||||
|
ssh://user@host - Use built-in SSH library (default, no prefix required)
|
||||||
|
ssh+cli://user@host - Use system SSH command (supports ProxyJump, SSH config)`,
|
||||||
Example: ` # Initialise a new cluster with default settings.
|
Example: ` # Initialise a new cluster with default settings.
|
||||||
uc machine init root@<your-server-ip>
|
uc machine init root@<your-server-ip>
|
||||||
|
|
||||||
@@ -45,31 +54,45 @@ func NewInitCommand() *cobra.Command {
|
|||||||
# Initialise with a non-root user and custom SSH port and key.
|
# Initialise with a non-root user and custom SSH port and key.
|
||||||
uc machine init ubuntu@<your-server-ip>:2222 -i ~/.ssh/mykey
|
uc machine init ubuntu@<your-server-ip>:2222 -i ~/.ssh/mykey
|
||||||
|
|
||||||
# Initialise without Caddy (no reverse proxy) and without an automatically managed domain name (xxxxxx.cluster.uncloud.run).
|
# Initialise without Caddy (no reverse proxy) and without an automatically managed domain name (xxxxxx.uncld.dev).
|
||||||
# You can deploy Caddy with 'uc caddy deploy' and reserve a domain with 'uc dns reserve' later.
|
# You can deploy Caddy with 'uc caddy deploy' and reserve a domain with 'uc dns reserve' later.
|
||||||
uc machine init root@<your-server-ip> --no-caddy --no-dns`,
|
uc machine init root@<your-server-ip> --no-caddy --no-dns`,
|
||||||
// TODO: support initialising a cluster on the local machine.
|
// TODO: support initialising a cluster on the local machine.
|
||||||
Args: cobra.MaximumNArgs(1),
|
Args: cobra.MaximumNArgs(1),
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
cli.BindEnvToFlag(cmd, "yes", "UNCLOUD_AUTO_CONFIRM")
|
||||||
|
|
||||||
uncli := cmd.Context().Value("cli").(*cli.CLI)
|
uncli := cmd.Context().Value("cli").(*cli.CLI)
|
||||||
|
|
||||||
var remoteMachine *cli.RemoteMachine
|
var remoteMachine *cli.RemoteMachine
|
||||||
if len(args) > 0 {
|
if len(args) > 0 {
|
||||||
user, host, port, err := config.SSHDestination(args[0]).Parse()
|
// Determine if SSH CLI is requested and strip scheme
|
||||||
|
destination := args[0]
|
||||||
|
useSSHCLI := strings.HasPrefix(destination, "ssh+cli://")
|
||||||
|
destination = strings.TrimPrefix(destination, "ssh+cli://")
|
||||||
|
destination = strings.TrimPrefix(destination, "ssh://")
|
||||||
|
|
||||||
|
user, host, port, err := config.SSHDestination(destination).Parse()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("parse remote machine: %w", err)
|
return fmt.Errorf("parse remote machine: %w", err)
|
||||||
}
|
}
|
||||||
remoteMachine = &cli.RemoteMachine{
|
remoteMachine = &cli.RemoteMachine{
|
||||||
User: user,
|
User: user,
|
||||||
Host: host,
|
Host: host,
|
||||||
Port: port,
|
Port: port,
|
||||||
KeyPath: opts.sshKey,
|
KeyPath: opts.sshKey,
|
||||||
|
UseSSHCLI: useSSHCLI,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return initCluster(cmd.Context(), uncli, remoteMachine, opts)
|
return initCluster(cmd.Context(), uncli, remoteMachine, opts)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cmd.Flags().StringVarP(
|
||||||
|
&opts.context, "context", "c", cli.DefaultContextName,
|
||||||
|
"Name of the new context to be created in the Uncloud config to manage the cluster.",
|
||||||
|
)
|
||||||
cmd.Flags().StringVar(&opts.dnsEndpoint, "dns-endpoint", dns.DefaultUncloudDNSAPIEndpoint,
|
cmd.Flags().StringVar(&opts.dnsEndpoint, "dns-endpoint", dns.DefaultUncloudDNSAPIEndpoint,
|
||||||
"API endpoint for the Uncloud DNS service.")
|
"API endpoint for the Uncloud DNS service.")
|
||||||
cmd.Flags().StringVarP(
|
cmd.Flags().StringVarP(
|
||||||
@@ -107,15 +130,19 @@ func NewInitCommand() *cobra.Command {
|
|||||||
&opts.version, "version", "latest",
|
&opts.version, "version", "latest",
|
||||||
"Version of the Uncloud daemon to install on the machine.",
|
"Version of the Uncloud daemon to install on the machine.",
|
||||||
)
|
)
|
||||||
cmd.Flags().StringVarP(
|
cmd.Flags().BoolVarP(&opts.yes, "yes", "y", false,
|
||||||
&opts.context, "context", "c", cli.DefaultContextName,
|
"Auto-confirm prompts (e.g., resetting an already initialised machine).\n"+
|
||||||
"Name of the new context to be created in the Uncloud config to manage the cluster.",
|
"Should be explicitly set when running non-interactively, e.g., in CI/CD pipelines. [$UNCLOUD_AUTO_CONFIRM]")
|
||||||
)
|
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
func initCluster(ctx context.Context, uncli *cli.CLI, remoteMachine *cli.RemoteMachine, opts initOptions) error {
|
func initCluster(ctx context.Context, uncli *cli.CLI, remoteMachine *cli.RemoteMachine, opts initOptions) error {
|
||||||
|
if uncli.Config == nil {
|
||||||
|
// Config is nil when connecting directly to a remote machine (--connect) without using Uncloud config.
|
||||||
|
return fmt.Errorf("do not specify --connect when initialising a new cluster")
|
||||||
|
}
|
||||||
|
|
||||||
netPrefix, err := netip.ParsePrefix(opts.network)
|
netPrefix, err := netip.ParsePrefix(opts.network)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("parse network CIDR: %w", err)
|
return fmt.Errorf("parse network CIDR: %w", err)
|
||||||
@@ -142,20 +169,34 @@ func initCluster(ctx context.Context, uncli *cli.CLI, remoteMachine *cli.RemoteM
|
|||||||
RemoteMachine: remoteMachine,
|
RemoteMachine: remoteMachine,
|
||||||
SkipInstall: opts.noInstall,
|
SkipInstall: opts.noInstall,
|
||||||
Version: opts.version,
|
Version: opts.version,
|
||||||
|
AutoConfirm: opts.yes,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer client.Close()
|
defer client.Close()
|
||||||
|
|
||||||
|
// Since the cluster API needs a few moments to become ready after cluster initialisation,
|
||||||
|
// we keep the user informed during this wait. We wait here even if no Caddy or DNS is requested
|
||||||
|
// as the cluster needs to be ready so that commands such as 'uc machine ls' work immediately after init.
|
||||||
|
err = spinner.New().
|
||||||
|
Title(" Waiting for the cluster to be ready...").
|
||||||
|
Type(spinner.MiniDot).
|
||||||
|
Style(lipgloss.NewStyle().Foreground(lipgloss.Color("3"))).
|
||||||
|
TitleStyle(lipgloss.NewStyle()).
|
||||||
|
ActionWithErr(func(ctx context.Context) error {
|
||||||
|
return client.WaitClusterReady(ctx, 1*time.Minute)
|
||||||
|
}).
|
||||||
|
Run()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("wait for cluster to be ready: %w", err)
|
||||||
|
}
|
||||||
|
fmt.Println("Cluster is ready.")
|
||||||
|
|
||||||
if opts.noCaddy && opts.noDNS {
|
if opts.noCaddy && opts.noDNS {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Deploy the Caddy service to the initialised machine.
|
|
||||||
// The creation of a deployment plan talks to cluster API. Since the API needs a few moments to become available
|
|
||||||
// after cluster initialisation, we keep the user informed during this wait.
|
|
||||||
fmt.Println("Waiting for the machine to be ready...")
|
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
|
|
||||||
if !opts.noDNS {
|
if !opts.noDNS {
|
||||||
|
|||||||
@@ -14,25 +14,20 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func NewListCommand() *cobra.Command {
|
func NewListCommand() *cobra.Command {
|
||||||
var contextName string
|
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "ls",
|
Use: "ls",
|
||||||
Aliases: []string{"list"},
|
Aliases: []string{"list"},
|
||||||
Short: "List machines in a cluster.",
|
Short: "List machines in a cluster.",
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
uncli := cmd.Context().Value("cli").(*cli.CLI)
|
uncli := cmd.Context().Value("cli").(*cli.CLI)
|
||||||
return list(cmd.Context(), uncli, contextName)
|
return list(cmd.Context(), uncli)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
cmd.Flags().StringVarP(
|
|
||||||
&contextName, "context", "c", "",
|
|
||||||
"Name of the cluster context. (default is the current context)",
|
|
||||||
)
|
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
func list(ctx context.Context, uncli *cli.CLI, clusterName string) error {
|
func list(ctx context.Context, uncli *cli.CLI) error {
|
||||||
client, err := uncli.ConnectCluster(ctx, clusterName)
|
client, err := uncli.ConnectCluster(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("connect to cluster: %w", err)
|
return fmt.Errorf("connect to cluster: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func NewRenameCommand() *cobra.Command {
|
func NewRenameCommand() *cobra.Command {
|
||||||
var contextName string
|
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "rename OLD_NAME NEW_NAME",
|
Use: "rename OLD_NAME NEW_NAME",
|
||||||
Short: "Rename a machine in the cluster.",
|
Short: "Rename a machine in the cluster.",
|
||||||
@@ -20,18 +19,14 @@ configuration including network settings, public IP, and cluster membership.`,
|
|||||||
Args: cobra.ExactArgs(2),
|
Args: cobra.ExactArgs(2),
|
||||||
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 rename(cmd.Context(), uncli, contextName, args[0], args[1])
|
return rename(cmd.Context(), uncli, args[0], args[1])
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
cmd.Flags().StringVarP(
|
|
||||||
&contextName, "context", "c", "",
|
|
||||||
"Name of the cluster context. (default is the current context)",
|
|
||||||
)
|
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
func rename(ctx context.Context, uncli *cli.CLI, contextName, oldName, newName string) error {
|
func rename(ctx context.Context, uncli *cli.CLI, oldName, newName string) error {
|
||||||
client, err := uncli.ConnectCluster(ctx, contextName)
|
client, err := uncli.ConnectCluster(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ import (
|
|||||||
type removeOptions struct {
|
type removeOptions struct {
|
||||||
noReset bool
|
noReset bool
|
||||||
yes bool
|
yes bool
|
||||||
context string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewRmCommand() *cobra.Command {
|
func NewRmCommand() *cobra.Command {
|
||||||
@@ -39,8 +38,6 @@ func NewRmCommand() *cobra.Command {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd.Flags().StringVarP(&opts.context, "context", "c", "",
|
|
||||||
"Name of the cluster context. (default is the current context)")
|
|
||||||
cmd.Flags().BoolVarP(&opts.yes, "yes", "y", false,
|
cmd.Flags().BoolVarP(&opts.yes, "yes", "y", false,
|
||||||
"Do not prompt for confirmation before removing the machine.")
|
"Do not prompt for confirmation before removing the machine.")
|
||||||
cmd.Flags().BoolVar(&opts.noReset, "no-reset", false,
|
cmd.Flags().BoolVar(&opts.noReset, "no-reset", false,
|
||||||
@@ -51,14 +48,14 @@ func NewRmCommand() *cobra.Command {
|
|||||||
|
|
||||||
func remove(ctx context.Context, uncli *cli.CLI, nameOrID string, opts removeOptions) error {
|
func remove(ctx context.Context, uncli *cli.CLI, nameOrID string, opts removeOptions) error {
|
||||||
// TODO: automatically choose a connection to the machine that is not being removed.
|
// TODO: automatically choose a connection to the machine that is not being removed.
|
||||||
client, err := uncli.ConnectCluster(ctx, opts.context)
|
client, err := uncli.ConnectCluster(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("connect to cluster: %w", err)
|
return fmt.Errorf("connect to cluster: %w", err)
|
||||||
}
|
}
|
||||||
defer client.Close()
|
defer client.Close()
|
||||||
|
|
||||||
// Verify the machine exists and list all service containers on it including stopped ones.
|
// Verify the machine exists and list all service containers on it including stopped ones.
|
||||||
mctx, machines, err := api.ProxyMachinesContext(ctx, client, []string{nameOrID})
|
mctx, machines, err := client.ProxyMachinesContext(ctx, []string{nameOrID})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -155,8 +152,21 @@ func remove(ctx context.Context, uncli *cli.CLI, nameOrID string, opts removeOpt
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: remove the connection to the machine from the uncloud config if it exists. We need a way to associate
|
// Remove the connection to the machine from the uncloud config if it exists.
|
||||||
// the machine with its connection in the config, e.g. by storing the machine name in the connection metadata.
|
if uncli.Config != nil {
|
||||||
|
contextName := uncli.GetContextOverrideOrCurrent()
|
||||||
|
if context, ok := uncli.Config.Contexts[contextName]; ok {
|
||||||
|
for i, c := range context.Connections {
|
||||||
|
if c.MachineID == m.Id {
|
||||||
|
context.Connections = slices.Delete(context.Connections, i, i+1)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := uncli.Config.Save(); err != nil {
|
||||||
|
return fmt.Errorf("save config: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: If Caddy was running on this machine and a cluster domain is reserved,
|
// TODO: If Caddy was running on this machine and a cluster domain is reserved,
|
||||||
// let the user know that the DNS records should be updated.
|
// let the user know that the DNS records should be updated.
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ func NewRootCommand() *cobra.Command {
|
|||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "machine",
|
Use: "machine",
|
||||||
Aliases: []string{"m"},
|
Aliases: []string{"m"},
|
||||||
Short: "Manage machines in an Uncloud cluster.",
|
Short: "Manage machines in the cluster.",
|
||||||
}
|
}
|
||||||
cmd.AddCommand(
|
cmd.AddCommand(
|
||||||
NewAddCommand(),
|
NewAddCommand(),
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import (
|
|||||||
type updateOptions struct {
|
type updateOptions struct {
|
||||||
name string
|
name string
|
||||||
publicIP string
|
publicIP string
|
||||||
context string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewUpdateCommand() *cobra.Command {
|
func NewUpdateCommand() *cobra.Command {
|
||||||
@@ -43,10 +42,6 @@ At least one flag must be specified to perform an update operation.`,
|
|||||||
&opts.publicIP, "public-ip", "",
|
&opts.publicIP, "public-ip", "",
|
||||||
fmt.Sprintf("Public IP address of the machine for ingress configuration. Use '%s' or '' to remove the public IP.", PublicIPNone),
|
fmt.Sprintf("Public IP address of the machine for ingress configuration. Use '%s' or '' to remove the public IP.", PublicIPNone),
|
||||||
)
|
)
|
||||||
cmd.Flags().StringVarP(
|
|
||||||
&opts.context, "context", "c", "",
|
|
||||||
"Name of the cluster context. (default is the current context)",
|
|
||||||
)
|
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
@@ -57,7 +52,7 @@ func update(ctx context.Context, uncli *cli.CLI, cmd *cobra.Command, opts update
|
|||||||
return fmt.Errorf("at least one update flag must be specified (--name, --public-ip)")
|
return fmt.Errorf("at least one update flag must be specified (--name, --public-ip)")
|
||||||
}
|
}
|
||||||
|
|
||||||
client, err := uncli.ConnectCluster(ctx, opts.context)
|
client, err := uncli.ConnectCluster(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
+48
-14
@@ -6,6 +6,7 @@ import (
|
|||||||
"net/netip"
|
"net/netip"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/lipgloss"
|
||||||
"github.com/psviderski/uncloud/cmd/uncloud/caddy"
|
"github.com/psviderski/uncloud/cmd/uncloud/caddy"
|
||||||
cmdcontext "github.com/psviderski/uncloud/cmd/uncloud/context"
|
cmdcontext "github.com/psviderski/uncloud/cmd/uncloud/context"
|
||||||
"github.com/psviderski/uncloud/cmd/uncloud/dns"
|
"github.com/psviderski/uncloud/cmd/uncloud/dns"
|
||||||
@@ -13,6 +14,7 @@ import (
|
|||||||
"github.com/psviderski/uncloud/cmd/uncloud/machine"
|
"github.com/psviderski/uncloud/cmd/uncloud/machine"
|
||||||
"github.com/psviderski/uncloud/cmd/uncloud/service"
|
"github.com/psviderski/uncloud/cmd/uncloud/service"
|
||||||
"github.com/psviderski/uncloud/cmd/uncloud/volume"
|
"github.com/psviderski/uncloud/cmd/uncloud/volume"
|
||||||
|
"github.com/psviderski/uncloud/cmd/uncloud/wg"
|
||||||
"github.com/psviderski/uncloud/internal/cli"
|
"github.com/psviderski/uncloud/internal/cli"
|
||||||
"github.com/psviderski/uncloud/internal/cli/config"
|
"github.com/psviderski/uncloud/internal/cli/config"
|
||||||
"github.com/psviderski/uncloud/internal/fs"
|
"github.com/psviderski/uncloud/internal/fs"
|
||||||
@@ -24,6 +26,7 @@ import (
|
|||||||
type globalOptions struct {
|
type globalOptions struct {
|
||||||
configPath string
|
configPath string
|
||||||
connect string
|
connect string
|
||||||
|
context string
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
@@ -38,6 +41,7 @@ func main() {
|
|||||||
SilenceErrors: true,
|
SilenceErrors: true,
|
||||||
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
|
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
|
||||||
cli.BindEnvToFlag(cmd, "connect", "UNCLOUD_CONNECT")
|
cli.BindEnvToFlag(cmd, "connect", "UNCLOUD_CONNECT")
|
||||||
|
cli.BindEnvToFlag(cmd, "context", "UNCLOUD_CONTEXT")
|
||||||
cli.BindEnvToFlag(cmd, "uncloud-config", "UNCLOUD_CONFIG")
|
cli.BindEnvToFlag(cmd, "uncloud-config", "UNCLOUD_CONFIG")
|
||||||
|
|
||||||
var conn *config.MachineConnection
|
var conn *config.MachineConnection
|
||||||
@@ -55,11 +59,12 @@ func main() {
|
|||||||
conn = &config.MachineConnection{
|
conn = &config.MachineConnection{
|
||||||
SSHCLI: config.SSHDestination(dest),
|
SSHCLI: config.SSHDestination(dest),
|
||||||
}
|
}
|
||||||
} else {
|
} else if strings.HasPrefix(opts.connect, "unix://") {
|
||||||
dest := opts.connect
|
conn = &config.MachineConnection{
|
||||||
if strings.HasPrefix(dest, "ssh://") {
|
Unix: opts.connect[len("unix://"):],
|
||||||
dest = dest[len("ssh://"):]
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
dest := strings.TrimPrefix(opts.connect, "ssh://")
|
||||||
conn = &config.MachineConnection{
|
conn = &config.MachineConnection{
|
||||||
SSH: config.SSHDestination(dest),
|
SSH: config.SSHDestination(dest),
|
||||||
}
|
}
|
||||||
@@ -67,7 +72,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
configPath := fs.ExpandHomeDir(opts.configPath)
|
configPath := fs.ExpandHomeDir(opts.configPath)
|
||||||
uncli, err := cli.New(configPath, conn)
|
uncli, err := cli.New(configPath, conn, opts.context)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("initialise CLI: %w", err)
|
return fmt.Errorf("initialise CLI: %w", err)
|
||||||
}
|
}
|
||||||
@@ -78,30 +83,59 @@ func main() {
|
|||||||
|
|
||||||
cmd.PersistentFlags().StringVar(&opts.connect, "connect", "",
|
cmd.PersistentFlags().StringVar(&opts.connect, "connect", "",
|
||||||
"Connect to a remote cluster machine without using the Uncloud configuration file. [$UNCLOUD_CONNECT]\n"+
|
"Connect to a remote cluster machine without using the Uncloud configuration file. [$UNCLOUD_CONNECT]\n"+
|
||||||
"Format: [ssh://]user@host[:port], ssh+cli://user@host[:port], or tcp://host:port")
|
"Format: [ssh://]user@host[:port], ssh+cli://user@host[:port], tcp://host:port, or unix:///path/to/uncloud.sock")
|
||||||
cmd.PersistentFlags().StringVar(&opts.configPath, "uncloud-config", "~/.config/uncloud/config.yaml",
|
cmd.PersistentFlags().StringVar(&opts.configPath, "uncloud-config", "~/.config/uncloud/config.yaml",
|
||||||
"Path to the Uncloud configuration file. [$UNCLOUD_CONFIG]")
|
"Path to the Uncloud configuration file. [$UNCLOUD_CONFIG]")
|
||||||
_ = cmd.MarkPersistentFlagFilename("uncloud-config", "yaml", "yml")
|
_ = cmd.MarkPersistentFlagFilename("uncloud-config", "yaml", "yml")
|
||||||
// TODO: make --context a global flag and pass it as a value of the command context.
|
cmd.PersistentFlags().StringVarP(&opts.context, "context", "c", "",
|
||||||
|
"Name of the cluster context to use (default is the current context). [$UNCLOUD_CONTEXT]")
|
||||||
|
|
||||||
|
// Set custom help function to show links to docs and Discord only for the root 'uc' command.
|
||||||
|
defaultHelpFunc := cmd.HelpFunc()
|
||||||
|
cmd.SetHelpFunc(func(c *cobra.Command, args []string) {
|
||||||
|
defaultHelpFunc(c, args)
|
||||||
|
// Only show links for the root 'uc' command.
|
||||||
|
if c.Name() == "uc" {
|
||||||
|
urlStyle := lipgloss.NewStyle().
|
||||||
|
Underline(true).
|
||||||
|
Foreground(lipgloss.Color("12")) // light blue
|
||||||
|
|
||||||
|
fmt.Fprintln(c.OutOrStdout())
|
||||||
|
fmt.Fprintf(c.OutOrStdout(), "Learn more about Uncloud: %s\n",
|
||||||
|
urlStyle.Render("https://uncloud.run/docs"))
|
||||||
|
fmt.Fprintf(c.OutOrStdout(), "Join our Discord community: %s\n",
|
||||||
|
urlStyle.Render("https://uncloud.run/discord"))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
cmd.AddGroup(&cobra.Group{
|
||||||
|
ID: "service",
|
||||||
|
Title: "Deploy and manage services:",
|
||||||
|
})
|
||||||
|
|
||||||
cmd.AddCommand(
|
cmd.AddCommand(
|
||||||
|
NewBuildCommand(),
|
||||||
NewDeployCommand(),
|
NewDeployCommand(),
|
||||||
NewDocsCommand(),
|
NewDocsCommand(),
|
||||||
NewBuildCommand(),
|
|
||||||
NewImagesCommand(),
|
NewImagesCommand(),
|
||||||
|
NewPsCommand(),
|
||||||
caddy.NewRootCommand(),
|
caddy.NewRootCommand(),
|
||||||
cmdcontext.NewRootCommand(),
|
cmdcontext.NewRootCommand(),
|
||||||
dns.NewRootCommand(),
|
dns.NewRootCommand(),
|
||||||
image.NewRootCommand(),
|
image.NewRootCommand(),
|
||||||
machine.NewRootCommand(),
|
machine.NewRootCommand(),
|
||||||
service.NewRootCommand(),
|
service.NewRootCommand(),
|
||||||
service.NewExecCommand(),
|
service.NewExecCommand("service"),
|
||||||
service.NewInspectCommand(),
|
service.NewInspectCommand("service"),
|
||||||
service.NewListCommand(),
|
service.NewListCommand("service"),
|
||||||
service.NewRmCommand(),
|
service.NewLogsCommand("service"),
|
||||||
service.NewRunCommand(),
|
service.NewRmCommand("service"),
|
||||||
service.NewScaleCommand(),
|
service.NewRunCommand("service"),
|
||||||
|
service.NewScaleCommand("service"),
|
||||||
|
service.NewStartCommand("service"),
|
||||||
|
service.NewStopCommand("service"),
|
||||||
volume.NewRootCommand(),
|
volume.NewRootCommand(),
|
||||||
|
wg.NewRootCommand(),
|
||||||
)
|
)
|
||||||
cobra.CheckErr(cmd.Execute())
|
cobra.CheckErr(cmd.Execute())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,284 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/huh/spinner"
|
||||||
|
"github.com/charmbracelet/lipgloss"
|
||||||
|
"github.com/charmbracelet/lipgloss/table"
|
||||||
|
"github.com/docker/docker/api/types/container"
|
||||||
|
"github.com/docker/go-units"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
|
"github.com/psviderski/uncloud/internal/cli"
|
||||||
|
"github.com/psviderski/uncloud/pkg/client"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
sortByService = "service"
|
||||||
|
sortByMachine = "machine"
|
||||||
|
sortByHealth = "health"
|
||||||
|
)
|
||||||
|
|
||||||
|
type containerHighlight int
|
||||||
|
|
||||||
|
const (
|
||||||
|
highlightDanger containerHighlight = iota
|
||||||
|
highlightWarning
|
||||||
|
highlightSuccess
|
||||||
|
highlightNormal
|
||||||
|
)
|
||||||
|
|
||||||
|
type psOptions struct {
|
||||||
|
sortBy string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPsCommand() *cobra.Command {
|
||||||
|
opts := psOptions{}
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "ps",
|
||||||
|
Short: "List all service containers.",
|
||||||
|
Long: `List all service containers across all machines in the cluster.
|
||||||
|
|
||||||
|
This command provides a comprehensive overview of all running containers that are part of a service,
|
||||||
|
making it easy to see the distribution and status of containers across the cluster.`,
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
uncli := cmd.Context().Value("cli").(*cli.CLI)
|
||||||
|
|
||||||
|
if opts.sortBy != sortByService && opts.sortBy != sortByMachine && opts.sortBy != sortByHealth {
|
||||||
|
return fmt.Errorf("invalid value for --sort: %q, must be one of '%s', '%s' or '%s'", opts.sortBy,
|
||||||
|
sortByService, sortByMachine, sortByHealth)
|
||||||
|
}
|
||||||
|
|
||||||
|
return runPs(cmd.Context(), uncli, opts)
|
||||||
|
},
|
||||||
|
GroupID: "service",
|
||||||
|
}
|
||||||
|
cmd.Flags().StringVarP(&opts.sortBy, "sort", "s", sortByService,
|
||||||
|
"Sort containers by 'service', 'machine', or 'health'.")
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
type containerInfo struct {
|
||||||
|
serviceName string
|
||||||
|
machineName string
|
||||||
|
id string
|
||||||
|
name string
|
||||||
|
image string
|
||||||
|
status string
|
||||||
|
highlight containerHighlight
|
||||||
|
created time.Time
|
||||||
|
ip string
|
||||||
|
}
|
||||||
|
|
||||||
|
func runPs(ctx context.Context, uncli *cli.CLI, opts psOptions) error {
|
||||||
|
clusterClient, err := uncli.ConnectCluster(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("connect to cluster: %w", err)
|
||||||
|
}
|
||||||
|
defer clusterClient.Close()
|
||||||
|
|
||||||
|
var containers []containerInfo
|
||||||
|
err = spinner.New().
|
||||||
|
Title(" Collecting container info...").
|
||||||
|
Type(spinner.MiniDot).
|
||||||
|
Style(lipgloss.NewStyle().Foreground(lipgloss.Color("3"))).
|
||||||
|
ActionWithErr(func(ctx context.Context) error {
|
||||||
|
containers, err = collectContainers(ctx, clusterClient)
|
||||||
|
return err
|
||||||
|
}).
|
||||||
|
Run()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("collect containers: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort the containers based on the sorting option.
|
||||||
|
sort.SliceStable(containers, func(i, j int) bool {
|
||||||
|
a, b := containers[i], containers[j]
|
||||||
|
switch opts.sortBy {
|
||||||
|
case sortByHealth:
|
||||||
|
if a.highlight != b.highlight {
|
||||||
|
return a.highlight < b.highlight
|
||||||
|
}
|
||||||
|
if a.serviceName != b.serviceName {
|
||||||
|
return a.serviceName < b.serviceName
|
||||||
|
}
|
||||||
|
case sortByMachine:
|
||||||
|
if a.machineName != b.machineName {
|
||||||
|
return a.machineName < b.machineName
|
||||||
|
}
|
||||||
|
if a.serviceName != b.serviceName {
|
||||||
|
return a.serviceName < b.serviceName
|
||||||
|
}
|
||||||
|
default: // sortByService
|
||||||
|
if a.serviceName != b.serviceName {
|
||||||
|
return a.serviceName < b.serviceName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Fallback to creation time (newest first).
|
||||||
|
return a.created.After(b.created)
|
||||||
|
})
|
||||||
|
|
||||||
|
return printContainers(containers)
|
||||||
|
}
|
||||||
|
|
||||||
|
func printContainers(containers []containerInfo) error {
|
||||||
|
t := table.New().
|
||||||
|
// Remove the default border.
|
||||||
|
Border(lipgloss.Border{}).
|
||||||
|
BorderTop(false).
|
||||||
|
BorderBottom(false).
|
||||||
|
BorderLeft(false).
|
||||||
|
BorderRight(false).
|
||||||
|
BorderHeader(false).
|
||||||
|
BorderColumn(false).
|
||||||
|
StyleFunc(func(row, col int) lipgloss.Style {
|
||||||
|
if row == table.HeaderRow {
|
||||||
|
return lipgloss.NewStyle().Bold(true).PaddingRight(3)
|
||||||
|
}
|
||||||
|
// Regular style for data rows with padding.
|
||||||
|
return lipgloss.NewStyle().PaddingRight(3)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Headers("SERVICE", "CONTAINER ID", "CONTAINER NAME", "IMAGE", "CREATED", "STATUS", "IP ADDRESS", "MACHINE")
|
||||||
|
|
||||||
|
for _, ctr := range containers {
|
||||||
|
id := ctr.id
|
||||||
|
if len(id) > 12 {
|
||||||
|
id = id[:12]
|
||||||
|
}
|
||||||
|
|
||||||
|
created := units.HumanDuration(time.Now().UTC().Sub(ctr.created)) + " ago"
|
||||||
|
|
||||||
|
var statusStyle lipgloss.Style
|
||||||
|
switch ctr.highlight {
|
||||||
|
case highlightSuccess:
|
||||||
|
statusStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("2")) // Green
|
||||||
|
case highlightDanger:
|
||||||
|
statusStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("1")) // Red
|
||||||
|
case highlightWarning:
|
||||||
|
statusStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("3")) // Yellow
|
||||||
|
default:
|
||||||
|
statusStyle = lipgloss.NewStyle() // Default
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Row(
|
||||||
|
ctr.serviceName,
|
||||||
|
id,
|
||||||
|
ctr.name,
|
||||||
|
ctr.image,
|
||||||
|
created,
|
||||||
|
statusStyle.Render(ctr.status),
|
||||||
|
ctr.ip,
|
||||||
|
ctr.machineName,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println(t)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func collectContainers(ctx context.Context, cli *client.Client) ([]containerInfo, error) {
|
||||||
|
listCtx, machines, err := cli.ProxyMachinesContext(ctx, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("proxy machines context: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a map of IP to machine name for resolving response metadata
|
||||||
|
machinesNamesByIP := make(map[string]string)
|
||||||
|
for _, m := range machines {
|
||||||
|
if addr, err := m.Machine.Network.ManagementIp.ToAddr(); err == nil {
|
||||||
|
machinesNamesByIP[addr.String()] = m.Machine.Name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// List all service containers across all machines in the cluster.
|
||||||
|
machineContainers, err := cli.Docker.ListServiceContainers(
|
||||||
|
listCtx, "", container.ListOptions{All: true},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("list service containers: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var containers []containerInfo
|
||||||
|
for _, msc := range machineContainers {
|
||||||
|
// Metadata can be nil if the request was broadcasted to only one machine.
|
||||||
|
if msc.Metadata == nil && len(machineContainers) > 1 {
|
||||||
|
return nil, fmt.Errorf("something went wrong with gRPC proxy: metadata is missing for a machine response")
|
||||||
|
}
|
||||||
|
|
||||||
|
machineName := "unknown"
|
||||||
|
if msc.Metadata != nil {
|
||||||
|
var ok bool
|
||||||
|
machineName, ok = machinesNamesByIP[msc.Metadata.Machine]
|
||||||
|
if !ok {
|
||||||
|
// Fallback to machine's IP as name.
|
||||||
|
machineName = msc.Metadata.Machine
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Fallback to the first available machine name.
|
||||||
|
if len(machines) > 0 {
|
||||||
|
machineName = machines[0].Machine.Name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if msc.Metadata != nil && msc.Metadata.Error != "" {
|
||||||
|
client.PrintWarning(fmt.Sprintf("failed to list containers on machine %s: %s", machineName,
|
||||||
|
msc.Metadata.Error))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, ctr := range msc.Containers {
|
||||||
|
if ctr.Container.State == nil || ctr.Container.Config == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
status, err := ctr.Container.HumanState()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("get human state for container %s: %w", ctr.Container.ID, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var highlight containerHighlight
|
||||||
|
healthStatus := ""
|
||||||
|
if ctr.Container.State.Health != nil {
|
||||||
|
healthStatus = ctr.Container.State.Health.Status
|
||||||
|
}
|
||||||
|
|
||||||
|
if healthStatus == container.Unhealthy || ctr.Container.State.Status == "dead" || ctr.Container.State.OOMKilled || ctr.Container.State.Dead {
|
||||||
|
highlight = highlightDanger
|
||||||
|
} else if healthStatus == container.Healthy {
|
||||||
|
highlight = highlightSuccess
|
||||||
|
} else if ctr.Container.State.Status == "running" {
|
||||||
|
highlight = highlightNormal
|
||||||
|
} else { // Other non-critical but noteworthy states
|
||||||
|
highlight = highlightWarning
|
||||||
|
}
|
||||||
|
|
||||||
|
created, _ := time.Parse(time.RFC3339Nano, ctr.Container.Created)
|
||||||
|
|
||||||
|
ip := ctr.Container.UncloudNetworkIP()
|
||||||
|
ipStr := ""
|
||||||
|
// The container might not have an IP if it's not running or uses the host network.
|
||||||
|
if ip.IsValid() {
|
||||||
|
ipStr = ip.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
info := containerInfo{
|
||||||
|
serviceName: ctr.ServiceName(),
|
||||||
|
machineName: machineName,
|
||||||
|
id: ctr.Container.ID,
|
||||||
|
name: ctr.Container.Name,
|
||||||
|
image: ctr.Container.Config.Image,
|
||||||
|
status: status,
|
||||||
|
highlight: highlight,
|
||||||
|
created: created,
|
||||||
|
ip: ipStr,
|
||||||
|
}
|
||||||
|
containers = append(containers, info)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return containers, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,442 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"net/netip"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/psviderski/uncloud/internal/machine/api/pb"
|
||||||
|
"github.com/psviderski/uncloud/internal/machine/docker"
|
||||||
|
"github.com/psviderski/uncloud/pkg/client"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
"google.golang.org/protobuf/types/known/emptypb"
|
||||||
|
)
|
||||||
|
|
||||||
|
// mockDockerClient implements pb.DockerClient
|
||||||
|
type mockDockerClient struct {
|
||||||
|
pb.DockerClient // Embed to avoid implementing all methods
|
||||||
|
listResp *pb.ListServiceContainersResponse
|
||||||
|
listErr error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockDockerClient) ListServiceContainers(ctx context.Context, in *pb.ListServiceContainersRequest, opts ...grpc.CallOption) (*pb.ListServiceContainersResponse, error) {
|
||||||
|
return m.listResp, m.listErr
|
||||||
|
}
|
||||||
|
|
||||||
|
// mockClusterClient implements pb.ClusterClient
|
||||||
|
type mockClusterClient struct {
|
||||||
|
pb.ClusterClient // Embed to avoid implementing all methods
|
||||||
|
machinesResp *pb.ListMachinesResponse
|
||||||
|
machinesErr error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockClusterClient) ListMachines(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*pb.ListMachinesResponse, error) {
|
||||||
|
return m.machinesResp, m.machinesErr
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCollectContainers_NilMetadata(t *testing.T) {
|
||||||
|
// Setup container data
|
||||||
|
containerData := map[string]interface{}{
|
||||||
|
"Id": "container1",
|
||||||
|
"Name": "test-container",
|
||||||
|
"Config": map[string]interface{}{
|
||||||
|
"Image": "test-image",
|
||||||
|
},
|
||||||
|
"State": map[string]interface{}{
|
||||||
|
"Status": "running",
|
||||||
|
"StartedAt": "2023-01-01T12:00:00Z",
|
||||||
|
"FinishedAt": "0001-01-01T00:00:00Z",
|
||||||
|
},
|
||||||
|
"NetworkSettings": map[string]interface{}{
|
||||||
|
"Networks": map[string]interface{}{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
containerJSON, err := json.Marshal(containerData)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
serviceSpecJSON, err := json.Marshal(map[string]interface{}{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Setup mocks
|
||||||
|
mockDocker := &mockDockerClient{
|
||||||
|
listResp: &pb.ListServiceContainersResponse{
|
||||||
|
Messages: []*pb.MachineServiceContainers{
|
||||||
|
{
|
||||||
|
Metadata: nil, // Simulating the issue: nil metadata
|
||||||
|
Containers: []*pb.ServiceContainer{
|
||||||
|
{
|
||||||
|
Container: containerJSON,
|
||||||
|
ServiceSpec: serviceSpecJSON,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
machineIP := "10.0.0.1"
|
||||||
|
mockCluster := &mockClusterClient{
|
||||||
|
machinesResp: &pb.ListMachinesResponse{
|
||||||
|
Machines: []*pb.MachineMember{
|
||||||
|
{
|
||||||
|
Machine: &pb.MachineInfo{
|
||||||
|
Name: "machine-1",
|
||||||
|
Network: &pb.NetworkConfig{
|
||||||
|
ManagementIp: pb.NewIP(netip.MustParseAddr(machineIP)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
State: pb.MachineMember_UP,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Construct client with mocks
|
||||||
|
cli := &client.Client{
|
||||||
|
Docker: &docker.Client{
|
||||||
|
GRPCClient: mockDocker,
|
||||||
|
},
|
||||||
|
ClusterClient: mockCluster,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute
|
||||||
|
containers, err := collectContainers(context.Background(), cli)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Verify
|
||||||
|
assert.Len(t, containers, 1)
|
||||||
|
if len(containers) > 0 {
|
||||||
|
c := containers[0]
|
||||||
|
assert.Equal(t, "container1", c.id)
|
||||||
|
assert.Equal(t, "test-container", c.name)
|
||||||
|
assert.Equal(t, "machine-1", c.machineName, "Should fall back to the single machine name when metadata is nil")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCollectContainers_NilMetadata_MultipleMachines_Error(t *testing.T) {
|
||||||
|
// If we have multiple machines but receive nil metadata, it should return an error as it is ambiguous
|
||||||
|
|
||||||
|
// Setup container data
|
||||||
|
containerData1 := map[string]interface{}{
|
||||||
|
"Id": "container1",
|
||||||
|
}
|
||||||
|
containerJSON1, _ := json.Marshal(containerData1)
|
||||||
|
|
||||||
|
containerData2 := map[string]interface{}{
|
||||||
|
"Id": "container2",
|
||||||
|
"Config": map[string]interface{}{
|
||||||
|
"Image": "test-image",
|
||||||
|
},
|
||||||
|
"State": map[string]interface{}{
|
||||||
|
"Status": "running",
|
||||||
|
"StartedAt": "2023-01-01T12:00:00Z",
|
||||||
|
"FinishedAt": "0001-01-01T00:00:00Z",
|
||||||
|
},
|
||||||
|
"NetworkSettings": map[string]interface{}{
|
||||||
|
"Networks": map[string]interface{}{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
containerJSON2, _ := json.Marshal(containerData2)
|
||||||
|
|
||||||
|
serviceSpecJSON, _ := json.Marshal(map[string]interface{}{})
|
||||||
|
|
||||||
|
// Setup mocks
|
||||||
|
mockDocker := &mockDockerClient{
|
||||||
|
listResp: &pb.ListServiceContainersResponse{
|
||||||
|
Messages: []*pb.MachineServiceContainers{
|
||||||
|
{
|
||||||
|
Metadata: nil, // Nil metadata
|
||||||
|
Containers: []*pb.ServiceContainer{
|
||||||
|
{
|
||||||
|
Container: containerJSON1,
|
||||||
|
ServiceSpec: serviceSpecJSON,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Metadata: &pb.Metadata{Machine: "10.0.0.2"},
|
||||||
|
Containers: []*pb.ServiceContainer{
|
||||||
|
{
|
||||||
|
Container: containerJSON2,
|
||||||
|
ServiceSpec: serviceSpecJSON,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
mockCluster := &mockClusterClient{
|
||||||
|
machinesResp: &pb.ListMachinesResponse{
|
||||||
|
Machines: []*pb.MachineMember{
|
||||||
|
{
|
||||||
|
Machine: &pb.MachineInfo{
|
||||||
|
Name: "machine-1",
|
||||||
|
Network: &pb.NetworkConfig{
|
||||||
|
ManagementIp: pb.NewIP(netip.MustParseAddr("10.0.0.1")),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Machine: &pb.MachineInfo{
|
||||||
|
Name: "machine-2",
|
||||||
|
Network: &pb.NetworkConfig{
|
||||||
|
ManagementIp: pb.NewIP(netip.MustParseAddr("10.0.0.2")),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Construct client with mocks
|
||||||
|
cli := &client.Client{
|
||||||
|
Docker: &docker.Client{
|
||||||
|
GRPCClient: mockDocker,
|
||||||
|
},
|
||||||
|
ClusterClient: mockCluster,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute
|
||||||
|
_, err := collectContainers(context.Background(), cli)
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "metadata is missing for a machine response")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCollectContainers_MetadataPresent_MultipleMachines(t *testing.T) {
|
||||||
|
// Verify correct mapping of containers to machines when metadata is present
|
||||||
|
|
||||||
|
// Setup container data
|
||||||
|
containerData1 := map[string]interface{}{
|
||||||
|
"Id": "container1",
|
||||||
|
"Name": "container-1",
|
||||||
|
"Config": map[string]interface{}{
|
||||||
|
"Image": "image-1",
|
||||||
|
},
|
||||||
|
"State": map[string]interface{}{
|
||||||
|
"Status": "running",
|
||||||
|
"StartedAt": "2023-01-01T12:00:00Z",
|
||||||
|
"FinishedAt": "0001-01-01T00:00:00Z",
|
||||||
|
},
|
||||||
|
"NetworkSettings": map[string]interface{}{
|
||||||
|
"Networks": map[string]interface{}{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
containerJSON1, _ := json.Marshal(containerData1)
|
||||||
|
|
||||||
|
containerData2 := map[string]interface{}{
|
||||||
|
"Id": "container2",
|
||||||
|
"Name": "container-2",
|
||||||
|
"Config": map[string]interface{}{
|
||||||
|
"Image": "image-2",
|
||||||
|
},
|
||||||
|
"State": map[string]interface{}{
|
||||||
|
"Status": "running",
|
||||||
|
"StartedAt": "2023-01-01T12:00:00Z",
|
||||||
|
"FinishedAt": "0001-01-01T00:00:00Z",
|
||||||
|
},
|
||||||
|
"NetworkSettings": map[string]interface{}{
|
||||||
|
"Networks": map[string]interface{}{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
containerJSON2, _ := json.Marshal(containerData2)
|
||||||
|
|
||||||
|
serviceSpecJSON, _ := json.Marshal(map[string]interface{}{})
|
||||||
|
|
||||||
|
// Setup mocks
|
||||||
|
mockDocker := &mockDockerClient{
|
||||||
|
listResp: &pb.ListServiceContainersResponse{
|
||||||
|
Messages: []*pb.MachineServiceContainers{
|
||||||
|
{
|
||||||
|
Metadata: &pb.Metadata{Machine: "10.0.0.1"},
|
||||||
|
Containers: []*pb.ServiceContainer{
|
||||||
|
{
|
||||||
|
Container: containerJSON1,
|
||||||
|
ServiceSpec: serviceSpecJSON,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Metadata: &pb.Metadata{Machine: "10.0.0.2"},
|
||||||
|
Containers: []*pb.ServiceContainer{
|
||||||
|
{
|
||||||
|
Container: containerJSON2,
|
||||||
|
ServiceSpec: serviceSpecJSON,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
mockCluster := &mockClusterClient{
|
||||||
|
machinesResp: &pb.ListMachinesResponse{
|
||||||
|
Machines: []*pb.MachineMember{
|
||||||
|
{
|
||||||
|
Machine: &pb.MachineInfo{
|
||||||
|
Name: "machine-1",
|
||||||
|
Network: &pb.NetworkConfig{
|
||||||
|
ManagementIp: pb.NewIP(netip.MustParseAddr("10.0.0.1")),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Machine: &pb.MachineInfo{
|
||||||
|
Name: "machine-2",
|
||||||
|
Network: &pb.NetworkConfig{
|
||||||
|
ManagementIp: pb.NewIP(netip.MustParseAddr("10.0.0.2")),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
cli := &client.Client{
|
||||||
|
Docker: &docker.Client{GRPCClient: mockDocker},
|
||||||
|
ClusterClient: mockCluster,
|
||||||
|
}
|
||||||
|
|
||||||
|
containers, err := collectContainers(context.Background(), cli)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Len(t, containers, 2)
|
||||||
|
// Order is not guaranteed by the map iteration in logic or parallel fetch (though here it's mocked sequential),
|
||||||
|
// but collectContainers just appends.
|
||||||
|
// We'll find them by ID.
|
||||||
|
for _, c := range containers {
|
||||||
|
if c.id == "container1" {
|
||||||
|
assert.Equal(t, "machine-1", c.machineName)
|
||||||
|
} else if c.id == "container2" {
|
||||||
|
assert.Equal(t, "machine-2", c.machineName)
|
||||||
|
} else {
|
||||||
|
t.Errorf("unexpected container id: %s", c.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCollectContainers_NilMetadata_NoMachines(t *testing.T) {
|
||||||
|
// Case: 1 msc with nil metadata but no machines at all
|
||||||
|
|
||||||
|
containerData := map[string]interface{}{
|
||||||
|
"Id": "container1",
|
||||||
|
"Config": map[string]interface{}{
|
||||||
|
"Image": "test-image",
|
||||||
|
},
|
||||||
|
"State": map[string]interface{}{
|
||||||
|
"Status": "running",
|
||||||
|
"StartedAt": "2023-01-01T12:00:00Z",
|
||||||
|
"FinishedAt": "0001-01-01T00:00:00Z",
|
||||||
|
},
|
||||||
|
"NetworkSettings": map[string]interface{}{
|
||||||
|
"Networks": map[string]interface{}{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
containerJSON, _ := json.Marshal(containerData)
|
||||||
|
serviceSpecJSON, _ := json.Marshal(map[string]interface{}{})
|
||||||
|
|
||||||
|
mockDocker := &mockDockerClient{
|
||||||
|
listResp: &pb.ListServiceContainersResponse{
|
||||||
|
Messages: []*pb.MachineServiceContainers{
|
||||||
|
{
|
||||||
|
Metadata: nil,
|
||||||
|
Containers: []*pb.ServiceContainer{
|
||||||
|
{
|
||||||
|
Container: containerJSON,
|
||||||
|
ServiceSpec: serviceSpecJSON,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// No machines in cluster response
|
||||||
|
mockCluster := &mockClusterClient{
|
||||||
|
machinesResp: &pb.ListMachinesResponse{
|
||||||
|
Machines: []*pb.MachineMember{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
cli := &client.Client{
|
||||||
|
Docker: &docker.Client{GRPCClient: mockDocker},
|
||||||
|
ClusterClient: mockCluster,
|
||||||
|
}
|
||||||
|
|
||||||
|
containers, err := collectContainers(context.Background(), cli)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Len(t, containers, 1)
|
||||||
|
if len(containers) > 0 {
|
||||||
|
assert.Equal(t, "unknown", containers[0].machineName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCollectContainers_MetadataPresent_NotInMapping(t *testing.T) {
|
||||||
|
// Case: msc with metadata that is not in the IP-to-name mapping
|
||||||
|
|
||||||
|
containerData := map[string]interface{}{
|
||||||
|
"Id": "container1",
|
||||||
|
"Config": map[string]interface{}{
|
||||||
|
"Image": "test-image",
|
||||||
|
},
|
||||||
|
"State": map[string]interface{}{
|
||||||
|
"Status": "running",
|
||||||
|
"StartedAt": "2023-01-01T12:00:00Z",
|
||||||
|
"FinishedAt": "0001-01-01T00:00:00Z",
|
||||||
|
},
|
||||||
|
"NetworkSettings": map[string]interface{}{
|
||||||
|
"Networks": map[string]interface{}{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
containerJSON, _ := json.Marshal(containerData)
|
||||||
|
serviceSpecJSON, _ := json.Marshal(map[string]interface{}{})
|
||||||
|
|
||||||
|
mockDocker := &mockDockerClient{
|
||||||
|
listResp: &pb.ListServiceContainersResponse{
|
||||||
|
Messages: []*pb.MachineServiceContainers{
|
||||||
|
{
|
||||||
|
Metadata: &pb.Metadata{Machine: "10.0.0.99"}, // Unknown IP
|
||||||
|
Containers: []*pb.ServiceContainer{
|
||||||
|
{
|
||||||
|
Container: containerJSON,
|
||||||
|
ServiceSpec: serviceSpecJSON,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
mockCluster := &mockClusterClient{
|
||||||
|
machinesResp: &pb.ListMachinesResponse{
|
||||||
|
Machines: []*pb.MachineMember{
|
||||||
|
{
|
||||||
|
Machine: &pb.MachineInfo{
|
||||||
|
Name: "machine-1",
|
||||||
|
Network: &pb.NetworkConfig{
|
||||||
|
ManagementIp: pb.NewIP(netip.MustParseAddr("10.0.0.1")),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
cli := &client.Client{
|
||||||
|
Docker: &docker.Client{GRPCClient: mockDocker},
|
||||||
|
ClusterClient: mockCluster,
|
||||||
|
}
|
||||||
|
|
||||||
|
containers, err := collectContainers(context.Background(), cli)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Len(t, containers, 1)
|
||||||
|
if len(containers) > 0 {
|
||||||
|
// Should fallback to the IP/string in metadata
|
||||||
|
assert.Equal(t, "10.0.0.99", containers[0].machineName)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,18 +15,17 @@ type execCliOptions struct {
|
|||||||
detach bool
|
detach bool
|
||||||
interactive bool
|
interactive bool
|
||||||
noTty bool
|
noTty bool
|
||||||
context string
|
|
||||||
containerId string
|
containerId string
|
||||||
}
|
}
|
||||||
|
|
||||||
var DEFAULT_COMMAND = []string{"sh", "-c", "command -v bash >/dev/null 2>&1 && exec bash || exec sh"}
|
var DEFAULT_COMMAND = []string{"sh", "-c", "command -v bash >/dev/null 2>&1 && exec bash || exec sh"}
|
||||||
|
|
||||||
func NewExecCommand() *cobra.Command {
|
func NewExecCommand(groupID string) *cobra.Command {
|
||||||
opts := execCliOptions{}
|
opts := execCliOptions{}
|
||||||
|
|
||||||
execCmd := &cobra.Command{
|
execCmd := &cobra.Command{
|
||||||
Use: "exec [OPTIONS] SERVICE [COMMAND ARGS...]",
|
Use: "exec [OPTIONS] SERVICE [COMMAND ARGS...]",
|
||||||
Short: "Execute a command in a running service container",
|
Short: "Execute a command in a running service container.",
|
||||||
Long: `Execute a command (interactive shell by default) in a running container within a service.
|
Long: `Execute a command (interactive shell by default) in a running container within a service.
|
||||||
If the service has multiple replicas and no container ID is specified, the command will be executed in a random container.
|
If the service has multiple replicas and no container ID is specified, the command will be executed in a random container.
|
||||||
`,
|
`,
|
||||||
@@ -55,11 +54,12 @@ If the service has multiple replicas and no container ID is specified, the comma
|
|||||||
}
|
}
|
||||||
return runExec(cmd.Context(), uncli, serviceName, command, opts)
|
return runExec(cmd.Context(), uncli, serviceName, command, opts)
|
||||||
},
|
},
|
||||||
|
GroupID: groupID,
|
||||||
}
|
}
|
||||||
|
|
||||||
execCmd.Flags().BoolVarP(&opts.detach, "detach", "d", false, "Detached mode: run command in the background")
|
execCmd.Flags().BoolVarP(&opts.detach, "detach", "d", false, "Detached mode: run command in the background")
|
||||||
|
|
||||||
execCmd.Flags().BoolVarP(&opts.noTty, "no-tty", "T", !cli.IsStdoutTerminal(),
|
execCmd.Flags().BoolVarP(&opts.noTty, "no-tty", "T", false,
|
||||||
"Disable pseudo-TTY allocation. By default 'uc exec' allocates a TTY when connected to a terminal.")
|
"Disable pseudo-TTY allocation. By default 'uc exec' allocates a TTY when connected to a terminal.")
|
||||||
|
|
||||||
// Keep "-i" and "-t" flags hidden for compatibility with docker exec
|
// Keep "-i" and "-t" flags hidden for compatibility with docker exec
|
||||||
@@ -69,9 +69,6 @@ If the service has multiple replicas and no container ID is specified, the comma
|
|||||||
execCmd.Flags().BoolP("tty", "t", false, "Allocate a pseudo-TTY")
|
execCmd.Flags().BoolP("tty", "t", false, "Allocate a pseudo-TTY")
|
||||||
execCmd.Flags().MarkHidden("tty")
|
execCmd.Flags().MarkHidden("tty")
|
||||||
|
|
||||||
execCmd.Flags().StringVarP(&opts.context, "context", "c", "",
|
|
||||||
"Name of the cluster context. (default is the current context)")
|
|
||||||
|
|
||||||
// Common flags
|
// Common flags
|
||||||
execCmd.Flags().StringVar(&opts.containerId, "container", "",
|
execCmd.Flags().StringVar(&opts.containerId, "container", "",
|
||||||
"ID of the container to exec into. Accepts full ID or a unique prefix "+
|
"ID of the container to exec into. Accepts full ID or a unique prefix "+
|
||||||
@@ -85,6 +82,11 @@ If the service has multiple replicas and no container ID is specified, the comma
|
|||||||
}
|
}
|
||||||
|
|
||||||
func runExec(ctx context.Context, uncli *cli.CLI, serviceName string, command []string, opts execCliOptions) error {
|
func runExec(ctx context.Context, uncli *cli.CLI, serviceName string, command []string, opts execCliOptions) error {
|
||||||
|
// Disable TTY allocation if not connected to a terminal
|
||||||
|
if !cli.IsStdoutTerminal() {
|
||||||
|
opts.noTty = true
|
||||||
|
}
|
||||||
|
|
||||||
if !opts.detach {
|
if !opts.detach {
|
||||||
// Check if we're trying to attach to a TTY from a non-TTY client, e.g.
|
// Check if we're trying to attach to a TTY from a non-TTY client, e.g.
|
||||||
// when doing an 'cmd | uc exec ...'
|
// when doing an 'cmd | uc exec ...'
|
||||||
@@ -95,7 +97,7 @@ func runExec(ctx context.Context, uncli *cli.CLI, serviceName string, command []
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
client, err := uncli.ConnectClusterWithOptions(ctx, opts.context, cli.ConnectOptions{
|
client, err := uncli.ConnectClusterWithOptions(ctx, cli.ConnectOptions{
|
||||||
ShowProgress: false,
|
ShowProgress: false,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -4,21 +4,22 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"slices"
|
||||||
"text/tabwriter"
|
"text/tabwriter"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/docker/docker/pkg/stringid"
|
"github.com/docker/docker/pkg/stringid"
|
||||||
"github.com/docker/go-units"
|
"github.com/docker/go-units"
|
||||||
"github.com/psviderski/uncloud/internal/cli"
|
"github.com/psviderski/uncloud/internal/cli"
|
||||||
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
type inspectOptions struct {
|
type inspectOptions struct {
|
||||||
service string
|
service string
|
||||||
context string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewInspectCommand() *cobra.Command {
|
func NewInspectCommand(groupID string) *cobra.Command {
|
||||||
opts := inspectOptions{}
|
opts := inspectOptions{}
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "inspect SERVICE",
|
Use: "inspect SERVICE",
|
||||||
@@ -29,16 +30,13 @@ func NewInspectCommand() *cobra.Command {
|
|||||||
opts.service = args[0]
|
opts.service = args[0]
|
||||||
return inspect(cmd.Context(), uncli, opts)
|
return inspect(cmd.Context(), uncli, opts)
|
||||||
},
|
},
|
||||||
|
GroupID: groupID,
|
||||||
}
|
}
|
||||||
cmd.Flags().StringVarP(
|
|
||||||
&opts.context, "context", "c", "",
|
|
||||||
"Name of the cluster context. (default is the current context)",
|
|
||||||
)
|
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
func inspect(ctx context.Context, uncli *cli.CLI, opts inspectOptions) error {
|
func inspect(ctx context.Context, uncli *cli.CLI, opts inspectOptions) error {
|
||||||
client, err := uncli.ConnectCluster(ctx, opts.context)
|
client, err := uncli.ConnectCluster(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("connect to cluster: %w", err)
|
return fmt.Errorf("connect to cluster: %w", err)
|
||||||
}
|
}
|
||||||
@@ -63,18 +61,26 @@ func inspect(ctx context.Context, uncli *cli.CLI, opts inspectOptions) error {
|
|||||||
fmt.Printf("Mode: %s\n", svc.Mode)
|
fmt.Printf("Mode: %s\n", svc.Mode)
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
|
|
||||||
|
// Parse created times for sorting and display.
|
||||||
|
createdTimes := make(map[string]time.Time, len(svc.Containers))
|
||||||
|
for _, ctr := range svc.Containers {
|
||||||
|
createdTimes[ctr.Container.ID], _ = time.Parse(time.RFC3339Nano, ctr.Container.Created)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort containers by created time (newest first).
|
||||||
|
slices.SortFunc(svc.Containers, func(a, b api.MachineServiceContainer) int {
|
||||||
|
return createdTimes[b.Container.ID].Compare(createdTimes[a.Container.ID])
|
||||||
|
})
|
||||||
|
|
||||||
// Print the list of containers in a table format.
|
// Print the list of containers in a table format.
|
||||||
tw := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', 0)
|
tw := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', 0)
|
||||||
if _, err = fmt.Fprintln(tw, "CONTAINER ID\tIMAGE\tCREATED\tSTATUS\tMACHINE"); err != nil {
|
if _, err = fmt.Fprintln(tw, "CONTAINER ID\tIMAGE\tCREATED\tSTATUS\tIP ADDRESS\tMACHINE"); err != nil {
|
||||||
return fmt.Errorf("write header: %w", err)
|
return fmt.Errorf("write header: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
now := time.Now().UTC()
|
||||||
for _, ctr := range svc.Containers {
|
for _, ctr := range svc.Containers {
|
||||||
createdAt, err := time.Parse(time.RFC3339Nano, ctr.Container.Created)
|
created := units.HumanDuration(now.Sub(createdTimes[ctr.Container.ID])) + " ago"
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("parse created time: %w", err)
|
|
||||||
}
|
|
||||||
created := units.HumanDuration(time.Now().UTC().Sub(createdAt)) + " ago"
|
|
||||||
|
|
||||||
machine := machinesNamesByID[ctr.MachineID]
|
machine := machinesNamesByID[ctr.MachineID]
|
||||||
if machine == "" {
|
if machine == "" {
|
||||||
@@ -85,13 +91,21 @@ func inspect(ctx context.Context, uncli *cli.CLI, opts inspectOptions) error {
|
|||||||
return fmt.Errorf("get human state: %w", err)
|
return fmt.Errorf("get human state: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ip := ctr.Container.UncloudNetworkIP()
|
||||||
|
ipStr := ""
|
||||||
|
// The container might not have an IP if it's not running or uses the host network.
|
||||||
|
if ip.IsValid() {
|
||||||
|
ipStr = ip.String()
|
||||||
|
}
|
||||||
|
|
||||||
_, err = fmt.Fprintf(
|
_, err = fmt.Fprintf(
|
||||||
tw,
|
tw,
|
||||||
"%s\t%s\t%s\t%s\t%s\n",
|
"%s\t%s\t%s\t%s\t%s\t%s\n",
|
||||||
stringid.TruncateID(ctr.Container.ID),
|
stringid.TruncateID(ctr.Container.ID),
|
||||||
ctr.Container.Config.Image,
|
ctr.Container.Config.Image,
|
||||||
created,
|
created,
|
||||||
state,
|
state,
|
||||||
|
ipStr,
|
||||||
machine,
|
machine,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -0,0 +1,336 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"slices"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/lipgloss"
|
||||||
|
mapset "github.com/deckarep/golang-set/v2"
|
||||||
|
"github.com/docker/docker/pkg/stringid"
|
||||||
|
"github.com/psviderski/uncloud/internal/cli"
|
||||||
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
|
"github.com/psviderski/uncloud/pkg/client"
|
||||||
|
"github.com/psviderski/uncloud/pkg/client/compose"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
type logsOptions struct {
|
||||||
|
files []string
|
||||||
|
follow bool
|
||||||
|
tail string
|
||||||
|
since string
|
||||||
|
until string
|
||||||
|
utc bool
|
||||||
|
machines []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewLogsCommand(groupID string) *cobra.Command {
|
||||||
|
var options logsOptions
|
||||||
|
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "logs [SERVICE...]",
|
||||||
|
Aliases: []string{"log"},
|
||||||
|
Short: "View service logs.",
|
||||||
|
Long: `View logs from all replicas of the specified service(s) across all machines in the cluster.
|
||||||
|
|
||||||
|
If no services are specified, streams logs from all services defined in the Compose file
|
||||||
|
(compose.yaml by default or the file(s) specified with --file).`,
|
||||||
|
Example: ` # View recent logs for a service.
|
||||||
|
uc logs web
|
||||||
|
|
||||||
|
# Stream logs in real-time (follow mode).
|
||||||
|
uc logs -f web
|
||||||
|
|
||||||
|
# View logs from multiple services.
|
||||||
|
uc logs web api db
|
||||||
|
|
||||||
|
# View logs from all services in compose.yaml.
|
||||||
|
uc logs
|
||||||
|
|
||||||
|
# Show last 20 lines per replica (default is 100).
|
||||||
|
uc logs -n 20 web
|
||||||
|
|
||||||
|
# Show all logs without line limit.
|
||||||
|
uc logs -n all web
|
||||||
|
|
||||||
|
# View logs from a specific time range.
|
||||||
|
uc logs --since 3h --until 1h30m web
|
||||||
|
|
||||||
|
# View logs only from replicas running on specific machines.
|
||||||
|
uc logs -m machine1,machine2 web api`,
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
uncli := cmd.Context().Value("cli").(*cli.CLI)
|
||||||
|
return runLogs(cmd.Context(), uncli, args, options)
|
||||||
|
},
|
||||||
|
GroupID: groupID,
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Flags().StringSliceVar(&options.files, "file", nil,
|
||||||
|
"One or more Compose files to load service names from when no services are specified. (default compose.yaml)")
|
||||||
|
cmd.Flags().BoolVarP(&options.follow, "follow", "f", false,
|
||||||
|
"Continually stream new logs.")
|
||||||
|
cmd.Flags().StringSliceVarP(&options.machines, "machine", "m", nil,
|
||||||
|
"Filter logs by machine name or ID. Can be specified multiple times or as a comma-separated list.")
|
||||||
|
cmd.Flags().StringVar(&options.since, "since", "",
|
||||||
|
"Show logs generated on or after the given timestamp. Accepts relative duration, RFC 3339 date, or Unix timestamp.\n"+
|
||||||
|
"Examples:\n"+
|
||||||
|
" --since 2m30s Relative duration (2 minutes 30 seconds ago)\n"+
|
||||||
|
" --since 1h Relative duration (1 hour ago)\n"+
|
||||||
|
" --since 2025-11-24 RFC 3339 date only (midnight using local timezone)\n"+
|
||||||
|
" --since 2024-05-14T22:50:00 RFC 3339 date/time using local timezone\n"+
|
||||||
|
" --since 2024-01-31T10:30:00Z RFC 3339 date/time in UTC\n"+
|
||||||
|
" --since 1763953966 Unix timestamp (seconds since January 1, 1970)")
|
||||||
|
cmd.Flags().StringVarP(&options.tail, "tail", "n", "100",
|
||||||
|
"Show the most recent logs and limit the number of lines shown per replica. Use 'all' to show all logs.")
|
||||||
|
cmd.Flags().StringVar(&options.until, "until", "",
|
||||||
|
"Show logs generated before the given timestamp. Accepts relative duration, RFC 3339 date, or Unix timestamp.\n"+
|
||||||
|
"See --since for examples.")
|
||||||
|
cmd.Flags().BoolVar(&options.utc, "utc", false,
|
||||||
|
"Print timestamps in UTC instead of local timezone.")
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func runLogs(ctx context.Context, uncli *cli.CLI, serviceNames []string, opts logsOptions) error {
|
||||||
|
// If no services specified, try to load them from the Compose file(s).
|
||||||
|
if len(serviceNames) == 0 {
|
||||||
|
project, err := compose.LoadProject(ctx, opts.files)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("load compose file(s): %w", err)
|
||||||
|
}
|
||||||
|
// View logs for all services, including disabled by inactive profiles.
|
||||||
|
serviceNames = append(project.ServiceNames(), project.DisabledServiceNames()...)
|
||||||
|
if len(serviceNames) == 0 {
|
||||||
|
return errors.New("no services found in compose file(s)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse tail option.
|
||||||
|
tail := -1
|
||||||
|
if opts.tail != "all" {
|
||||||
|
tailInt, err := strconv.Atoi(opts.tail)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid --tail value '%s': %w", opts.tail, err)
|
||||||
|
}
|
||||||
|
tail = tailInt
|
||||||
|
}
|
||||||
|
|
||||||
|
c, err := uncli.ConnectCluster(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("connect to cluster: %w", err)
|
||||||
|
}
|
||||||
|
defer c.Close()
|
||||||
|
|
||||||
|
logsOpts := api.ServiceLogsOptions{
|
||||||
|
Follow: opts.follow,
|
||||||
|
Tail: tail,
|
||||||
|
Since: opts.since,
|
||||||
|
Until: opts.until,
|
||||||
|
Machines: cli.ExpandCommaSeparatedValues(opts.machines),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect log streams from all services.
|
||||||
|
machineIDsSet := mapset.NewSet[string]()
|
||||||
|
svcStreams := make([]<-chan api.ServiceLogEntry, 0, len(serviceNames))
|
||||||
|
for _, serviceName := range serviceNames {
|
||||||
|
svc, ch, err := c.ServiceLogs(ctx, serviceName, logsOpts)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("stream logs for service '%s': %w", serviceName, err)
|
||||||
|
}
|
||||||
|
svcStreams = append(svcStreams, ch)
|
||||||
|
|
||||||
|
machineIDs := svc.MachineIDs()
|
||||||
|
machineIDsSet.Append(machineIDs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
var stream <-chan api.ServiceLogEntry
|
||||||
|
if len(serviceNames) == 1 {
|
||||||
|
stream = svcStreams[0]
|
||||||
|
} else {
|
||||||
|
// Merge all service streams into a single sorted stream without stall detection as its handled per-service.
|
||||||
|
merger := client.NewLogMerger(svcStreams, client.LogMergerOptions{})
|
||||||
|
stream = merger.Stream()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch machine names for all machines (machineIDsSet) service containers are running on.
|
||||||
|
machines, err := c.ListMachines(ctx, &api.MachineFilter{NamesOrIDs: machineIDsSet.ToSlice()})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("list machines: %w", err)
|
||||||
|
}
|
||||||
|
machineNames := make([]string, 0, len(machines))
|
||||||
|
for _, m := range machines {
|
||||||
|
machineNames = append(machineNames, m.Machine.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
formatter := newLogFormatter(machineNames, serviceNames, opts.utc)
|
||||||
|
|
||||||
|
// Print merged logs.
|
||||||
|
for entry := range stream {
|
||||||
|
if entry.Err != nil {
|
||||||
|
formatter.printError(entry)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
formatter.printEntry(entry)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Available colors for machine/service differentiation.
|
||||||
|
var colorPalette = []lipgloss.Color{
|
||||||
|
lipgloss.Color("10"), // Bright green
|
||||||
|
lipgloss.Color("11"), // Bright yellow
|
||||||
|
lipgloss.Color("12"), // Bright blue
|
||||||
|
lipgloss.Color("13"), // Bright magenta
|
||||||
|
lipgloss.Color("14"), // Bright cyan
|
||||||
|
lipgloss.Color("2"), // Green
|
||||||
|
lipgloss.Color("3"), // Yellow
|
||||||
|
lipgloss.Color("4"), // Blue
|
||||||
|
lipgloss.Color("5"), // Magenta
|
||||||
|
lipgloss.Color("6"), // Cyan
|
||||||
|
}
|
||||||
|
|
||||||
|
// logFormatter handles formatting and printing of log entries with dynamic column alignment.
|
||||||
|
type logFormatter struct {
|
||||||
|
machineNames []string
|
||||||
|
serviceNames []string
|
||||||
|
|
||||||
|
maxMachineWidth int
|
||||||
|
maxServiceWidth int
|
||||||
|
|
||||||
|
utc bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func newLogFormatter(machineNames, serviceNames []string, utc bool) *logFormatter {
|
||||||
|
slices.Sort(machineNames)
|
||||||
|
slices.Sort(serviceNames)
|
||||||
|
|
||||||
|
maxMachineWidth := 0
|
||||||
|
for _, name := range machineNames {
|
||||||
|
if len(name) > maxMachineWidth {
|
||||||
|
maxMachineWidth = len(name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
maxServiceWidth := 0
|
||||||
|
for _, name := range serviceNames {
|
||||||
|
if len(name) > maxServiceWidth {
|
||||||
|
maxServiceWidth = len(name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &logFormatter{
|
||||||
|
machineNames: machineNames,
|
||||||
|
serviceNames: serviceNames,
|
||||||
|
maxMachineWidth: maxMachineWidth,
|
||||||
|
maxServiceWidth: maxServiceWidth,
|
||||||
|
utc: utc,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// formatTimestamp formats timestamp using local timezone or UTC if configured.
|
||||||
|
func (f *logFormatter) formatTimestamp(t time.Time) string {
|
||||||
|
if f.utc {
|
||||||
|
t = t.UTC()
|
||||||
|
} else {
|
||||||
|
t = t.In(time.Local)
|
||||||
|
}
|
||||||
|
dimStyle := lipgloss.NewStyle().Faint(true)
|
||||||
|
|
||||||
|
return dimStyle.Render(t.Format(time.StampMilli))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *logFormatter) formatMachine(name string) string {
|
||||||
|
style := lipgloss.NewStyle().Bold(true).PaddingRight(f.maxMachineWidth - len(name))
|
||||||
|
|
||||||
|
if len(f.serviceNames) == 1 {
|
||||||
|
// Machine name is coloured for single-service logs.
|
||||||
|
i := slices.Index(f.machineNames, name)
|
||||||
|
if i == -1 {
|
||||||
|
f.machineNames = append(f.machineNames, name)
|
||||||
|
i = len(f.machineNames) - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
style = style.Foreground(colorPalette[i%len(colorPalette)])
|
||||||
|
}
|
||||||
|
|
||||||
|
return style.Render(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *logFormatter) formatServiceContainer(serviceName, containerID string) string {
|
||||||
|
styleService := lipgloss.NewStyle().Bold(true).PaddingRight(f.maxServiceWidth - len(serviceName))
|
||||||
|
styleContainer := lipgloss.NewStyle().Faint(true)
|
||||||
|
|
||||||
|
if len(f.serviceNames) > 1 {
|
||||||
|
// Service name is coloured for multi-service logs.
|
||||||
|
i := slices.Index(f.serviceNames, serviceName)
|
||||||
|
if i == -1 {
|
||||||
|
f.serviceNames = append(f.serviceNames, serviceName)
|
||||||
|
i = len(f.serviceNames) - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
styleService = styleService.Foreground(colorPalette[i%len(colorPalette)])
|
||||||
|
}
|
||||||
|
|
||||||
|
return styleService.Render(serviceName) + styleContainer.Render("["+containerID[:5]+"]")
|
||||||
|
}
|
||||||
|
|
||||||
|
// printEntry prints a single log entry with proper formatting.
|
||||||
|
func (f *logFormatter) printEntry(entry api.ServiceLogEntry) {
|
||||||
|
if entry.Stream != api.LogStreamStdout && entry.Stream != api.LogStreamStderr {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var output strings.Builder
|
||||||
|
|
||||||
|
// Timestamp
|
||||||
|
output.WriteString(f.formatTimestamp(entry.Timestamp))
|
||||||
|
output.WriteString(" ")
|
||||||
|
|
||||||
|
// Machine name
|
||||||
|
output.WriteString(f.formatMachine(entry.Metadata.MachineName))
|
||||||
|
output.WriteString(" ")
|
||||||
|
|
||||||
|
// Service[container_id]
|
||||||
|
output.WriteString(f.formatServiceContainer(entry.Metadata.ServiceName, entry.Metadata.ContainerID))
|
||||||
|
output.WriteString(" ")
|
||||||
|
|
||||||
|
// Message
|
||||||
|
output.Write(entry.Message)
|
||||||
|
|
||||||
|
// Print to appropriate stream.
|
||||||
|
if entry.Stream == api.LogStreamStderr {
|
||||||
|
fmt.Fprint(os.Stderr, output.String())
|
||||||
|
} else {
|
||||||
|
fmt.Print(output.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// printError prints an error entry (e.g., stalled stream warning).
|
||||||
|
func (f *logFormatter) printError(entry api.ServiceLogEntry) {
|
||||||
|
if entry.Metadata.ContainerID != "" {
|
||||||
|
msg := fmt.Sprintf("WARNING: log stream from %s[%s] on machine '%s'",
|
||||||
|
entry.Metadata.ServiceName,
|
||||||
|
stringid.TruncateID(entry.Metadata.ContainerID),
|
||||||
|
entry.Metadata.MachineName)
|
||||||
|
|
||||||
|
if errors.Is(entry.Err, api.ErrLogStreamStalled) {
|
||||||
|
msg += " stopped responding"
|
||||||
|
} else {
|
||||||
|
msg += fmt.Sprintf(": %v", entry.Err)
|
||||||
|
}
|
||||||
|
|
||||||
|
style := lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("11")) // Bold bright yellow
|
||||||
|
fmt.Fprintln(os.Stderr, style.Render(msg))
|
||||||
|
} else {
|
||||||
|
msg := fmt.Sprintf("ERROR: %v", entry.Err)
|
||||||
|
style := lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("9")) // Bold bright red
|
||||||
|
fmt.Fprintln(os.Stderr, style.Render(msg))
|
||||||
|
}
|
||||||
|
}
|
||||||
+13
-15
@@ -4,33 +4,31 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"text/tabwriter"
|
"text/tabwriter"
|
||||||
|
|
||||||
"github.com/psviderski/uncloud/internal/cli"
|
"github.com/psviderski/uncloud/internal/cli"
|
||||||
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewListCommand() *cobra.Command {
|
func NewListCommand(groupID string) *cobra.Command {
|
||||||
var contextName string
|
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "ls",
|
Use: "ls",
|
||||||
Aliases: []string{"list"},
|
Aliases: []string{"list"},
|
||||||
Short: "List services.",
|
Short: "List services.",
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
uncli := cmd.Context().Value("cli").(*cli.CLI)
|
uncli := cmd.Context().Value("cli").(*cli.CLI)
|
||||||
return list(cmd.Context(), uncli, contextName)
|
return list(cmd.Context(), uncli)
|
||||||
},
|
},
|
||||||
|
GroupID: groupID,
|
||||||
}
|
}
|
||||||
cmd.Flags().StringVarP(
|
|
||||||
&contextName, "context", "c", "",
|
|
||||||
"Name of the cluster context. (default is the current context)",
|
|
||||||
)
|
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
func list(ctx context.Context, uncli *cli.CLI, contextName string) error {
|
func list(ctx context.Context, uncli *cli.CLI) error {
|
||||||
client, err := uncli.ConnectCluster(ctx, contextName)
|
client, err := uncli.ConnectCluster(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("connect to cluster: %w", err)
|
return fmt.Errorf("connect to cluster: %w", err)
|
||||||
}
|
}
|
||||||
@@ -41,15 +39,15 @@ func list(ctx context.Context, uncli *cli.CLI, contextName string) error {
|
|||||||
return fmt.Errorf("list services: %w", err)
|
return fmt.Errorf("list services: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
serviceNames := make(map[string]struct{}, len(services))
|
// Sort services by name.
|
||||||
haveDuplicateNames := false
|
haveDuplicateNames := false
|
||||||
for _, svc := range services {
|
slices.SortFunc(services, func(a, b api.Service) int {
|
||||||
if _, exists := serviceNames[svc.Name]; exists {
|
if a.Name == b.Name {
|
||||||
haveDuplicateNames = true
|
haveDuplicateNames = true
|
||||||
break
|
return strings.Compare(a.ID, b.ID)
|
||||||
}
|
}
|
||||||
serviceNames[svc.Name] = struct{}{}
|
return strings.Compare(a.Name, b.Name)
|
||||||
}
|
})
|
||||||
|
|
||||||
// Print the list of services in a table format.
|
// Print the list of services in a table format.
|
||||||
tw := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', 0)
|
tw := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', 0)
|
||||||
|
|||||||
@@ -11,31 +11,32 @@ import (
|
|||||||
|
|
||||||
type rmOptions struct {
|
type rmOptions struct {
|
||||||
services []string
|
services []string
|
||||||
context string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewRmCommand() *cobra.Command {
|
func NewRmCommand(groupID string) *cobra.Command {
|
||||||
opts := rmOptions{}
|
opts := rmOptions{}
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "rm SERVICE [SERVICE...]",
|
Use: "rm SERVICE [SERVICE...]",
|
||||||
Aliases: []string{"remove", "delete"},
|
Aliases: []string{"remove", "delete"},
|
||||||
Short: "Remove one or more services.",
|
Short: "Remove one or more services.",
|
||||||
Args: cobra.MinimumNArgs(1),
|
Long: `Remove one or more services.
|
||||||
|
|
||||||
|
The volumes used by the services are preserved and should be removed separately
|
||||||
|
with 'uc volume rm'. Anonymous Docker volumes (automatically created from VOLUME
|
||||||
|
directives in image Dockerfiles) are automatically removed with their containers.`,
|
||||||
|
Args: cobra.MinimumNArgs(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)
|
||||||
opts.services = args
|
opts.services = args
|
||||||
return rm(cmd.Context(), uncli, opts)
|
return rm(cmd.Context(), uncli, opts)
|
||||||
},
|
},
|
||||||
|
GroupID: groupID,
|
||||||
}
|
}
|
||||||
cmd.Flags().StringVarP(
|
|
||||||
&opts.context, "context", "c", "",
|
|
||||||
"Name of the cluster context. (default is the current context)",
|
|
||||||
)
|
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
func rm(ctx context.Context, uncli *cli.CLI, opts rmOptions) error {
|
func rm(ctx context.Context, uncli *cli.CLI, opts rmOptions) error {
|
||||||
client, err := uncli.ConnectCluster(ctx, opts.context)
|
client, err := uncli.ConnectCluster(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("connect to cluster: %w", err)
|
return fmt.Errorf("connect to cluster: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,15 +8,18 @@ func NewRootCommand() *cobra.Command {
|
|||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "service",
|
Use: "service",
|
||||||
Aliases: []string{"svc"},
|
Aliases: []string{"svc"},
|
||||||
Short: "Manage services in an Uncloud cluster.",
|
Short: "Manage services in the cluster.",
|
||||||
}
|
}
|
||||||
cmd.AddCommand(
|
cmd.AddCommand(
|
||||||
NewInspectCommand(),
|
NewExecCommand(""),
|
||||||
NewListCommand(),
|
NewInspectCommand(""),
|
||||||
NewRmCommand(),
|
NewListCommand(""),
|
||||||
NewRunCommand(),
|
NewLogsCommand(""),
|
||||||
NewScaleCommand(),
|
NewRmCommand(""),
|
||||||
NewExecCommand(),
|
NewRunCommand(""),
|
||||||
|
NewScaleCommand(""),
|
||||||
|
NewStartCommand(""),
|
||||||
|
NewStopCommand(""),
|
||||||
)
|
)
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,11 +34,9 @@ type runOptions struct {
|
|||||||
replicas uint
|
replicas uint
|
||||||
user string
|
user string
|
||||||
volumes []string
|
volumes []string
|
||||||
|
|
||||||
context string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewRunCommand() *cobra.Command {
|
func NewRunCommand(groupID string) *cobra.Command {
|
||||||
opts := runOptions{}
|
opts := runOptions{}
|
||||||
|
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
@@ -56,6 +54,7 @@ func NewRunCommand() *cobra.Command {
|
|||||||
|
|
||||||
return run(cmd.Context(), uncli, opts)
|
return run(cmd.Context(), uncli, opts)
|
||||||
},
|
},
|
||||||
|
GroupID: groupID,
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd.Flags().StringVar(&opts.caddyfile, "caddyfile", "",
|
cmd.Flags().StringVar(&opts.caddyfile, "caddyfile", "",
|
||||||
@@ -112,11 +111,6 @@ func NewRunCommand() *cobra.Command {
|
|||||||
" -v /data/uploads:/app/uploads Bind mount /data/uploads host directory to /app/uploads in container\n"+
|
" -v /data/uploads:/app/uploads Bind mount /data/uploads host directory to /app/uploads in container\n"+
|
||||||
" -v /host/path:/container/path:ro Bind mount a host directory or file as read-only")
|
" -v /host/path:/container/path:ro Bind mount a host directory or file as read-only")
|
||||||
|
|
||||||
cmd.Flags().StringVarP(
|
|
||||||
&opts.context, "context", "c", "",
|
|
||||||
"Name of the cluster context to run the service in. (default is the current context)",
|
|
||||||
)
|
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,7 +120,7 @@ func run(ctx context.Context, uncli *cli.CLI, opts runOptions) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
clusterClient, err := uncli.ConnectCluster(ctx, opts.context)
|
clusterClient, err := uncli.ConnectCluster(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("connect to cluster: %w", err)
|
return fmt.Errorf("connect to cluster: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,10 +15,9 @@ import (
|
|||||||
type scaleOptions struct {
|
type scaleOptions struct {
|
||||||
service string
|
service string
|
||||||
replicas uint
|
replicas uint
|
||||||
context string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewScaleCommand() *cobra.Command {
|
func NewScaleCommand(groupID string) *cobra.Command {
|
||||||
opts := scaleOptions{}
|
opts := scaleOptions{}
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "scale SERVICE REPLICAS",
|
Use: "scale SERVICE REPLICAS",
|
||||||
@@ -37,13 +36,9 @@ func NewScaleCommand() *cobra.Command {
|
|||||||
|
|
||||||
return scale(cmd.Context(), uncli, opts)
|
return scale(cmd.Context(), uncli, opts)
|
||||||
},
|
},
|
||||||
|
GroupID: groupID,
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd.Flags().StringVarP(
|
|
||||||
&opts.context, "context", "c", "",
|
|
||||||
"Name of the cluster context. (default is the current context)",
|
|
||||||
)
|
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,7 +52,7 @@ func scale(ctx context.Context, uncli *cli.CLI, opts scaleOptions) error {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
clusterClient, err := uncli.ConnectCluster(ctx, opts.context)
|
clusterClient, err := uncli.ConnectCluster(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("connect to cluster: %w", err)
|
return fmt.Errorf("connect to cluster: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
//nolint:dupl
|
||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/docker/compose/v2/pkg/progress"
|
||||||
|
"github.com/psviderski/uncloud/internal/cli"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
type startOptions struct {
|
||||||
|
services []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewStartCommand(groupID string) *cobra.Command {
|
||||||
|
opts := startOptions{}
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "start SERVICE [SERVICE...]",
|
||||||
|
Short: "Start one or more services.",
|
||||||
|
Long: `Start one or more previously stopped services.
|
||||||
|
|
||||||
|
Starts all containers of the specified service(s) across all machines in the cluster.
|
||||||
|
Services can be specified by name or ID.`,
|
||||||
|
Args: cobra.MinimumNArgs(1),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
uncli := cmd.Context().Value("cli").(*cli.CLI)
|
||||||
|
opts.services = args
|
||||||
|
return start(cmd.Context(), uncli, opts)
|
||||||
|
},
|
||||||
|
GroupID: groupID,
|
||||||
|
}
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func start(ctx context.Context, uncli *cli.CLI, opts startOptions) error {
|
||||||
|
client, err := uncli.ConnectCluster(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("connect to cluster: %w", err)
|
||||||
|
}
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
for _, s := range opts.services {
|
||||||
|
err = progress.RunWithTitle(ctx, func(ctx context.Context) error {
|
||||||
|
if err = client.StartService(ctx, s); err != nil {
|
||||||
|
return fmt.Errorf("start service '%s': %w", s, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}, uncli.ProgressOut(), "Starting service "+s)
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
//nolint:dupl
|
||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/docker/compose/v2/pkg/progress"
|
||||||
|
"github.com/docker/docker/api/types/container"
|
||||||
|
"github.com/psviderski/uncloud/internal/cli"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
type stopOptions struct {
|
||||||
|
services []string
|
||||||
|
signal string
|
||||||
|
timeout int
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewStopCommand(groupID string) *cobra.Command {
|
||||||
|
opts := stopOptions{}
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "stop SERVICE [SERVICE...]",
|
||||||
|
Short: "Stop one or more services.",
|
||||||
|
Long: `Stop one or more running services.
|
||||||
|
|
||||||
|
Gracefully stops all containers of the specified service(s) across all machines in the cluster.
|
||||||
|
Services can be specified by name or ID. Stopped services can be restarted with 'uc start'.`,
|
||||||
|
Args: cobra.MinimumNArgs(1),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
uncli := cmd.Context().Value("cli").(*cli.CLI)
|
||||||
|
opts.services = args
|
||||||
|
return stop(cmd.Context(), uncli, opts)
|
||||||
|
},
|
||||||
|
GroupID: groupID,
|
||||||
|
}
|
||||||
|
cmd.Flags().StringVarP(&opts.signal, "signal", "s", "",
|
||||||
|
"Signal to send to each container's main process.\n"+
|
||||||
|
"Can be a signal name (SIGTERM, SIGINT, SIGHUP, etc.) or a number. (default SIGTERM)")
|
||||||
|
cmd.Flags().IntVarP(&opts.timeout, "timeout", "t", 10,
|
||||||
|
"Seconds to wait for each container to stop gracefully before forcibly killing it with SIGKILL.\n"+
|
||||||
|
"Use -1 to wait indefinitely.")
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func stop(ctx context.Context, uncli *cli.CLI, opts stopOptions) error {
|
||||||
|
client, err := uncli.ConnectCluster(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("connect to cluster: %w", err)
|
||||||
|
}
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
stopOpts := container.StopOptions{
|
||||||
|
Signal: opts.signal,
|
||||||
|
Timeout: &opts.timeout,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, s := range opts.services {
|
||||||
|
err = progress.RunWithTitle(ctx, func(ctx context.Context) error {
|
||||||
|
if err = client.StopService(ctx, s, stopOpts); err != nil {
|
||||||
|
return fmt.Errorf("stop service '%s': %w", s, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}, uncli.ProgressOut(), "Stopping service "+s)
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
@@ -19,7 +19,6 @@ type createOptions struct {
|
|||||||
driverOpts []string
|
driverOpts []string
|
||||||
labels []string
|
labels []string
|
||||||
machine string
|
machine string
|
||||||
context string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewCreateCommand() *cobra.Command {
|
func NewCreateCommand() *cobra.Command {
|
||||||
@@ -50,14 +49,12 @@ func NewCreateCommand() *cobra.Command {
|
|||||||
"Labels to assign to the volume in the form of 'key=value' pairs. Can be specified multiple times.")
|
"Labels to assign to the volume in the form of 'key=value' pairs. Can be specified multiple times.")
|
||||||
cmd.Flags().StringVarP(&opts.machine, "machine", "m", "",
|
cmd.Flags().StringVarP(&opts.machine, "machine", "m", "",
|
||||||
"Name or ID of the machine to create the volume on.")
|
"Name or ID of the machine to create the volume on.")
|
||||||
cmd.Flags().StringVarP(&opts.context, "context", "c", "",
|
|
||||||
"Name of the cluster context. (default is the current context)")
|
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
func create(ctx context.Context, uncli *cli.CLI, name string, opts createOptions) error {
|
func create(ctx context.Context, uncli *cli.CLI, name string, opts createOptions) error {
|
||||||
client, err := uncli.ConnectCluster(ctx, opts.context)
|
client, err := uncli.ConnectCluster(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("connect to cluster: %w", err)
|
return fmt.Errorf("connect to cluster: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import (
|
|||||||
|
|
||||||
type inspectOptions struct {
|
type inspectOptions struct {
|
||||||
machine string
|
machine string
|
||||||
context string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewInspectCommand() *cobra.Command {
|
func NewInspectCommand() *cobra.Command {
|
||||||
@@ -32,14 +31,12 @@ func NewInspectCommand() *cobra.Command {
|
|||||||
cmd.Flags().StringVarP(&opts.machine, "machine", "m", "",
|
cmd.Flags().StringVarP(&opts.machine, "machine", "m", "",
|
||||||
"Name or ID of the machine where the volume is located. "+
|
"Name or ID of the machine where the volume is located. "+
|
||||||
"If not specified, the volume will be searched across all machines.")
|
"If not specified, the volume will be searched across all machines.")
|
||||||
cmd.Flags().StringVarP(&opts.context, "context", "c", "",
|
|
||||||
"Name of the cluster context. (default is the current context)")
|
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
func inspect(ctx context.Context, uncli *cli.CLI, name string, opts inspectOptions) error {
|
func inspect(ctx context.Context, uncli *cli.CLI, name string, opts inspectOptions) error {
|
||||||
client, err := uncli.ConnectCluster(ctx, opts.context)
|
client, err := uncli.ConnectCluster(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("connect to cluster: %w", err)
|
return fmt.Errorf("connect to cluster: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ import (
|
|||||||
type listOptions struct {
|
type listOptions struct {
|
||||||
machines []string
|
machines []string
|
||||||
quiet bool
|
quiet bool
|
||||||
context string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewListCommand() *cobra.Command {
|
func NewListCommand() *cobra.Command {
|
||||||
@@ -38,14 +37,11 @@ func NewListCommand() *cobra.Command {
|
|||||||
cmd.Flags().BoolVarP(&opts.quiet, "quiet", "q", false,
|
cmd.Flags().BoolVarP(&opts.quiet, "quiet", "q", false,
|
||||||
"Only display volume names.")
|
"Only display volume names.")
|
||||||
|
|
||||||
cmd.Flags().StringVarP(&opts.context, "context", "c", "",
|
|
||||||
"Name of the cluster context. (default is the current context)")
|
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
func list(ctx context.Context, uncli *cli.CLI, opts listOptions) error {
|
func list(ctx context.Context, uncli *cli.CLI, opts listOptions) error {
|
||||||
client, err := uncli.ConnectCluster(ctx, opts.context)
|
client, err := uncli.ConnectCluster(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("connect to cluster: %w", err)
|
return fmt.Errorf("connect to cluster: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ type removeOptions struct {
|
|||||||
force bool
|
force bool
|
||||||
machines []string
|
machines []string
|
||||||
yes bool
|
yes bool
|
||||||
context string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewRemoveCommand() *cobra.Command {
|
func NewRemoveCommand() *cobra.Command {
|
||||||
@@ -41,14 +40,11 @@ func NewRemoveCommand() *cobra.Command {
|
|||||||
cmd.Flags().BoolVarP(&opts.yes, "yes", "y", false,
|
cmd.Flags().BoolVarP(&opts.yes, "yes", "y", false,
|
||||||
"Do not prompt for confirmation before removing the volume(s).")
|
"Do not prompt for confirmation before removing the volume(s).")
|
||||||
|
|
||||||
cmd.Flags().StringVarP(&opts.context, "context", "c", "",
|
|
||||||
"Name of the cluster context. (default is the current context)")
|
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
func remove(ctx context.Context, uncli *cli.CLI, names []string, opts removeOptions) error {
|
func remove(ctx context.Context, uncli *cli.CLI, names []string, opts removeOptions) error {
|
||||||
client, err := uncli.ConnectCluster(ctx, opts.context)
|
client, err := uncli.ConnectCluster(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("connect to cluster: %w", err)
|
return fmt.Errorf("connect to cluster: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
func NewRootCommand() *cobra.Command {
|
func NewRootCommand() *cobra.Command {
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "volume",
|
Use: "volume",
|
||||||
Short: "Manage volumes in an Uncloud cluster.",
|
Short: "Manage volumes in the cluster.",
|
||||||
}
|
}
|
||||||
cmd.AddCommand(
|
cmd.AddCommand(
|
||||||
NewCreateCommand(),
|
NewCreateCommand(),
|
||||||
|
|||||||
@@ -0,0 +1,132 @@
|
|||||||
|
package wg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"text/tabwriter"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/docker/go-units"
|
||||||
|
"github.com/psviderski/uncloud/internal/cli"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
||||||
|
"google.golang.org/grpc/codes"
|
||||||
|
"google.golang.org/grpc/status"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewRootCommand() *cobra.Command {
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "wg",
|
||||||
|
Short: "Inspect WireGuard network",
|
||||||
|
}
|
||||||
|
cmd.AddCommand(newShowCommand())
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
type showOptions struct {
|
||||||
|
machine string
|
||||||
|
}
|
||||||
|
|
||||||
|
func newShowCommand() *cobra.Command {
|
||||||
|
opts := showOptions{}
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "show",
|
||||||
|
Short: "Show WireGuard network configuration for a machine.",
|
||||||
|
Long: "Show the WireGuard network configuration for the machine currently connected to " +
|
||||||
|
"(or specified by the global --connect flag).",
|
||||||
|
Args: cobra.NoArgs,
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
uncli := cmd.Context().Value("cli").(*cli.CLI)
|
||||||
|
return runShow(cmd.Context(), uncli, opts)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
cmd.Flags().StringVarP(&opts.machine, "machine", "m", "",
|
||||||
|
"Name or ID of the machine to show the configuration for. (default is connected machine)")
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func runShow(ctx context.Context, uncli *cli.CLI, opts showOptions) error {
|
||||||
|
client, err := uncli.ConnectCluster(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("connection failed: %w", err)
|
||||||
|
}
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
if opts.machine != "" {
|
||||||
|
// Proxy requests to the specified machine.
|
||||||
|
ctx, _, err = client.ProxyMachinesContext(ctx, []string{opts.machine})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := client.MachineClient.InspectWireGuardNetwork(ctx, nil)
|
||||||
|
if err != nil {
|
||||||
|
if status.Code(err) == codes.Unimplemented {
|
||||||
|
return fmt.Errorf("inspect WireGuard network: "+
|
||||||
|
"make sure the target machine is running uncloudd daemon version >= 0.16.0: %w", err)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
machines, err := client.ListMachines(ctx, nil)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("list machines: %w", err)
|
||||||
|
}
|
||||||
|
machinesNamesByPublicKey := make(map[string]string)
|
||||||
|
for _, m := range machines {
|
||||||
|
publicKey := wgtypes.Key(m.Machine.Network.PublicKey).String()
|
||||||
|
machinesNamesByPublicKey[publicKey] = m.Machine.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch the machine's name for more descriptive output
|
||||||
|
inspectResp, err := client.MachineClient.InspectMachine(ctx, nil)
|
||||||
|
if err == nil {
|
||||||
|
fmt.Printf("Machine name: %s\n", inspectResp.Machines[0].Machine.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("WireGuard interface: %s\n", resp.InterfaceName)
|
||||||
|
fmt.Printf("WireGuard public key: %s\n", wgtypes.Key(resp.PublicKey).String())
|
||||||
|
fmt.Printf("WireGuard port: %d\n", resp.ListenPort)
|
||||||
|
fmt.Println()
|
||||||
|
|
||||||
|
if len(resp.Peers) == 0 {
|
||||||
|
fmt.Println("No WireGuard peers configured.")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
tw := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', 0)
|
||||||
|
if _, err = fmt.Fprintln(tw, "PEER\tPUBLIC KEY\tENDPOINT\tHANDSHAKE\tRECEIVED\tSENT\tALLOWED IPS"); err != nil {
|
||||||
|
return fmt.Errorf("write header: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, peer := range resp.Peers {
|
||||||
|
machineName, ok := machinesNamesByPublicKey[wgtypes.Key(peer.PublicKey).String()]
|
||||||
|
if !ok {
|
||||||
|
machineName = "(unknown)"
|
||||||
|
}
|
||||||
|
|
||||||
|
lastHandshake := ""
|
||||||
|
if peer.LastHandshakeTime != nil {
|
||||||
|
lastHandshake = time.Since(peer.LastHandshakeTime.AsTime()).Round(time.Second).String() + " ago"
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = fmt.Fprintf(
|
||||||
|
tw,
|
||||||
|
"%s\t%s\t%s\t%s\t%s\t%s\t%s\n",
|
||||||
|
machineName,
|
||||||
|
wgtypes.Key(peer.PublicKey).String(),
|
||||||
|
peer.Endpoint,
|
||||||
|
lastHandshake,
|
||||||
|
units.HumanSize(float64(peer.ReceiveBytes)),
|
||||||
|
units.HumanSize(float64(peer.TransmitBytes)),
|
||||||
|
strings.Join(peer.AllowedIps, ", "),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("write row: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return tw.Flush()
|
||||||
|
}
|
||||||
@@ -9,9 +9,10 @@ require (
|
|||||||
github.com/alecthomas/chroma/v2 v2.20.0
|
github.com/alecthomas/chroma/v2 v2.20.0
|
||||||
github.com/caddyserver/caddy/v2 v2.8.4
|
github.com/caddyserver/caddy/v2 v2.8.4
|
||||||
github.com/cenkalti/backoff/v4 v4.3.0
|
github.com/cenkalti/backoff/v4 v4.3.0
|
||||||
github.com/charmbracelet/bubbles v0.20.0
|
github.com/charmbracelet/bubbles v0.21.0
|
||||||
github.com/charmbracelet/bubbletea v1.3.9
|
github.com/charmbracelet/bubbletea v1.3.10
|
||||||
github.com/charmbracelet/huh v0.6.0
|
github.com/charmbracelet/huh v0.6.0
|
||||||
|
github.com/charmbracelet/huh/spinner v0.0.0-20251110114415-25888d17260b
|
||||||
github.com/charmbracelet/lipgloss v1.1.0
|
github.com/charmbracelet/lipgloss v1.1.0
|
||||||
github.com/compose-spec/compose-go/v2 v2.9.0
|
github.com/compose-spec/compose-go/v2 v2.9.0
|
||||||
github.com/containerd/errdefs v1.0.0
|
github.com/containerd/errdefs v1.0.0
|
||||||
@@ -45,7 +46,7 @@ require (
|
|||||||
github.com/muesli/termenv v0.16.0
|
github.com/muesli/termenv v0.16.0
|
||||||
github.com/opencontainers/go-digest v1.0.0
|
github.com/opencontainers/go-digest v1.0.0
|
||||||
github.com/opencontainers/image-spec v1.1.1
|
github.com/opencontainers/image-spec v1.1.1
|
||||||
github.com/psviderski/unregistry v0.3.1
|
github.com/psviderski/unregistry v0.4.1
|
||||||
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/siderolabs/grpc-proxy v0.5.1
|
||||||
@@ -149,7 +150,7 @@ require (
|
|||||||
github.com/eiannone/keyboard v0.0.0-20220611211555-0d226195f203 // indirect
|
github.com/eiannone/keyboard v0.0.0-20220611211555-0d226195f203 // indirect
|
||||||
github.com/emicklei/go-restful/v3 v3.11.0 // indirect
|
github.com/emicklei/go-restful/v3 v3.11.0 // 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.18.0 // indirect
|
||||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||||
github.com/fsnotify/fsevents v0.2.0 // indirect
|
github.com/fsnotify/fsevents v0.2.0 // indirect
|
||||||
github.com/fvbommel/sortorder v1.1.0 // indirect
|
github.com/fvbommel/sortorder v1.1.0 // indirect
|
||||||
|
|||||||
@@ -168,22 +168,24 @@ github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL
|
|||||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
github.com/charmbracelet/bubbles v0.20.0 h1:jSZu6qD8cRQ6k9OMfR1WlM+ruM8fkPWkHvQWD9LIutE=
|
github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs=
|
||||||
github.com/charmbracelet/bubbles v0.20.0/go.mod h1:39slydyswPy+uVOHZ5x/GjwVAFkCsV8IIVy+4MhzwwU=
|
github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg=
|
||||||
github.com/charmbracelet/bubbletea v1.3.9 h1:OBYdfRo6QnlIcXNmcoI2n1NNS65Nk6kI2L2FO1puS/4=
|
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
|
||||||
github.com/charmbracelet/bubbletea v1.3.9/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
|
github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
|
||||||
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
|
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
|
||||||
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
|
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
|
||||||
github.com/charmbracelet/huh v0.6.0 h1:mZM8VvZGuE0hoDXq6XLxRtgfWyTI3b2jZNKh0xWmax8=
|
github.com/charmbracelet/huh v0.6.0 h1:mZM8VvZGuE0hoDXq6XLxRtgfWyTI3b2jZNKh0xWmax8=
|
||||||
github.com/charmbracelet/huh v0.6.0/go.mod h1:GGNKeWCeNzKpEOh/OJD8WBwTQjV3prFAtQPpLv+AVwU=
|
github.com/charmbracelet/huh v0.6.0/go.mod h1:GGNKeWCeNzKpEOh/OJD8WBwTQjV3prFAtQPpLv+AVwU=
|
||||||
|
github.com/charmbracelet/huh/spinner v0.0.0-20251110114415-25888d17260b h1:oefmPctgff7OB4cDz3Ndp+ewwrh7i+fwth70qvZCieI=
|
||||||
|
github.com/charmbracelet/huh/spinner v0.0.0-20251110114415-25888d17260b/go.mod h1:OMqKat/mm9a/qOnpuNOPyYO9bPzRNnmzLnRZT5KYltg=
|
||||||
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
|
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
|
||||||
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
|
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
|
||||||
github.com/charmbracelet/x/ansi v0.10.1 h1:rL3Koar5XvX0pHGfovN03f5cxLbCF2YvLeyz7D2jVDQ=
|
github.com/charmbracelet/x/ansi v0.10.1 h1:rL3Koar5XvX0pHGfovN03f5cxLbCF2YvLeyz7D2jVDQ=
|
||||||
github.com/charmbracelet/x/ansi v0.10.1/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE=
|
github.com/charmbracelet/x/ansi v0.10.1/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE=
|
||||||
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8=
|
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8=
|
||||||
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
|
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
|
||||||
github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b h1:MnAMdlwSltxJyULnrYbkZpp4k58Co7Tah3ciKhSNo0Q=
|
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ=
|
||||||
github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
|
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
|
||||||
github.com/charmbracelet/x/exp/strings v0.0.0-20240919170804-a4978c8e603a h1:JMdM89Udp/cOl5tC3MuUJXTPE/nAdU1oyt9jRU44qq8=
|
github.com/charmbracelet/x/exp/strings v0.0.0-20240919170804-a4978c8e603a h1:JMdM89Udp/cOl5tC3MuUJXTPE/nAdU1oyt9jRU44qq8=
|
||||||
github.com/charmbracelet/x/exp/strings v0.0.0-20240919170804-a4978c8e603a/go.mod h1:pBhA0ybfXv6hDjQUZ7hk1lVxBiUbupdw5R31yPUViVQ=
|
github.com/charmbracelet/x/exp/strings v0.0.0-20240919170804-a4978c8e603a/go.mod h1:pBhA0ybfXv6hDjQUZ7hk1lVxBiUbupdw5R31yPUViVQ=
|
||||||
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
|
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
|
||||||
@@ -344,8 +346,8 @@ github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97
|
|||||||
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0=
|
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0=
|
||||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||||
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
|
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
|
||||||
github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4=
|
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||||
github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI=
|
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
||||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||||
github.com/flynn/noise v1.1.0 h1:KjPQoQCEFdZDiP03phOvGi11+SVVhBG2wOWAorLsstg=
|
github.com/flynn/noise v1.1.0 h1:KjPQoQCEFdZDiP03phOvGi11+SVVhBG2wOWAorLsstg=
|
||||||
@@ -1038,8 +1040,8 @@ github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDa
|
|||||||
github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
|
github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
|
||||||
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
|
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
|
||||||
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
|
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
|
||||||
github.com/psviderski/unregistry v0.3.1 h1:2xQ3uMNMyXAASqq9+kGUgNMIFOvPSMIFGdz47uDfjhQ=
|
github.com/psviderski/unregistry v0.4.1 h1:KedQTjyj8hHbzygK9uw5YS10amMZ5cjSYHqSjUUBhoE=
|
||||||
github.com/psviderski/unregistry v0.3.1/go.mod h1:DXLnzrskO5NuRlzZhpbyo1E6KpirnctUvq6WtCGkpUQ=
|
github.com/psviderski/unregistry v0.4.1/go.mod h1:DXLnzrskO5NuRlzZhpbyo1E6KpirnctUvq6WtCGkpUQ=
|
||||||
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
|
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
|
||||||
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
|
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
|
||||||
github.com/quic-go/quic-go v0.48.2 h1:wsKXZPeGWpMpCGSWqOcqpW2wZYic/8T3aqiOID0/KWE=
|
github.com/quic-go/quic-go v0.48.2 h1:wsKXZPeGWpMpCGSWqOcqpW2wZYic/8T3aqiOID0/KWE=
|
||||||
|
|||||||
@@ -40,8 +40,6 @@ type BuildServicesOptions struct {
|
|||||||
PushRegistry bool
|
PushRegistry bool
|
||||||
|
|
||||||
// Cluster-specific options (only used if PushCluster is true).
|
// Cluster-specific options (only used if PushCluster is true).
|
||||||
// Context is the name of the cluster context.
|
|
||||||
Context string
|
|
||||||
// Machines is a list of machine names or IDs to push the image to. If empty, images are pushed to all machines.
|
// Machines is a list of machine names or IDs to push the image to. If empty, images are pushed to all machines.
|
||||||
Machines []string
|
Machines []string
|
||||||
}
|
}
|
||||||
@@ -95,7 +93,7 @@ func (cli *CLI) BuildServices(ctx context.Context, project *composetypes.Project
|
|||||||
// Add a line break after the build output.
|
// Add a line break after the build output.
|
||||||
fmt.Fprintln(cli.ProgressOut())
|
fmt.Fprintln(cli.ProgressOut())
|
||||||
|
|
||||||
clusterClient, err := cli.ConnectCluster(ctx, opts.Context)
|
clusterClient, err := cli.ConnectCluster(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("connect to cluster: %w", err)
|
return fmt.Errorf("connect to cluster: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
+112
-24
@@ -28,14 +28,16 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type CLI struct {
|
type CLI struct {
|
||||||
Config *config.Config
|
Config *config.Config
|
||||||
conn *config.MachineConnection
|
conn *config.MachineConnection
|
||||||
|
contextOverride string
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates a new CLI instance with the given config path or remote machine connection.
|
// New creates a new CLI instance with the given config path or remote machine connection.
|
||||||
// If the connection is provided, the config is ignored for all operations which is useful for interacting with
|
// If the connection is provided, the config is ignored for all operations which is useful for interacting with
|
||||||
// a cluster without creating a config.
|
// a cluster without creating a config.
|
||||||
func New(configPath string, conn *config.MachineConnection) (*CLI, error) {
|
// If a non-empty context name is given, it will override the current default.
|
||||||
|
func New(configPath string, conn *config.MachineConnection, contextName string) (*CLI, error) {
|
||||||
if conn != nil {
|
if conn != nil {
|
||||||
return &CLI{conn: conn}, nil
|
return &CLI{conn: conn}, nil
|
||||||
}
|
}
|
||||||
@@ -46,7 +48,8 @@ func New(configPath string, conn *config.MachineConnection) (*CLI, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return &CLI{
|
return &CLI{
|
||||||
Config: cfg,
|
Config: cfg,
|
||||||
|
contextOverride: contextName,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,19 +71,28 @@ func (cli *CLI) SetCurrentContext(name string) error {
|
|||||||
return cli.Config.Save()
|
return cli.Config.Save()
|
||||||
}
|
}
|
||||||
|
|
||||||
// ConnectCluster connects to a cluster using the given context name or the current context if not specified.
|
func (cli *CLI) GetContextOverrideOrCurrent() string {
|
||||||
|
contextName := cli.contextOverride
|
||||||
|
if contextName == "" {
|
||||||
|
contextName = cli.Config.CurrentContext
|
||||||
|
}
|
||||||
|
return contextName
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConnectCluster connects to a cluster using the context override or the current context if not specified.
|
||||||
// If the CLI was initialised with a machine connection, the config is ignored and the connection is used instead.
|
// If the CLI was initialised with a machine connection, the config is ignored and the connection is used instead.
|
||||||
func (cli *CLI) ConnectCluster(ctx context.Context, contextName string) (*client.Client, error) {
|
func (cli *CLI) ConnectCluster(ctx context.Context) (*client.Client, error) {
|
||||||
return cli.ConnectClusterWithOptions(ctx, contextName, ConnectOptions{
|
return cli.ConnectClusterWithOptions(ctx, ConnectOptions{
|
||||||
// Default to showing progress for CLI usage.
|
// Default to showing progress for CLI usage.
|
||||||
ShowProgress: true,
|
ShowProgress: true,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// ConnectClusterWithOptions connects to a cluster using the given context name and options.
|
// ConnectClusterWithOptions connects to a cluster with the given options.
|
||||||
// If the CLI was initialised with a machine connection, the config is ignored and the connection is used instead.
|
// If the CLI was initialised with a machine connection, the config is ignored and the connection is used instead.
|
||||||
|
// If the CLI has an override context, it is used instead of the current default.
|
||||||
// Options are useful when using the CLI as a library where you may want to disable visual feedback.
|
// Options are useful when using the CLI as a library where you may want to disable visual feedback.
|
||||||
func (cli *CLI) ConnectClusterWithOptions(ctx context.Context, contextName string, opts ConnectOptions) (*client.Client, error) {
|
func (cli *CLI) ConnectClusterWithOptions(ctx context.Context, opts ConnectOptions) (*client.Client, error) {
|
||||||
if cli.conn != nil {
|
if cli.conn != nil {
|
||||||
return ConnectCluster(ctx, *cli.conn, opts)
|
return ConnectCluster(ctx, *cli.conn, opts)
|
||||||
}
|
}
|
||||||
@@ -92,6 +104,8 @@ func (cli *CLI) ConnectClusterWithOptions(ctx context.Context, contextName strin
|
|||||||
cli.Config.Path(),
|
cli.Config.Path(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
contextName := cli.contextOverride
|
||||||
if contextName == "" {
|
if contextName == "" {
|
||||||
// If the cluster is not specified, use the current cluster if set.
|
// If the cluster is not specified, use the current cluster if set.
|
||||||
if cli.Config.CurrentContext == "" {
|
if cli.Config.CurrentContext == "" {
|
||||||
@@ -148,6 +162,7 @@ type InitClusterOptions struct {
|
|||||||
RemoteMachine *RemoteMachine
|
RemoteMachine *RemoteMachine
|
||||||
SkipInstall bool
|
SkipInstall bool
|
||||||
Version string
|
Version string
|
||||||
|
AutoConfirm bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// InitCluster initialises a new cluster on a remote machine and returns a client to interact with the cluster.
|
// InitCluster initialises a new cluster on a remote machine and returns a client to interact with the cluster.
|
||||||
@@ -157,7 +172,7 @@ func (cli *CLI) InitCluster(ctx context.Context, opts InitClusterOptions) (*clie
|
|||||||
return cli.initRemoteMachine(ctx, opts)
|
return cli.initRemoteMachine(ctx, opts)
|
||||||
}
|
}
|
||||||
// TODO: implement local machine initialisation
|
// TODO: implement local machine initialisation
|
||||||
return nil, fmt.Errorf("local machine initialisation is not implemented yet")
|
return nil, fmt.Errorf("local machine initialisation is not implemented yet. Please specify a remote machine")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cli *CLI) initRemoteMachine(ctx context.Context, opts InitClusterOptions) (*client.Client, error) {
|
func (cli *CLI) initRemoteMachine(ctx context.Context, opts InitClusterOptions) (*client.Client, error) {
|
||||||
@@ -183,7 +198,12 @@ func (cli *CLI) initRemoteMachine(ctx context.Context, opts InitClusterOptions)
|
|||||||
return nil, fmt.Errorf("inspect machine: %w", err)
|
return nil, fmt.Errorf("inspect machine: %w", err)
|
||||||
}
|
}
|
||||||
if minfo.Id != "" {
|
if minfo.Id != "" {
|
||||||
if err = promptResetMachine(ctx, machineClient.MachineClient); err != nil {
|
if !opts.AutoConfirm {
|
||||||
|
if err = promptResetMachine(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err = resetAndWaitMachine(ctx, machineClient.MachineClient); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -228,8 +248,21 @@ func (cli *CLI) initRemoteMachine(ctx context.Context, opts InitClusterOptions)
|
|||||||
|
|
||||||
// Save the machine's SSH connection details in the context config.
|
// Save the machine's SSH connection details in the context config.
|
||||||
connCfg := config.MachineConnection{
|
connCfg := config.MachineConnection{
|
||||||
SSH: config.NewSSHDestination(opts.RemoteMachine.User, opts.RemoteMachine.Host, opts.RemoteMachine.Port),
|
|
||||||
SSHKeyFile: opts.RemoteMachine.KeyPath,
|
SSHKeyFile: opts.RemoteMachine.KeyPath,
|
||||||
|
MachineID: resp.Machine.Id,
|
||||||
|
}
|
||||||
|
if opts.RemoteMachine.UseSSHCLI {
|
||||||
|
connCfg.SSHCLI = config.NewSSHDestination(
|
||||||
|
opts.RemoteMachine.User,
|
||||||
|
opts.RemoteMachine.Host,
|
||||||
|
opts.RemoteMachine.Port,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
connCfg.SSH = config.NewSSHDestination(
|
||||||
|
opts.RemoteMachine.User,
|
||||||
|
opts.RemoteMachine.Host,
|
||||||
|
opts.RemoteMachine.Port,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
cli.Config.Contexts[contextName].Connections = append(cli.Config.Contexts[contextName].Connections, connCfg)
|
cli.Config.Contexts[contextName].Connections = append(cli.Config.Contexts[contextName].Connections, connCfg)
|
||||||
if err = cli.Config.Save(); err != nil {
|
if err = cli.Config.Save(); err != nil {
|
||||||
@@ -265,12 +298,12 @@ func (cli *CLI) newContextName(name string) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type AddMachineOptions struct {
|
type AddMachineOptions struct {
|
||||||
Context string
|
|
||||||
MachineName string
|
MachineName string
|
||||||
PublicIP *netip.Addr
|
PublicIP *netip.Addr
|
||||||
RemoteMachine *RemoteMachine
|
RemoteMachine *RemoteMachine
|
||||||
SkipInstall bool
|
SkipInstall bool
|
||||||
Version string
|
Version string
|
||||||
|
AutoConfirm bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddMachine provisions a remote machine and adds it to the cluster. It returns a cluster client and a machine client.
|
// AddMachine provisions a remote machine and adds it to the cluster. It returns a cluster client and a machine client.
|
||||||
@@ -278,11 +311,8 @@ type AddMachineOptions struct {
|
|||||||
// cluster. The machine client is connected to the new machine and can be used to interact with it.
|
// cluster. The machine client is connected to the new machine and can be used to interact with it.
|
||||||
// Both client should be closed after use by the caller.
|
// Both client should be closed after use by the caller.
|
||||||
func (cli *CLI) AddMachine(ctx context.Context, opts AddMachineOptions) (*client.Client, *client.Client, error) {
|
func (cli *CLI) AddMachine(ctx context.Context, opts AddMachineOptions) (*client.Client, *client.Client, error) {
|
||||||
contextName := opts.Context
|
contextName := cli.GetContextOverrideOrCurrent()
|
||||||
if contextName == "" {
|
c, err := cli.ConnectCluster(ctx)
|
||||||
contextName = cli.Config.CurrentContext
|
|
||||||
}
|
|
||||||
c, err := cli.ConnectCluster(ctx, contextName)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, fmt.Errorf("connect to cluster (context '%s'): %w", contextName, err)
|
return nil, nil, fmt.Errorf("connect to cluster (context '%s'): %w", contextName, err)
|
||||||
}
|
}
|
||||||
@@ -303,6 +333,7 @@ func (cli *CLI) AddMachine(ctx context.Context, opts AddMachineOptions) (*client
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
// Check if the machine is already initialised as a cluster member and prompt the user to reset it first.
|
// Check if the machine is already initialised as a cluster member and prompt the user to reset it first.
|
||||||
|
// TODO: refactor to use client.InspectMachine.
|
||||||
minfo, err := machineClient.Inspect(ctx, &emptypb.Empty{})
|
minfo, err := machineClient.Inspect(ctx, &emptypb.Empty{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, fmt.Errorf("inspect machine: %w", err)
|
return nil, nil, fmt.Errorf("inspect machine: %w", err)
|
||||||
@@ -319,7 +350,12 @@ func (cli *CLI) AddMachine(ctx context.Context, opts AddMachineOptions) (*client
|
|||||||
return nil, nil, fmt.Errorf("machine is already a member of this cluster (%s)", minfo.Name)
|
return nil, nil, fmt.Errorf("machine is already a member of this cluster (%s)", minfo.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = promptResetMachine(ctx, machineClient.MachineClient); err != nil {
|
if !opts.AutoConfirm {
|
||||||
|
if err = promptResetMachine(); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err = resetAndWaitMachine(ctx, machineClient.MachineClient); err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -370,6 +406,18 @@ func (cli *CLI) AddMachine(ctx context.Context, opts AddMachineOptions) (*client
|
|||||||
return nil, nil, fmt.Errorf("add machine to cluster (context '%s'): %w", contextName, err)
|
return nil, nil, fmt.Errorf("add machine to cluster (context '%s'): %w", contextName, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get the current store DB version from the cluster to pass to the join request.
|
||||||
|
var storeDBVersion int64
|
||||||
|
inspectResp, err := c.MachineClient.InspectMachine(ctx, &emptypb.Empty{})
|
||||||
|
if err != nil {
|
||||||
|
// TODO(lhf): remove Unimplemented check when v0.17.0 is released.
|
||||||
|
if status.Convert(err).Code() != codes.Unimplemented {
|
||||||
|
return nil, nil, fmt.Errorf("inspect current cluster machine: %w", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
storeDBVersion = inspectResp.Machines[0].StoreDbVersion
|
||||||
|
}
|
||||||
|
|
||||||
// Get the most up-to-date list of other machines in the cluster to include them in the join request.
|
// Get the most up-to-date list of other machines in the cluster to include them in the join request.
|
||||||
machines, err := c.ListMachines(ctx, nil)
|
machines, err := c.ListMachines(ctx, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -384,8 +432,9 @@ func (cli *CLI) AddMachine(ctx context.Context, opts AddMachineOptions) (*client
|
|||||||
|
|
||||||
// Configure the remote machine to join the cluster.
|
// Configure the remote machine to join the cluster.
|
||||||
joinReq := &pb.JoinClusterRequest{
|
joinReq := &pb.JoinClusterRequest{
|
||||||
Machine: addResp.Machine,
|
Machine: addResp.Machine,
|
||||||
OtherMachines: otherMachines,
|
OtherMachines: otherMachines,
|
||||||
|
MinStoreDbVersion: storeDBVersion,
|
||||||
}
|
}
|
||||||
if _, err = machineClient.JoinCluster(ctx, joinReq); err != nil {
|
if _, err = machineClient.JoinCluster(ctx, joinReq); err != nil {
|
||||||
return nil, nil, fmt.Errorf("join cluster: %w", err)
|
return nil, nil, fmt.Errorf("join cluster: %w", err)
|
||||||
@@ -396,11 +445,21 @@ func (cli *CLI) AddMachine(ctx context.Context, opts AddMachineOptions) (*client
|
|||||||
|
|
||||||
// Save the machine's SSH connection details in the context config.
|
// Save the machine's SSH connection details in the context config.
|
||||||
connCfg := config.MachineConnection{
|
connCfg := config.MachineConnection{
|
||||||
SSH: config.NewSSHDestination(opts.RemoteMachine.User, opts.RemoteMachine.Host, opts.RemoteMachine.Port),
|
|
||||||
SSHKeyFile: opts.RemoteMachine.KeyPath,
|
SSHKeyFile: opts.RemoteMachine.KeyPath,
|
||||||
|
MachineID: addResp.Machine.Id,
|
||||||
}
|
}
|
||||||
if contextName == "" {
|
if opts.RemoteMachine.UseSSHCLI {
|
||||||
contextName = cli.Config.CurrentContext
|
connCfg.SSHCLI = config.NewSSHDestination(
|
||||||
|
opts.RemoteMachine.User,
|
||||||
|
opts.RemoteMachine.Host,
|
||||||
|
opts.RemoteMachine.Port,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
connCfg.SSH = config.NewSSHDestination(
|
||||||
|
opts.RemoteMachine.User,
|
||||||
|
opts.RemoteMachine.Host,
|
||||||
|
opts.RemoteMachine.Port,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
cli.Config.Contexts[contextName].Connections = append(cli.Config.Contexts[contextName].Connections, connCfg)
|
cli.Config.Contexts[contextName].Connections = append(cli.Config.Contexts[contextName].Connections, connCfg)
|
||||||
if err = cli.Config.Save(); err != nil {
|
if err = cli.Config.Save(); err != nil {
|
||||||
@@ -420,6 +479,35 @@ func (cli *CLI) AddMachine(ctx context.Context, opts AddMachineOptions) (*client
|
|||||||
func provisionOrConnectRemoteMachine(
|
func provisionOrConnectRemoteMachine(
|
||||||
ctx context.Context, remoteMachine *RemoteMachine, skipInstall bool, version string,
|
ctx context.Context, remoteMachine *RemoteMachine, skipInstall bool, version string,
|
||||||
) (*client.Client, error) {
|
) (*client.Client, error) {
|
||||||
|
// Use SSH CLI
|
||||||
|
if remoteMachine.UseSSHCLI {
|
||||||
|
exec := sshexec.NewSSHCLIRemote(
|
||||||
|
remoteMachine.User,
|
||||||
|
remoteMachine.Host,
|
||||||
|
remoteMachine.Port,
|
||||||
|
remoteMachine.KeyPath,
|
||||||
|
)
|
||||||
|
|
||||||
|
if !skipInstall {
|
||||||
|
if err := provisionMachine(ctx, exec, version); err != nil {
|
||||||
|
return nil, fmt.Errorf("provision machine: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sshConfig := &connector.SSHConnectorConfig{
|
||||||
|
User: remoteMachine.User,
|
||||||
|
Host: remoteMachine.Host,
|
||||||
|
Port: remoteMachine.Port,
|
||||||
|
KeyPath: remoteMachine.KeyPath,
|
||||||
|
}
|
||||||
|
machineClient, err := client.New(ctx, connector.NewSSHCLIConnector(sshConfig))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("connect to remote machine: %w", err)
|
||||||
|
}
|
||||||
|
return machineClient, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use Go SSH
|
||||||
sshClient, err := sshexec.Connect(remoteMachine.User, remoteMachine.Host, remoteMachine.Port, remoteMachine.KeyPath)
|
sshClient, err := sshexec.Connect(remoteMachine.User, remoteMachine.Host, remoteMachine.Port, remoteMachine.KeyPath)
|
||||||
// If the SSH connection using SSH agent fails and no key path is provided, try to use the default SSH key.
|
// If the SSH connection using SSH agent fails and no key path is provided, try to use the default SSH key.
|
||||||
if err != nil && remoteMachine.KeyPath == "" {
|
if err != nil && remoteMachine.KeyPath == "" {
|
||||||
|
|||||||
@@ -22,9 +22,12 @@ type MachineConnection struct {
|
|||||||
SSHKeyFile string `yaml:"ssh_key_file,omitempty"`
|
SSHKeyFile string `yaml:"ssh_key_file,omitempty"`
|
||||||
// TCP is the address and port of the machine's API server.
|
// TCP is the address and port of the machine's API server.
|
||||||
// The pointer is used to omit the field when not set. Otherwise, yaml marshalling includes an empty object.
|
// The pointer is used to omit the field when not set. Otherwise, yaml marshalling includes an empty object.
|
||||||
TCP *netip.AddrPort `yaml:"tcp,omitempty"`
|
TCP *netip.AddrPort `yaml:"tcp,omitempty"`
|
||||||
Host string `yaml:"host,omitempty"`
|
// Unix is the path to the machine's API unix socket.
|
||||||
PublicKey secret.Secret `yaml:"public_key,omitempty"`
|
Unix string `yaml:"unix,omitempty"`
|
||||||
|
Host string `yaml:"host,omitempty"`
|
||||||
|
PublicKey secret.Secret `yaml:"public_key,omitempty"`
|
||||||
|
MachineID string `yaml:"machine_id,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c MachineConnection) String() string {
|
func (c MachineConnection) String() string {
|
||||||
@@ -34,6 +37,8 @@ func (c MachineConnection) String() string {
|
|||||||
return "ssh+cli://" + string(c.SSHCLI)
|
return "ssh+cli://" + string(c.SSHCLI)
|
||||||
} else if c.TCP != nil && c.TCP.IsValid() {
|
} else if c.TCP != nil && c.TCP.IsValid() {
|
||||||
return fmt.Sprintf("tcp://%s", c.TCP)
|
return fmt.Sprintf("tcp://%s", c.TCP)
|
||||||
|
} else if c.Unix != "" {
|
||||||
|
return fmt.Sprintf("unix://%s", c.Unix)
|
||||||
}
|
}
|
||||||
return "unknown connection"
|
return "unknown connection"
|
||||||
}
|
}
|
||||||
@@ -49,12 +54,15 @@ func (c *MachineConnection) Validate() error {
|
|||||||
if c.TCP != nil && c.TCP.IsValid() {
|
if c.TCP != nil && c.TCP.IsValid() {
|
||||||
setCount++
|
setCount++
|
||||||
}
|
}
|
||||||
|
if c.Unix != "" {
|
||||||
|
setCount++
|
||||||
|
}
|
||||||
|
|
||||||
if setCount == 0 {
|
if setCount == 0 {
|
||||||
return errors.New("no connection method specified (ssh, ssh_cli, or tcp required)")
|
return errors.New("no connection method specified (ssh, ssh_cli, tcp, or unix required)")
|
||||||
}
|
}
|
||||||
if setCount > 1 {
|
if setCount > 1 {
|
||||||
return errors.New("only one connection method allowed per connection (ssh, ssh_cli, or tcp)")
|
return errors.New("only one connection method allowed per connection (ssh, ssh_cli, tcp, or unix)")
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -53,11 +53,27 @@ func TestMachineConnection_String(t *testing.T) {
|
|||||||
},
|
},
|
||||||
want: "tcp://10.0.0.1:8080",
|
want: "tcp://10.0.0.1:8080",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "unix connection",
|
||||||
|
conn: MachineConnection{
|
||||||
|
Unix: "/run/uncloud/uncloud.sock",
|
||||||
|
},
|
||||||
|
want: "unix:///run/uncloud/uncloud.sock",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "no connection",
|
name: "no connection",
|
||||||
conn: MachineConnection{},
|
conn: MachineConnection{},
|
||||||
want: "unknown connection",
|
want: "unknown connection",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "ssh connection with machine id",
|
||||||
|
conn: MachineConnection{
|
||||||
|
SSH: "user@host.com",
|
||||||
|
MachineID: "ed98b9f7575308c340263cd279e3b568",
|
||||||
|
},
|
||||||
|
// String() does not use MachineID; just verifying entry with it is valid
|
||||||
|
want: "ssh://user@host.com",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
@@ -103,6 +119,13 @@ func TestMachineConnection_Validate(t *testing.T) {
|
|||||||
},
|
},
|
||||||
wantErr: false,
|
wantErr: false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "unix only - valid",
|
||||||
|
conn: MachineConnection{
|
||||||
|
Unix: "/path/to/socket",
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "no connection method - error",
|
name: "no connection method - error",
|
||||||
conn: MachineConnection{},
|
conn: MachineConnection{},
|
||||||
@@ -118,6 +141,15 @@ func TestMachineConnection_Validate(t *testing.T) {
|
|||||||
wantErr: true,
|
wantErr: true,
|
||||||
errMsg: "only one connection method allowed",
|
errMsg: "only one connection method allowed",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "ssh and unix - error",
|
||||||
|
conn: MachineConnection{
|
||||||
|
SSH: "user@host",
|
||||||
|
Unix: "/path/to/socket",
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
errMsg: "only one connection method allowed",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "ssh and tcp - error",
|
name: "ssh and tcp - error",
|
||||||
conn: MachineConnection{
|
conn: MachineConnection{
|
||||||
|
|||||||
@@ -4,3 +4,21 @@ type Context struct {
|
|||||||
Name string `yaml:"-"`
|
Name string `yaml:"-"`
|
||||||
Connections []MachineConnection `yaml:"connections"`
|
Connections []MachineConnection `yaml:"connections"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Context) SetDefaultConnection(index int) {
|
||||||
|
// Do nothing if the index is out of range.
|
||||||
|
if index < 0 || index >= len(c.Connections) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
selected := c.Connections[index]
|
||||||
|
newConnections := make([]MachineConnection, 0, len(c.Connections))
|
||||||
|
newConnections = append(newConnections, selected)
|
||||||
|
|
||||||
|
for i, conn := range c.Connections {
|
||||||
|
if i != index {
|
||||||
|
newConnections = append(newConnections, conn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.Connections = newConnections
|
||||||
|
}
|
||||||
|
|||||||
@@ -73,6 +73,8 @@ func connectCluster(ctx context.Context, conn config.MachineConnection) (*client
|
|||||||
useSSHCLI = true
|
useSSHCLI = true
|
||||||
} else if conn.TCP != nil && conn.TCP.IsValid() {
|
} else if conn.TCP != nil && conn.TCP.IsValid() {
|
||||||
return client.New(ctx, connector.NewTCPConnector(*conn.TCP))
|
return client.New(ctx, connector.NewTCPConnector(*conn.TCP))
|
||||||
|
} else if conn.Unix != "" {
|
||||||
|
return client.New(ctx, connector.NewUnixConnector(conn.Unix))
|
||||||
} else {
|
} else {
|
||||||
return nil, errors.New("connection configuration is invalid")
|
return nil, errors.New("connection configuration is invalid")
|
||||||
}
|
}
|
||||||
|
|||||||
+18
-5
@@ -2,6 +2,7 @@ package cli
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -21,10 +22,11 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type RemoteMachine struct {
|
type RemoteMachine struct {
|
||||||
User string
|
User string
|
||||||
Host string
|
Host string
|
||||||
Port int
|
Port int
|
||||||
KeyPath string
|
KeyPath string
|
||||||
|
UseSSHCLI bool // indicates ssh+cli:// should be used
|
||||||
}
|
}
|
||||||
|
|
||||||
func installCmd(user string, version string) string {
|
func installCmd(user string, version string) string {
|
||||||
@@ -85,7 +87,13 @@ func provisionMachine(ctx context.Context, exec sshexec.Executor, version string
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func promptResetMachine(ctx context.Context, machineClient pb.MachineClient) error {
|
func promptResetMachine() error {
|
||||||
|
if !IsStdinTerminal() {
|
||||||
|
return errors.New("the remote machine is already initialised as a cluster member; " +
|
||||||
|
"cannot ask to confirm reset in non-interactive mode, " +
|
||||||
|
"use --yes flag or set UNCLOUD_AUTO_CONFIRM=true to auto-confirm")
|
||||||
|
}
|
||||||
|
|
||||||
var confirm bool
|
var confirm bool
|
||||||
form := huh.NewForm(
|
form := huh.NewForm(
|
||||||
huh.NewGroup(
|
huh.NewGroup(
|
||||||
@@ -109,10 +117,15 @@ func promptResetMachine(ctx context.Context, machineClient pb.MachineClient) err
|
|||||||
return fmt.Errorf("remote machine is already initialised as a cluster member")
|
return fmt.Errorf("remote machine is already initialised as a cluster member")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func resetAndWaitMachine(ctx context.Context, machineClient pb.MachineClient) error {
|
||||||
if _, err := machineClient.Reset(ctx, &pb.ResetRequest{}); err != nil {
|
if _, err := machineClient.Reset(ctx, &pb.ResetRequest{}); err != nil {
|
||||||
return fmt.Errorf("reset remote machine: %w. You can also manually run 'uncloud-uninstall' "+
|
return fmt.Errorf("reset remote machine: %w. You can also manually run 'uncloud-uninstall' "+
|
||||||
"on the remote machine to fully uninstall Uncloud from it", err)
|
"on the remote machine to fully uninstall Uncloud from it", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println("Resetting the remote machine...")
|
fmt.Println("Resetting the remote machine...")
|
||||||
if err := waitMachineReady(ctx, machineClient, 1*time.Minute); err != nil {
|
if err := waitMachineReady(ctx, machineClient, 1*time.Minute); err != nil {
|
||||||
return fmt.Errorf("wait for machine to be ready after reset: %w", err)
|
return fmt.Errorf("wait for machine to be ready after reset: %w", err)
|
||||||
|
|||||||
+2037
-1776
File diff suppressed because it is too large
Load Diff
@@ -5,6 +5,7 @@ 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 "google/protobuf/timestamp.proto";
|
||||||
import "internal/machine/api/pb/common.proto";
|
import "internal/machine/api/pb/common.proto";
|
||||||
|
|
||||||
service Docker {
|
service Docker {
|
||||||
@@ -15,6 +16,9 @@ service Docker {
|
|||||||
rpc ListContainers(ListContainersRequest) returns (ListContainersResponse);
|
rpc ListContainers(ListContainersRequest) returns (ListContainersResponse);
|
||||||
rpc RemoveContainer(RemoveContainerRequest) returns (google.protobuf.Empty);
|
rpc RemoveContainer(RemoveContainerRequest) returns (google.protobuf.Empty);
|
||||||
|
|
||||||
|
rpc ExecContainer(stream ExecContainerRequest) returns (stream ExecContainerResponse);
|
||||||
|
rpc ContainerLogs(ContainerLogsRequest) returns (stream ContainerLogEntry);
|
||||||
|
|
||||||
rpc PullImage(PullImageRequest) returns (stream JSONMessage);
|
rpc PullImage(PullImageRequest) returns (stream JSONMessage);
|
||||||
rpc InspectImage(InspectImageRequest) returns (InspectImageResponse);
|
rpc InspectImage(InspectImageRequest) returns (InspectImageResponse);
|
||||||
// InspectRemoteImage returns the image metadata for an image in a remote registry using the machine's
|
// InspectRemoteImage returns the image metadata for an image in a remote registry using the machine's
|
||||||
@@ -30,8 +34,6 @@ service Docker {
|
|||||||
rpc InspectServiceContainer(InspectContainerRequest) returns (ServiceContainer);
|
rpc InspectServiceContainer(InspectContainerRequest) returns (ServiceContainer);
|
||||||
rpc ListServiceContainers(ListServiceContainersRequest) returns (ListServiceContainersResponse);
|
rpc ListServiceContainers(ListServiceContainersRequest) returns (ListServiceContainersResponse);
|
||||||
rpc RemoveServiceContainer(RemoveContainerRequest) returns (google.protobuf.Empty);
|
rpc RemoveServiceContainer(RemoveContainerRequest) returns (google.protobuf.Empty);
|
||||||
|
|
||||||
rpc ExecContainer(stream ExecContainerRequest) returns (stream ExecContainerResponse);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
message CreateContainerRequest {
|
message CreateContainerRequest {
|
||||||
@@ -94,6 +96,64 @@ message RemoveContainerRequest {
|
|||||||
bytes options = 2;
|
bytes options = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message ExecContainerRequest {
|
||||||
|
oneof payload {
|
||||||
|
// Initial configuration for the exec session. Must be sent as the first message.
|
||||||
|
ExecConfig config = 1;
|
||||||
|
// Raw stdin data to be written to the exec process.
|
||||||
|
bytes stdin = 2;
|
||||||
|
// TTY resize event (only used when TTY is enabled).
|
||||||
|
ResizeEvent resize = 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
message ExecConfig {
|
||||||
|
// Container ID to execute the command in.
|
||||||
|
string container_id = 1;
|
||||||
|
// JSON serialised ExecOptions
|
||||||
|
bytes options = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ResizeEvent {
|
||||||
|
uint32 height = 1;
|
||||||
|
uint32 width = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ExecContainerResponse {
|
||||||
|
oneof payload {
|
||||||
|
// Exec instance ID returned after creating the exec.
|
||||||
|
string exec_id = 1;
|
||||||
|
// Raw stdout data from the exec process.
|
||||||
|
bytes stdout = 2;
|
||||||
|
// Raw stderr data from the exec process (only when TTY is disabled).
|
||||||
|
bytes stderr = 3;
|
||||||
|
// Exit code of the exec process. Sent as the final message.
|
||||||
|
int32 exit_code = 4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
message ContainerLogsRequest {
|
||||||
|
string container_id = 1;
|
||||||
|
// Options for logs retrieval.
|
||||||
|
bool follow = 2;
|
||||||
|
int32 tail = 3; // -1 means all
|
||||||
|
string since = 4; // https://www.rfc-editor.org/rfc/rfc3339.html timestamp or Go duration string
|
||||||
|
string until = 5; // https://www.rfc-editor.org/rfc/rfc3339.html timestamp or Go duration string
|
||||||
|
}
|
||||||
|
|
||||||
|
message ContainerLogEntry {
|
||||||
|
enum StreamType {
|
||||||
|
UNKNOWN = 0;
|
||||||
|
STDOUT = 1;
|
||||||
|
STDERR = 2;
|
||||||
|
HEARTBEAT = 3;
|
||||||
|
}
|
||||||
|
StreamType stream = 1;
|
||||||
|
google.protobuf.Timestamp timestamp = 2;
|
||||||
|
// Log line content. Empty for heartbeat entries.
|
||||||
|
bytes message = 3;
|
||||||
|
}
|
||||||
|
|
||||||
message PullImageRequest {
|
message PullImageRequest {
|
||||||
string image = 1;
|
string image = 1;
|
||||||
// JSON serialised image.PullOptions.
|
// JSON serialised image.PullOptions.
|
||||||
@@ -216,39 +276,3 @@ message MachineServiceContainers {
|
|||||||
Metadata metadata = 1;
|
Metadata metadata = 1;
|
||||||
repeated ServiceContainer containers = 2;
|
repeated ServiceContainer containers = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
message ExecContainerRequest {
|
|
||||||
oneof payload {
|
|
||||||
// Initial configuration for the exec session. Must be sent as the first message.
|
|
||||||
ExecConfig config = 1;
|
|
||||||
// Raw stdin data to be written to the exec process.
|
|
||||||
bytes stdin = 2;
|
|
||||||
// TTY resize event (only used when TTY is enabled).
|
|
||||||
ResizeEvent resize = 3;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
message ExecConfig {
|
|
||||||
// Container ID to execute the command in.
|
|
||||||
string container_id = 1;
|
|
||||||
// JSON serialised ExecOptions
|
|
||||||
bytes options = 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
message ResizeEvent {
|
|
||||||
uint32 height = 1;
|
|
||||||
uint32 width = 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
message ExecContainerResponse {
|
|
||||||
oneof payload {
|
|
||||||
// Exec instance ID returned after creating the exec.
|
|
||||||
string exec_id = 1;
|
|
||||||
// Raw stdout data from the exec process.
|
|
||||||
bytes stdout = 2;
|
|
||||||
// Raw stderr data from the exec process (only when TTY is disabled).
|
|
||||||
bytes stderr = 3;
|
|
||||||
// Exit code of the exec process. Sent as the final message.
|
|
||||||
int32 exit_code = 4;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ const (
|
|||||||
Docker_StopContainer_FullMethodName = "/api.Docker/StopContainer"
|
Docker_StopContainer_FullMethodName = "/api.Docker/StopContainer"
|
||||||
Docker_ListContainers_FullMethodName = "/api.Docker/ListContainers"
|
Docker_ListContainers_FullMethodName = "/api.Docker/ListContainers"
|
||||||
Docker_RemoveContainer_FullMethodName = "/api.Docker/RemoveContainer"
|
Docker_RemoveContainer_FullMethodName = "/api.Docker/RemoveContainer"
|
||||||
|
Docker_ExecContainer_FullMethodName = "/api.Docker/ExecContainer"
|
||||||
|
Docker_ContainerLogs_FullMethodName = "/api.Docker/ContainerLogs"
|
||||||
Docker_PullImage_FullMethodName = "/api.Docker/PullImage"
|
Docker_PullImage_FullMethodName = "/api.Docker/PullImage"
|
||||||
Docker_InspectImage_FullMethodName = "/api.Docker/InspectImage"
|
Docker_InspectImage_FullMethodName = "/api.Docker/InspectImage"
|
||||||
Docker_InspectRemoteImage_FullMethodName = "/api.Docker/InspectRemoteImage"
|
Docker_InspectRemoteImage_FullMethodName = "/api.Docker/InspectRemoteImage"
|
||||||
@@ -37,7 +39,6 @@ const (
|
|||||||
Docker_InspectServiceContainer_FullMethodName = "/api.Docker/InspectServiceContainer"
|
Docker_InspectServiceContainer_FullMethodName = "/api.Docker/InspectServiceContainer"
|
||||||
Docker_ListServiceContainers_FullMethodName = "/api.Docker/ListServiceContainers"
|
Docker_ListServiceContainers_FullMethodName = "/api.Docker/ListServiceContainers"
|
||||||
Docker_RemoveServiceContainer_FullMethodName = "/api.Docker/RemoveServiceContainer"
|
Docker_RemoveServiceContainer_FullMethodName = "/api.Docker/RemoveServiceContainer"
|
||||||
Docker_ExecContainer_FullMethodName = "/api.Docker/ExecContainer"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// DockerClient is the client API for Docker service.
|
// DockerClient is the client API for Docker service.
|
||||||
@@ -50,6 +51,8 @@ type DockerClient interface {
|
|||||||
StopContainer(ctx context.Context, in *StopContainerRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
|
StopContainer(ctx context.Context, in *StopContainerRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
|
||||||
ListContainers(ctx context.Context, in *ListContainersRequest, opts ...grpc.CallOption) (*ListContainersResponse, error)
|
ListContainers(ctx context.Context, in *ListContainersRequest, opts ...grpc.CallOption) (*ListContainersResponse, error)
|
||||||
RemoveContainer(ctx context.Context, in *RemoveContainerRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
|
RemoveContainer(ctx context.Context, in *RemoveContainerRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
|
||||||
|
ExecContainer(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[ExecContainerRequest, ExecContainerResponse], error)
|
||||||
|
ContainerLogs(ctx context.Context, in *ContainerLogsRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[ContainerLogEntry], error)
|
||||||
PullImage(ctx context.Context, in *PullImageRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[JSONMessage], error)
|
PullImage(ctx context.Context, in *PullImageRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[JSONMessage], error)
|
||||||
InspectImage(ctx context.Context, in *InspectImageRequest, opts ...grpc.CallOption) (*InspectImageResponse, error)
|
InspectImage(ctx context.Context, in *InspectImageRequest, opts ...grpc.CallOption) (*InspectImageResponse, error)
|
||||||
// InspectRemoteImage returns the image metadata for an image in a remote registry using the machine's
|
// InspectRemoteImage returns the image metadata for an image in a remote registry using the machine's
|
||||||
@@ -63,7 +66,6 @@ type DockerClient interface {
|
|||||||
InspectServiceContainer(ctx context.Context, in *InspectContainerRequest, opts ...grpc.CallOption) (*ServiceContainer, error)
|
InspectServiceContainer(ctx context.Context, in *InspectContainerRequest, opts ...grpc.CallOption) (*ServiceContainer, error)
|
||||||
ListServiceContainers(ctx context.Context, in *ListServiceContainersRequest, opts ...grpc.CallOption) (*ListServiceContainersResponse, error)
|
ListServiceContainers(ctx context.Context, in *ListServiceContainersRequest, opts ...grpc.CallOption) (*ListServiceContainersResponse, error)
|
||||||
RemoveServiceContainer(ctx context.Context, in *RemoveContainerRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
|
RemoveServiceContainer(ctx context.Context, in *RemoveContainerRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
|
||||||
ExecContainer(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[ExecContainerRequest, ExecContainerResponse], error)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type dockerClient struct {
|
type dockerClient struct {
|
||||||
@@ -134,9 +136,41 @@ func (c *dockerClient) RemoveContainer(ctx context.Context, in *RemoveContainerR
|
|||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *dockerClient) ExecContainer(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[ExecContainerRequest, ExecContainerResponse], error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
stream, err := c.cc.NewStream(ctx, &Docker_ServiceDesc.Streams[0], Docker_ExecContainer_FullMethodName, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
x := &grpc.GenericClientStream[ExecContainerRequest, ExecContainerResponse]{ClientStream: stream}
|
||||||
|
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_ExecContainerClient = grpc.BidiStreamingClient[ExecContainerRequest, ExecContainerResponse]
|
||||||
|
|
||||||
|
func (c *dockerClient) ContainerLogs(ctx context.Context, in *ContainerLogsRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[ContainerLogEntry], error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
stream, err := c.cc.NewStream(ctx, &Docker_ServiceDesc.Streams[1], Docker_ContainerLogs_FullMethodName, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
x := &grpc.GenericClientStream[ContainerLogsRequest, ContainerLogEntry]{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_ContainerLogsClient = grpc.ServerStreamingClient[ContainerLogEntry]
|
||||||
|
|
||||||
func (c *dockerClient) PullImage(ctx context.Context, in *PullImageRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[JSONMessage], error) {
|
func (c *dockerClient) PullImage(ctx context.Context, in *PullImageRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[JSONMessage], error) {
|
||||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
stream, err := c.cc.NewStream(ctx, &Docker_ServiceDesc.Streams[0], Docker_PullImage_FullMethodName, cOpts...)
|
stream, err := c.cc.NewStream(ctx, &Docker_ServiceDesc.Streams[2], Docker_PullImage_FullMethodName, cOpts...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -253,19 +287,6 @@ func (c *dockerClient) RemoveServiceContainer(ctx context.Context, in *RemoveCon
|
|||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *dockerClient) ExecContainer(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[ExecContainerRequest, ExecContainerResponse], error) {
|
|
||||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
|
||||||
stream, err := c.cc.NewStream(ctx, &Docker_ServiceDesc.Streams[1], Docker_ExecContainer_FullMethodName, cOpts...)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
x := &grpc.GenericClientStream[ExecContainerRequest, ExecContainerResponse]{ClientStream: stream}
|
|
||||||
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_ExecContainerClient = grpc.BidiStreamingClient[ExecContainerRequest, ExecContainerResponse]
|
|
||||||
|
|
||||||
// DockerServer is the server API for Docker service.
|
// DockerServer is the server API for Docker service.
|
||||||
// All implementations must embed UnimplementedDockerServer
|
// All implementations must embed UnimplementedDockerServer
|
||||||
// for forward compatibility.
|
// for forward compatibility.
|
||||||
@@ -276,6 +297,8 @@ type DockerServer interface {
|
|||||||
StopContainer(context.Context, *StopContainerRequest) (*emptypb.Empty, error)
|
StopContainer(context.Context, *StopContainerRequest) (*emptypb.Empty, error)
|
||||||
ListContainers(context.Context, *ListContainersRequest) (*ListContainersResponse, error)
|
ListContainers(context.Context, *ListContainersRequest) (*ListContainersResponse, error)
|
||||||
RemoveContainer(context.Context, *RemoveContainerRequest) (*emptypb.Empty, error)
|
RemoveContainer(context.Context, *RemoveContainerRequest) (*emptypb.Empty, error)
|
||||||
|
ExecContainer(grpc.BidiStreamingServer[ExecContainerRequest, ExecContainerResponse]) error
|
||||||
|
ContainerLogs(*ContainerLogsRequest, grpc.ServerStreamingServer[ContainerLogEntry]) error
|
||||||
PullImage(*PullImageRequest, grpc.ServerStreamingServer[JSONMessage]) error
|
PullImage(*PullImageRequest, grpc.ServerStreamingServer[JSONMessage]) error
|
||||||
InspectImage(context.Context, *InspectImageRequest) (*InspectImageResponse, error)
|
InspectImage(context.Context, *InspectImageRequest) (*InspectImageResponse, error)
|
||||||
// InspectRemoteImage returns the image metadata for an image in a remote registry using the machine's
|
// InspectRemoteImage returns the image metadata for an image in a remote registry using the machine's
|
||||||
@@ -289,7 +312,6 @@ type DockerServer interface {
|
|||||||
InspectServiceContainer(context.Context, *InspectContainerRequest) (*ServiceContainer, error)
|
InspectServiceContainer(context.Context, *InspectContainerRequest) (*ServiceContainer, error)
|
||||||
ListServiceContainers(context.Context, *ListServiceContainersRequest) (*ListServiceContainersResponse, error)
|
ListServiceContainers(context.Context, *ListServiceContainersRequest) (*ListServiceContainersResponse, error)
|
||||||
RemoveServiceContainer(context.Context, *RemoveContainerRequest) (*emptypb.Empty, error)
|
RemoveServiceContainer(context.Context, *RemoveContainerRequest) (*emptypb.Empty, error)
|
||||||
ExecContainer(grpc.BidiStreamingServer[ExecContainerRequest, ExecContainerResponse]) error
|
|
||||||
mustEmbedUnimplementedDockerServer()
|
mustEmbedUnimplementedDockerServer()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -318,6 +340,12 @@ func (UnimplementedDockerServer) ListContainers(context.Context, *ListContainers
|
|||||||
func (UnimplementedDockerServer) RemoveContainer(context.Context, *RemoveContainerRequest) (*emptypb.Empty, error) {
|
func (UnimplementedDockerServer) RemoveContainer(context.Context, *RemoveContainerRequest) (*emptypb.Empty, error) {
|
||||||
return nil, status.Errorf(codes.Unimplemented, "method RemoveContainer not implemented")
|
return nil, status.Errorf(codes.Unimplemented, "method RemoveContainer not implemented")
|
||||||
}
|
}
|
||||||
|
func (UnimplementedDockerServer) ExecContainer(grpc.BidiStreamingServer[ExecContainerRequest, ExecContainerResponse]) error {
|
||||||
|
return status.Errorf(codes.Unimplemented, "method ExecContainer not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedDockerServer) ContainerLogs(*ContainerLogsRequest, grpc.ServerStreamingServer[ContainerLogEntry]) error {
|
||||||
|
return status.Errorf(codes.Unimplemented, "method ContainerLogs not implemented")
|
||||||
|
}
|
||||||
func (UnimplementedDockerServer) PullImage(*PullImageRequest, grpc.ServerStreamingServer[JSONMessage]) error {
|
func (UnimplementedDockerServer) PullImage(*PullImageRequest, grpc.ServerStreamingServer[JSONMessage]) error {
|
||||||
return status.Errorf(codes.Unimplemented, "method PullImage not implemented")
|
return status.Errorf(codes.Unimplemented, "method PullImage not implemented")
|
||||||
}
|
}
|
||||||
@@ -351,9 +379,6 @@ func (UnimplementedDockerServer) ListServiceContainers(context.Context, *ListSer
|
|||||||
func (UnimplementedDockerServer) RemoveServiceContainer(context.Context, *RemoveContainerRequest) (*emptypb.Empty, error) {
|
func (UnimplementedDockerServer) RemoveServiceContainer(context.Context, *RemoveContainerRequest) (*emptypb.Empty, error) {
|
||||||
return nil, status.Errorf(codes.Unimplemented, "method RemoveServiceContainer not implemented")
|
return nil, status.Errorf(codes.Unimplemented, "method RemoveServiceContainer not implemented")
|
||||||
}
|
}
|
||||||
func (UnimplementedDockerServer) ExecContainer(grpc.BidiStreamingServer[ExecContainerRequest, ExecContainerResponse]) error {
|
|
||||||
return status.Errorf(codes.Unimplemented, "method ExecContainer not implemented")
|
|
||||||
}
|
|
||||||
func (UnimplementedDockerServer) mustEmbedUnimplementedDockerServer() {}
|
func (UnimplementedDockerServer) mustEmbedUnimplementedDockerServer() {}
|
||||||
func (UnimplementedDockerServer) testEmbeddedByValue() {}
|
func (UnimplementedDockerServer) testEmbeddedByValue() {}
|
||||||
|
|
||||||
@@ -483,6 +508,24 @@ func _Docker_RemoveContainer_Handler(srv interface{}, ctx context.Context, dec f
|
|||||||
return interceptor(ctx, in, info, handler)
|
return interceptor(ctx, in, info, handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func _Docker_ExecContainer_Handler(srv interface{}, stream grpc.ServerStream) error {
|
||||||
|
return srv.(DockerServer).ExecContainer(&grpc.GenericServerStream[ExecContainerRequest, ExecContainerResponse]{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_ExecContainerServer = grpc.BidiStreamingServer[ExecContainerRequest, ExecContainerResponse]
|
||||||
|
|
||||||
|
func _Docker_ContainerLogs_Handler(srv interface{}, stream grpc.ServerStream) error {
|
||||||
|
m := new(ContainerLogsRequest)
|
||||||
|
if err := stream.RecvMsg(m); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return srv.(DockerServer).ContainerLogs(m, &grpc.GenericServerStream[ContainerLogsRequest, ContainerLogEntry]{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_ContainerLogsServer = grpc.ServerStreamingServer[ContainerLogEntry]
|
||||||
|
|
||||||
func _Docker_PullImage_Handler(srv interface{}, stream grpc.ServerStream) error {
|
func _Docker_PullImage_Handler(srv interface{}, stream grpc.ServerStream) error {
|
||||||
m := new(PullImageRequest)
|
m := new(PullImageRequest)
|
||||||
if err := stream.RecvMsg(m); err != nil {
|
if err := stream.RecvMsg(m); err != nil {
|
||||||
@@ -674,13 +717,6 @@ func _Docker_RemoveServiceContainer_Handler(srv interface{}, ctx context.Context
|
|||||||
return interceptor(ctx, in, info, handler)
|
return interceptor(ctx, in, info, handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
func _Docker_ExecContainer_Handler(srv interface{}, stream grpc.ServerStream) error {
|
|
||||||
return srv.(DockerServer).ExecContainer(&grpc.GenericServerStream[ExecContainerRequest, ExecContainerResponse]{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_ExecContainerServer = grpc.BidiStreamingServer[ExecContainerRequest, ExecContainerResponse]
|
|
||||||
|
|
||||||
// Docker_ServiceDesc is the grpc.ServiceDesc for Docker service.
|
// Docker_ServiceDesc is the grpc.ServiceDesc for Docker service.
|
||||||
// It's only intended for direct use with grpc.RegisterService,
|
// It's only intended for direct use with grpc.RegisterService,
|
||||||
// and not to be introspected or modified (even as a copy)
|
// and not to be introspected or modified (even as a copy)
|
||||||
@@ -754,17 +790,22 @@ var Docker_ServiceDesc = grpc.ServiceDesc{
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
Streams: []grpc.StreamDesc{
|
Streams: []grpc.StreamDesc{
|
||||||
{
|
|
||||||
StreamName: "PullImage",
|
|
||||||
Handler: _Docker_PullImage_Handler,
|
|
||||||
ServerStreams: true,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
StreamName: "ExecContainer",
|
StreamName: "ExecContainer",
|
||||||
Handler: _Docker_ExecContainer_Handler,
|
Handler: _Docker_ExecContainer_Handler,
|
||||||
ServerStreams: true,
|
ServerStreams: true,
|
||||||
ClientStreams: true,
|
ClientStreams: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
StreamName: "ContainerLogs",
|
||||||
|
Handler: _Docker_ContainerLogs_Handler,
|
||||||
|
ServerStreams: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
StreamName: "PullImage",
|
||||||
|
Handler: _Docker_PullImage_Handler,
|
||||||
|
ServerStreams: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
Metadata: "internal/machine/api/pb/docker.proto",
|
Metadata: "internal/machine/api/pb/docker.proto",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
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"
|
||||||
emptypb "google.golang.org/protobuf/types/known/emptypb"
|
emptypb "google.golang.org/protobuf/types/known/emptypb"
|
||||||
|
timestamppb "google.golang.org/protobuf/types/known/timestamppb"
|
||||||
reflect "reflect"
|
reflect "reflect"
|
||||||
sync "sync"
|
sync "sync"
|
||||||
)
|
)
|
||||||
@@ -371,6 +372,8 @@ type JoinClusterRequest struct {
|
|||||||
|
|
||||||
Machine *MachineInfo `protobuf:"bytes,1,opt,name=machine,proto3" json:"machine,omitempty"`
|
Machine *MachineInfo `protobuf:"bytes,1,opt,name=machine,proto3" json:"machine,omitempty"`
|
||||||
OtherMachines []*MachineInfo `protobuf:"bytes,3,rep,name=other_machines,json=otherMachines,proto3" json:"other_machines,omitempty"`
|
OtherMachines []*MachineInfo `protobuf:"bytes,3,rep,name=other_machines,json=otherMachines,proto3" json:"other_machines,omitempty"`
|
||||||
|
// Minimum store database version the new machine should sync to before starting cluster operations.
|
||||||
|
MinStoreDbVersion int64 `protobuf:"varint,4,opt,name=min_store_db_version,json=minStoreDbVersion,proto3" json:"min_store_db_version,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *JoinClusterRequest) Reset() {
|
func (x *JoinClusterRequest) Reset() {
|
||||||
@@ -419,6 +422,125 @@ func (x *JoinClusterRequest) GetOtherMachines() []*MachineInfo {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (x *JoinClusterRequest) GetMinStoreDbVersion() int64 {
|
||||||
|
if x != nil {
|
||||||
|
return x.MinStoreDbVersion
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
type InspectMachineResponse struct {
|
||||||
|
state protoimpl.MessageState
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
|
||||||
|
// Must contain only one repeated messages field to allow broadcasting InspectMachine requests to multiple machines.
|
||||||
|
Machines []*MachineDetails `protobuf:"bytes,1,rep,name=machines,proto3" json:"machines,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *InspectMachineResponse) Reset() {
|
||||||
|
*x = InspectMachineResponse{}
|
||||||
|
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 *InspectMachineResponse) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*InspectMachineResponse) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *InspectMachineResponse) 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 InspectMachineResponse.ProtoReflect.Descriptor instead.
|
||||||
|
func (*InspectMachineResponse) Descriptor() ([]byte, []int) {
|
||||||
|
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{6}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *InspectMachineResponse) GetMachines() []*MachineDetails {
|
||||||
|
if x != nil {
|
||||||
|
return x.Machines
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type MachineDetails struct {
|
||||||
|
state protoimpl.MessageState
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
|
||||||
|
Metadata *Metadata `protobuf:"bytes,1,opt,name=metadata,proto3" json:"metadata,omitempty"`
|
||||||
|
Machine *MachineInfo `protobuf:"bytes,2,opt,name=machine,proto3" json:"machine,omitempty"`
|
||||||
|
// Current Corrosion cr-sqlite database version (Lamport timestamp) of the cluster store.
|
||||||
|
StoreDbVersion int64 `protobuf:"varint,3,opt,name=store_db_version,json=storeDbVersion,proto3" json:"store_db_version,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *MachineDetails) Reset() {
|
||||||
|
*x = MachineDetails{}
|
||||||
|
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 *MachineDetails) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*MachineDetails) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *MachineDetails) 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 MachineDetails.ProtoReflect.Descriptor instead.
|
||||||
|
func (*MachineDetails) Descriptor() ([]byte, []int) {
|
||||||
|
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{7}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *MachineDetails) GetMetadata() *Metadata {
|
||||||
|
if x != nil {
|
||||||
|
return x.Metadata
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *MachineDetails) GetMachine() *MachineInfo {
|
||||||
|
if x != nil {
|
||||||
|
return x.Machine
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *MachineDetails) GetStoreDbVersion() int64 {
|
||||||
|
if x != nil {
|
||||||
|
return x.StoreDbVersion
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
type TokenResponse struct {
|
type TokenResponse struct {
|
||||||
state protoimpl.MessageState
|
state protoimpl.MessageState
|
||||||
sizeCache protoimpl.SizeCache
|
sizeCache protoimpl.SizeCache
|
||||||
@@ -430,7 +552,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[6]
|
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[8]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@@ -443,7 +565,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[6]
|
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[8]
|
||||||
if protoimpl.UnsafeEnabled && x != nil {
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
@@ -456,7 +578,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{6}
|
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{8}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *TokenResponse) GetToken() string {
|
func (x *TokenResponse) GetToken() string {
|
||||||
@@ -475,7 +597,7 @@ type ResetRequest struct {
|
|||||||
func (x *ResetRequest) Reset() {
|
func (x *ResetRequest) Reset() {
|
||||||
*x = ResetRequest{}
|
*x = ResetRequest{}
|
||||||
if protoimpl.UnsafeEnabled {
|
if protoimpl.UnsafeEnabled {
|
||||||
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[7]
|
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[9]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@@ -488,7 +610,7 @@ func (x *ResetRequest) String() string {
|
|||||||
func (*ResetRequest) ProtoMessage() {}
|
func (*ResetRequest) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *ResetRequest) ProtoReflect() protoreflect.Message {
|
func (x *ResetRequest) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[7]
|
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[9]
|
||||||
if protoimpl.UnsafeEnabled && x != nil {
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
@@ -501,7 +623,7 @@ func (x *ResetRequest) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use ResetRequest.ProtoReflect.Descriptor instead.
|
// Deprecated: Use ResetRequest.ProtoReflect.Descriptor instead.
|
||||||
func (*ResetRequest) Descriptor() ([]byte, []int) {
|
func (*ResetRequest) Descriptor() ([]byte, []int) {
|
||||||
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{7}
|
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{9}
|
||||||
}
|
}
|
||||||
|
|
||||||
type Service struct {
|
type Service struct {
|
||||||
@@ -518,7 +640,7 @@ type Service struct {
|
|||||||
func (x *Service) Reset() {
|
func (x *Service) Reset() {
|
||||||
*x = Service{}
|
*x = Service{}
|
||||||
if protoimpl.UnsafeEnabled {
|
if protoimpl.UnsafeEnabled {
|
||||||
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[8]
|
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[10]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@@ -531,7 +653,7 @@ func (x *Service) String() string {
|
|||||||
func (*Service) ProtoMessage() {}
|
func (*Service) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *Service) ProtoReflect() protoreflect.Message {
|
func (x *Service) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[8]
|
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[10]
|
||||||
if protoimpl.UnsafeEnabled && x != nil {
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
@@ -544,7 +666,7 @@ func (x *Service) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use Service.ProtoReflect.Descriptor instead.
|
// Deprecated: Use Service.ProtoReflect.Descriptor instead.
|
||||||
func (*Service) Descriptor() ([]byte, []int) {
|
func (*Service) Descriptor() ([]byte, []int) {
|
||||||
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{8}
|
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{10}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *Service) GetId() string {
|
func (x *Service) GetId() string {
|
||||||
@@ -586,7 +708,7 @@ type InspectServiceRequest struct {
|
|||||||
func (x *InspectServiceRequest) Reset() {
|
func (x *InspectServiceRequest) Reset() {
|
||||||
*x = InspectServiceRequest{}
|
*x = InspectServiceRequest{}
|
||||||
if protoimpl.UnsafeEnabled {
|
if protoimpl.UnsafeEnabled {
|
||||||
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[9]
|
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[11]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@@ -599,7 +721,7 @@ func (x *InspectServiceRequest) String() string {
|
|||||||
func (*InspectServiceRequest) ProtoMessage() {}
|
func (*InspectServiceRequest) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *InspectServiceRequest) ProtoReflect() protoreflect.Message {
|
func (x *InspectServiceRequest) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[9]
|
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[11]
|
||||||
if protoimpl.UnsafeEnabled && x != nil {
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
@@ -612,7 +734,7 @@ func (x *InspectServiceRequest) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use InspectServiceRequest.ProtoReflect.Descriptor instead.
|
// Deprecated: Use InspectServiceRequest.ProtoReflect.Descriptor instead.
|
||||||
func (*InspectServiceRequest) Descriptor() ([]byte, []int) {
|
func (*InspectServiceRequest) Descriptor() ([]byte, []int) {
|
||||||
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{9}
|
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{11}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *InspectServiceRequest) GetId() string {
|
func (x *InspectServiceRequest) GetId() string {
|
||||||
@@ -633,7 +755,7 @@ type InspectServiceResponse struct {
|
|||||||
func (x *InspectServiceResponse) Reset() {
|
func (x *InspectServiceResponse) Reset() {
|
||||||
*x = InspectServiceResponse{}
|
*x = InspectServiceResponse{}
|
||||||
if protoimpl.UnsafeEnabled {
|
if protoimpl.UnsafeEnabled {
|
||||||
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[10]
|
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[12]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@@ -646,7 +768,7 @@ func (x *InspectServiceResponse) String() string {
|
|||||||
func (*InspectServiceResponse) ProtoMessage() {}
|
func (*InspectServiceResponse) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *InspectServiceResponse) ProtoReflect() protoreflect.Message {
|
func (x *InspectServiceResponse) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[10]
|
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[12]
|
||||||
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 {
|
||||||
@@ -659,7 +781,7 @@ func (x *InspectServiceResponse) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use InspectServiceResponse.ProtoReflect.Descriptor instead.
|
// Deprecated: Use InspectServiceResponse.ProtoReflect.Descriptor instead.
|
||||||
func (*InspectServiceResponse) Descriptor() ([]byte, []int) {
|
func (*InspectServiceResponse) Descriptor() ([]byte, []int) {
|
||||||
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{10}
|
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{12}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *InspectServiceResponse) GetService() *Service {
|
func (x *InspectServiceResponse) GetService() *Service {
|
||||||
@@ -669,6 +791,164 @@ func (x *InspectServiceResponse) GetService() *Service {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type InspectWireGuardNetworkResponse struct {
|
||||||
|
state protoimpl.MessageState
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
|
||||||
|
InterfaceName string `protobuf:"bytes,1,opt,name=interface_name,json=interfaceName,proto3" json:"interface_name,omitempty"`
|
||||||
|
PublicKey []byte `protobuf:"bytes,2,opt,name=public_key,json=publicKey,proto3" json:"public_key,omitempty"`
|
||||||
|
ListenPort int32 `protobuf:"varint,3,opt,name=listen_port,json=listenPort,proto3" json:"listen_port,omitempty"`
|
||||||
|
Peers []*WireGuardPeer `protobuf:"bytes,4,rep,name=peers,proto3" json:"peers,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *InspectWireGuardNetworkResponse) Reset() {
|
||||||
|
*x = InspectWireGuardNetworkResponse{}
|
||||||
|
if protoimpl.UnsafeEnabled {
|
||||||
|
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[13]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *InspectWireGuardNetworkResponse) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*InspectWireGuardNetworkResponse) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *InspectWireGuardNetworkResponse) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[13]
|
||||||
|
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 InspectWireGuardNetworkResponse.ProtoReflect.Descriptor instead.
|
||||||
|
func (*InspectWireGuardNetworkResponse) Descriptor() ([]byte, []int) {
|
||||||
|
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{13}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *InspectWireGuardNetworkResponse) GetInterfaceName() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.InterfaceName
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *InspectWireGuardNetworkResponse) GetPublicKey() []byte {
|
||||||
|
if x != nil {
|
||||||
|
return x.PublicKey
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *InspectWireGuardNetworkResponse) GetListenPort() int32 {
|
||||||
|
if x != nil {
|
||||||
|
return x.ListenPort
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *InspectWireGuardNetworkResponse) GetPeers() []*WireGuardPeer {
|
||||||
|
if x != nil {
|
||||||
|
return x.Peers
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type WireGuardPeer struct {
|
||||||
|
state protoimpl.MessageState
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
|
||||||
|
PublicKey []byte `protobuf:"bytes,1,opt,name=public_key,json=publicKey,proto3" json:"public_key,omitempty"`
|
||||||
|
Endpoint string `protobuf:"bytes,2,opt,name=endpoint,proto3" json:"endpoint,omitempty"`
|
||||||
|
LastHandshakeTime *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=last_handshake_time,json=lastHandshakeTime,proto3" json:"last_handshake_time,omitempty"`
|
||||||
|
ReceiveBytes int64 `protobuf:"varint,4,opt,name=receive_bytes,json=receiveBytes,proto3" json:"receive_bytes,omitempty"`
|
||||||
|
TransmitBytes int64 `protobuf:"varint,5,opt,name=transmit_bytes,json=transmitBytes,proto3" json:"transmit_bytes,omitempty"`
|
||||||
|
AllowedIps []string `protobuf:"bytes,6,rep,name=allowed_ips,json=allowedIps,proto3" json:"allowed_ips,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *WireGuardPeer) Reset() {
|
||||||
|
*x = WireGuardPeer{}
|
||||||
|
if protoimpl.UnsafeEnabled {
|
||||||
|
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[14]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *WireGuardPeer) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*WireGuardPeer) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *WireGuardPeer) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[14]
|
||||||
|
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 WireGuardPeer.ProtoReflect.Descriptor instead.
|
||||||
|
func (*WireGuardPeer) Descriptor() ([]byte, []int) {
|
||||||
|
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{14}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *WireGuardPeer) GetPublicKey() []byte {
|
||||||
|
if x != nil {
|
||||||
|
return x.PublicKey
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *WireGuardPeer) GetEndpoint() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Endpoint
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *WireGuardPeer) GetLastHandshakeTime() *timestamppb.Timestamp {
|
||||||
|
if x != nil {
|
||||||
|
return x.LastHandshakeTime
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *WireGuardPeer) GetReceiveBytes() int64 {
|
||||||
|
if x != nil {
|
||||||
|
return x.ReceiveBytes
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *WireGuardPeer) GetTransmitBytes() int64 {
|
||||||
|
if x != nil {
|
||||||
|
return x.TransmitBytes
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *WireGuardPeer) GetAllowedIps() []string {
|
||||||
|
if x != nil {
|
||||||
|
return x.AllowedIps
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
type Service_Container struct {
|
type Service_Container struct {
|
||||||
state protoimpl.MessageState
|
state protoimpl.MessageState
|
||||||
sizeCache protoimpl.SizeCache
|
sizeCache protoimpl.SizeCache
|
||||||
@@ -682,7 +962,7 @@ type Service_Container struct {
|
|||||||
func (x *Service_Container) Reset() {
|
func (x *Service_Container) Reset() {
|
||||||
*x = Service_Container{}
|
*x = Service_Container{}
|
||||||
if protoimpl.UnsafeEnabled {
|
if protoimpl.UnsafeEnabled {
|
||||||
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[11]
|
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[15]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@@ -695,7 +975,7 @@ func (x *Service_Container) String() string {
|
|||||||
func (*Service_Container) ProtoMessage() {}
|
func (*Service_Container) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *Service_Container) ProtoReflect() protoreflect.Message {
|
func (x *Service_Container) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[11]
|
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[15]
|
||||||
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 {
|
||||||
@@ -708,7 +988,7 @@ func (x *Service_Container) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use Service_Container.ProtoReflect.Descriptor instead.
|
// Deprecated: Use Service_Container.ProtoReflect.Descriptor instead.
|
||||||
func (*Service_Container) Descriptor() ([]byte, []int) {
|
func (*Service_Container) Descriptor() ([]byte, []int) {
|
||||||
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{8, 0}
|
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{10, 0}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *Service_Container) GetMachineId() string {
|
func (x *Service_Container) GetMachineId() string {
|
||||||
@@ -732,113 +1012,170 @@ var file_internal_machine_api_pb_machine_proto_rawDesc = []byte{
|
|||||||
0x6e, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x62, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e,
|
0x6e, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x62, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e,
|
||||||
0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x03, 0x61, 0x70, 0x69, 0x1a, 0x1b, 0x67, 0x6f,
|
0x65, 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, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c,
|
||||||
0x6e, 0x61, 0x6c, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f,
|
0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73,
|
||||||
0x70, 0x62, 0x2f, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22,
|
0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x24, 0x69, 0x6e, 0x74, 0x65,
|
||||||
0x85, 0x01, 0x0a, 0x0b, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x12,
|
0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x2f, 0x61, 0x70, 0x69,
|
||||||
0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12,
|
0x2f, 0x70, 0x62, 0x2f, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f,
|
||||||
0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e,
|
0x22, 0x85, 0x01, 0x0a, 0x0b, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f,
|
||||||
0x61, 0x6d, 0x65, 0x12, 0x2c, 0x0a, 0x07, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x18, 0x03,
|
0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64,
|
||||||
0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4e, 0x65, 0x74, 0x77, 0x6f,
|
0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04,
|
||||||
0x72, 0x6b, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x07, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72,
|
0x6e, 0x61, 0x6d, 0x65, 0x12, 0x2c, 0x0a, 0x07, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x18,
|
||||||
0x6b, 0x12, 0x24, 0x0a, 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x69, 0x70, 0x18, 0x04,
|
0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4e, 0x65, 0x74, 0x77,
|
||||||
0x20, 0x01, 0x28, 0x0b, 0x32, 0x07, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x50, 0x52, 0x08, 0x70,
|
0x6f, 0x72, 0x6b, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x07, 0x6e, 0x65, 0x74, 0x77, 0x6f,
|
||||||
0x75, 0x62, 0x6c, 0x69, 0x63, 0x49, 0x70, 0x22, 0xae, 0x01, 0x0a, 0x0d, 0x4e, 0x65, 0x74, 0x77,
|
0x72, 0x6b, 0x12, 0x24, 0x0a, 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x69, 0x70, 0x18,
|
||||||
0x6f, 0x72, 0x6b, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x25, 0x0a, 0x06, 0x73, 0x75, 0x62,
|
0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x07, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x50, 0x52, 0x08,
|
||||||
0x6e, 0x65, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x61, 0x70, 0x69, 0x2e,
|
0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x49, 0x70, 0x22, 0xae, 0x01, 0x0a, 0x0d, 0x4e, 0x65, 0x74,
|
||||||
0x49, 0x50, 0x50, 0x72, 0x65, 0x66, 0x69, 0x78, 0x52, 0x06, 0x73, 0x75, 0x62, 0x6e, 0x65, 0x74,
|
0x77, 0x6f, 0x72, 0x6b, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x25, 0x0a, 0x06, 0x73, 0x75,
|
||||||
0x12, 0x2c, 0x0a, 0x0d, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x5f, 0x69,
|
0x62, 0x6e, 0x65, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x61, 0x70, 0x69,
|
||||||
0x70, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x07, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x50,
|
0x2e, 0x49, 0x50, 0x50, 0x72, 0x65, 0x66, 0x69, 0x78, 0x52, 0x06, 0x73, 0x75, 0x62, 0x6e, 0x65,
|
||||||
0x52, 0x0c, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x49, 0x70, 0x12, 0x29,
|
0x74, 0x12, 0x2c, 0x0a, 0x0d, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x5f,
|
||||||
0x0a, 0x09, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28,
|
0x69, 0x70, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x07, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49,
|
||||||
0x0b, 0x32, 0x0b, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x50, 0x50, 0x6f, 0x72, 0x74, 0x52, 0x09,
|
0x50, 0x52, 0x0c, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x49, 0x70, 0x12,
|
||||||
0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x70, 0x75, 0x62,
|
0x29, 0x0a, 0x09, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x18, 0x03, 0x20, 0x03,
|
||||||
0x6c, 0x69, 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x70,
|
0x28, 0x0b, 0x32, 0x0b, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x50, 0x50, 0x6f, 0x72, 0x74, 0x52,
|
||||||
0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x22, 0x50, 0x0a, 0x1a, 0x43, 0x68, 0x65, 0x63,
|
0x09, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x70, 0x75,
|
||||||
0x6b, 0x50, 0x72, 0x65, 0x72, 0x65, 0x71, 0x75, 0x69, 0x73, 0x69, 0x74, 0x65, 0x73, 0x52, 0x65,
|
0x62, 0x6c, 0x69, 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09,
|
||||||
0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x61, 0x74, 0x69, 0x73, 0x66,
|
0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x22, 0x50, 0x0a, 0x1a, 0x43, 0x68, 0x65,
|
||||||
0x69, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x73, 0x61, 0x74, 0x69, 0x73,
|
0x63, 0x6b, 0x50, 0x72, 0x65, 0x72, 0x65, 0x71, 0x75, 0x69, 0x73, 0x69, 0x74, 0x65, 0x73, 0x52,
|
||||||
0x66, 0x69, 0x65, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x02, 0x20,
|
0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x61, 0x74, 0x69, 0x73,
|
||||||
0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x22, 0xc3, 0x01, 0x0a, 0x12, 0x49,
|
0x66, 0x69, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x73, 0x61, 0x74, 0x69,
|
||||||
0x6e, 0x69, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
|
0x73, 0x66, 0x69, 0x65, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x02,
|
||||||
0x74, 0x12, 0x20, 0x0a, 0x0b, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x4e, 0x61, 0x6d, 0x65,
|
0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x22, 0xc3, 0x01, 0x0a, 0x12,
|
||||||
0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x4e,
|
0x49, 0x6e, 0x69, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65,
|
||||||
0x61, 0x6d, 0x65, 0x12, 0x27, 0x0a, 0x07, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x18, 0x02,
|
0x73, 0x74, 0x12, 0x20, 0x0a, 0x0b, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x4e, 0x61, 0x6d,
|
||||||
0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x50, 0x50, 0x72, 0x65,
|
0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65,
|
||||||
0x66, 0x69, 0x78, 0x52, 0x07, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x12, 0x26, 0x0a, 0x09,
|
0x4e, 0x61, 0x6d, 0x65, 0x12, 0x27, 0x0a, 0x07, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x18,
|
||||||
0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x69, 0x70, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32,
|
0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x50, 0x50, 0x72,
|
||||||
0x07, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x50, 0x48, 0x00, 0x52, 0x08, 0x70, 0x75, 0x62, 0x6c,
|
0x65, 0x66, 0x69, 0x78, 0x52, 0x07, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x12, 0x26, 0x0a,
|
||||||
0x69, 0x63, 0x49, 0x70, 0x12, 0x26, 0x0a, 0x0e, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x69,
|
0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x69, 0x70, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b,
|
||||||
0x70, 0x5f, 0x61, 0x75, 0x74, 0x6f, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x48, 0x00, 0x52, 0x0c,
|
0x32, 0x07, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x50, 0x48, 0x00, 0x52, 0x08, 0x70, 0x75, 0x62,
|
||||||
0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x49, 0x70, 0x41, 0x75, 0x74, 0x6f, 0x42, 0x12, 0x0a, 0x10,
|
0x6c, 0x69, 0x63, 0x49, 0x70, 0x12, 0x26, 0x0a, 0x0e, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f,
|
||||||
0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x69, 0x70, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67,
|
0x69, 0x70, 0x5f, 0x61, 0x75, 0x74, 0x6f, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x48, 0x00, 0x52,
|
||||||
0x22, 0x41, 0x0a, 0x13, 0x49, 0x6e, 0x69, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x52,
|
0x0c, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x49, 0x70, 0x41, 0x75, 0x74, 0x6f, 0x42, 0x12, 0x0a,
|
||||||
0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2a, 0x0a, 0x07, 0x6d, 0x61, 0x63, 0x68, 0x69,
|
0x10, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x69, 0x70, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69,
|
||||||
0x6e, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d,
|
0x67, 0x22, 0x41, 0x0a, 0x13, 0x49, 0x6e, 0x69, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72,
|
||||||
0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x07, 0x6d, 0x61, 0x63, 0x68,
|
0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2a, 0x0a, 0x07, 0x6d, 0x61, 0x63, 0x68,
|
||||||
0x69, 0x6e, 0x65, 0x22, 0x79, 0x0a, 0x12, 0x4a, 0x6f, 0x69, 0x6e, 0x43, 0x6c, 0x75, 0x73, 0x74,
|
0x69, 0x6e, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x61, 0x70, 0x69, 0x2e,
|
||||||
0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2a, 0x0a, 0x07, 0x6d, 0x61, 0x63,
|
0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x07, 0x6d, 0x61, 0x63,
|
||||||
0x68, 0x69, 0x6e, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x61, 0x70, 0x69,
|
0x68, 0x69, 0x6e, 0x65, 0x22, 0xaa, 0x01, 0x0a, 0x12, 0x4a, 0x6f, 0x69, 0x6e, 0x43, 0x6c, 0x75,
|
||||||
0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x07, 0x6d, 0x61,
|
0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2a, 0x0a, 0x07, 0x6d,
|
||||||
0x63, 0x68, 0x69, 0x6e, 0x65, 0x12, 0x37, 0x0a, 0x0e, 0x6f, 0x74, 0x68, 0x65, 0x72, 0x5f, 0x6d,
|
0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x61,
|
||||||
0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x10, 0x2e,
|
0x70, 0x69, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x07,
|
||||||
0x61, 0x70, 0x69, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52,
|
0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x12, 0x37, 0x0a, 0x0e, 0x6f, 0x74, 0x68, 0x65, 0x72,
|
||||||
0x0d, 0x6f, 0x74, 0x68, 0x65, 0x72, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x22, 0x25,
|
0x5f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32,
|
||||||
0x0a, 0x0d, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12,
|
0x10, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66,
|
||||||
0x14, 0x0a, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05,
|
0x6f, 0x52, 0x0d, 0x6f, 0x74, 0x68, 0x65, 0x72, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73,
|
||||||
0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0x0e, 0x0a, 0x0c, 0x52, 0x65, 0x73, 0x65, 0x74, 0x52, 0x65,
|
0x12, 0x2f, 0x0a, 0x14, 0x6d, 0x69, 0x6e, 0x5f, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x5f, 0x64, 0x62,
|
||||||
0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0xc3, 0x01, 0x0a, 0x07, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63,
|
0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x11,
|
||||||
0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69,
|
0x6d, 0x69, 0x6e, 0x53, 0x74, 0x6f, 0x72, 0x65, 0x44, 0x62, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f,
|
||||||
0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52,
|
0x6e, 0x22, 0x49, 0x0a, 0x16, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x4d, 0x61, 0x63, 0x68,
|
||||||
0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x18, 0x03, 0x20,
|
0x69, 0x6e, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2f, 0x0a, 0x08, 0x6d,
|
||||||
0x01, 0x28, 0x09, 0x52, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x12, 0x36, 0x0a, 0x0a, 0x63, 0x6f, 0x6e,
|
0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e,
|
||||||
0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e,
|
0x61, 0x70, 0x69, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x44, 0x65, 0x74, 0x61, 0x69,
|
||||||
0x61, 0x70, 0x69, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x43, 0x6f, 0x6e, 0x74,
|
0x6c, 0x73, 0x52, 0x08, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x22, 0x91, 0x01, 0x0a,
|
||||||
0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x0a, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72,
|
0x0e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x12,
|
||||||
0x73, 0x1a, 0x48, 0x0a, 0x09, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x1d,
|
0x29, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28,
|
||||||
0x0a, 0x0a, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01,
|
0x0b, 0x32, 0x0d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61,
|
||||||
0x28, 0x09, 0x52, 0x09, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x64, 0x12, 0x1c, 0x0a,
|
0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x2a, 0x0a, 0x07, 0x6d, 0x61,
|
||||||
0x09, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c,
|
0x63, 0x68, 0x69, 0x6e, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x61, 0x70,
|
||||||
0x52, 0x09, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x22, 0x27, 0x0a, 0x15, 0x49,
|
0x69, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x07, 0x6d,
|
||||||
0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x71,
|
0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x12, 0x28, 0x0a, 0x10, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x5f,
|
||||||
0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
|
0x64, 0x62, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03,
|
||||||
0x52, 0x02, 0x69, 0x64, 0x22, 0x40, 0x0a, 0x16, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53,
|
0x52, 0x0e, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x44, 0x62, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e,
|
||||||
0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x26,
|
0x22, 0x25, 0x0a, 0x0d, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
|
||||||
0x0a, 0x07, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32,
|
0x65, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
|
||||||
0x0c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x07, 0x73,
|
0x52, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0x0e, 0x0a, 0x0c, 0x52, 0x65, 0x73, 0x65, 0x74,
|
||||||
0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x32, 0xc3, 0x03, 0x0a, 0x07, 0x4d, 0x61, 0x63, 0x68, 0x69,
|
0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0xc3, 0x01, 0x0a, 0x07, 0x53, 0x65, 0x72, 0x76,
|
||||||
0x6e, 0x65, 0x12, 0x4d, 0x0a, 0x12, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x50, 0x72, 0x65, 0x72, 0x65,
|
0x69, 0x63, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52,
|
||||||
0x71, 0x75, 0x69, 0x73, 0x69, 0x74, 0x65, 0x73, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c,
|
0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28,
|
||||||
|
0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x18,
|
||||||
|
0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x12, 0x36, 0x0a, 0x0a, 0x63,
|
||||||
|
0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32,
|
||||||
|
0x16, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x43, 0x6f,
|
||||||
|
0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x0a, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e,
|
||||||
|
0x65, 0x72, 0x73, 0x1a, 0x48, 0x0a, 0x09, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72,
|
||||||
|
0x12, 0x1d, 0x0a, 0x0a, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01,
|
||||||
|
0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x64, 0x12,
|
||||||
|
0x1c, 0x0a, 0x09, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01,
|
||||||
|
0x28, 0x0c, 0x52, 0x09, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x22, 0x27, 0x0a,
|
||||||
|
0x15, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52,
|
||||||
|
0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01,
|
||||||
|
0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x22, 0x40, 0x0a, 0x16, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63,
|
||||||
|
0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
|
||||||
|
0x12, 0x26, 0x0a, 0x07, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28,
|
||||||
|
0x0b, 0x32, 0x0c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52,
|
||||||
|
0x07, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x22, 0xb2, 0x01, 0x0a, 0x1f, 0x49, 0x6e, 0x73,
|
||||||
|
0x70, 0x65, 0x63, 0x74, 0x57, 0x69, 0x72, 0x65, 0x47, 0x75, 0x61, 0x72, 0x64, 0x4e, 0x65, 0x74,
|
||||||
|
0x77, 0x6f, 0x72, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x25, 0x0a, 0x0e,
|
||||||
|
0x69, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01,
|
||||||
|
0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x4e,
|
||||||
|
0x61, 0x6d, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x6b, 0x65,
|
||||||
|
0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b,
|
||||||
|
0x65, 0x79, 0x12, 0x1f, 0x0a, 0x0b, 0x6c, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x5f, 0x70, 0x6f, 0x72,
|
||||||
|
0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0a, 0x6c, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x50,
|
||||||
|
0x6f, 0x72, 0x74, 0x12, 0x28, 0x0a, 0x05, 0x70, 0x65, 0x65, 0x72, 0x73, 0x18, 0x04, 0x20, 0x03,
|
||||||
|
0x28, 0x0b, 0x32, 0x12, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x57, 0x69, 0x72, 0x65, 0x47, 0x75, 0x61,
|
||||||
|
0x72, 0x64, 0x50, 0x65, 0x65, 0x72, 0x52, 0x05, 0x70, 0x65, 0x65, 0x72, 0x73, 0x22, 0x83, 0x02,
|
||||||
|
0x0a, 0x0d, 0x57, 0x69, 0x72, 0x65, 0x47, 0x75, 0x61, 0x72, 0x64, 0x50, 0x65, 0x65, 0x72, 0x12,
|
||||||
|
0x1d, 0x0a, 0x0a, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20,
|
||||||
|
0x01, 0x28, 0x0c, 0x52, 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x12, 0x1a,
|
||||||
|
0x0a, 0x08, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09,
|
||||||
|
0x52, 0x08, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x4a, 0x0a, 0x13, 0x6c, 0x61,
|
||||||
|
0x73, 0x74, 0x5f, 0x68, 0x61, 0x6e, 0x64, 0x73, 0x68, 0x61, 0x6b, 0x65, 0x5f, 0x74, 0x69, 0x6d,
|
||||||
|
0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65,
|
||||||
|
0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74,
|
||||||
|
0x61, 0x6d, 0x70, 0x52, 0x11, 0x6c, 0x61, 0x73, 0x74, 0x48, 0x61, 0x6e, 0x64, 0x73, 0x68, 0x61,
|
||||||
|
0x6b, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x65, 0x63, 0x65, 0x69, 0x76,
|
||||||
|
0x65, 0x5f, 0x62, 0x79, 0x74, 0x65, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x72,
|
||||||
|
0x65, 0x63, 0x65, 0x69, 0x76, 0x65, 0x42, 0x79, 0x74, 0x65, 0x73, 0x12, 0x25, 0x0a, 0x0e, 0x74,
|
||||||
|
0x72, 0x61, 0x6e, 0x73, 0x6d, 0x69, 0x74, 0x5f, 0x62, 0x79, 0x74, 0x65, 0x73, 0x18, 0x05, 0x20,
|
||||||
|
0x01, 0x28, 0x03, 0x52, 0x0d, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x6d, 0x69, 0x74, 0x42, 0x79, 0x74,
|
||||||
|
0x65, 0x73, 0x12, 0x1f, 0x0a, 0x0b, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x5f, 0x69, 0x70,
|
||||||
|
0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0a, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64,
|
||||||
|
0x49, 0x70, 0x73, 0x32, 0xe3, 0x04, 0x0a, 0x07, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x12,
|
||||||
|
0x4d, 0x0a, 0x12, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x50, 0x72, 0x65, 0x72, 0x65, 0x71, 0x75, 0x69,
|
||||||
|
0x73, 0x69, 0x74, 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, 0x1f, 0x2e,
|
||||||
|
0x61, 0x70, 0x69, 0x2e, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x50, 0x72, 0x65, 0x72, 0x65, 0x71, 0x75,
|
||||||
|
0x69, 0x73, 0x69, 0x74, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x40,
|
||||||
|
0x0a, 0x0b, 0x49, 0x6e, 0x69, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x12, 0x17, 0x2e,
|
||||||
|
0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x69, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x52,
|
||||||
|
0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x69,
|
||||||
|
0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
|
||||||
|
0x12, 0x3e, 0x0a, 0x0b, 0x4a, 0x6f, 0x69, 0x6e, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x12,
|
||||||
|
0x17, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4a, 0x6f, 0x69, 0x6e, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65,
|
||||||
|
0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c,
|
||||||
0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79,
|
0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79,
|
||||||
0x1a, 0x1f, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x50, 0x72, 0x65, 0x72,
|
0x12, 0x33, 0x0a, 0x05, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67,
|
||||||
0x65, 0x71, 0x75, 0x69, 0x73, 0x69, 0x74, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
|
|
||||||
0x65, 0x12, 0x40, 0x0a, 0x0b, 0x49, 0x6e, 0x69, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72,
|
|
||||||
0x12, 0x17, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x69, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74,
|
|
||||||
0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x61, 0x70, 0x69, 0x2e,
|
|
||||||
0x49, 0x6e, 0x69, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f,
|
|
||||||
0x6e, 0x73, 0x65, 0x12, 0x3e, 0x0a, 0x0b, 0x4a, 0x6f, 0x69, 0x6e, 0x43, 0x6c, 0x75, 0x73, 0x74,
|
|
||||||
0x65, 0x72, 0x12, 0x17, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4a, 0x6f, 0x69, 0x6e, 0x43, 0x6c, 0x75,
|
|
||||||
0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f,
|
|
||||||
0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d,
|
|
||||||
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, 0x32, 0x0a,
|
|
||||||
0x05, 0x52, 0x65, 0x73, 0x65, 0x74, 0x12, 0x11, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x52, 0x65, 0x73,
|
|
||||||
0x65, 0x74, 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,
|
0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74,
|
||||||
0x79, 0x12, 0x49, 0x0a, 0x0e, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72, 0x76,
|
0x79, 0x1a, 0x12, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x73,
|
||||||
0x69, 0x63, 0x65, 0x12, 0x1a, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63,
|
0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x33, 0x0a, 0x07, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74,
|
||||||
0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a,
|
0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62,
|
||||||
0x1b, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72,
|
0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x10, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d,
|
||||||
0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x37, 0x5a, 0x35,
|
0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x45, 0x0a, 0x0e, 0x49, 0x6e,
|
||||||
0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x70, 0x73, 0x76, 0x69, 0x64,
|
0x73, 0x70, 0x65, 0x63, 0x74, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x12, 0x16, 0x2e, 0x67,
|
||||||
0x65, 0x72, 0x73, 0x6b, 0x69, 0x2f, 0x75, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2f, 0x69, 0x6e,
|
0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45,
|
||||||
0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x2f, 0x61,
|
0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1b, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x73, 0x70, 0x65,
|
||||||
0x70, 0x69, 0x2f, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
0x63, 0x74, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
|
||||||
|
0x65, 0x12, 0x57, 0x0a, 0x17, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x57, 0x69, 0x72, 0x65,
|
||||||
|
0x47, 0x75, 0x61, 0x72, 0x64, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 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, 0x24, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x73, 0x70, 0x65,
|
||||||
|
0x63, 0x74, 0x57, 0x69, 0x72, 0x65, 0x47, 0x75, 0x61, 0x72, 0x64, 0x4e, 0x65, 0x74, 0x77, 0x6f,
|
||||||
|
0x72, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x32, 0x0a, 0x05, 0x52, 0x65,
|
||||||
|
0x73, 0x65, 0x74, 0x12, 0x11, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x52, 0x65, 0x73, 0x65, 0x74, 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, 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 (
|
||||||
@@ -853,57 +1190,72 @@ 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, 12)
|
var file_internal_machine_api_pb_machine_proto_msgTypes = make([]protoimpl.MessageInfo, 16)
|
||||||
var file_internal_machine_api_pb_machine_proto_goTypes = []any{
|
var file_internal_machine_api_pb_machine_proto_goTypes = []any{
|
||||||
(*MachineInfo)(nil), // 0: api.MachineInfo
|
(*MachineInfo)(nil), // 0: api.MachineInfo
|
||||||
(*NetworkConfig)(nil), // 1: api.NetworkConfig
|
(*NetworkConfig)(nil), // 1: api.NetworkConfig
|
||||||
(*CheckPrerequisitesResponse)(nil), // 2: api.CheckPrerequisitesResponse
|
(*CheckPrerequisitesResponse)(nil), // 2: api.CheckPrerequisitesResponse
|
||||||
(*InitClusterRequest)(nil), // 3: api.InitClusterRequest
|
(*InitClusterRequest)(nil), // 3: api.InitClusterRequest
|
||||||
(*InitClusterResponse)(nil), // 4: api.InitClusterResponse
|
(*InitClusterResponse)(nil), // 4: api.InitClusterResponse
|
||||||
(*JoinClusterRequest)(nil), // 5: api.JoinClusterRequest
|
(*JoinClusterRequest)(nil), // 5: api.JoinClusterRequest
|
||||||
(*TokenResponse)(nil), // 6: api.TokenResponse
|
(*InspectMachineResponse)(nil), // 6: api.InspectMachineResponse
|
||||||
(*ResetRequest)(nil), // 7: api.ResetRequest
|
(*MachineDetails)(nil), // 7: api.MachineDetails
|
||||||
(*Service)(nil), // 8: api.Service
|
(*TokenResponse)(nil), // 8: api.TokenResponse
|
||||||
(*InspectServiceRequest)(nil), // 9: api.InspectServiceRequest
|
(*ResetRequest)(nil), // 9: api.ResetRequest
|
||||||
(*InspectServiceResponse)(nil), // 10: api.InspectServiceResponse
|
(*Service)(nil), // 10: api.Service
|
||||||
(*Service_Container)(nil), // 11: api.Service.Container
|
(*InspectServiceRequest)(nil), // 11: api.InspectServiceRequest
|
||||||
(*IP)(nil), // 12: api.IP
|
(*InspectServiceResponse)(nil), // 12: api.InspectServiceResponse
|
||||||
(*IPPrefix)(nil), // 13: api.IPPrefix
|
(*InspectWireGuardNetworkResponse)(nil), // 13: api.InspectWireGuardNetworkResponse
|
||||||
(*IPPort)(nil), // 14: api.IPPort
|
(*WireGuardPeer)(nil), // 14: api.WireGuardPeer
|
||||||
(*emptypb.Empty)(nil), // 15: google.protobuf.Empty
|
(*Service_Container)(nil), // 15: api.Service.Container
|
||||||
|
(*IP)(nil), // 16: api.IP
|
||||||
|
(*IPPrefix)(nil), // 17: api.IPPrefix
|
||||||
|
(*IPPort)(nil), // 18: api.IPPort
|
||||||
|
(*Metadata)(nil), // 19: api.Metadata
|
||||||
|
(*timestamppb.Timestamp)(nil), // 20: google.protobuf.Timestamp
|
||||||
|
(*emptypb.Empty)(nil), // 21: google.protobuf.Empty
|
||||||
}
|
}
|
||||||
var file_internal_machine_api_pb_machine_proto_depIdxs = []int32{
|
var file_internal_machine_api_pb_machine_proto_depIdxs = []int32{
|
||||||
1, // 0: api.MachineInfo.network:type_name -> api.NetworkConfig
|
1, // 0: api.MachineInfo.network:type_name -> api.NetworkConfig
|
||||||
12, // 1: api.MachineInfo.public_ip:type_name -> api.IP
|
16, // 1: api.MachineInfo.public_ip:type_name -> api.IP
|
||||||
13, // 2: api.NetworkConfig.subnet:type_name -> api.IPPrefix
|
17, // 2: api.NetworkConfig.subnet:type_name -> api.IPPrefix
|
||||||
12, // 3: api.NetworkConfig.management_ip:type_name -> api.IP
|
16, // 3: api.NetworkConfig.management_ip:type_name -> api.IP
|
||||||
14, // 4: api.NetworkConfig.endpoints:type_name -> api.IPPort
|
18, // 4: api.NetworkConfig.endpoints:type_name -> api.IPPort
|
||||||
13, // 5: api.InitClusterRequest.network:type_name -> api.IPPrefix
|
17, // 5: api.InitClusterRequest.network:type_name -> api.IPPrefix
|
||||||
12, // 6: api.InitClusterRequest.public_ip:type_name -> api.IP
|
16, // 6: api.InitClusterRequest.public_ip:type_name -> api.IP
|
||||||
0, // 7: api.InitClusterResponse.machine:type_name -> api.MachineInfo
|
0, // 7: api.InitClusterResponse.machine:type_name -> api.MachineInfo
|
||||||
0, // 8: api.JoinClusterRequest.machine:type_name -> api.MachineInfo
|
0, // 8: api.JoinClusterRequest.machine:type_name -> api.MachineInfo
|
||||||
0, // 9: api.JoinClusterRequest.other_machines:type_name -> api.MachineInfo
|
0, // 9: api.JoinClusterRequest.other_machines:type_name -> api.MachineInfo
|
||||||
11, // 10: api.Service.containers:type_name -> api.Service.Container
|
7, // 10: api.InspectMachineResponse.machines:type_name -> api.MachineDetails
|
||||||
8, // 11: api.InspectServiceResponse.service:type_name -> api.Service
|
19, // 11: api.MachineDetails.metadata:type_name -> api.Metadata
|
||||||
15, // 12: api.Machine.CheckPrerequisites:input_type -> google.protobuf.Empty
|
0, // 12: api.MachineDetails.machine:type_name -> api.MachineInfo
|
||||||
3, // 13: api.Machine.InitCluster:input_type -> api.InitClusterRequest
|
15, // 13: api.Service.containers:type_name -> api.Service.Container
|
||||||
5, // 14: api.Machine.JoinCluster:input_type -> api.JoinClusterRequest
|
10, // 14: api.InspectServiceResponse.service:type_name -> api.Service
|
||||||
15, // 15: api.Machine.Token:input_type -> google.protobuf.Empty
|
14, // 15: api.InspectWireGuardNetworkResponse.peers:type_name -> api.WireGuardPeer
|
||||||
15, // 16: api.Machine.Inspect:input_type -> google.protobuf.Empty
|
20, // 16: api.WireGuardPeer.last_handshake_time:type_name -> google.protobuf.Timestamp
|
||||||
7, // 17: api.Machine.Reset:input_type -> api.ResetRequest
|
21, // 17: api.Machine.CheckPrerequisites:input_type -> google.protobuf.Empty
|
||||||
9, // 18: api.Machine.InspectService:input_type -> api.InspectServiceRequest
|
3, // 18: api.Machine.InitCluster:input_type -> api.InitClusterRequest
|
||||||
2, // 19: api.Machine.CheckPrerequisites:output_type -> api.CheckPrerequisitesResponse
|
5, // 19: api.Machine.JoinCluster:input_type -> api.JoinClusterRequest
|
||||||
4, // 20: api.Machine.InitCluster:output_type -> api.InitClusterResponse
|
21, // 20: api.Machine.Token:input_type -> google.protobuf.Empty
|
||||||
15, // 21: api.Machine.JoinCluster:output_type -> google.protobuf.Empty
|
21, // 21: api.Machine.Inspect:input_type -> google.protobuf.Empty
|
||||||
6, // 22: api.Machine.Token:output_type -> api.TokenResponse
|
21, // 22: api.Machine.InspectMachine:input_type -> google.protobuf.Empty
|
||||||
0, // 23: api.Machine.Inspect:output_type -> api.MachineInfo
|
21, // 23: api.Machine.InspectWireGuardNetwork:input_type -> google.protobuf.Empty
|
||||||
15, // 24: api.Machine.Reset:output_type -> google.protobuf.Empty
|
9, // 24: api.Machine.Reset:input_type -> api.ResetRequest
|
||||||
10, // 25: api.Machine.InspectService:output_type -> api.InspectServiceResponse
|
11, // 25: api.Machine.InspectService:input_type -> api.InspectServiceRequest
|
||||||
19, // [19:26] is the sub-list for method output_type
|
2, // 26: api.Machine.CheckPrerequisites:output_type -> api.CheckPrerequisitesResponse
|
||||||
12, // [12:19] is the sub-list for method input_type
|
4, // 27: api.Machine.InitCluster:output_type -> api.InitClusterResponse
|
||||||
12, // [12:12] is the sub-list for extension type_name
|
21, // 28: api.Machine.JoinCluster:output_type -> google.protobuf.Empty
|
||||||
12, // [12:12] is the sub-list for extension extendee
|
8, // 29: api.Machine.Token:output_type -> api.TokenResponse
|
||||||
0, // [0:12] is the sub-list for field type_name
|
0, // 30: api.Machine.Inspect:output_type -> api.MachineInfo
|
||||||
|
6, // 31: api.Machine.InspectMachine:output_type -> api.InspectMachineResponse
|
||||||
|
13, // 32: api.Machine.InspectWireGuardNetwork:output_type -> api.InspectWireGuardNetworkResponse
|
||||||
|
21, // 33: api.Machine.Reset:output_type -> google.protobuf.Empty
|
||||||
|
12, // 34: api.Machine.InspectService:output_type -> api.InspectServiceResponse
|
||||||
|
26, // [26:35] is the sub-list for method output_type
|
||||||
|
17, // [17:26] is the sub-list for method input_type
|
||||||
|
17, // [17:17] is the sub-list for extension type_name
|
||||||
|
17, // [17:17] is the sub-list for extension extendee
|
||||||
|
0, // [0:17] 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() }
|
||||||
@@ -986,7 +1338,7 @@ func file_internal_machine_api_pb_machine_proto_init() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
file_internal_machine_api_pb_machine_proto_msgTypes[6].Exporter = func(v any, i int) any {
|
file_internal_machine_api_pb_machine_proto_msgTypes[6].Exporter = func(v any, i int) any {
|
||||||
switch v := v.(*TokenResponse); i {
|
switch v := v.(*InspectMachineResponse); i {
|
||||||
case 0:
|
case 0:
|
||||||
return &v.state
|
return &v.state
|
||||||
case 1:
|
case 1:
|
||||||
@@ -998,7 +1350,7 @@ func file_internal_machine_api_pb_machine_proto_init() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
file_internal_machine_api_pb_machine_proto_msgTypes[7].Exporter = func(v any, i int) any {
|
file_internal_machine_api_pb_machine_proto_msgTypes[7].Exporter = func(v any, i int) any {
|
||||||
switch v := v.(*ResetRequest); i {
|
switch v := v.(*MachineDetails); i {
|
||||||
case 0:
|
case 0:
|
||||||
return &v.state
|
return &v.state
|
||||||
case 1:
|
case 1:
|
||||||
@@ -1010,7 +1362,7 @@ func file_internal_machine_api_pb_machine_proto_init() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
file_internal_machine_api_pb_machine_proto_msgTypes[8].Exporter = func(v any, i int) any {
|
file_internal_machine_api_pb_machine_proto_msgTypes[8].Exporter = func(v any, i int) any {
|
||||||
switch v := v.(*Service); i {
|
switch v := v.(*TokenResponse); i {
|
||||||
case 0:
|
case 0:
|
||||||
return &v.state
|
return &v.state
|
||||||
case 1:
|
case 1:
|
||||||
@@ -1022,7 +1374,7 @@ func file_internal_machine_api_pb_machine_proto_init() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
file_internal_machine_api_pb_machine_proto_msgTypes[9].Exporter = func(v any, i int) any {
|
file_internal_machine_api_pb_machine_proto_msgTypes[9].Exporter = func(v any, i int) any {
|
||||||
switch v := v.(*InspectServiceRequest); i {
|
switch v := v.(*ResetRequest); i {
|
||||||
case 0:
|
case 0:
|
||||||
return &v.state
|
return &v.state
|
||||||
case 1:
|
case 1:
|
||||||
@@ -1034,7 +1386,7 @@ func file_internal_machine_api_pb_machine_proto_init() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
file_internal_machine_api_pb_machine_proto_msgTypes[10].Exporter = func(v any, i int) any {
|
file_internal_machine_api_pb_machine_proto_msgTypes[10].Exporter = func(v any, i int) any {
|
||||||
switch v := v.(*InspectServiceResponse); i {
|
switch v := v.(*Service); i {
|
||||||
case 0:
|
case 0:
|
||||||
return &v.state
|
return &v.state
|
||||||
case 1:
|
case 1:
|
||||||
@@ -1046,6 +1398,54 @@ func file_internal_machine_api_pb_machine_proto_init() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
file_internal_machine_api_pb_machine_proto_msgTypes[11].Exporter = func(v any, i int) any {
|
file_internal_machine_api_pb_machine_proto_msgTypes[11].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[12].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[13].Exporter = func(v any, i int) any {
|
||||||
|
switch v := v.(*InspectWireGuardNetworkResponse); 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[14].Exporter = func(v any, i int) any {
|
||||||
|
switch v := v.(*WireGuardPeer); 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[15].Exporter = func(v any, i int) any {
|
||||||
switch v := v.(*Service_Container); i {
|
switch v := v.(*Service_Container); i {
|
||||||
case 0:
|
case 0:
|
||||||
return &v.state
|
return &v.state
|
||||||
@@ -1068,7 +1468,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: 12,
|
NumMessages: 16,
|
||||||
NumExtensions: 0,
|
NumExtensions: 0,
|
||||||
NumServices: 1,
|
NumServices: 1,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ 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 "google/protobuf/timestamp.proto";
|
||||||
import "internal/machine/api/pb/common.proto";
|
import "internal/machine/api/pb/common.proto";
|
||||||
|
|
||||||
service Machine {
|
service Machine {
|
||||||
@@ -13,7 +14,12 @@ 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);
|
||||||
|
// Deprecated: use InspectMachine instead.
|
||||||
rpc Inspect(google.protobuf.Empty) returns (MachineInfo);
|
rpc Inspect(google.protobuf.Empty) returns (MachineInfo);
|
||||||
|
// InspectMachine retrieves detailed information about the machine. Supports broadcasting to multiple machines.
|
||||||
|
rpc InspectMachine(google.protobuf.Empty) returns (InspectMachineResponse);
|
||||||
|
// InspectWireGuardNetwork retrieves the current WireGuard network configuration and peer status.
|
||||||
|
rpc InspectWireGuardNetwork(google.protobuf.Empty) returns (InspectWireGuardNetworkResponse);
|
||||||
// Reset restores the machine to a clean state, removing all cluster-related configuration and data.
|
// Reset restores the machine to a clean state, removing all cluster-related configuration and data.
|
||||||
rpc Reset(ResetRequest) returns (google.protobuf.Empty);
|
rpc Reset(ResetRequest) returns (google.protobuf.Empty);
|
||||||
|
|
||||||
@@ -58,6 +64,20 @@ message InitClusterResponse {
|
|||||||
message JoinClusterRequest {
|
message JoinClusterRequest {
|
||||||
MachineInfo machine = 1;
|
MachineInfo machine = 1;
|
||||||
repeated MachineInfo other_machines = 3;
|
repeated MachineInfo other_machines = 3;
|
||||||
|
// Minimum store database version the new machine should sync to before starting cluster operations.
|
||||||
|
int64 min_store_db_version = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message InspectMachineResponse {
|
||||||
|
// Must contain only one repeated messages field to allow broadcasting InspectMachine requests to multiple machines.
|
||||||
|
repeated MachineDetails machines = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message MachineDetails {
|
||||||
|
Metadata metadata = 1;
|
||||||
|
MachineInfo machine = 2;
|
||||||
|
// Current Corrosion cr-sqlite database version (Lamport timestamp) of the cluster store.
|
||||||
|
int64 store_db_version = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
message TokenResponse {
|
message TokenResponse {
|
||||||
@@ -88,3 +108,19 @@ message InspectServiceRequest {
|
|||||||
message InspectServiceResponse {
|
message InspectServiceResponse {
|
||||||
Service service = 1;
|
Service service = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message InspectWireGuardNetworkResponse {
|
||||||
|
string interface_name = 1;
|
||||||
|
bytes public_key = 2;
|
||||||
|
int32 listen_port = 3;
|
||||||
|
repeated WireGuardPeer peers = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message WireGuardPeer {
|
||||||
|
bytes public_key = 1;
|
||||||
|
string endpoint = 2;
|
||||||
|
google.protobuf.Timestamp last_handshake_time = 3;
|
||||||
|
int64 receive_bytes = 4;
|
||||||
|
int64 transmit_bytes = 5;
|
||||||
|
repeated string allowed_ips = 6;
|
||||||
|
}
|
||||||
|
|||||||
@@ -20,13 +20,15 @@ import (
|
|||||||
const _ = grpc.SupportPackageIsVersion9
|
const _ = grpc.SupportPackageIsVersion9
|
||||||
|
|
||||||
const (
|
const (
|
||||||
Machine_CheckPrerequisites_FullMethodName = "/api.Machine/CheckPrerequisites"
|
Machine_CheckPrerequisites_FullMethodName = "/api.Machine/CheckPrerequisites"
|
||||||
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_Reset_FullMethodName = "/api.Machine/Reset"
|
Machine_InspectMachine_FullMethodName = "/api.Machine/InspectMachine"
|
||||||
Machine_InspectService_FullMethodName = "/api.Machine/InspectService"
|
Machine_InspectWireGuardNetwork_FullMethodName = "/api.Machine/InspectWireGuardNetwork"
|
||||||
|
Machine_Reset_FullMethodName = "/api.Machine/Reset"
|
||||||
|
Machine_InspectService_FullMethodName = "/api.Machine/InspectService"
|
||||||
)
|
)
|
||||||
|
|
||||||
// MachineClient is the client API for Machine service.
|
// MachineClient is the client API for Machine service.
|
||||||
@@ -38,7 +40,12 @@ type MachineClient interface {
|
|||||||
InitCluster(ctx context.Context, in *InitClusterRequest, opts ...grpc.CallOption) (*InitClusterResponse, error)
|
InitCluster(ctx context.Context, in *InitClusterRequest, opts ...grpc.CallOption) (*InitClusterResponse, error)
|
||||||
JoinCluster(ctx context.Context, in *JoinClusterRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
|
JoinCluster(ctx context.Context, in *JoinClusterRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
|
||||||
Token(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*TokenResponse, error)
|
Token(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*TokenResponse, error)
|
||||||
|
// Deprecated: use InspectMachine instead.
|
||||||
Inspect(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*MachineInfo, error)
|
Inspect(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*MachineInfo, error)
|
||||||
|
// InspectMachine retrieves detailed information about the machine. Supports broadcasting to multiple machines.
|
||||||
|
InspectMachine(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*InspectMachineResponse, error)
|
||||||
|
// InspectWireGuardNetwork retrieves the current WireGuard network configuration and peer status.
|
||||||
|
InspectWireGuardNetwork(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*InspectWireGuardNetworkResponse, error)
|
||||||
// Reset restores the machine to a clean state, removing all cluster-related configuration and data.
|
// Reset restores the machine to a clean state, removing all cluster-related configuration and data.
|
||||||
Reset(ctx context.Context, in *ResetRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
|
Reset(ctx context.Context, in *ResetRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
|
||||||
InspectService(ctx context.Context, in *InspectServiceRequest, opts ...grpc.CallOption) (*InspectServiceResponse, error)
|
InspectService(ctx context.Context, in *InspectServiceRequest, opts ...grpc.CallOption) (*InspectServiceResponse, error)
|
||||||
@@ -102,6 +109,26 @@ func (c *machineClient) Inspect(ctx context.Context, in *emptypb.Empty, opts ...
|
|||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *machineClient) InspectMachine(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*InspectMachineResponse, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(InspectMachineResponse)
|
||||||
|
err := c.cc.Invoke(ctx, Machine_InspectMachine_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *machineClient) InspectWireGuardNetwork(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*InspectWireGuardNetworkResponse, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(InspectWireGuardNetworkResponse)
|
||||||
|
err := c.cc.Invoke(ctx, Machine_InspectWireGuardNetwork_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (c *machineClient) Reset(ctx context.Context, in *ResetRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) {
|
func (c *machineClient) Reset(ctx context.Context, in *ResetRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) {
|
||||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
out := new(emptypb.Empty)
|
out := new(emptypb.Empty)
|
||||||
@@ -131,7 +158,12 @@ type MachineServer interface {
|
|||||||
InitCluster(context.Context, *InitClusterRequest) (*InitClusterResponse, error)
|
InitCluster(context.Context, *InitClusterRequest) (*InitClusterResponse, error)
|
||||||
JoinCluster(context.Context, *JoinClusterRequest) (*emptypb.Empty, error)
|
JoinCluster(context.Context, *JoinClusterRequest) (*emptypb.Empty, error)
|
||||||
Token(context.Context, *emptypb.Empty) (*TokenResponse, error)
|
Token(context.Context, *emptypb.Empty) (*TokenResponse, error)
|
||||||
|
// Deprecated: use InspectMachine instead.
|
||||||
Inspect(context.Context, *emptypb.Empty) (*MachineInfo, error)
|
Inspect(context.Context, *emptypb.Empty) (*MachineInfo, error)
|
||||||
|
// InspectMachine retrieves detailed information about the machine. Supports broadcasting to multiple machines.
|
||||||
|
InspectMachine(context.Context, *emptypb.Empty) (*InspectMachineResponse, error)
|
||||||
|
// InspectWireGuardNetwork retrieves the current WireGuard network configuration and peer status.
|
||||||
|
InspectWireGuardNetwork(context.Context, *emptypb.Empty) (*InspectWireGuardNetworkResponse, error)
|
||||||
// Reset restores the machine to a clean state, removing all cluster-related configuration and data.
|
// Reset restores the machine to a clean state, removing all cluster-related configuration and data.
|
||||||
Reset(context.Context, *ResetRequest) (*emptypb.Empty, error)
|
Reset(context.Context, *ResetRequest) (*emptypb.Empty, error)
|
||||||
InspectService(context.Context, *InspectServiceRequest) (*InspectServiceResponse, error)
|
InspectService(context.Context, *InspectServiceRequest) (*InspectServiceResponse, error)
|
||||||
@@ -160,6 +192,12 @@ 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) InspectMachine(context.Context, *emptypb.Empty) (*InspectMachineResponse, error) {
|
||||||
|
return nil, status.Errorf(codes.Unimplemented, "method InspectMachine not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedMachineServer) InspectWireGuardNetwork(context.Context, *emptypb.Empty) (*InspectWireGuardNetworkResponse, error) {
|
||||||
|
return nil, status.Errorf(codes.Unimplemented, "method InspectWireGuardNetwork not implemented")
|
||||||
|
}
|
||||||
func (UnimplementedMachineServer) Reset(context.Context, *ResetRequest) (*emptypb.Empty, error) {
|
func (UnimplementedMachineServer) Reset(context.Context, *ResetRequest) (*emptypb.Empty, error) {
|
||||||
return nil, status.Errorf(codes.Unimplemented, "method Reset not implemented")
|
return nil, status.Errorf(codes.Unimplemented, "method Reset not implemented")
|
||||||
}
|
}
|
||||||
@@ -277,6 +315,42 @@ 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_InspectMachine_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(emptypb.Empty)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(MachineServer).InspectMachine(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: Machine_InspectMachine_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(MachineServer).InspectMachine(ctx, req.(*emptypb.Empty))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _Machine_InspectWireGuardNetwork_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(emptypb.Empty)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(MachineServer).InspectWireGuardNetwork(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: Machine_InspectWireGuardNetwork_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(MachineServer).InspectWireGuardNetwork(ctx, req.(*emptypb.Empty))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
func _Machine_Reset_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
func _Machine_Reset_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
in := new(ResetRequest)
|
in := new(ResetRequest)
|
||||||
if err := dec(in); err != nil {
|
if err := dec(in); err != nil {
|
||||||
@@ -340,6 +414,14 @@ var Machine_ServiceDesc = grpc.ServiceDesc{
|
|||||||
MethodName: "Inspect",
|
MethodName: "Inspect",
|
||||||
Handler: _Machine_Inspect_Handler,
|
Handler: _Machine_Inspect_Handler,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
MethodName: "InspectMachine",
|
||||||
|
Handler: _Machine_InspectMachine_Handler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MethodName: "InspectWireGuardNetwork",
|
||||||
|
Handler: _Machine_InspectWireGuardNetwork_Handler,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
MethodName: "Reset",
|
MethodName: "Reset",
|
||||||
Handler: _Machine_Reset_Handler,
|
Handler: _Machine_Reset_Handler,
|
||||||
|
|||||||
@@ -80,6 +80,8 @@ func (b *RemoteBackend) GetConnection(ctx context.Context, _ string) (context.Co
|
|||||||
backoffConfig.MaxDelay = 15 * time.Second
|
backoffConfig.MaxDelay = 15 * time.Second
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
|
// This client keeps retrying connection in the background indefinitely even after the first connection fails
|
||||||
|
// and the Unavailable error is returned to the caller.
|
||||||
b.conn, err = grpc.NewClient(
|
b.conn, err = grpc.NewClient(
|
||||||
b.target,
|
b.target,
|
||||||
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
||||||
@@ -88,7 +90,7 @@ func (b *RemoteBackend) GetConnection(ctx context.Context, _ string) (context.Co
|
|||||||
// Not published as a constant in gRPC library.
|
// Not published as a constant in gRPC library.
|
||||||
// See: https://github.com/grpc/grpc-go/blob/d5dee5fdbdeb52f6ea10b37b2cc7ce37814642d7/clientconn.go#L55-L56
|
// See: https://github.com/grpc/grpc-go/blob/d5dee5fdbdeb52f6ea10b37b2cc7ce37814642d7/clientconn.go#L55-L56
|
||||||
// Each connection attempt can take up to MinConnectTimeout.
|
// Each connection attempt can take up to MinConnectTimeout.
|
||||||
MinConnectTimeout: 20 * time.Second,
|
MinConnectTimeout: 10 * time.Second,
|
||||||
}),
|
}),
|
||||||
grpc.WithDefaultCallOptions(
|
grpc.WithDefaultCallOptions(
|
||||||
grpc.ForceCodecV2(proxy.Codec()),
|
grpc.ForceCodecV2(proxy.Codec()),
|
||||||
|
|||||||
@@ -12,14 +12,16 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"text/template"
|
"text/template"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/psviderski/uncloud/internal/machine/store"
|
"github.com/psviderski/uncloud/internal/machine/store"
|
||||||
"github.com/psviderski/uncloud/pkg/api"
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
caddyfileHeader = `# This file is autogenerated by Uncloud based on the configuration of running services.
|
caddyfileHeaderFmt = `# Caddyfile autogenerated by Uncloud (DO NOT EDIT): %s
|
||||||
# Do not edit manually. Any manual changes will be overwritten on the next update.
|
# Automatically updated on service or health status changes.
|
||||||
|
# Docs: https://uncloud.run/docs/concepts/ingress/overview
|
||||||
`
|
`
|
||||||
caddyfileTemplate = `# Health check endpoint to verify Caddy reachability on this machine.
|
caddyfileTemplate = `# Health check endpoint to verify Caddy reachability on this machine.
|
||||||
http:// {
|
http:// {
|
||||||
@@ -56,7 +58,9 @@ https://{{$hostname}} {
|
|||||||
}{{end}}
|
}{{end}}
|
||||||
`
|
`
|
||||||
caddyfileUnavailabeFooter = `# NOTE: User-defined configs for services were skipped because Caddy is not running on this machine
|
caddyfileUnavailabeFooter = `# NOTE: User-defined configs for services were skipped because Caddy is not running on this machine
|
||||||
# or the latest generated config is invalid. Please check the Caddy logs if it's running.
|
# (not accessible via the shared admin socket /run/uncloud/caddy/admin.sock) or the latest
|
||||||
|
# generated config is invalid. Please check the service 'caddy' is running (uc inspect caddy)
|
||||||
|
# and its logs for more details (uc logs caddy).
|
||||||
`
|
`
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -120,6 +124,7 @@ func (g *CaddyfileGenerator) Generate(
|
|||||||
return "", fmt.Errorf("generate base Caddyfile from service ports: %w", err)
|
return "", fmt.Errorf("generate base Caddyfile from service ports: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
caddyfileHeader := fmt.Sprintf(caddyfileHeaderFmt, time.Now().UTC().Format(time.RFC3339))
|
||||||
if !includeCustom {
|
if !includeCustom {
|
||||||
return fmt.Sprintf("%s\n%s\n%s", caddyfileHeader, caddyfile, caddyfileUnavailabeFooter), nil
|
return fmt.Sprintf("%s\n%s\n%s", caddyfileHeader, caddyfile, caddyfileUnavailabeFooter), nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package caddyconfig
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@@ -17,8 +18,17 @@ import (
|
|||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
const testCaddyfileHeader = `# This file is autogenerated by Uncloud based on the configuration of running services.
|
// generatedTimestampRegex matches the "(DO NOT EDIT): <RFC3339 timestamp>" part of the header.
|
||||||
# Do not edit manually. Any manual changes will be overwritten on the next update.
|
var generatedTimestampRegex = regexp.MustCompile(`\(DO NOT EDIT\): \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z`)
|
||||||
|
|
||||||
|
// normaliseGeneratedTimestamp replaces dynamic timestamps with a placeholder for test comparison.
|
||||||
|
func normaliseGeneratedTimestamp(caddyfile string) string {
|
||||||
|
return generatedTimestampRegex.ReplaceAllString(caddyfile, "(DO NOT EDIT): TIMESTAMP_PLACEHOLDER")
|
||||||
|
}
|
||||||
|
|
||||||
|
const testCaddyfileHeader = `# Caddyfile autogenerated by Uncloud (DO NOT EDIT): TIMESTAMP_PLACEHOLDER
|
||||||
|
# Automatically updated on service or health status changes.
|
||||||
|
# Docs: https://uncloud.run/docs/concepts/ingress/overview
|
||||||
|
|
||||||
# Health check endpoint to verify Caddy reachability on this machine.
|
# Health check endpoint to verify Caddy reachability on this machine.
|
||||||
http:// {
|
http:// {
|
||||||
@@ -190,7 +200,7 @@ https://secure.example.com {
|
|||||||
}
|
}
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.Equal(t, tt.want, config, "Generated Caddyfile doesn't match")
|
assert.Equal(t, tt.want, normaliseGeneratedTimestamp(config), "Generated Caddyfile doesn't match")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -216,8 +226,9 @@ func TestCaddyfileGeneratorWithCustomConfigs(t *testing.T) {
|
|||||||
time.Now(),
|
time.Now(),
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
want: `# This file is autogenerated by Uncloud based on the configuration of running services.
|
want: `# Caddyfile autogenerated by Uncloud (DO NOT EDIT): TIMESTAMP_PLACEHOLDER
|
||||||
# Do not edit manually. Any manual changes will be overwritten on the next update.
|
# Automatically updated on service or health status changes.
|
||||||
|
# Docs: https://uncloud.run/docs/concepts/ingress/overview
|
||||||
|
|
||||||
# User-defined global config from service 'caddy'.
|
# User-defined global config from service 'caddy'.
|
||||||
# Global Caddy configuration
|
# Global Caddy configuration
|
||||||
@@ -412,8 +423,9 @@ web.example.com {
|
|||||||
time.Now(),
|
time.Now(),
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
want: `# This file is autogenerated by Uncloud based on the configuration of running services.
|
want: `# Caddyfile autogenerated by Uncloud (DO NOT EDIT): TIMESTAMP_PLACEHOLDER
|
||||||
# Do not edit manually. Any manual changes will be overwritten on the next update.
|
# Automatically updated on service or health status changes.
|
||||||
|
# Docs: https://uncloud.run/docs/concepts/ingress/overview
|
||||||
|
|
||||||
# User-defined global config from service 'caddy'.
|
# User-defined global config from service 'caddy'.
|
||||||
# Global config
|
# Global config
|
||||||
@@ -649,8 +661,9 @@ badconfig.com {
|
|||||||
time.Now(),
|
time.Now(),
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
want: `# This file is autogenerated by Uncloud based on the configuration of running services.
|
want: `# Caddyfile autogenerated by Uncloud (DO NOT EDIT): TIMESTAMP_PLACEHOLDER
|
||||||
# Do not edit manually. Any manual changes will be overwritten on the next update.
|
# Automatically updated on service or health status changes.
|
||||||
|
# Docs: https://uncloud.run/docs/concepts/ingress/overview
|
||||||
|
|
||||||
# User-defined global config from service 'caddy'.
|
# User-defined global config from service 'caddy'.
|
||||||
# Global config from test machine
|
# Global config from test machine
|
||||||
@@ -831,7 +844,7 @@ valid.example.com {
|
|||||||
}
|
}
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.Equal(t, tt.want, config, "Generated Caddyfile doesn't match")
|
assert.Equal(t, tt.want, normaliseGeneratedTimestamp(config), "Generated Caddyfile doesn't match")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -926,7 +939,9 @@ http://api.example.com {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# NOTE: User-defined configs for services were skipped because Caddy is not running on this machine
|
# NOTE: User-defined configs for services were skipped because Caddy is not running on this machine
|
||||||
# or the latest generated config is invalid. Please check the Caddy logs if it's running.
|
# (not accessible via the shared admin socket /run/uncloud/caddy/admin.sock) or the latest
|
||||||
|
# generated config is invalid. Please check the service 'caddy' is running (uc inspect caddy)
|
||||||
|
# and its logs for more details (uc logs caddy).
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -950,7 +965,9 @@ http://api.example.com {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# NOTE: User-defined configs for services were skipped because Caddy is not running on this machine
|
# NOTE: User-defined configs for services were skipped because Caddy is not running on this machine
|
||||||
# or the latest generated config is invalid. Please check the Caddy logs if it's running.
|
# (not accessible via the shared admin socket /run/uncloud/caddy/admin.sock) or the latest
|
||||||
|
# generated config is invalid. Please check the service 'caddy' is running (uc inspect caddy)
|
||||||
|
# and its logs for more details (uc logs caddy).
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -964,7 +981,7 @@ http://api.example.com {
|
|||||||
config, err := generator.Generate(ctx, tt.containers, false)
|
config, err := generator.Generate(ctx, tt.containers, false)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.Equal(t, tt.want, config, "Generated Caddyfile doesn't match")
|
assert.Equal(t, tt.want, normaliseGeneratedTimestamp(config), "Generated Caddyfile doesn't match")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+171
-52
@@ -40,7 +40,9 @@ type clusterController struct {
|
|||||||
corroService corroservice.Service
|
corroService corroservice.Service
|
||||||
dockerCtrl *docker.Controller
|
dockerCtrl *docker.Controller
|
||||||
// dockerReady is signalled when Docker is configured and ready for containers.
|
// dockerReady is signalled when Docker is configured and ready for containers.
|
||||||
dockerReady chan<- struct{}
|
dockerReady chan<- struct{}
|
||||||
|
// clusterReady is signalled when the cluster controller has finished initializing all components.
|
||||||
|
clusterReady chan<- struct{}
|
||||||
caddyconfigCtrl *caddyconfig.Controller
|
caddyconfigCtrl *caddyconfig.Controller
|
||||||
|
|
||||||
// dnsServer is the embedded internal DNS server for the cluster listening on the machine IP.
|
// dnsServer is the embedded internal DNS server for the cluster listening on the machine IP.
|
||||||
@@ -60,6 +62,7 @@ func newClusterController(
|
|||||||
corroService corroservice.Service,
|
corroService corroservice.Service,
|
||||||
dockerService *docker.Service,
|
dockerService *docker.Service,
|
||||||
dockerReady chan<- struct{},
|
dockerReady chan<- struct{},
|
||||||
|
clusterReady chan<- struct{},
|
||||||
caddyfileCtrl *caddyconfig.Controller,
|
caddyfileCtrl *caddyconfig.Controller,
|
||||||
dnsServer *dns.Server,
|
dnsServer *dns.Server,
|
||||||
dnsResolver *dns.ClusterResolver,
|
dnsResolver *dns.ClusterResolver,
|
||||||
@@ -81,6 +84,7 @@ func newClusterController(
|
|||||||
corroService: corroService,
|
corroService: corroService,
|
||||||
dockerCtrl: docker.NewController(state.ID, dockerService, store),
|
dockerCtrl: docker.NewController(state.ID, dockerService, store),
|
||||||
dockerReady: dockerReady,
|
dockerReady: dockerReady,
|
||||||
|
clusterReady: clusterReady,
|
||||||
caddyconfigCtrl: caddyfileCtrl,
|
caddyconfigCtrl: caddyfileCtrl,
|
||||||
dnsServer: dnsServer,
|
dnsServer: dnsServer,
|
||||||
dnsResolver: dnsResolver,
|
dnsResolver: dnsResolver,
|
||||||
@@ -123,6 +127,21 @@ func (cc *clusterController) Run(ctx context.Context) error {
|
|||||||
|
|
||||||
errGroup, ctx := errgroup.WithContext(ctx)
|
errGroup, ctx := errgroup.WithContext(ctx)
|
||||||
|
|
||||||
|
// Start the WireGuard control loop before waiting for store sync. This ensures endpoint rotation happens
|
||||||
|
// while waiting, allowing Corrosion to connect to peers.
|
||||||
|
errGroup.Go(func() error {
|
||||||
|
if err := cc.wgnet.Run(ctx); err != nil {
|
||||||
|
return fmt.Errorf("WireGuard network failed: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
// Watch for WireGuard peer endpoint changes and update the machine state accordingly.
|
||||||
|
errGroup.Go(func() error {
|
||||||
|
cc.handleEndpointChanges(ctx)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
// 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.
|
||||||
apiAddr := net.JoinHostPort(cc.state.Network.ManagementIP.String(), strconv.Itoa(constants.MachineAPIPort))
|
apiAddr := net.JoinHostPort(cc.state.Network.ManagementIP.String(), strconv.Itoa(constants.MachineAPIPort))
|
||||||
listener, err := net.Listen("tcp", apiAddr)
|
listener, err := net.Listen("tcp", apiAddr)
|
||||||
@@ -137,6 +156,22 @@ func (cc *clusterController) Run(ctx context.Context) error {
|
|||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Wait for the store database to sync to the minimum version before starting store-dependent components.
|
||||||
|
// This prevents issues with using partially replicated data when the machine just joined the cluster,
|
||||||
|
// e.g., an empty machine list causing WireGuard peer misconfiguration.
|
||||||
|
cc.waitStoreSync(ctx)
|
||||||
|
|
||||||
|
// Check if waitStoreSync exited because the context was cancelled. Return early in that case.
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
cc.stopAPIServer()
|
||||||
|
|
||||||
|
err := errGroup.Wait()
|
||||||
|
if corroErr := cc.stopCorrosion(); corroErr != nil {
|
||||||
|
err = errors.Join(err, corroErr)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
errGroup.Go(func() error {
|
errGroup.Go(func() error {
|
||||||
slog.Info("Starting embedded DNS resolver.")
|
slog.Info("Starting embedded DNS resolver.")
|
||||||
if err := cc.dnsResolver.Run(ctx); err != nil {
|
if err := cc.dnsResolver.Run(ctx); err != nil {
|
||||||
@@ -169,44 +204,6 @@ func (cc *clusterController) Run(ctx context.Context) error {
|
|||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
// Watch for WireGuard peer endpoint changes and update the machine state accordingly.
|
|
||||||
errGroup.Go(func() error {
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case e, ok := <-cc.endpointChanges:
|
|
||||||
if !ok {
|
|
||||||
// The channel was closed, stop watching for changes.
|
|
||||||
cc.endpointChanges = nil
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
cc.state.mu.Lock()
|
|
||||||
for i := range cc.state.Network.Peers {
|
|
||||||
if cc.state.Network.Peers[i].PublicKey.Equal(e.PublicKey) {
|
|
||||||
cc.state.Network.Peers[i].Endpoint = &e.Endpoint
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err := cc.state.Save(); err != nil {
|
|
||||||
slog.Error("Failed to save machine state.", "err", err)
|
|
||||||
}
|
|
||||||
cc.state.mu.Unlock()
|
|
||||||
|
|
||||||
slog.Debug("Preserved endpoint change in the machine state.",
|
|
||||||
"public_key", e.PublicKey, "endpoint", e.Endpoint)
|
|
||||||
case <-ctx.Done():
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
errGroup.Go(func() error {
|
|
||||||
if err := cc.wgnet.Run(ctx); err != nil {
|
|
||||||
return fmt.Errorf("WireGuard network failed: %w", err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
|
|
||||||
errGroup.Go(func() error {
|
errGroup.Go(func() error {
|
||||||
slog.Info("Starting caddyconfig controller.")
|
slog.Info("Starting caddyconfig controller.")
|
||||||
if err := cc.caddyconfigCtrl.Run(ctx); err != nil {
|
if err := cc.caddyconfigCtrl.Run(ctx); err != nil {
|
||||||
@@ -225,12 +222,13 @@ func (cc *clusterController) Run(ctx context.Context) error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait for the context to be done and stop the network API server.
|
// Signal that the cluster controller has finished starting all components.
|
||||||
|
close(cc.clusterReady)
|
||||||
|
slog.Info("Cluster controller finished starting all components.")
|
||||||
|
// Wait for the context to be done and stop all servers and controllers.
|
||||||
<-ctx.Done()
|
<-ctx.Done()
|
||||||
slog.Info("Stopping network API server.")
|
|
||||||
// TODO: implement timeout for graceful shutdown.
|
cc.stopAPIServer()
|
||||||
cc.server.GracefulStop()
|
|
||||||
slog.Info("Network API server stopped.")
|
|
||||||
|
|
||||||
// Stop the unregistry server with a timeout if it was started.
|
// Stop the unregistry server with a timeout if it was started.
|
||||||
if cc.unregistry != nil {
|
if cc.unregistry != nil {
|
||||||
@@ -248,19 +246,50 @@ func (cc *clusterController) Run(ctx context.Context) error {
|
|||||||
// Wait for all controllers to finish.
|
// Wait for all controllers to finish.
|
||||||
err = errGroup.Wait()
|
err = errGroup.Wait()
|
||||||
|
|
||||||
// It's safe to stop the Corrosion service after the controllers depending on it and API server are stopped.
|
// Stop Corrosion after all controllers depending on it and API server are stopped.
|
||||||
// Use a new context with a timeout as the current context is already canceled.
|
if corroErr := cc.stopCorrosion(); corroErr != nil {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
err = errors.Join(err, corroErr)
|
||||||
defer cancel()
|
|
||||||
if corroErr := cc.corroService.Stop(ctx); corroErr != nil {
|
|
||||||
err = errors.Join(err, fmt.Errorf("stop corrosion service: %w", corroErr))
|
|
||||||
} else {
|
|
||||||
slog.Info("Corrosion service stopped.")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// stopAPIServer gracefully stops the network API server with a timeout.
|
||||||
|
func (cc *clusterController) stopAPIServer() {
|
||||||
|
timeout := 10 * time.Second
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
stopped := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
slog.Info("Stopping network API server.")
|
||||||
|
cc.server.GracefulStop()
|
||||||
|
close(stopped)
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
slog.Warn("Network API server graceful stop timed out, forcing stop.", "timeout", timeout)
|
||||||
|
cc.server.Stop()
|
||||||
|
case <-stopped:
|
||||||
|
}
|
||||||
|
|
||||||
|
slog.Info("Network API server stopped.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// stopCorrosion stops the Corrosion service with a timeout.
|
||||||
|
func (cc *clusterController) stopCorrosion() error {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
if err := cc.corroService.Stop(ctx); err != nil {
|
||||||
|
return fmt.Errorf("stop corrosion service: %w", err)
|
||||||
|
}
|
||||||
|
slog.Info("Corrosion service stopped.")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// ensureDockerNetwork ensures that the Docker network is configured and ready for containers.
|
// ensureDockerNetwork ensures that the Docker network is configured and ready for containers.
|
||||||
func (cc *clusterController) ensureDockerNetwork(ctx context.Context) error {
|
func (cc *clusterController) ensureDockerNetwork(ctx context.Context) error {
|
||||||
if err := cc.dockerCtrl.WaitDaemonReady(ctx); err != nil {
|
if err := cc.dockerCtrl.WaitDaemonReady(ctx); err != nil {
|
||||||
@@ -281,6 +310,95 @@ func (cc *clusterController) ensureDockerNetwork(ctx context.Context) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// handleEndpointChanges watches for WireGuard peer endpoint changes and persists them to the machine state.
|
||||||
|
func (cc *clusterController) handleEndpointChanges(ctx context.Context) {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case e, ok := <-cc.endpointChanges:
|
||||||
|
if !ok {
|
||||||
|
// The channel was closed, stop watching for changes.
|
||||||
|
cc.endpointChanges = nil
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cc.state.mu.Lock()
|
||||||
|
for i := range cc.state.Network.Peers {
|
||||||
|
if cc.state.Network.Peers[i].PublicKey.Equal(e.PublicKey) {
|
||||||
|
cc.state.Network.Peers[i].Endpoint = &e.Endpoint
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := cc.state.Save(); err != nil {
|
||||||
|
slog.Error("Failed to save machine state.", "err", err)
|
||||||
|
}
|
||||||
|
cc.state.mu.Unlock()
|
||||||
|
|
||||||
|
slog.Debug("Preserved endpoint change in the machine state.",
|
||||||
|
"public_key", e.PublicKey, "endpoint", e.Endpoint)
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// waitStoreSync waits for the store database to sync to the minimum required DB version if set in the machine state.
|
||||||
|
// Blocks until synced or context is cancelled.
|
||||||
|
func (cc *clusterController) waitStoreSync(ctx context.Context) {
|
||||||
|
minVersion := cc.state.MinStoreDBVersion
|
||||||
|
if minVersion == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
slog.Info("Waiting for the initial cluster store sync.", "min_version", minVersion)
|
||||||
|
|
||||||
|
ticker := time.NewTicker(500 * time.Millisecond)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
var (
|
||||||
|
lastVersion int64
|
||||||
|
lastLogTime time.Time
|
||||||
|
lastErrLogTime time.Time
|
||||||
|
)
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
case <-ticker.C:
|
||||||
|
version, err := cc.store.DBVersion(ctx)
|
||||||
|
if err != nil {
|
||||||
|
// Log errors at most once every 5 seconds.
|
||||||
|
if time.Since(lastErrLogTime) >= 5*time.Second {
|
||||||
|
slog.Error("Failed to get the cluster store DB version, retrying.", "err", err)
|
||||||
|
lastErrLogTime = time.Now()
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if version >= minVersion {
|
||||||
|
slog.Info("Cluster store completed the initial sync.", "version", version, "min_version", minVersion)
|
||||||
|
|
||||||
|
// Clear MinStoreDBVersion so next restart doesn't wait for sync.
|
||||||
|
cc.state.mu.Lock()
|
||||||
|
cc.state.MinStoreDBVersion = 0
|
||||||
|
if err := cc.state.Save(); err != nil {
|
||||||
|
slog.Error("Failed to save machine state after the initial cluster store sync.", "err", err)
|
||||||
|
}
|
||||||
|
cc.state.mu.Unlock()
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log progress only once a second.
|
||||||
|
if version != lastVersion && time.Since(lastLogTime) >= 1*time.Second {
|
||||||
|
slog.Info("Syncing cluster store.", "version", version, "min_version", minVersion)
|
||||||
|
lastLogTime = time.Now()
|
||||||
|
lastVersion = version
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// syncDockerContainers watches local Docker containers and syncs them to the cluster store.
|
// syncDockerContainers watches local Docker containers and syncs them to the cluster store.
|
||||||
// TODO: move this to the Docker controller.
|
// TODO: move this to the Docker controller.
|
||||||
func (cc *clusterController) syncDockerContainers(ctx context.Context) error {
|
func (cc *clusterController) syncDockerContainers(ctx context.Context) error {
|
||||||
@@ -340,6 +458,7 @@ func (cc *clusterController) handleMachineChanges(ctx context.Context) error {
|
|||||||
|
|
||||||
// The machine store may be empty when a machine first joins the cluster, before store synchronization
|
// 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.
|
// completes. Skip configuration now and apply it when the store changes are received.
|
||||||
|
// TODO: remove this check after releasing 0.17.0 and assuming cluster machines wait for store sync on join.
|
||||||
if len(machines) > 0 {
|
if len(machines) > 0 {
|
||||||
slog.Info("Reconfiguring network peers with the current machines.", "machines", len(machines))
|
slog.Info("Reconfiguring network peers with the current machines.", "machines", len(machines))
|
||||||
if err = cc.configurePeers(machines); err != nil {
|
if err = cc.configurePeers(machines); err != nil {
|
||||||
|
|||||||
@@ -26,12 +26,19 @@ type Cluster struct {
|
|||||||
corroAdmin *corrosion.AdminClient
|
corroAdmin *corrosion.AdminClient
|
||||||
// machineID is the ID of the current machine that is running the cluster service.
|
// machineID is the ID of the current machine that is running the cluster service.
|
||||||
machineID string
|
machineID string
|
||||||
|
// initialised is closed when the machine is configured as a member of a cluster.
|
||||||
|
initialised <-chan struct{}
|
||||||
|
// ready is closed when the cluster controller has finished starting all components
|
||||||
|
// and the machine is ready to serve cluster requests.
|
||||||
|
ready <-chan struct{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewCluster(store *store.Store, corroAdmin *corrosion.AdminClient) *Cluster {
|
func NewCluster(store *store.Store, corroAdmin *corrosion.AdminClient, initialised, ready <-chan struct{}) *Cluster {
|
||||||
return &Cluster{
|
return &Cluster{
|
||||||
store: store,
|
store: store,
|
||||||
corroAdmin: corroAdmin,
|
corroAdmin: corroAdmin,
|
||||||
|
initialised: initialised,
|
||||||
|
ready: ready,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,67 +48,45 @@ func (c *Cluster) UpdateMachineID(mid string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *Cluster) Init(ctx context.Context, network netip.Prefix) error {
|
func (c *Cluster) Init(ctx context.Context, network netip.Prefix) error {
|
||||||
initialised, err := c.Initialised(ctx)
|
select {
|
||||||
if err != nil {
|
case <-c.initialised:
|
||||||
return err
|
return fmt.Errorf("cluster is already initialised on this machine")
|
||||||
}
|
default:
|
||||||
if initialised {
|
|
||||||
return fmt.Errorf("cluster is already initialised")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = c.store.Put(ctx, "network", network.String()); err != nil {
|
if err := c.store.Put(ctx, "network", network.String()); err != nil {
|
||||||
return fmt.Errorf("put network to store: %w", err)
|
return fmt.Errorf("put network to store: %w", err)
|
||||||
}
|
}
|
||||||
if err = c.store.Put(ctx, "created_at", time.Now().UTC().Format(time.RFC3339)); err != nil {
|
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)
|
return fmt.Errorf("put created_at to store: %w", err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Cluster) Initialised(ctx context.Context) (bool, error) {
|
// checkReady checks if the machine is ready to serve cluster requests (store synced, cluster components started).
|
||||||
var createdAt string
|
func (c *Cluster) checkReady() error {
|
||||||
if err := c.store.Get(ctx, "created_at", &createdAt); err != nil {
|
select {
|
||||||
if errors.Is(err, store.ErrKeyNotFound) {
|
case <-c.ready:
|
||||||
return false, nil
|
return nil
|
||||||
}
|
default:
|
||||||
return false, status.Errorf(codes.Internal, "get created_at from store: %v", err)
|
return status.Error(codes.Unavailable, "machine is not ready to serve cluster requests")
|
||||||
}
|
}
|
||||||
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 err := c.checkInitialised(ctx); err != nil {
|
if err := c.checkReady(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return c.AddMachineWithoutReadyCheck(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddMachineWithoutReadyCheck adds a machine to the cluster without checking if the cluster is ready.
|
||||||
|
// This is used internally during cluster initialisation to add the first machine.
|
||||||
|
func (c *Cluster) AddMachineWithoutReadyCheck(
|
||||||
|
ctx context.Context, req *pb.AddMachineRequest,
|
||||||
|
) (*pb.AddMachineResponse, error) {
|
||||||
if req.Network == nil {
|
if req.Network == nil {
|
||||||
return nil, status.Error(codes.InvalidArgument, "network not set")
|
return nil, status.Error(codes.InvalidArgument, "network not set")
|
||||||
}
|
}
|
||||||
@@ -162,7 +147,7 @@ func (c *Cluster) AddMachine(ctx context.Context, req *pb.AddMachineRequest) (*p
|
|||||||
manageIP = pb.NewIP(network.ManagementIP(req.Network.PublicKey))
|
manageIP = pb.NewIP(network.ManagementIP(req.Network.PublicKey))
|
||||||
}
|
}
|
||||||
// Allocate a subnet for the machine from the cluster network.
|
// Allocate a subnet for the machine from the cluster network.
|
||||||
clusterNetwork, err := c.Network(ctx)
|
clusterNetwork, err := c.network(ctx)
|
||||||
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)
|
||||||
}
|
}
|
||||||
@@ -198,9 +183,21 @@ func (c *Cluster) AddMachine(ctx context.Context, req *pb.AddMachineRequest) (*p
|
|||||||
return resp, nil
|
return resp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Cluster) network(ctx context.Context) (netip.Prefix, error) {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
// UpdateMachine updates machine configuration in the cluster.
|
// UpdateMachine updates machine configuration in the cluster.
|
||||||
func (c *Cluster) UpdateMachine(ctx context.Context, req *pb.UpdateMachineRequest) (*pb.UpdateMachineResponse, error) {
|
func (c *Cluster) UpdateMachine(ctx context.Context, req *pb.UpdateMachineRequest) (*pb.UpdateMachineResponse, error) {
|
||||||
if err := c.checkInitialised(ctx); err != nil {
|
if err := c.checkReady(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -283,7 +280,7 @@ func (c *Cluster) UpdateMachine(ctx context.Context, req *pb.UpdateMachineReques
|
|||||||
|
|
||||||
// ListMachines lists all machines in the cluster including their membership states.
|
// ListMachines lists all machines in the cluster including their membership states.
|
||||||
func (c *Cluster) ListMachines(ctx context.Context, _ *emptypb.Empty) (*pb.ListMachinesResponse, error) {
|
func (c *Cluster) ListMachines(ctx context.Context, _ *emptypb.Empty) (*pb.ListMachinesResponse, error) {
|
||||||
if err := c.checkInitialised(ctx); err != nil {
|
if err := c.checkReady(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -329,7 +326,7 @@ func (c *Cluster) ListMachines(ctx context.Context, _ *emptypb.Empty) (*pb.ListM
|
|||||||
|
|
||||||
// RemoveMachine removes a machine from the cluster.
|
// RemoveMachine removes a machine from the cluster.
|
||||||
func (c *Cluster) RemoveMachine(ctx context.Context, req *pb.RemoveMachineRequest) (*emptypb.Empty, error) {
|
func (c *Cluster) RemoveMachine(ctx context.Context, req *pb.RemoveMachineRequest) (*emptypb.Empty, error) {
|
||||||
if err := c.checkInitialised(ctx); err != nil {
|
if err := c.checkReady(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ type uncloudDNSDomain struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *Cluster) ReserveDomain(ctx context.Context, req *pb.ReserveDomainRequest) (*pb.Domain, error) {
|
func (c *Cluster) ReserveDomain(ctx context.Context, req *pb.ReserveDomainRequest) (*pb.Domain, error) {
|
||||||
if err := c.checkInitialised(ctx); err != nil {
|
if err := c.checkReady(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,7 +64,7 @@ func (c *Cluster) ReserveDomain(ctx context.Context, req *pb.ReserveDomainReques
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *Cluster) GetDomain(ctx context.Context, _ *emptypb.Empty) (*pb.Domain, error) {
|
func (c *Cluster) GetDomain(ctx context.Context, _ *emptypb.Empty) (*pb.Domain, error) {
|
||||||
if err := c.checkInitialised(ctx); err != nil {
|
if err := c.checkReady(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,7 +95,7 @@ func (c *Cluster) storedDomain(ctx context.Context) (uncloudDNSDomain, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *Cluster) ReleaseDomain(ctx context.Context, _ *emptypb.Empty) (*pb.Domain, error) {
|
func (c *Cluster) ReleaseDomain(ctx context.Context, _ *emptypb.Empty) (*pb.Domain, error) {
|
||||||
if err := c.checkInitialised(ctx); err != nil {
|
if err := c.checkReady(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,7 +115,7 @@ func (c *Cluster) ReleaseDomain(ctx context.Context, _ *emptypb.Empty) (*pb.Doma
|
|||||||
func (c *Cluster) CreateDomainRecords(
|
func (c *Cluster) CreateDomainRecords(
|
||||||
ctx context.Context, req *pb.CreateDomainRecordsRequest,
|
ctx context.Context, req *pb.CreateDomainRecordsRequest,
|
||||||
) (*pb.CreateDomainRecordsResponse, error) {
|
) (*pb.CreateDomainRecordsResponse, error) {
|
||||||
if err := c.checkInitialised(ctx); err != nil {
|
if err := c.checkReady(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -201,10 +201,10 @@ func handleClientOutputStream(ctx context.Context, stream pb.Docker_ExecContaine
|
|||||||
// ExecContainer executes a command in a running container with bidirectional streaming.
|
// ExecContainer executes a command in a running container with bidirectional streaming.
|
||||||
// TODO: This can be merged with pkg/client as it's an unnecessary logic split.
|
// TODO: This can be merged with pkg/client as it's an unnecessary logic split.
|
||||||
func (c *Client) ExecContainer(ctx context.Context, opts ExecConfig) (exitCode int, err error) {
|
func (c *Client) ExecContainer(ctx context.Context, opts ExecConfig) (exitCode int, err error) {
|
||||||
// TODO: We need to handle Ctrl-C and other signals here to forward them to the container process.
|
// Note: In non-interactive mode (without TTY), signals like SIGTERM/SIGINT will terminate the client
|
||||||
// Right now, Ctrl-C will just terminate the client process, which is not ideal.
|
// process without being forwarded to the remote container process. We could catch and forward these
|
||||||
// We should catch the signal, send it to the container process, and only exit
|
// signals (useful for long-running commands), but for example "kubectl exec" doesn't do this either (as of November 2025),
|
||||||
// when the container process exits.
|
// so we keep it simple for now.
|
||||||
|
|
||||||
slog.Debug("starting ExecContainer", "containerID", opts.ContainerID, "options", opts.Options)
|
slog.Debug("starting ExecContainer", "containerID", opts.ContainerID, "options", opts.Options)
|
||||||
|
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ import (
|
|||||||
"google.golang.org/grpc/codes"
|
"google.golang.org/grpc/codes"
|
||||||
"google.golang.org/grpc/status"
|
"google.golang.org/grpc/status"
|
||||||
"google.golang.org/protobuf/types/known/emptypb"
|
"google.golang.org/protobuf/types/known/emptypb"
|
||||||
|
"google.golang.org/protobuf/types/known/timestamppb"
|
||||||
)
|
)
|
||||||
|
|
||||||
var fullDockerIDRegex = regexp.MustCompile(`^[a-f0-9]{64}$`)
|
var fullDockerIDRegex = regexp.MustCompile(`^[a-f0-9]{64}$`)
|
||||||
@@ -604,6 +605,8 @@ func (s *Server) CreateServiceContainer(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
hostConfig := &container.HostConfig{
|
hostConfig := &container.HostConfig{
|
||||||
|
CapAdd: spec.Container.CapAdd,
|
||||||
|
CapDrop: spec.Container.CapDrop,
|
||||||
Binds: spec.Container.Volumes,
|
Binds: spec.Container.Volumes,
|
||||||
Init: spec.Container.Init,
|
Init: spec.Container.Init,
|
||||||
Mounts: mounts,
|
Mounts: mounts,
|
||||||
@@ -620,6 +623,7 @@ func (s *Server) CreateServiceContainer(
|
|||||||
RestartPolicy: container.RestartPolicy{
|
RestartPolicy: container.RestartPolicy{
|
||||||
Name: container.RestartPolicyUnlessStopped,
|
Name: container.RestartPolicyUnlessStopped,
|
||||||
},
|
},
|
||||||
|
Sysctls: spec.Container.Sysctls,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Configure the container to use the internal DNS server if it's available.
|
// Configure the container to use the internal DNS server if it's available.
|
||||||
@@ -800,14 +804,17 @@ func (s *Server) injectConfigs(ctx context.Context, containerID string, configs
|
|||||||
}
|
}
|
||||||
|
|
||||||
// copyContentToContainer copies content directly to a file in the container using Docker's CopyToContainer API.
|
// copyContentToContainer copies content directly to a file in the container using Docker's CopyToContainer API.
|
||||||
|
// It will create any intermediate directories in the target path that don't exist.
|
||||||
func (s *Server) copyContentToContainer(ctx context.Context, containerID string, content []byte, targetPath string, uid *uint64, gid *uint64, fileMode os.FileMode) error {
|
func (s *Server) copyContentToContainer(ctx context.Context, containerID string, content []byte, targetPath string, uid *uint64, gid *uint64, fileMode os.FileMode) error {
|
||||||
// Create a tar archive containing the file
|
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
tw := tar.NewWriter(&buf)
|
tw := tar.NewWriter(&buf)
|
||||||
|
|
||||||
// Create tar header
|
// Trim leading slash(es) to avoid double slashes in the tar path
|
||||||
|
tarPath := strings.TrimPrefix(targetPath, "/")
|
||||||
|
|
||||||
|
// Create tar header with full path
|
||||||
header := &tar.Header{
|
header := &tar.Header{
|
||||||
Name: filepath.Base(targetPath),
|
Name: tarPath,
|
||||||
Size: int64(len(content)),
|
Size: int64(len(content)),
|
||||||
Mode: int64(fileMode),
|
Mode: int64(fileMode),
|
||||||
ModTime: time.Now(),
|
ModTime: time.Now(),
|
||||||
@@ -833,16 +840,12 @@ func (s *Server) copyContentToContainer(ctx context.Context, containerID string,
|
|||||||
return fmt.Errorf("close tar writer: %w", err)
|
return fmt.Errorf("close tar writer: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copy the tar archive to the container
|
// Always extract to root. The tar archive contains the full path, so tar will
|
||||||
targetDir := filepath.Dir(targetPath)
|
// automatically create any intermediate directories that don't exist in the container.
|
||||||
if targetDir == "." {
|
|
||||||
targetDir = "/"
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := s.client.CopyToContainer(
|
if err := s.client.CopyToContainer(
|
||||||
ctx,
|
ctx,
|
||||||
containerID,
|
containerID,
|
||||||
targetDir,
|
"/",
|
||||||
&buf,
|
&buf,
|
||||||
container.CopyToContainerOptions{CopyUIDGID: true},
|
container.CopyToContainerOptions{CopyUIDGID: true},
|
||||||
); err != nil {
|
); err != nil {
|
||||||
@@ -1015,6 +1018,97 @@ func (s *Server) RemoveServiceContainer(ctx context.Context, req *pb.RemoveConta
|
|||||||
return resp, nil
|
return resp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// logsHeartbeatInterval is the interval at which heartbeat entries are sent when there are no logs to stream.
|
||||||
|
const logsHeartbeatInterval = 200 * time.Millisecond
|
||||||
|
|
||||||
|
// ContainerLogs streams logs from a container.
|
||||||
|
func (s *Server) ContainerLogs(
|
||||||
|
req *pb.ContainerLogsRequest, stream grpc.ServerStreamingServer[pb.ContainerLogEntry],
|
||||||
|
) error {
|
||||||
|
// Stream context is cancelled when the client has disconnected or the stream has ended.
|
||||||
|
ctx := stream.Context()
|
||||||
|
|
||||||
|
opts := ContainerLogsOptions{
|
||||||
|
ContainerID: req.ContainerId,
|
||||||
|
Follow: req.Follow,
|
||||||
|
Tail: int(req.Tail),
|
||||||
|
Since: req.Since,
|
||||||
|
Until: req.Until,
|
||||||
|
}
|
||||||
|
|
||||||
|
logsCh, err := s.service.ContainerLogs(ctx, opts)
|
||||||
|
if err != nil {
|
||||||
|
if errdefs.IsNotFound(err) {
|
||||||
|
return status.Error(codes.NotFound, err.Error())
|
||||||
|
}
|
||||||
|
return status.Errorf(codes.Internal, "get container logs: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log := slog.With("container_id", req.ContainerId, "stream_id", fmt.Sprintf("%p", stream)[2:])
|
||||||
|
log.Debug("Starting container logs streaming.",
|
||||||
|
"follow", req.Follow, "tail", req.Tail, "since", req.Since, "until", req.Until)
|
||||||
|
|
||||||
|
// Heartbeats are needed only when following logs to let the client know when there are no new log entries
|
||||||
|
// to allow it to advance the watermark of last received log timestamp.
|
||||||
|
var heartbeatCh <-chan time.Time
|
||||||
|
if req.Follow {
|
||||||
|
heartbeatTicker := time.NewTicker(logsHeartbeatInterval)
|
||||||
|
defer heartbeatTicker.Stop()
|
||||||
|
heartbeatCh = heartbeatTicker.C
|
||||||
|
}
|
||||||
|
|
||||||
|
started := time.Now()
|
||||||
|
lastSent := time.Time{}
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case entry, ok := <-logsCh:
|
||||||
|
if !ok {
|
||||||
|
// Channel closed, no more log entries.
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if entry.Err != nil {
|
||||||
|
return status.Error(codes.Internal, entry.Err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
pbEntry := &pb.ContainerLogEntry{
|
||||||
|
Stream: api.LogStreamTypeToProto(entry.Stream),
|
||||||
|
Timestamp: timestamppb.New(entry.Timestamp),
|
||||||
|
Message: entry.Message,
|
||||||
|
}
|
||||||
|
if err = stream.Send(pbEntry); err != nil {
|
||||||
|
return status.Errorf(codes.Internal, "send log entry: %v", err)
|
||||||
|
}
|
||||||
|
lastSent = entry.Timestamp
|
||||||
|
|
||||||
|
case now := <-heartbeatCh:
|
||||||
|
// Only send heartbeat if no log entries have been sent since the last heartbeat interval or
|
||||||
|
// if no log entries have been sent at all for at least a heartbeat interval since starting.
|
||||||
|
if now.Sub(lastSent) < logsHeartbeatInterval ||
|
||||||
|
(lastSent.IsZero() && now.Sub(started) < logsHeartbeatInterval) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use the timestamp one heartbeat in the past to be conservative. This reduces the chance of sending
|
||||||
|
// a timestamp that is greater than a log entry currently being parsed but not yet sent, which would
|
||||||
|
// cause the client to incorrectly believe it has received all logs up to that point.
|
||||||
|
heartbeat := &pb.ContainerLogEntry{
|
||||||
|
Stream: pb.ContainerLogEntry_HEARTBEAT,
|
||||||
|
Timestamp: timestamppb.New(now.Add(-logsHeartbeatInterval)),
|
||||||
|
}
|
||||||
|
if err = stream.Send(heartbeat); err != nil {
|
||||||
|
return status.Errorf(codes.Internal, "send log stream heartbeat: %v", err)
|
||||||
|
}
|
||||||
|
lastSent = heartbeat.Timestamp.AsTime()
|
||||||
|
log.Debug("Sent log stream heartbeat.", "timestamp", lastSent)
|
||||||
|
|
||||||
|
case <-ctx.Done():
|
||||||
|
return status.Error(codes.Canceled, ctx.Err().Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// receiveExecConfig receives and validates the initial exec configuration from the stream.
|
// receiveExecConfig receives and validates the initial exec configuration from the stream.
|
||||||
func (s *Server) receiveExecConfig(stream pb.Docker_ExecContainerServer) (*pb.ExecConfig, api.ExecOptions, error) {
|
func (s *Server) receiveExecConfig(stream pb.Docker_ExecContainerServer) (*pb.ExecConfig, api.ExecOptions, error) {
|
||||||
req, err := stream.Recv()
|
req, err := stream.Recv()
|
||||||
|
|||||||
@@ -1,18 +1,22 @@
|
|||||||
package docker
|
package docker
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/docker/docker/api/types/container"
|
"github.com/docker/docker/api/types/container"
|
||||||
"github.com/docker/docker/api/types/filters"
|
"github.com/docker/docker/api/types/filters"
|
||||||
"github.com/docker/docker/api/types/image"
|
"github.com/docker/docker/api/types/image"
|
||||||
"github.com/docker/docker/client"
|
"github.com/docker/docker/client"
|
||||||
|
"github.com/docker/docker/pkg/stdcopy"
|
||||||
"github.com/jmoiron/sqlx"
|
"github.com/jmoiron/sqlx"
|
||||||
"github.com/psviderski/uncloud/pkg/api"
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
"google.golang.org/grpc/codes"
|
"google.golang.org/grpc/codes"
|
||||||
@@ -150,3 +154,104 @@ func (s *Service) ListImages(ctx context.Context, opts image.ListOptions) (Image
|
|||||||
|
|
||||||
return imagesResp, nil
|
return imagesResp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ContainerLogsOptions specifies parameters for ContainerLogs.
|
||||||
|
type ContainerLogsOptions struct {
|
||||||
|
ContainerID string
|
||||||
|
Follow bool
|
||||||
|
Tail int
|
||||||
|
Since string
|
||||||
|
Until string
|
||||||
|
}
|
||||||
|
|
||||||
|
// ContainerLogs streams logs from a container and returns demultiplexed entries via a channel.
|
||||||
|
// The channel is closed when streaming completes or context is cancelled.
|
||||||
|
func (s *Service) ContainerLogs(ctx context.Context, opts ContainerLogsOptions) (<-chan api.ContainerLogEntry, error) {
|
||||||
|
dockerOpts := container.LogsOptions{
|
||||||
|
ShowStdout: true,
|
||||||
|
ShowStderr: true,
|
||||||
|
Follow: opts.Follow,
|
||||||
|
Tail: strconv.FormatInt(int64(opts.Tail), 10),
|
||||||
|
Since: opts.Since,
|
||||||
|
Until: opts.Until,
|
||||||
|
Timestamps: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
reader, err := s.Client.ContainerLogs(ctx, opts.ContainerID, dockerOpts)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
outCh := make(chan api.ContainerLogEntry)
|
||||||
|
stdoutWriter := &logsChannelWriter{ctx: ctx, ch: outCh, isStderr: false}
|
||||||
|
stderrWriter := &logsChannelWriter{ctx: ctx, ch: outCh, isStderr: true}
|
||||||
|
|
||||||
|
// Wrap the context in a cancellable one to unblock the second goroutine below when StdCopy completes.
|
||||||
|
ctx, cancel := context.WithCancel(ctx)
|
||||||
|
|
||||||
|
// Run StdCopy in a goroutine to be able to handle context cancellation.
|
||||||
|
go func() {
|
||||||
|
defer close(outCh)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
// StdCopy is blocking and will return when the reader is closed in another goroutine below or on error.
|
||||||
|
if _, err := stdcopy.StdCopy(stdoutWriter, stderrWriter, reader); err != nil {
|
||||||
|
// Send error as the last entry.
|
||||||
|
select {
|
||||||
|
case outCh <- api.ContainerLogEntry{Err: fmt.Errorf("demultiplex container logs: %w", err)}:
|
||||||
|
case <-ctx.Done():
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Close the reader when the context is done to cancel StdCopy if it's still running.
|
||||||
|
go func() {
|
||||||
|
<-ctx.Done()
|
||||||
|
reader.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
|
return outCh, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// logsChannelWriter is a writer for stdcopy.StdCopy that sends demultiplexed container logs to a channel.
|
||||||
|
type logsChannelWriter struct {
|
||||||
|
ctx context.Context
|
||||||
|
ch chan<- api.ContainerLogEntry
|
||||||
|
isStderr bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *logsChannelWriter) Write(data []byte) (n int, err error) {
|
||||||
|
// Parse timestamp and message from the demultiplexed Docker log payload if the data looks like it contains one.
|
||||||
|
// Format: 2025-01-01T00:00:00.000000000Z message
|
||||||
|
timestamp := time.Time{}
|
||||||
|
message := data
|
||||||
|
if len(data) > 30 && data[4] == '-' && data[7] == '-' && data[10] == 'T' {
|
||||||
|
timestampPart, messagePart, found := bytes.Cut(data, []byte(" "))
|
||||||
|
if found {
|
||||||
|
timestamp, err = time.Parse(time.RFC3339Nano, string(timestampPart))
|
||||||
|
if err != nil {
|
||||||
|
timestamp = time.Time{}
|
||||||
|
}
|
||||||
|
message = messagePart
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
entry := api.ContainerLogEntry{
|
||||||
|
Timestamp: timestamp,
|
||||||
|
// Clone is required because message is a slice into data, which stdcopy.StdCopy may reuse
|
||||||
|
// after Write returns but before the entry is consumed from the channel.
|
||||||
|
Message: bytes.Clone(message),
|
||||||
|
}
|
||||||
|
if w.isStderr {
|
||||||
|
entry.Stream = api.LogStreamStderr
|
||||||
|
} else {
|
||||||
|
entry.Stream = api.LogStreamStdout
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case w.ch <- entry:
|
||||||
|
return len(data), nil
|
||||||
|
case <-w.ctx.Done():
|
||||||
|
return 0, w.ctx.Err()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -33,10 +33,12 @@ import (
|
|||||||
"github.com/psviderski/unregistry"
|
"github.com/psviderski/unregistry"
|
||||||
"github.com/siderolabs/grpc-proxy/proxy"
|
"github.com/siderolabs/grpc-proxy/proxy"
|
||||||
"golang.org/x/sync/errgroup"
|
"golang.org/x/sync/errgroup"
|
||||||
|
"golang.zx2c4.com/wireguard/wgctrl"
|
||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
"google.golang.org/grpc/codes"
|
"google.golang.org/grpc/codes"
|
||||||
"google.golang.org/grpc/status"
|
"google.golang.org/grpc/status"
|
||||||
"google.golang.org/protobuf/types/known/emptypb"
|
"google.golang.org/protobuf/types/known/emptypb"
|
||||||
|
"google.golang.org/protobuf/types/known/timestamppb"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -174,10 +176,13 @@ type Machine struct {
|
|||||||
state *State
|
state *State
|
||||||
// started is closed when the machine is ready to serve requests on the local API server.
|
// started is closed when the machine is ready to serve requests on the local API server.
|
||||||
started chan struct{}
|
started chan struct{}
|
||||||
// initialised is signalled when the machine is configured as a member of a cluster.
|
// initialised is closed when the machine is configured as a member of a cluster.
|
||||||
initialised chan struct{}
|
initialised chan struct{}
|
||||||
// networkReady is signalled when the Docker network is configured and ready for containers.
|
// networkReady is closed when the Docker network is configured and ready for containers.
|
||||||
networkReady chan struct{}
|
networkReady chan struct{}
|
||||||
|
// clusterReady is closed when the cluster controller has finished starting all components
|
||||||
|
// and the machine is ready to serve cluster requests.
|
||||||
|
clusterReady chan struct{}
|
||||||
// resetting is true when the machine is being reset.
|
// resetting is true when the machine is being reset.
|
||||||
resetting bool
|
resetting bool
|
||||||
// stop cancels the Run method context to stop the machine gracefully.
|
// stop cancels the Run method context to stop the machine gracefully.
|
||||||
@@ -246,7 +251,10 @@ func NewMachine(config *Config) (*Machine, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("create corrosion admin client: %w", err)
|
return nil, fmt.Errorf("create corrosion admin client: %w", err)
|
||||||
}
|
}
|
||||||
c := cluster.NewCluster(corroStore, corroAdmin)
|
|
||||||
|
initialised := make(chan struct{})
|
||||||
|
clusterReady := make(chan struct{})
|
||||||
|
c := cluster.NewCluster(corroStore, corroAdmin, initialised, clusterReady)
|
||||||
|
|
||||||
// Init dependencies for a gRPC Docker server that proxies requests to the local Docker daemon.
|
// Init dependencies for a gRPC Docker server that proxies requests to the local Docker daemon.
|
||||||
dbFilePath := filepath.Join(config.DataDir, DBFileName)
|
dbFilePath := filepath.Join(config.DataDir, DBFileName)
|
||||||
@@ -269,8 +277,9 @@ func NewMachine(config *Config) (*Machine, error) {
|
|||||||
config: *config,
|
config: *config,
|
||||||
state: state,
|
state: state,
|
||||||
started: make(chan struct{}),
|
started: make(chan struct{}),
|
||||||
initialised: make(chan struct{}, 1),
|
initialised: initialised,
|
||||||
networkReady: make(chan struct{}),
|
networkReady: make(chan struct{}),
|
||||||
|
clusterReady: clusterReady,
|
||||||
store: corroStore,
|
store: corroStore,
|
||||||
cluster: c,
|
cluster: c,
|
||||||
dockerService: dockerService,
|
dockerService: dockerService,
|
||||||
@@ -294,7 +303,7 @@ func NewMachine(config *Config) (*Machine, error) {
|
|||||||
m.localMachineServer = newGRPCServer(m, c, m.dockerServer, caddyServer)
|
m.localMachineServer = newGRPCServer(m, c, m.dockerServer, caddyServer)
|
||||||
|
|
||||||
if m.Initialised() {
|
if m.Initialised() {
|
||||||
m.initialised <- struct{}{}
|
close(m.initialised)
|
||||||
}
|
}
|
||||||
|
|
||||||
return m, nil
|
return m, nil
|
||||||
@@ -475,6 +484,7 @@ func (m *Machine) Run(ctx context.Context) error {
|
|||||||
m.config.CorrosionService,
|
m.config.CorrosionService,
|
||||||
m.dockerService,
|
m.dockerService,
|
||||||
m.networkReady,
|
m.networkReady,
|
||||||
|
m.clusterReady,
|
||||||
caddyconfigCtrl,
|
caddyconfigCtrl,
|
||||||
dnsServer,
|
dnsServer,
|
||||||
dnsResolver,
|
dnsResolver,
|
||||||
@@ -722,7 +732,7 @@ func (m *Machine) InitCluster(ctx context.Context, req *pb.InitClusterRequest) (
|
|||||||
addReq.PublicIp = pb.NewIP(publicIP)
|
addReq.PublicIp = pb.NewIP(publicIP)
|
||||||
}
|
}
|
||||||
|
|
||||||
addResp, err := m.cluster.AddMachine(ctx, addReq)
|
addResp, err := m.cluster.AddMachineWithoutReadyCheck(ctx, addReq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, status.Errorf(codes.Internal, "add machine to cluster: %v", err)
|
return nil, status.Errorf(codes.Internal, "add machine to cluster: %v", err)
|
||||||
}
|
}
|
||||||
@@ -749,7 +759,7 @@ func (m *Machine) InitCluster(ctx context.Context, req *pb.InitClusterRequest) (
|
|||||||
}
|
}
|
||||||
slog.Info("Cluster initialised with machine.", "id", m.state.ID, "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{}{}
|
close(m.initialised)
|
||||||
|
|
||||||
resp := &pb.InitClusterResponse{
|
resp := &pb.InitClusterResponse{
|
||||||
Machine: addResp.Machine,
|
Machine: addResp.Machine,
|
||||||
@@ -792,6 +802,7 @@ func (m *Machine) JoinCluster(_ context.Context, req *pb.JoinClusterRequest) (*e
|
|||||||
PrivateKey: m.state.Network.PrivateKey,
|
PrivateKey: m.state.Network.PrivateKey,
|
||||||
PublicKey: m.state.Network.PublicKey,
|
PublicKey: m.state.Network.PublicKey,
|
||||||
}
|
}
|
||||||
|
m.state.MinStoreDBVersion = req.MinStoreDbVersion
|
||||||
|
|
||||||
// Build a peers config from other cluster machines.
|
// Build a peers config from other cluster machines.
|
||||||
m.state.Network.Peers = make([]network.PeerConfig, 0, len(req.OtherMachines))
|
m.state.Network.Peers = make([]network.PeerConfig, 0, len(req.OtherMachines))
|
||||||
@@ -830,7 +841,7 @@ func (m *Machine) JoinCluster(_ context.Context, req *pb.JoinClusterRequest) (*e
|
|||||||
"peers", len(m.state.Network.Peers),
|
"peers", len(m.state.Network.Peers),
|
||||||
)
|
)
|
||||||
// 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{}{}
|
close(m.initialised)
|
||||||
|
|
||||||
return &emptypb.Empty{}, nil
|
return &emptypb.Empty{}, nil
|
||||||
}
|
}
|
||||||
@@ -864,6 +875,7 @@ func (m *Machine) Token(_ context.Context, _ *emptypb.Empty) (*pb.TokenResponse,
|
|||||||
return &pb.TokenResponse{Token: tokenStr}, nil
|
return &pb.TokenResponse{Token: tokenStr}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Deprecated: use InspectMachine instead.
|
||||||
func (m *Machine) Inspect(_ context.Context, _ *emptypb.Empty) (*pb.MachineInfo, error) {
|
func (m *Machine) Inspect(_ context.Context, _ *emptypb.Empty) (*pb.MachineInfo, error) {
|
||||||
return &pb.MachineInfo{
|
return &pb.MachineInfo{
|
||||||
Id: m.state.ID,
|
Id: m.state.ID,
|
||||||
@@ -876,6 +888,31 @@ func (m *Machine) Inspect(_ context.Context, _ *emptypb.Empty) (*pb.MachineInfo,
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *Machine) InspectMachine(ctx context.Context, _ *emptypb.Empty) (*pb.InspectMachineResponse, error) {
|
||||||
|
dbVersion, err := m.store.DBVersion(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, status.Errorf(codes.Internal, "get database version of the cluster store: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &pb.InspectMachineResponse{
|
||||||
|
Machines: []*pb.MachineDetails{
|
||||||
|
{
|
||||||
|
// Metadata is injected by the gRPC proxy.
|
||||||
|
Machine: &pb.MachineInfo{
|
||||||
|
Id: m.state.ID,
|
||||||
|
Name: m.state.Name,
|
||||||
|
Network: &pb.NetworkConfig{
|
||||||
|
Subnet: pb.NewIPPrefix(m.state.Network.Subnet),
|
||||||
|
ManagementIp: pb.NewIP(m.state.Network.ManagementIP),
|
||||||
|
PublicKey: m.state.Network.PublicKey,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
StoreDbVersion: dbVersion,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
// IsNetworkReady returns true if the Docker network is ready for containers.
|
// IsNetworkReady returns true if the Docker network is ready for containers.
|
||||||
func (m *Machine) IsNetworkReady() bool {
|
func (m *Machine) IsNetworkReady() bool {
|
||||||
if !m.Initialised() {
|
if !m.Initialised() {
|
||||||
@@ -909,6 +946,58 @@ func (m *Machine) WaitForNetworkReady(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// InspectWireGuardNetwork retrieves the current WireGuard network configuration and peer status.
|
||||||
|
func (m *Machine) InspectWireGuardNetwork(
|
||||||
|
_ context.Context, _ *emptypb.Empty,
|
||||||
|
) (*pb.InspectWireGuardNetworkResponse, error) {
|
||||||
|
deviceName := network.WireGuardInterfaceName
|
||||||
|
|
||||||
|
wg, err := wgctrl.New()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("create WireGuard client: %w", err)
|
||||||
|
}
|
||||||
|
defer wg.Close()
|
||||||
|
|
||||||
|
dev, err := wg.Device(deviceName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("get WireGuard device '%s': %w", deviceName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
peers := make([]*pb.WireGuardPeer, len(dev.Peers))
|
||||||
|
for i, p := range dev.Peers {
|
||||||
|
var lastHandshake *timestamppb.Timestamp
|
||||||
|
if !p.LastHandshakeTime.IsZero() {
|
||||||
|
lastHandshake = timestamppb.New(p.LastHandshakeTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
allowedIPs := make([]string, len(p.AllowedIPs))
|
||||||
|
for j, ip := range p.AllowedIPs {
|
||||||
|
allowedIPs[j] = ip.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
var endpoint string
|
||||||
|
if p.Endpoint != nil {
|
||||||
|
endpoint = p.Endpoint.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
peers[i] = &pb.WireGuardPeer{
|
||||||
|
PublicKey: p.PublicKey[:],
|
||||||
|
Endpoint: endpoint,
|
||||||
|
LastHandshakeTime: lastHandshake,
|
||||||
|
ReceiveBytes: p.ReceiveBytes,
|
||||||
|
TransmitBytes: p.TransmitBytes,
|
||||||
|
AllowedIps: allowedIPs,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &pb.InspectWireGuardNetworkResponse{
|
||||||
|
InterfaceName: dev.Name,
|
||||||
|
PublicKey: dev.PublicKey[:],
|
||||||
|
ListenPort: int32(dev.ListenPort),
|
||||||
|
Peers: peers,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
// Reset restores the machine to a clean state, scheduling a graceful shutdown and removing all cluster-related
|
// Reset restores the machine to a clean state, scheduling a graceful shutdown and removing all cluster-related
|
||||||
// configuration and resource. The uncloud daemon will restart the machine if managed by systemd.
|
// configuration and resource. The uncloud daemon will restart the machine if managed by systemd.
|
||||||
func (m *Machine) Reset(_ context.Context, _ *pb.ResetRequest) (*emptypb.Empty, error) {
|
func (m *Machine) Reset(_ context.Context, _ *pb.ResetRequest) (*emptypb.Empty, error) {
|
||||||
|
|||||||
@@ -24,6 +24,9 @@ type State struct {
|
|||||||
Name string
|
Name string
|
||||||
// Network specifies the network configuration for this machine.
|
// Network specifies the network configuration for this machine.
|
||||||
Network *network.Config
|
Network *network.Config
|
||||||
|
// MinStoreDBVersion is the latest database version of one of the existing cluster machines at the time this machine
|
||||||
|
// joined the cluster. The machine should sync to at least this version before starting any cluster operations.
|
||||||
|
MinStoreDBVersion int64 `json:",omitempty"`
|
||||||
|
|
||||||
// path is the file path config is read from and saved to.
|
// path is the file path config is read from and saved to.
|
||||||
path string
|
path string
|
||||||
@@ -66,7 +69,7 @@ func (c *State) Encode() ([]byte, error) {
|
|||||||
return data, nil
|
return data, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save writes the state data to the file at the given path.
|
// Save atomically writes the state data to the file at the configured path.
|
||||||
func (c *State) Save() error {
|
func (c *State) Save() error {
|
||||||
if c.path == "" {
|
if c.path == "" {
|
||||||
return fmt.Errorf("state path not set")
|
return fmt.Errorf("state path not set")
|
||||||
@@ -80,5 +83,37 @@ func (c *State) Save() error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return os.WriteFile(c.path, data, 0o600)
|
|
||||||
|
// Write to a temporary file and rename for atomic save. CreateTemp creates a file with mode 0o600.
|
||||||
|
tmpFile, err := os.CreateTemp(dir, ".machine.json.*")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("create temp file: %w", err)
|
||||||
|
}
|
||||||
|
tmpPath := tmpFile.Name()
|
||||||
|
|
||||||
|
// Clean up temp file on error.
|
||||||
|
defer func() {
|
||||||
|
if err != nil {
|
||||||
|
os.Remove(tmpPath)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
if _, err = tmpFile.Write(data); err != nil {
|
||||||
|
tmpFile.Close()
|
||||||
|
return fmt.Errorf("write temp file: %w", err)
|
||||||
|
}
|
||||||
|
// Without fsync, the data may only be in the OS buffer cache. If the system crashes before the OS flushes
|
||||||
|
// it to disk, the file could be empty or corrupted even after rename.
|
||||||
|
if err = tmpFile.Sync(); err != nil {
|
||||||
|
tmpFile.Close()
|
||||||
|
return fmt.Errorf("sync temp file: %w", err)
|
||||||
|
}
|
||||||
|
if err = tmpFile.Close(); err != nil {
|
||||||
|
return fmt.Errorf("close temp file: %w", err)
|
||||||
|
}
|
||||||
|
if err = os.Rename(tmpPath, c.path); err != nil {
|
||||||
|
return fmt.Errorf("rename temp file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ func (s *Store) CreateOrUpdateContainer(ctx context.Context, ctr api.ServiceCont
|
|||||||
|
|
||||||
// ListContainers returns a list of container records from the store database that match the given options.
|
// ListContainers returns a list of container records from the store database that match the given options.
|
||||||
func (s *Store) ListContainers(ctx context.Context, opts ListOptions) ([]ContainerRecord, error) {
|
func (s *Store) ListContainers(ctx context.Context, opts ListOptions) ([]ContainerRecord, error) {
|
||||||
q := sq.Select("container", "machine_id", "sync_status", "updated_at").From("containers").
|
q := sq.Select("id", "container", "machine_id", "sync_status", "updated_at").From("containers").
|
||||||
Where(sq.Eq{"sync_status": SyncStatusSynced})
|
Where(sq.Eq{"sync_status": SyncStatusSynced})
|
||||||
|
|
||||||
if len(opts.MachineIDs) > 0 {
|
if len(opts.MachineIDs) > 0 {
|
||||||
@@ -112,14 +112,23 @@ func (s *Store) ListContainers(ctx context.Context, opts ListOptions) ([]Contain
|
|||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
|
|
||||||
var containers []ContainerRecord
|
var containers []ContainerRecord
|
||||||
var cJSON, machineID, syncStatus, updatedAtStr string
|
var id, cJSON, machineID, syncStatus, updatedAtStr string
|
||||||
var updatedAt time.Time
|
var updatedAt time.Time
|
||||||
|
skipped := 0
|
||||||
|
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
if err = rows.Scan(&cJSON, &machineID, &syncStatus, &updatedAtStr); err != nil {
|
if err = rows.Scan(&id, &cJSON, &machineID, &syncStatus, &updatedAtStr); err != nil {
|
||||||
return nil, fmt.Errorf("scan container record: %w", err)
|
return nil, fmt.Errorf("scan container record: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Skip containers with empty JSON data. This can happen during partial replication
|
||||||
|
// when cr-sqlite has created the row but the container column hasn't been synced yet.
|
||||||
|
if cJSON == "" || cJSON == "{}" {
|
||||||
|
slog.Debug("Skipping container with empty data in the store (partial replication?).", "id", id)
|
||||||
|
skipped++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
var c api.ServiceContainer
|
var c api.ServiceContainer
|
||||||
if err = json.Unmarshal([]byte(cJSON), &c); err != nil {
|
if err = json.Unmarshal([]byte(cJSON), &c); err != nil {
|
||||||
return nil, fmt.Errorf("unmarshal container: %w", err)
|
return nil, fmt.Errorf("unmarshal container: %w", err)
|
||||||
@@ -135,6 +144,11 @@ func (s *Store) ListContainers(ctx context.Context, opts ListOptions) ([]Contain
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if skipped > 0 {
|
||||||
|
slog.Warn("Listing containers from the store skipped empty records (possibly due to partial replication).",
|
||||||
|
"skipped", skipped, "valid", len(containers))
|
||||||
|
}
|
||||||
|
|
||||||
return containers, nil
|
return containers, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,7 +180,7 @@ func (s *Store) DeleteContainers(ctx context.Context, opts DeleteOptions) error
|
|||||||
// receive any values, it just signals when a container(s) has been added, updated, or deleted in the database.
|
// receive any values, it just signals when a container(s) has been added, updated, or deleted in the database.
|
||||||
func (s *Store) SubscribeContainers(ctx context.Context) ([]ContainerRecord, <-chan struct{}, error) {
|
func (s *Store) SubscribeContainers(ctx context.Context) ([]ContainerRecord, <-chan struct{}, error) {
|
||||||
// TODO: figure out whether we need sync_status at all (not used at the moment).
|
// TODO: figure out whether we need sync_status at all (not used at the moment).
|
||||||
q := sq.Select("container", "machine_id", "sync_status", "updated_at").From("containers").
|
q := sq.Select("id", "container", "machine_id", "sync_status", "updated_at").From("containers").
|
||||||
Where(sq.Eq{"sync_status": SyncStatusSynced})
|
Where(sq.Eq{"sync_status": SyncStatusSynced})
|
||||||
query, args, err := q.ToSql()
|
query, args, err := q.ToSql()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -179,15 +193,24 @@ func (s *Store) SubscribeContainers(ctx context.Context) ([]ContainerRecord, <-c
|
|||||||
}
|
}
|
||||||
|
|
||||||
var containers []ContainerRecord
|
var containers []ContainerRecord
|
||||||
var cJSON, updatedAtStr string
|
var id, cJSON, updatedAtStr string
|
||||||
|
skipped := 0
|
||||||
|
|
||||||
rows := sub.Rows()
|
rows := sub.Rows()
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var cr ContainerRecord
|
var cr ContainerRecord
|
||||||
if err = rows.Scan(&cJSON, &cr.MachineID, &cr.SyncStatus, &updatedAtStr); err != nil {
|
if err = rows.Scan(&id, &cJSON, &cr.MachineID, &cr.SyncStatus, &updatedAtStr); err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Skip containers with empty JSON data. This can happen during partial replication
|
||||||
|
// when cr-sqlite has created the row but the container column hasn't been synced yet.
|
||||||
|
if cJSON == "" || cJSON == "{}" {
|
||||||
|
slog.Debug("Skipping container with empty data in the store (partial replication?).", "id", id)
|
||||||
|
skipped++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
if err = json.Unmarshal([]byte(cJSON), &cr.Container); err != nil {
|
if err = json.Unmarshal([]byte(cJSON), &cr.Container); err != nil {
|
||||||
return nil, nil, fmt.Errorf("unmarshal container: %w", err)
|
return nil, nil, fmt.Errorf("unmarshal container: %w", err)
|
||||||
}
|
}
|
||||||
@@ -196,6 +219,12 @@ func (s *Store) SubscribeContainers(ctx context.Context) ([]ContainerRecord, <-c
|
|||||||
}
|
}
|
||||||
containers = append(containers, cr)
|
containers = append(containers, cr)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if skipped > 0 {
|
||||||
|
slog.Warn("Container subscription skipped empty records in the store (possibly due to partial replication).",
|
||||||
|
"skipped", skipped, "valid", len(containers))
|
||||||
|
}
|
||||||
|
|
||||||
events, err := sub.Changes()
|
events, err := sub.Changes()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, fmt.Errorf("get subscription changes: %w", err)
|
return nil, nil, fmt.Errorf("get subscription changes: %w", err)
|
||||||
|
|||||||
@@ -56,6 +56,25 @@ func (s *Store) Delete(ctx context.Context, key string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DBVersion returns the current cr-sqlite database version (Lamport timestamp).
|
||||||
|
func (s *Store) DBVersion(ctx context.Context) (int64, error) {
|
||||||
|
rows, err := s.corro.QueryContext(ctx, "SELECT crsql_db_version()")
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("query crsql_db_version(): %w", err)
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
if !rows.Next() {
|
||||||
|
return 0, fmt.Errorf("no result from crsql_db_version()")
|
||||||
|
}
|
||||||
|
|
||||||
|
var version int64
|
||||||
|
if err = rows.Scan(&version); err != nil {
|
||||||
|
return 0, fmt.Errorf("scan db version: %w", err)
|
||||||
|
}
|
||||||
|
return version, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Store) CreateMachine(ctx context.Context, m *pb.MachineInfo) error {
|
func (s *Store) CreateMachine(ctx context.Context, m *pb.MachineInfo) error {
|
||||||
mJSON, err := protojson.Marshal(m)
|
mJSON, err := protojson.Marshal(m)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -116,19 +135,29 @@ func (s *Store) GetMachine(ctx context.Context, machineID string) (*pb.MachineIn
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) ListMachines(ctx context.Context) ([]*pb.MachineInfo, error) {
|
func (s *Store) ListMachines(ctx context.Context) ([]*pb.MachineInfo, error) {
|
||||||
rows, err := s.corro.QueryContext(ctx, "SELECT info FROM machines ORDER BY name")
|
rows, err := s.corro.QueryContext(ctx, "SELECT id, info FROM machines ORDER BY name")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
|
|
||||||
var machines []*pb.MachineInfo
|
var machines []*pb.MachineInfo
|
||||||
|
skipped := 0
|
||||||
|
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var mJSON string
|
var id, mJSON string
|
||||||
if err = rows.Scan(&mJSON); err != nil {
|
if err = rows.Scan(&id, &mJSON); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Skip machines with empty JSON data. This can happen during partial replication
|
||||||
|
// when cr-sqlite has created the row but the info column hasn't been synced yet.
|
||||||
|
if mJSON == "" || mJSON == "{}" {
|
||||||
|
slog.Debug("Skipping machine with empty data in the store (partial replication?).", "id", id)
|
||||||
|
skipped++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
protojsonParser := protojson.UnmarshalOptions{DiscardUnknown: true}
|
protojsonParser := protojson.UnmarshalOptions{DiscardUnknown: true}
|
||||||
var m pb.MachineInfo
|
var m pb.MachineInfo
|
||||||
if err = protojsonParser.Unmarshal([]byte(mJSON), &m); err != nil {
|
if err = protojsonParser.Unmarshal([]byte(mJSON), &m); err != nil {
|
||||||
@@ -141,6 +170,12 @@ func (s *Store) ListMachines(ctx context.Context) ([]*pb.MachineInfo, error) {
|
|||||||
}
|
}
|
||||||
machines = append(machines, &m)
|
machines = append(machines, &m)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if skipped > 0 {
|
||||||
|
slog.Warn("Listing machines from the store skipped empty records (possibly due to partial replication).",
|
||||||
|
"skipped", skipped, "valid", len(machines))
|
||||||
|
}
|
||||||
|
|
||||||
return machines, nil
|
return machines, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -186,24 +221,41 @@ func (s *Store) DeleteMachine(ctx context.Context, id string) error {
|
|||||||
// SubscribeMachines returns a list of machines and a channel that signals changes to the list. The channel doesn't
|
// 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.
|
// 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) {
|
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)
|
sub, err := s.corro.SubscribeContext(ctx, "SELECT id, info FROM machines ORDER BY name", nil, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
rows := sub.Rows()
|
rows := sub.Rows()
|
||||||
var machines []*pb.MachineInfo
|
var machines []*pb.MachineInfo
|
||||||
|
skipped := 0
|
||||||
|
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var mJSON string
|
var id, mJSON string
|
||||||
if err = rows.Scan(&mJSON); err != nil {
|
if err = rows.Scan(&id, &mJSON); err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Skip machines with empty JSON data. This can happen during partial replication
|
||||||
|
// when cr-sqlite has created the row but the info column hasn't been synced yet.
|
||||||
|
if mJSON == "" || mJSON == "{}" {
|
||||||
|
slog.Debug("Skipping machine with empty data in the store (partial replication?).", "id", id)
|
||||||
|
skipped++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
var m pb.MachineInfo
|
var m pb.MachineInfo
|
||||||
if err = protojson.Unmarshal([]byte(mJSON), &m); err != nil {
|
if err = protojson.Unmarshal([]byte(mJSON), &m); err != nil {
|
||||||
return nil, nil, fmt.Errorf("unmarshal machine info: %w", err)
|
return nil, nil, fmt.Errorf("unmarshal machine info: %w", err)
|
||||||
}
|
}
|
||||||
machines = append(machines, &m)
|
machines = append(machines, &m)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if skipped > 0 {
|
||||||
|
slog.Warn("Machine subscription skipped empty records in the store (possibly due to partial replication).",
|
||||||
|
"skipped", skipped, "valid", len(machines))
|
||||||
|
}
|
||||||
|
|
||||||
events, err := sub.Changes()
|
events, err := sub.Changes()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, fmt.Errorf("get subscription changes: %w", err)
|
return nil, nil, fmt.Errorf("get subscription changes: %w", err)
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
package sshexec
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os/exec"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SSHCLIRemote struct {
|
||||||
|
user string
|
||||||
|
host string
|
||||||
|
port int
|
||||||
|
keyPath string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSSHCLIRemote(user, host string, port int, keyPath string) *SSHCLIRemote {
|
||||||
|
return &SSHCLIRemote{
|
||||||
|
user: user,
|
||||||
|
host: host,
|
||||||
|
port: port,
|
||||||
|
keyPath: keyPath,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Refactor and reuse this with buildDialArgs and buildSSHArgs from
|
||||||
|
// SSHCLI Connector.
|
||||||
|
func (r *SSHCLIRemote) buildSSHArgs() []string {
|
||||||
|
args := []string{"-o", "ConnectTimeout=5"}
|
||||||
|
|
||||||
|
if r.port != 0 && r.port != 22 {
|
||||||
|
args = append(args, "-p", strconv.Itoa(r.port))
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.keyPath != "" {
|
||||||
|
args = append(args, "-i", r.keyPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
args = append(args, r.user+"@"+r.host)
|
||||||
|
return args
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *SSHCLIRemote) Run(ctx context.Context, cmd string) (string, error) {
|
||||||
|
args := r.buildSSHArgs()
|
||||||
|
args = append(args, cmd)
|
||||||
|
|
||||||
|
execCmd := exec.CommandContext(ctx, "ssh", args...)
|
||||||
|
output, err := execCmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
return strings.TrimSpace(string(output)),
|
||||||
|
fmt.Errorf("run command on remote host: %w: %s", err, string(output))
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(string(output)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *SSHCLIRemote) Stream(ctx context.Context, cmd string, stdout, stderr io.Writer) error {
|
||||||
|
args := r.buildSSHArgs()
|
||||||
|
args = append(args, cmd)
|
||||||
|
|
||||||
|
execCmd := exec.CommandContext(ctx, "ssh", args...)
|
||||||
|
execCmd.Stdout = stdout
|
||||||
|
execCmd.Stderr = stderr
|
||||||
|
|
||||||
|
return execCmd.Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
// no-op as there is no persistent connection.
|
||||||
|
func (r *SSHCLIRemote) Close() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
package sshexec
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSSHCLIRemote_buildSSHArgs(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
user string
|
||||||
|
host string
|
||||||
|
port int
|
||||||
|
keyPath string
|
||||||
|
expected []string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "default port, no key",
|
||||||
|
user: "root",
|
||||||
|
host: "example.com",
|
||||||
|
port: 22,
|
||||||
|
keyPath: "",
|
||||||
|
expected: []string{"-o", "ConnectTimeout=5", "root@example.com"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "custom port",
|
||||||
|
user: "ubuntu",
|
||||||
|
host: "192.168.1.10",
|
||||||
|
port: 2222,
|
||||||
|
keyPath: "",
|
||||||
|
expected: []string{"-o", "ConnectTimeout=5", "-p", "2222", "ubuntu@192.168.1.10"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "with key path",
|
||||||
|
user: "root",
|
||||||
|
host: "example.com",
|
||||||
|
port: 22,
|
||||||
|
keyPath: "/path/to/key",
|
||||||
|
expected: []string{"-o", "ConnectTimeout=5", "-i", "/path/to/key", "root@example.com"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "all options",
|
||||||
|
user: "admin",
|
||||||
|
host: "server.local",
|
||||||
|
port: 2222,
|
||||||
|
keyPath: "~/.ssh/id_rsa",
|
||||||
|
expected: []string{"-o", "ConnectTimeout=5", "-p", "2222", "-i", "~/.ssh/id_rsa", "admin@server.local"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "port zero (default)",
|
||||||
|
user: "root",
|
||||||
|
host: "example.com",
|
||||||
|
port: 0,
|
||||||
|
keyPath: "",
|
||||||
|
expected: []string{"-o", "ConnectTimeout=5", "root@example.com"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
remote := &SSHCLIRemote{
|
||||||
|
user: tt.user,
|
||||||
|
host: tt.host,
|
||||||
|
port: tt.port,
|
||||||
|
keyPath: tt.keyPath,
|
||||||
|
}
|
||||||
|
result := remote.buildSSHArgs()
|
||||||
|
assert.Equal(t, tt.expected, result)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
+46
-16
@@ -119,17 +119,16 @@ func (p *Provisioner) CreateCluster(ctx context.Context, name string, opts Creat
|
|||||||
func (p *Provisioner) initCluster(ctx context.Context, machines []Machine) error {
|
func (p *Provisioner) initCluster(ctx context.Context, machines []Machine) error {
|
||||||
// Init a new cluster on the first machine.
|
// Init a new cluster on the first machine.
|
||||||
initMachine := machines[0]
|
initMachine := machines[0]
|
||||||
|
|
||||||
if err := WaitMachineReady(ctx, initMachine, 30*time.Second); err != nil {
|
|
||||||
return fmt.Errorf("wait for machine %q to be ready: %w", initMachine.Name, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
initClient, err := initMachine.Connect(ctx)
|
initClient, err := initMachine.Connect(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("create machine client over TCP '%s': %w", initMachine.APIAddress, err)
|
return fmt.Errorf("create machine client over TCP '%s': %w", initMachine.APIAddress, err)
|
||||||
}
|
}
|
||||||
defer initClient.Close()
|
defer initClient.Close()
|
||||||
|
|
||||||
|
if err := initClient.WaitMachineReady(ctx, 30*time.Second); err != nil {
|
||||||
|
return fmt.Errorf("wait for machine %q to be ready: %w", initMachine.Name, err)
|
||||||
|
}
|
||||||
|
|
||||||
req := &pb.InitClusterRequest{
|
req := &pb.InitClusterRequest{
|
||||||
MachineName: initMachine.Name,
|
MachineName: initMachine.Name,
|
||||||
Network: pb.NewIPPrefix(cluster.DefaultNetwork),
|
Network: pb.NewIPPrefix(cluster.DefaultNetwork),
|
||||||
@@ -138,14 +137,22 @@ func (p *Provisioner) initCluster(ctx context.Context, machines []Machine) error
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("init cluster: %w", err)
|
return fmt.Errorf("init cluster: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("Cluster %q initialised with machine %q\n", initMachine.ClusterName, initResp.Machine.Name)
|
fmt.Printf("Cluster %q initialised with machine %q\n", initMachine.ClusterName, initResp.Machine.Name)
|
||||||
|
fmt.Printf("Waiting for cluster to be ready...")
|
||||||
|
if err = initClient.WaitClusterReady(ctx, 30*time.Second); err != nil {
|
||||||
|
return fmt.Errorf("wait for cluster to be ready: %w", err)
|
||||||
|
}
|
||||||
|
fmt.Println(" done.")
|
||||||
|
|
||||||
|
// Get the current store DB version from the init machine to pass to the join requests.
|
||||||
|
inspectResp, err := initClient.MachineClient.InspectMachine(ctx, &emptypb.Empty{})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("inspect init machine: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
// Join the rest of the machines to the cluster.
|
// Join the rest of the machines to the cluster.
|
||||||
for _, m := range machines[1:] {
|
for _, m := range machines[1:] {
|
||||||
if err = WaitMachineReady(ctx, m, 30*time.Second); err != nil {
|
|
||||||
return fmt.Errorf("wait for machine %q to be ready: %w", m.Name, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
cli, err := m.Connect(ctx)
|
cli, err := m.Connect(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("create machine client over TCP '%s': %w", m.APIAddress, err)
|
return fmt.Errorf("create machine client over TCP '%s': %w", m.APIAddress, err)
|
||||||
@@ -153,6 +160,10 @@ func (p *Provisioner) initCluster(ctx context.Context, machines []Machine) error
|
|||||||
//goland:noinspection GoDeferInLoop
|
//goland:noinspection GoDeferInLoop
|
||||||
defer cli.Close()
|
defer cli.Close()
|
||||||
|
|
||||||
|
if err := cli.WaitMachineReady(ctx, 30*time.Second); err != nil {
|
||||||
|
return fmt.Errorf("wait for machine %q to be ready: %w", m.Name, err)
|
||||||
|
}
|
||||||
|
|
||||||
tokenResp, err := cli.Token(ctx, &emptypb.Empty{})
|
tokenResp, err := cli.Token(ctx, &emptypb.Empty{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("get machine token: %w", err)
|
return fmt.Errorf("get machine token: %w", err)
|
||||||
@@ -181,8 +192,9 @@ func (p *Provisioner) initCluster(ctx context.Context, machines []Machine) error
|
|||||||
|
|
||||||
// Configure the machine to join the cluster.
|
// Configure the machine to join the cluster.
|
||||||
joinReq := &pb.JoinClusterRequest{
|
joinReq := &pb.JoinClusterRequest{
|
||||||
Machine: addResp.Machine,
|
Machine: addResp.Machine,
|
||||||
OtherMachines: []*pb.MachineInfo{initResp.Machine},
|
OtherMachines: []*pb.MachineInfo{initResp.Machine},
|
||||||
|
MinStoreDbVersion: inspectResp.Machines[0].StoreDbVersion,
|
||||||
}
|
}
|
||||||
if _, err = cli.JoinCluster(ctx, joinReq); err != nil {
|
if _, err = cli.JoinCluster(ctx, joinReq); err != nil {
|
||||||
return fmt.Errorf("join cluster: %w", err)
|
return fmt.Errorf("join cluster: %w", err)
|
||||||
@@ -256,16 +268,16 @@ func (p *Provisioner) InspectCluster(ctx context.Context, name string) (Cluster,
|
|||||||
// WaitClusterReady waits for all machines in the cluster to be ready and UP.
|
// 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 {
|
func (p *Provisioner) WaitClusterReady(ctx context.Context, c Cluster, timeout time.Duration) error {
|
||||||
firstMachine := c.Machines[0]
|
firstMachine := c.Machines[0]
|
||||||
if err := 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)
|
cli, err := firstMachine.Connect(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("connect to machine over TCP '%s': %w", firstMachine.APIAddress, err)
|
return fmt.Errorf("connect to machine over TCP '%s': %w", firstMachine.APIAddress, err)
|
||||||
}
|
}
|
||||||
defer cli.Close()
|
defer cli.Close()
|
||||||
|
|
||||||
|
if err = cli.WaitClusterReady(ctx, timeout); err != nil {
|
||||||
|
return fmt.Errorf("wait for cluster to be ready: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
boff := backoff.WithContext(backoff.NewExponentialBackOff(
|
boff := backoff.WithContext(backoff.NewExponentialBackOff(
|
||||||
backoff.WithInitialInterval(100*time.Millisecond),
|
backoff.WithInitialInterval(100*time.Millisecond),
|
||||||
backoff.WithMaxInterval(1*time.Second),
|
backoff.WithMaxInterval(1*time.Second),
|
||||||
@@ -289,7 +301,25 @@ func (p *Provisioner) WaitClusterReady(ctx context.Context, c Cluster, timeout t
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return backoff.Retry(checkMachinesUp, boff)
|
if err = backoff.Retry(checkMachinesUp, boff); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for each machine to sync the cluster store and be ready to serve cluster requests.
|
||||||
|
for _, m := range c.Machines[1:] {
|
||||||
|
mcli, err := m.Connect(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("connect to machine over TCP '%s': %w", m.APIAddress, err)
|
||||||
|
}
|
||||||
|
//goland:noinspection GoDeferInLoop
|
||||||
|
defer mcli.Close()
|
||||||
|
|
||||||
|
if err = mcli.WaitClusterReady(ctx, timeout); err != nil {
|
||||||
|
return fmt.Errorf("wait for cluster to be ready on machine '%s': %w", m.Name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Provisioner) RemoveCluster(ctx context.Context, name string) error {
|
func (p *Provisioner) RemoveCluster(ctx context.Context, name string) error {
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import (
|
|||||||
"net/netip"
|
"net/netip"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/cenkalti/backoff/v4"
|
|
||||||
"github.com/containerd/errdefs"
|
"github.com/containerd/errdefs"
|
||||||
"github.com/docker/docker/api/types/container"
|
"github.com/docker/docker/api/types/container"
|
||||||
"github.com/docker/docker/api/types/image"
|
"github.com/docker/docker/api/types/image"
|
||||||
@@ -17,7 +16,6 @@ import (
|
|||||||
"github.com/psviderski/uncloud/internal/secret"
|
"github.com/psviderski/uncloud/internal/secret"
|
||||||
"github.com/psviderski/uncloud/pkg/client"
|
"github.com/psviderski/uncloud/pkg/client"
|
||||||
"github.com/psviderski/uncloud/pkg/client/connector"
|
"github.com/psviderski/uncloud/pkg/client/connector"
|
||||||
"google.golang.org/protobuf/types/known/emptypb"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -194,26 +192,3 @@ func randomMachineName() (string, error) {
|
|||||||
}
|
}
|
||||||
return "machine-" + suffix, nil
|
return "machine-" + suffix, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// WaitMachineReady waits for the machine API to respond.
|
|
||||||
func 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(10*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)
|
|
||||||
}
|
|
||||||
|
|||||||
+1
-1
@@ -22,7 +22,7 @@ Uncloud stores its configuration in `~/.config/uncloud/config.yaml`. If you wish
|
|||||||
|
|
||||||
Begin by initialising the first node in your cluster with `uc machine init [USER@HOST:PORT]`. If you do not have a need for Caddy reverse proxy, you may disable this feature with `--no-caddy`. If you want to avoid using uncloud's managed DNS service, add the `--no-dns` flag.
|
Begin by initialising the first node in your cluster with `uc machine init [USER@HOST:PORT]`. If you do not have a need for Caddy reverse proxy, you may disable this feature with `--no-caddy`. If you want to avoid using uncloud's managed DNS service, add the `--no-dns` flag.
|
||||||
|
|
||||||
This command will idempotently install Docker, uncloudd, uncloud-corrosion. If Caddy is enabled, it will set up a reverse proxy. If Uncloud DNS is enabled, it will create a DNS A record for the machine's public IP address under `*.[CLUSTER ID].cluster.uncloud.run`.
|
This command will idempotently install Docker, uncloudd, uncloud-corrosion. If Caddy is enabled, it will set up a reverse proxy. If Uncloud DNS is enabled, it will create a DNS A record for the machine's public IP address under `*.[CLUSTER ID].uncld.dev`.
|
||||||
|
|
||||||
If you wish to uninstall Uncloud and its components, run `uncloud-uninstall`.
|
If you wish to uninstall Uncloud and its components, run `uncloud-uninstall`.
|
||||||
|
|
||||||
|
|||||||
+2
-41
@@ -2,13 +2,10 @@ package api
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/docker/docker/api/types/container"
|
"github.com/docker/docker/api/types/container"
|
||||||
"github.com/docker/docker/api/types/volume"
|
"github.com/docker/docker/api/types/volume"
|
||||||
"github.com/psviderski/uncloud/internal/machine/api/pb"
|
"github.com/psviderski/uncloud/internal/machine/api/pb"
|
||||||
"google.golang.org/grpc/metadata"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Client interface {
|
type Client interface {
|
||||||
@@ -51,6 +48,8 @@ type ServiceClient interface {
|
|||||||
RunService(ctx context.Context, spec ServiceSpec) (RunServiceResponse, error)
|
RunService(ctx context.Context, spec ServiceSpec) (RunServiceResponse, error)
|
||||||
InspectService(ctx context.Context, id string) (Service, error)
|
InspectService(ctx context.Context, id string) (Service, error)
|
||||||
RemoveService(ctx context.Context, id string) error
|
RemoveService(ctx context.Context, id string) error
|
||||||
|
StopService(ctx context.Context, id string, opts container.StopOptions) error
|
||||||
|
StartService(ctx context.Context, id string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type VolumeClient interface {
|
type VolumeClient interface {
|
||||||
@@ -58,41 +57,3 @@ type VolumeClient interface {
|
|||||||
ListVolumes(ctx context.Context, filter *VolumeFilter) ([]MachineVolume, error)
|
ListVolumes(ctx context.Context, filter *VolumeFilter) ([]MachineVolume, error)
|
||||||
RemoveVolume(ctx context.Context, machineNameOrID, volumeName string, force bool) error
|
RemoveVolume(ctx context.Context, machineNameOrID, volumeName string, force bool) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// ProxyMachinesContext returns a new context that proxies gRPC requests to the specified machines.
|
|
||||||
// If namesOrIDs is nil, all machines are included.
|
|
||||||
func ProxyMachinesContext(
|
|
||||||
ctx context.Context, cli MachineClient, namesOrIDs []string,
|
|
||||||
) (context.Context, MachineMembersList, error) {
|
|
||||||
// TODO: move the machine IP resolution to the proxy router to allow setting machine names and IDs in the metadata.
|
|
||||||
machines, err := cli.ListMachines(ctx, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, fmt.Errorf("list machines: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var proxiedMachines MachineMembersList
|
|
||||||
var notFound []string
|
|
||||||
for _, nameOrID := range namesOrIDs {
|
|
||||||
if m := machines.FindByNameOrID(nameOrID); m != nil {
|
|
||||||
proxiedMachines = append(proxiedMachines, m)
|
|
||||||
} else {
|
|
||||||
notFound = append(notFound, nameOrID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(notFound) > 0 {
|
|
||||||
return nil, nil, fmt.Errorf("machines not found: %s", strings.Join(notFound, ", "))
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(namesOrIDs) == 0 {
|
|
||||||
proxiedMachines = machines
|
|
||||||
}
|
|
||||||
|
|
||||||
md := metadata.New(nil)
|
|
||||||
for _, m := range proxiedMachines {
|
|
||||||
machineIP, _ := m.Machine.Network.ManagementIp.ToAddr()
|
|
||||||
md.Append("machines", machineIP.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
return metadata.NewOutgoingContext(ctx, md), proxiedMachines, nil
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -98,6 +99,76 @@ func (c *ConfigMount) Validate() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Compare compares this ConfigMount with another.
|
||||||
|
// Returns:
|
||||||
|
//
|
||||||
|
// -1 if c < other
|
||||||
|
// 0 if c == other
|
||||||
|
// +1 if c > other
|
||||||
|
func (c *ConfigMount) Compare(other *ConfigMount) int {
|
||||||
|
if c.ConfigName != other.ConfigName {
|
||||||
|
if c.ConfigName < other.ConfigName {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
if c.ContainerPath != other.ContainerPath {
|
||||||
|
if c.ContainerPath < other.ContainerPath {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
if c.Uid != other.Uid {
|
||||||
|
if c.Uid < other.Uid {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
if c.Gid != other.Gid {
|
||||||
|
if c.Gid < other.Gid {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
// Compare Mode (handle nil cases)
|
||||||
|
if c.Mode == nil && other.Mode != nil {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
if c.Mode != nil && other.Mode == nil {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
if c.Mode != nil && other.Mode != nil {
|
||||||
|
if *c.Mode < *other.Mode {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
if *c.Mode > *other.Mode {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Equals compares two ConfigMount instances for equality.
|
||||||
|
func (c *ConfigMount) Equals(other *ConfigMount) bool {
|
||||||
|
return c.Compare(other) == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ConfigMount) Clone() ConfigMount {
|
||||||
|
clone := *c
|
||||||
|
if c.Mode != nil {
|
||||||
|
mode := *c.Mode
|
||||||
|
clone.Mode = &mode
|
||||||
|
}
|
||||||
|
return clone
|
||||||
|
}
|
||||||
|
|
||||||
|
// sortConfigMounts sorts a slice of ConfigMount instances.
|
||||||
|
func sortConfigMounts(mounts []ConfigMount) {
|
||||||
|
sort.Slice(mounts, func(i, j int) bool {
|
||||||
|
return mounts[i].Compare(&mounts[j]) < 0
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// ValidateConfigsAndMounts takes config specs and config mounts and validates that all mounts refer to existing specs
|
// ValidateConfigsAndMounts takes config specs and config mounts and validates that all mounts refer to existing specs
|
||||||
func ValidateConfigsAndMounts(configs []ConfigSpec, mounts []ConfigMount) error {
|
func ValidateConfigsAndMounts(configs []ConfigSpec, mounts []ConfigMount) error {
|
||||||
configMap := make(map[string]struct{})
|
configMap := make(map[string]struct{})
|
||||||
|
|||||||
@@ -135,6 +135,10 @@ func (c *Container) UnmarshalJSON(data []byte) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
*c = Container(temp)
|
*c = Container(temp)
|
||||||
|
if c.ContainerJSONBase == nil {
|
||||||
|
return fmt.Errorf("container data is missing mandatory base fields: %s", data)
|
||||||
|
}
|
||||||
|
|
||||||
c.Name = strings.TrimPrefix(c.Name, "/")
|
c.Name = strings.TrimPrefix(c.Name, "/")
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/psviderski/uncloud/internal/machine/api/pb"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
LogStreamUnknown LogStreamType = iota
|
||||||
|
LogStreamStdout
|
||||||
|
LogStreamStderr
|
||||||
|
// LogStreamHeartbeat represents a heartbeat log entry with a timestamp indicating that
|
||||||
|
// there are no older logs than this timestamp.
|
||||||
|
LogStreamHeartbeat
|
||||||
|
)
|
||||||
|
|
||||||
|
type LogStreamType int
|
||||||
|
|
||||||
|
// LogStreamTypeFromProto converts a protobuf ContainerLogEntry.StreamType to the internal LogStreamType.
|
||||||
|
func LogStreamTypeFromProto(s pb.ContainerLogEntry_StreamType) LogStreamType {
|
||||||
|
switch s {
|
||||||
|
case pb.ContainerLogEntry_STDOUT:
|
||||||
|
return LogStreamStdout
|
||||||
|
case pb.ContainerLogEntry_STDERR:
|
||||||
|
return LogStreamStderr
|
||||||
|
case pb.ContainerLogEntry_HEARTBEAT:
|
||||||
|
return LogStreamHeartbeat
|
||||||
|
default:
|
||||||
|
return LogStreamUnknown
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// LogStreamTypeToProto converts LogStreamType to protobuf ContainerLogEntry.StreamType.
|
||||||
|
func LogStreamTypeToProto(s LogStreamType) pb.ContainerLogEntry_StreamType {
|
||||||
|
switch s {
|
||||||
|
case LogStreamStdout:
|
||||||
|
return pb.ContainerLogEntry_STDOUT
|
||||||
|
case LogStreamStderr:
|
||||||
|
return pb.ContainerLogEntry_STDERR
|
||||||
|
case LogStreamHeartbeat:
|
||||||
|
return pb.ContainerLogEntry_HEARTBEAT
|
||||||
|
default:
|
||||||
|
return pb.ContainerLogEntry_UNKNOWN
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServiceLogsOptions specifies parameters for ServiceLogs.
|
||||||
|
type ServiceLogsOptions struct {
|
||||||
|
Follow bool
|
||||||
|
Tail int
|
||||||
|
Since string
|
||||||
|
Until string
|
||||||
|
// Machines filters logs to only include containers running on the specified machines (names or IDs).
|
||||||
|
// If empty, logs from all machines are included.
|
||||||
|
Machines []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServiceLogEntry represents a single log entry from a service container.
|
||||||
|
type ServiceLogEntry struct {
|
||||||
|
// Metadata may not be set if an error occurred (Err is not nil).
|
||||||
|
Metadata ServiceLogEntryMetadata
|
||||||
|
ContainerLogEntry
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServiceLogEntryMetadata contains metadata about the source of a log entry.
|
||||||
|
type ServiceLogEntryMetadata struct {
|
||||||
|
ServiceID string
|
||||||
|
ServiceName string
|
||||||
|
ContainerID string
|
||||||
|
MachineID string
|
||||||
|
MachineName string
|
||||||
|
}
|
||||||
|
|
||||||
|
// ContainerLogEntry represents a single log entry from a container.
|
||||||
|
type ContainerLogEntry struct {
|
||||||
|
Stream LogStreamType
|
||||||
|
Timestamp time.Time
|
||||||
|
Message []byte
|
||||||
|
// Err indicates that an error occurred while streaming logs from a container.
|
||||||
|
// Other fields are not set if Err is not nil.
|
||||||
|
Err error
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrLogStreamStalled indicates that a log stream stopped sending data and may be unresponsive.
|
||||||
|
var ErrLogStreamStalled = errors.New("log stream stopped responding")
|
||||||
+49
-1
@@ -8,6 +8,7 @@ import (
|
|||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
mapset "github.com/deckarep/golang-set/v2"
|
||||||
"github.com/distribution/reference"
|
"github.com/distribution/reference"
|
||||||
"github.com/google/go-cmp/cmp"
|
"github.com/google/go-cmp/cmp"
|
||||||
"github.com/google/go-cmp/cmp/cmpopts"
|
"github.com/google/go-cmp/cmp/cmpopts"
|
||||||
@@ -224,6 +225,10 @@ func (s *ServiceSpec) Clone() ServiceSpec {
|
|||||||
// ContainerSpec defines the desired state of a container in a service.
|
// ContainerSpec defines the desired state of a container in a service.
|
||||||
// ATTENTION: after changing this struct, verify if deploy.EvalContainerSpecChange needs to be updated.
|
// ATTENTION: after changing this struct, verify if deploy.EvalContainerSpecChange needs to be updated.
|
||||||
type ContainerSpec struct {
|
type ContainerSpec struct {
|
||||||
|
// Specifies which additional capabilities should be added for the container.
|
||||||
|
CapAdd []string
|
||||||
|
// Specifies which capabilities should be dropped from the container.
|
||||||
|
CapDrop []string
|
||||||
// Command overrides the default CMD of the image to be executed when running a container.
|
// Command overrides the default CMD of the image to be executed when running a container.
|
||||||
Command []string
|
Command []string
|
||||||
// Entrypoint overrides the default ENTRYPOINT of the image.
|
// Entrypoint overrides the default ENTRYPOINT of the image.
|
||||||
@@ -242,6 +247,8 @@ type ContainerSpec struct {
|
|||||||
PullPolicy string
|
PullPolicy string
|
||||||
// Resource allocation for the container.
|
// Resource allocation for the container.
|
||||||
Resources ContainerResources
|
Resources ContainerResources
|
||||||
|
// Namespaced kernel parameters to be set in container
|
||||||
|
Sysctls map[string]string
|
||||||
// User overrides the default user of the image used to run the container. Format: user|UID[:group|GID].
|
// User overrides the default user of the image used to run the container. Format: user|UID[:group|GID].
|
||||||
User string
|
User string
|
||||||
// VolumeMounts specifies how volumes are mounted into the container filesystem.
|
// VolumeMounts specifies how volumes are mounted into the container filesystem.
|
||||||
@@ -289,18 +296,32 @@ func (s *ContainerSpec) Equals(spec ContainerSpec) bool {
|
|||||||
orig := s.SetDefaults()
|
orig := s.SetDefaults()
|
||||||
spec = spec.SetDefaults()
|
spec = spec.SetDefaults()
|
||||||
|
|
||||||
|
// Volumes
|
||||||
slices.Sort(orig.Volumes)
|
slices.Sort(orig.Volumes)
|
||||||
slices.Sort(spec.Volumes)
|
slices.Sort(spec.Volumes)
|
||||||
|
|
||||||
|
// Volume mounts
|
||||||
sortVolumeMounts(orig.VolumeMounts)
|
sortVolumeMounts(orig.VolumeMounts)
|
||||||
sortVolumeMounts(spec.VolumeMounts)
|
sortVolumeMounts(spec.VolumeMounts)
|
||||||
|
|
||||||
|
// Config mounts
|
||||||
|
sortConfigMounts(orig.ConfigMounts)
|
||||||
|
sortConfigMounts(spec.ConfigMounts)
|
||||||
|
|
||||||
return cmp.Equal(orig, spec, cmpopts.EquateEmpty())
|
return cmp.Equal(orig, spec, cmpopts.EquateEmpty())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ContainerSpec) Clone() ContainerSpec {
|
func (s *ContainerSpec) Clone() ContainerSpec {
|
||||||
spec := *s
|
spec := *s
|
||||||
|
|
||||||
|
if s.CapAdd != nil {
|
||||||
|
spec.CapAdd = make([]string, len(s.CapAdd))
|
||||||
|
copy(spec.CapAdd, s.CapAdd)
|
||||||
|
}
|
||||||
|
if s.CapDrop != nil {
|
||||||
|
spec.CapDrop = make([]string, len(s.CapDrop))
|
||||||
|
copy(spec.CapDrop, s.CapDrop)
|
||||||
|
}
|
||||||
if s.Command != nil {
|
if s.Command != nil {
|
||||||
spec.Command = make([]string, len(s.Command))
|
spec.Command = make([]string, len(s.Command))
|
||||||
copy(spec.Command, s.Command)
|
copy(spec.Command, s.Command)
|
||||||
@@ -316,6 +337,12 @@ func (s *ContainerSpec) Clone() ContainerSpec {
|
|||||||
}
|
}
|
||||||
spec.LogDriver = &logDriver
|
spec.LogDriver = &logDriver
|
||||||
}
|
}
|
||||||
|
if s.Env != nil {
|
||||||
|
spec.Env = make(EnvVars, len(s.Env))
|
||||||
|
for k, v := range s.Env {
|
||||||
|
spec.Env[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
if s.Volumes != nil {
|
if s.Volumes != nil {
|
||||||
spec.Volumes = make([]string, len(s.Volumes))
|
spec.Volumes = make([]string, len(s.Volumes))
|
||||||
copy(spec.Volumes, s.Volumes)
|
copy(spec.Volumes, s.Volumes)
|
||||||
@@ -324,7 +351,18 @@ func (s *ContainerSpec) Clone() ContainerSpec {
|
|||||||
spec.VolumeMounts = make([]VolumeMount, len(s.VolumeMounts))
|
spec.VolumeMounts = make([]VolumeMount, len(s.VolumeMounts))
|
||||||
copy(spec.VolumeMounts, s.VolumeMounts)
|
copy(spec.VolumeMounts, s.VolumeMounts)
|
||||||
}
|
}
|
||||||
|
if s.ConfigMounts != nil {
|
||||||
|
spec.ConfigMounts = make([]ConfigMount, len(s.ConfigMounts))
|
||||||
|
for i, cm := range s.ConfigMounts {
|
||||||
|
spec.ConfigMounts[i] = cm.Clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if s.Sysctls != nil {
|
||||||
|
spec.Sysctls = make(map[string]string, len(s.Sysctls))
|
||||||
|
for k, v := range s.Sysctls {
|
||||||
|
spec.Sysctls[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
return spec
|
return spec
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -366,6 +404,16 @@ type MachineServiceContainer struct {
|
|||||||
Container ServiceContainer
|
Container ServiceContainer
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MachineIDs returns a list of unique machine IDs where the service containers are running.
|
||||||
|
func (s *Service) MachineIDs() []string {
|
||||||
|
ids := mapset.NewSet[string]()
|
||||||
|
for _, mc := range s.Containers {
|
||||||
|
ids.Add(mc.MachineID)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ids.ToSlice()
|
||||||
|
}
|
||||||
|
|
||||||
// Images returns a sorted list of unique images used by the service containers.
|
// Images returns a sorted list of unique images used by the service containers.
|
||||||
func (s *Service) Images() []string {
|
func (s *Service) Images() []string {
|
||||||
images := make(map[string]struct{})
|
images := make(map[string]struct{})
|
||||||
|
|||||||
@@ -1,11 +1,19 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// boolPtr is a convenience function to create a pointer to a uint64 value
|
||||||
|
// TODO: Make this a generic function that works for any type
|
||||||
|
func boolPtr(b bool) *bool {
|
||||||
|
return &b
|
||||||
|
}
|
||||||
|
|
||||||
func TestServiceSpec_Validate_CaddyAndPorts(t *testing.T) {
|
func TestServiceSpec_Validate_CaddyAndPorts(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
@@ -197,3 +205,89 @@ func TestServiceSpec_Validate_CaddyAndPorts(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestContainerSpec_Clone(t *testing.T) {
|
||||||
|
mode := os.FileMode(0o644)
|
||||||
|
original := ContainerSpec{
|
||||||
|
CapAdd: []string{"NET_ADMIN"},
|
||||||
|
CapDrop: []string{"ALL"},
|
||||||
|
Command: []string{"sh", "-c", "echo hello"},
|
||||||
|
Entrypoint: []string{"/bin/bash"},
|
||||||
|
Env: EnvVars{
|
||||||
|
"FOO": "bar",
|
||||||
|
"BAZ": "qux",
|
||||||
|
},
|
||||||
|
Image: "nginx:latest",
|
||||||
|
Init: boolPtr(true),
|
||||||
|
LogDriver: &LogDriver{
|
||||||
|
Name: "json-file",
|
||||||
|
Options: map[string]string{
|
||||||
|
"max-size": "10m",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Privileged: true,
|
||||||
|
PullPolicy: PullPolicyAlways,
|
||||||
|
Resources: ContainerResources{
|
||||||
|
CPU: 1234,
|
||||||
|
Memory: 2345,
|
||||||
|
MemoryReservation: 3456,
|
||||||
|
},
|
||||||
|
Sysctls: map[string]string{
|
||||||
|
"net.ipv4.ip_forward": "1",
|
||||||
|
},
|
||||||
|
User: "1000:1000",
|
||||||
|
Volumes: []string{"/data", "/config"},
|
||||||
|
VolumeMounts: []VolumeMount{
|
||||||
|
{VolumeName: "data", ContainerPath: "/data"},
|
||||||
|
},
|
||||||
|
ConfigMounts: []ConfigMount{
|
||||||
|
{ConfigName: "app-config", ContainerPath: "/etc/config", Mode: &mode},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
cloned := original.Clone()
|
||||||
|
|
||||||
|
// Check ContainerSpec equality
|
||||||
|
assert.True(t, original.Equals(cloned))
|
||||||
|
|
||||||
|
// Verify deep copy by modifying the original
|
||||||
|
stringModified := "modified"
|
||||||
|
original.CapAdd[0] = stringModified
|
||||||
|
original.CapDrop[0] = stringModified
|
||||||
|
original.Command[0] = stringModified
|
||||||
|
original.Entrypoint[0] = stringModified
|
||||||
|
original.Env["FOO"] = stringModified
|
||||||
|
original.LogDriver.Options["max-size"] = stringModified
|
||||||
|
original.Volumes[0] = stringModified
|
||||||
|
original.VolumeMounts[0].ContainerPath = stringModified
|
||||||
|
original.ConfigMounts[0].ContainerPath = stringModified
|
||||||
|
*original.ConfigMounts[0].Mode = 0o755 // Modify the Mode pointer value
|
||||||
|
original.Sysctls["net.ipv4.ip_forward"] = stringModified
|
||||||
|
|
||||||
|
assert.False(t, original.Equals(cloned))
|
||||||
|
// Assert cloned values are unchanged
|
||||||
|
assert.Equal(t, "NET_ADMIN", cloned.CapAdd[0])
|
||||||
|
assert.Equal(t, "ALL", cloned.CapDrop[0])
|
||||||
|
assert.Equal(t, "sh", cloned.Command[0])
|
||||||
|
assert.Equal(t, "/bin/bash", cloned.Entrypoint[0])
|
||||||
|
assert.Equal(t, "bar", cloned.Env["FOO"])
|
||||||
|
assert.Equal(t, "qux", cloned.Env["BAZ"])
|
||||||
|
assert.Equal(t, "nginx:latest", cloned.Image)
|
||||||
|
assert.NotNil(t, cloned.Init)
|
||||||
|
assert.Equal(t, true, *cloned.Init)
|
||||||
|
assert.NotNil(t, cloned.LogDriver)
|
||||||
|
assert.Equal(t, "json-file", cloned.LogDriver.Name)
|
||||||
|
assert.Equal(t, "10m", cloned.LogDriver.Options["max-size"])
|
||||||
|
assert.Equal(t, true, cloned.Privileged)
|
||||||
|
assert.Equal(t, PullPolicyAlways, cloned.PullPolicy)
|
||||||
|
assert.Equal(t, int64(1234), cloned.Resources.CPU)
|
||||||
|
assert.Equal(t, int64(2345), cloned.Resources.Memory)
|
||||||
|
assert.Equal(t, int64(3456), cloned.Resources.MemoryReservation)
|
||||||
|
assert.Equal(t, "1000:1000", cloned.User)
|
||||||
|
assert.Equal(t, "/data", cloned.Volumes[0])
|
||||||
|
assert.Equal(t, "/data", cloned.VolumeMounts[0].ContainerPath)
|
||||||
|
assert.Equal(t, "/etc/config", cloned.ConfigMounts[0].ContainerPath)
|
||||||
|
assert.NotNil(t, cloned.ConfigMounts[0].Mode)
|
||||||
|
assert.Equal(t, os.FileMode(0o644), *cloned.ConfigMounts[0].Mode, "Mode should be deep copied")
|
||||||
|
assert.Equal(t, "1", cloned.Sysctls["net.ipv4.ip_forward"])
|
||||||
|
}
|
||||||
|
|||||||
@@ -83,6 +83,9 @@ func (v *VolumeSpec) SetDefaults() VolumeSpec {
|
|||||||
if spec.VolumeOptions.Name == "" {
|
if spec.VolumeOptions.Name == "" {
|
||||||
spec.VolumeOptions.Name = spec.Name
|
spec.VolumeOptions.Name = spec.Name
|
||||||
}
|
}
|
||||||
|
if spec.VolumeOptions.Driver != nil && spec.VolumeOptions.Driver.Name == "" {
|
||||||
|
spec.VolumeOptions.Driver.Name = VolumeDriverLocal
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// TODO: set explicit default values for Propagation and Recursive for bind mounts?
|
// TODO: set explicit default values for Propagation and Recursive for bind mounts?
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,142 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/docker/docker/api/types/mount"
|
||||||
|
"github.com/docker/docker/api/types/volume"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestVolumeSpec_MatchesDockerVolume(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
spec VolumeSpec
|
||||||
|
vol volume.Volume
|
||||||
|
expected bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "match with explicit local driver",
|
||||||
|
spec: VolumeSpec{
|
||||||
|
Type: VolumeTypeVolume,
|
||||||
|
VolumeOptions: &VolumeOptions{
|
||||||
|
Driver: &mount.Driver{
|
||||||
|
Name: "local",
|
||||||
|
Options: map[string]string{
|
||||||
|
"foo": "bar",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
vol: volume.Volume{
|
||||||
|
Driver: "local",
|
||||||
|
Options: map[string]string{
|
||||||
|
"foo": "bar",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "match with empty driver name in spec (implicit local)",
|
||||||
|
spec: VolumeSpec{
|
||||||
|
Type: VolumeTypeVolume,
|
||||||
|
VolumeOptions: &VolumeOptions{
|
||||||
|
Driver: &mount.Driver{
|
||||||
|
Name: "", // Implicitly local
|
||||||
|
Options: map[string]string{
|
||||||
|
"foo": "bar",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
vol: volume.Volume{
|
||||||
|
Driver: "local",
|
||||||
|
Options: map[string]string{
|
||||||
|
"foo": "bar",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "non-match with different driver options",
|
||||||
|
spec: VolumeSpec{
|
||||||
|
Type: VolumeTypeVolume,
|
||||||
|
VolumeOptions: &VolumeOptions{
|
||||||
|
Driver: &mount.Driver{
|
||||||
|
Name: "local",
|
||||||
|
Options: map[string]string{
|
||||||
|
"foo": "baz",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
vol: volume.Volume{
|
||||||
|
Driver: "local",
|
||||||
|
Options: map[string]string{
|
||||||
|
"foo": "bar",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "non-match with different driver name",
|
||||||
|
spec: VolumeSpec{
|
||||||
|
Type: VolumeTypeVolume,
|
||||||
|
VolumeOptions: &VolumeOptions{
|
||||||
|
Driver: &mount.Driver{
|
||||||
|
Name: "custom",
|
||||||
|
Options: map[string]string{
|
||||||
|
"foo": "bar",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
vol: volume.Volume{
|
||||||
|
Driver: "local",
|
||||||
|
Options: map[string]string{
|
||||||
|
"foo": "bar",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "match external volume without driver by name only",
|
||||||
|
spec: VolumeSpec{
|
||||||
|
Name: "external",
|
||||||
|
Type: VolumeTypeVolume,
|
||||||
|
},
|
||||||
|
vol: volume.Volume{
|
||||||
|
Name: "external",
|
||||||
|
Driver: "local",
|
||||||
|
Options: map[string]string{
|
||||||
|
"foo": "bar",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "non-match external volume by name",
|
||||||
|
spec: VolumeSpec{
|
||||||
|
Name: "unknown",
|
||||||
|
Type: VolumeTypeVolume,
|
||||||
|
},
|
||||||
|
vol: volume.Volume{
|
||||||
|
Name: "external",
|
||||||
|
Driver: "local",
|
||||||
|
Options: map[string]string{
|
||||||
|
"foo": "bar",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
matches := tt.spec.MatchesDockerVolume(tt.vol)
|
||||||
|
assert.Equal(t, tt.expected, matches)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -71,6 +71,13 @@ func (cli *Client) NewCaddyDeployment(image, config string, placement api.Placem
|
|||||||
Protocol: api.ProtocolTCP,
|
Protocol: api.ProtocolTCP,
|
||||||
Mode: api.PortModeHost,
|
Mode: api.PortModeHost,
|
||||||
},
|
},
|
||||||
|
// Needed for HTTP/3 (QUIC)
|
||||||
|
{
|
||||||
|
PublishedPort: 443,
|
||||||
|
ContainerPort: 443,
|
||||||
|
Protocol: api.ProtocolUDP,
|
||||||
|
Mode: api.PortModeHost,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
Volumes: []api.VolumeSpec{
|
Volumes: []api.VolumeSpec{
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/docker/cli/cli/streams"
|
"github.com/docker/cli/cli/streams"
|
||||||
"github.com/psviderski/uncloud/internal/machine/api/pb"
|
"github.com/psviderski/uncloud/internal/machine/api/pb"
|
||||||
@@ -76,3 +77,41 @@ func proxyToMachine(ctx context.Context, machine *pb.MachineInfo) context.Contex
|
|||||||
md := metadata.Pairs("machines", machineIP.String())
|
md := metadata.Pairs("machines", machineIP.String())
|
||||||
return metadata.NewOutgoingContext(ctx, md)
|
return metadata.NewOutgoingContext(ctx, md)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ProxyMachinesContext returns a new context that proxies gRPC requests to the specified machines.
|
||||||
|
// If namesOrIDs is nil, all machines are included.
|
||||||
|
func (cli *Client) ProxyMachinesContext(
|
||||||
|
ctx context.Context, namesOrIDs []string,
|
||||||
|
) (context.Context, api.MachineMembersList, error) {
|
||||||
|
// TODO: move the machine IP resolution to the proxy router to allow setting machine names and IDs in the metadata.
|
||||||
|
machines, err := cli.ListMachines(ctx, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("list machines: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var proxiedMachines api.MachineMembersList
|
||||||
|
var notFound []string
|
||||||
|
for _, nameOrID := range namesOrIDs {
|
||||||
|
if m := machines.FindByNameOrID(nameOrID); m != nil {
|
||||||
|
proxiedMachines = append(proxiedMachines, m)
|
||||||
|
} else {
|
||||||
|
notFound = append(notFound, nameOrID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(notFound) > 0 {
|
||||||
|
return nil, nil, fmt.Errorf("machines not found: %s", strings.Join(notFound, ", "))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(namesOrIDs) == 0 {
|
||||||
|
proxiedMachines = machines
|
||||||
|
}
|
||||||
|
|
||||||
|
md := metadata.New(nil)
|
||||||
|
for _, m := range proxiedMachines {
|
||||||
|
machineIP, _ := m.Machine.Network.ManagementIp.ToAddr()
|
||||||
|
md.Append("machines", machineIP.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
return metadata.NewOutgoingContext(ctx, md), proxiedMachines, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
package compose
|
package compose
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
composecli "github.com/compose-spec/compose-go/v2/cli"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
@@ -178,7 +181,15 @@ services:
|
|||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
project, err := loadProjectFromContent(t, tt.composeYAML)
|
// Get current working directory for relative path resolution to testdata directory.
|
||||||
|
wd, err := os.Getwd()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
project, err := LoadProjectFromContent(
|
||||||
|
context.Background(),
|
||||||
|
tt.composeYAML,
|
||||||
|
composecli.WithWorkingDirectory(wd),
|
||||||
|
)
|
||||||
|
|
||||||
if tt.wantErr != "" {
|
if tt.wantErr != "" {
|
||||||
require.ErrorContains(t, err, tt.wantErr)
|
require.ErrorContains(t, err, tt.wantErr)
|
||||||
|
|||||||
@@ -13,18 +13,27 @@ import (
|
|||||||
func configSpecsFromCompose(
|
func configSpecsFromCompose(
|
||||||
configs types.Configs, serviceConfigs []types.ServiceConfigObjConfig, workingDir string,
|
configs types.Configs, serviceConfigs []types.ServiceConfigObjConfig, workingDir string,
|
||||||
) ([]api.ConfigSpec, []api.ConfigMount, error) {
|
) ([]api.ConfigSpec, []api.ConfigMount, error) {
|
||||||
var configSpecs []api.ConfigSpec
|
|
||||||
var configMounts []api.ConfigMount
|
var configMounts []api.ConfigMount
|
||||||
|
|
||||||
|
// Temporary map to hold config specs
|
||||||
|
configSpecsMap := make(map[string]api.ConfigSpec)
|
||||||
|
|
||||||
|
// We iterate over all service config objects (config mounts)
|
||||||
for _, serviceConfig := range serviceConfigs {
|
for _, serviceConfig := range serviceConfigs {
|
||||||
var spec api.ConfigSpec
|
var spec api.ConfigSpec
|
||||||
|
|
||||||
if projectConfig, exists := configs[serviceConfig.Source]; exists {
|
projectConfig, exists := configs[serviceConfig.Source]
|
||||||
if projectConfig.External {
|
if !exists {
|
||||||
return nil, nil, fmt.Errorf("external configs are not supported: %s",
|
return nil, nil, fmt.Errorf("config '%s' not found in project configs", serviceConfig.Source)
|
||||||
serviceConfig.Source)
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
if projectConfig.External {
|
||||||
|
return nil, nil, fmt.Errorf("external configs are not supported: %s",
|
||||||
|
serviceConfig.Source)
|
||||||
|
}
|
||||||
|
|
||||||
|
spec, exists = configSpecsMap[serviceConfig.Source]
|
||||||
|
if !exists {
|
||||||
spec = api.ConfigSpec{
|
spec = api.ConfigSpec{
|
||||||
Name: serviceConfig.Source,
|
Name: serviceConfig.Source,
|
||||||
Content: []byte(projectConfig.Content),
|
Content: []byte(projectConfig.Content),
|
||||||
@@ -44,11 +53,9 @@ func configSpecsFromCompose(
|
|||||||
}
|
}
|
||||||
spec.Content = fileContent
|
spec.Content = fileContent
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
return nil, nil, fmt.Errorf("config '%s' not found in project configs", serviceConfig.Source)
|
|
||||||
}
|
|
||||||
|
|
||||||
configSpecs = append(configSpecs, spec)
|
configSpecsMap[serviceConfig.Source] = spec
|
||||||
|
}
|
||||||
|
|
||||||
// Create config mount
|
// Create config mount
|
||||||
target := serviceConfig.Target
|
target := serviceConfig.Target
|
||||||
@@ -71,5 +78,11 @@ func configSpecsFromCompose(
|
|||||||
configMounts = append(configMounts, mount)
|
configMounts = append(configMounts, mount)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var configSpecs []api.ConfigSpec
|
||||||
|
// Convert config spec map to slice
|
||||||
|
for _, spec := range configSpecsMap {
|
||||||
|
configSpecs = append(configSpecs, spec)
|
||||||
|
}
|
||||||
|
|
||||||
return configSpecs, configMounts, nil
|
return configSpecs, configMounts, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,6 +77,147 @@ func TestConfigSpecsFromCompose(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "same source mounted to different targets",
|
||||||
|
configs: types.Configs{
|
||||||
|
"shared-config": types.ConfigObjConfig{
|
||||||
|
File: "testdata/config1.txt",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
serviceConfigs: []types.ServiceConfigObjConfig{
|
||||||
|
{
|
||||||
|
Source: "shared-config",
|
||||||
|
Target: "/app/config.json",
|
||||||
|
UID: "1000",
|
||||||
|
GID: "1000",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Source: "shared-config",
|
||||||
|
Target: "/backup/config.json",
|
||||||
|
UID: "1001",
|
||||||
|
GID: "1001",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedSpecs: []api.ConfigSpec{
|
||||||
|
{
|
||||||
|
Name: "shared-config",
|
||||||
|
Content: []byte("test config content\n"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedMounts: []api.ConfigMount{
|
||||||
|
{
|
||||||
|
ConfigName: "shared-config",
|
||||||
|
ContainerPath: "/app/config.json",
|
||||||
|
Uid: "1000",
|
||||||
|
Gid: "1000",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ConfigName: "shared-config",
|
||||||
|
ContainerPath: "/backup/config.json",
|
||||||
|
Uid: "1001",
|
||||||
|
Gid: "1001",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "config with default target path",
|
||||||
|
configs: types.Configs{
|
||||||
|
"default-config": types.ConfigObjConfig{
|
||||||
|
Content: "inline config content",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
serviceConfigs: []types.ServiceConfigObjConfig{
|
||||||
|
{
|
||||||
|
Source: "default-config",
|
||||||
|
// No Target specified - should use default
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedSpecs: []api.ConfigSpec{
|
||||||
|
{
|
||||||
|
Name: "default-config",
|
||||||
|
Content: []byte("inline config content"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedMounts: []api.ConfigMount{
|
||||||
|
{
|
||||||
|
ConfigName: "default-config",
|
||||||
|
ContainerPath: "/default-config",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "config with inline content",
|
||||||
|
configs: types.Configs{
|
||||||
|
"inline-config": types.ConfigObjConfig{
|
||||||
|
Content: "server {\n listen 80;\n}",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
serviceConfigs: []types.ServiceConfigObjConfig{
|
||||||
|
{
|
||||||
|
Source: "inline-config",
|
||||||
|
Target: "/etc/nginx/sites-available/default",
|
||||||
|
Mode: func() *types.FileMode { m := types.FileMode(0o755); return &m }(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedSpecs: []api.ConfigSpec{
|
||||||
|
{
|
||||||
|
Name: "inline-config",
|
||||||
|
Content: []byte("server {\n listen 80;\n}"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedMounts: []api.ConfigMount{
|
||||||
|
{
|
||||||
|
ConfigName: "inline-config",
|
||||||
|
ContainerPath: "/etc/nginx/sites-available/default",
|
||||||
|
Mode: func() *os.FileMode { m := os.FileMode(0o755); return &m }(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "config not found error",
|
||||||
|
configs: types.Configs{
|
||||||
|
"existing-config": types.ConfigObjConfig{
|
||||||
|
Content: "some content",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
serviceConfigs: []types.ServiceConfigObjConfig{
|
||||||
|
{
|
||||||
|
Source: "missing-config",
|
||||||
|
Target: "/app/config.json",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "external config error",
|
||||||
|
configs: types.Configs{
|
||||||
|
"external-config": types.ConfigObjConfig{
|
||||||
|
External: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
serviceConfigs: []types.ServiceConfigObjConfig{
|
||||||
|
{
|
||||||
|
Source: "external-config",
|
||||||
|
Target: "/app/config.json",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "file not found error",
|
||||||
|
configs: types.Configs{
|
||||||
|
"missing-file-config": types.ConfigObjConfig{
|
||||||
|
File: "testdata/nonexistent.txt",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
serviceConfigs: []types.ServiceConfigObjConfig{
|
||||||
|
{
|
||||||
|
Source: "missing-file-config",
|
||||||
|
Target: "/app/config.json",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
|
|||||||
@@ -49,10 +49,6 @@ func NewDeploymentWithStrategy(ctx context.Context, cli Client, project *types.P
|
|||||||
ClusterDomain: domain,
|
ClusterDomain: domain,
|
||||||
}
|
}
|
||||||
|
|
||||||
if strategy == nil {
|
|
||||||
strategy = &deploy.RollingStrategy{State: state}
|
|
||||||
}
|
|
||||||
|
|
||||||
return &Deployment{
|
return &Deployment{
|
||||||
Client: cli,
|
Client: cli,
|
||||||
Project: project,
|
Project: project,
|
||||||
@@ -88,6 +84,7 @@ func (d *Deployment) Plan(ctx context.Context) (deploy.SequenceOperation, error)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check external volumes and plan the creation of missing volumes before deploying services.
|
// Check external volumes and plan the creation of missing volumes before deploying services.
|
||||||
|
// Updates the cluster state (d.state) with the scheduled volumes.
|
||||||
volumeOps, err := d.planVolumes(serviceSpecs)
|
volumeOps, err := d.planVolumes(serviceSpecs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return plan, err
|
return plan, err
|
||||||
@@ -98,8 +95,8 @@ func (d *Deployment) Plan(ctx context.Context) (deploy.SequenceOperation, error)
|
|||||||
|
|
||||||
for _, spec := range serviceSpecs {
|
for _, spec := range serviceSpecs {
|
||||||
// TODO: properly handle depends_on conditions in the service deployment plan as the first operation.
|
// TODO: properly handle depends_on conditions in the service deployment plan as the first operation.
|
||||||
// Pass the update cluster state with scheduled volumes to the deployment.
|
// Pass the updated cluster state with the scheduled volumes to the deployment.
|
||||||
deployment := deploy.NewDeployment(d.Client, spec, d.Strategy)
|
deployment := deploy.NewDeploymentWithClusterState(d.Client, spec, d.Strategy, d.state)
|
||||||
servicePlan, err := deployment.Plan(ctx)
|
servicePlan, err := deployment.Plan(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return plan, fmt.Errorf("create deployment plan for service '%s': %w", spec.Name, err)
|
return plan, fmt.Errorf("create deployment plan for service '%s': %w", spec.Name, err)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package compose
|
package compose
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@@ -301,7 +302,7 @@ services:
|
|||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
project, err := loadProjectFromContent(t, tt.content)
|
project, err := LoadProjectFromContent(context.Background(), tt.content)
|
||||||
if tt.wantErr != "" {
|
if tt.wantErr != "" {
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
assert.Contains(t, err.Error(), tt.wantErr)
|
assert.Contains(t, err.Error(), tt.wantErr)
|
||||||
@@ -375,7 +376,7 @@ services:
|
|||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
project, err := loadProjectFromContent(t, tt.content)
|
project, err := LoadProjectFromContent(context.Background(), tt.content)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
service, err := project.GetService("web")
|
service, err := project.GetService("web")
|
||||||
@@ -402,7 +403,7 @@ services:
|
|||||||
- "9090:9090/tcp@host"
|
- "9090:9090/tcp@host"
|
||||||
`
|
`
|
||||||
|
|
||||||
project, err := loadProjectFromContent(t, content)
|
project, err := LoadProjectFromContent(context.Background(), content)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
service, err := project.GetService("web")
|
service, err := project.GetService("web")
|
||||||
|
|||||||
@@ -3,12 +3,15 @@ package compose
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
composecli "github.com/compose-spec/compose-go/v2/cli"
|
composecli "github.com/compose-spec/compose-go/v2/cli"
|
||||||
"github.com/compose-spec/compose-go/v2/types"
|
"github.com/compose-spec/compose-go/v2/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// LoadProject loads a Compose project from the default locations or the given paths.
|
||||||
func LoadProject(ctx context.Context, paths []string, opts ...composecli.ProjectOptionsFn) (*types.Project, error) {
|
func LoadProject(ctx context.Context, paths []string, opts ...composecli.ProjectOptionsFn) (*types.Project, error) {
|
||||||
defaultOpts := []composecli.ProjectOptionsFn{
|
defaultOpts := []composecli.ProjectOptionsFn{
|
||||||
// First apply os.Environment, always wins.
|
// First apply os.Environment, always wins.
|
||||||
@@ -61,6 +64,26 @@ func LoadProject(ctx context.Context, paths []string, opts ...composecli.Project
|
|||||||
return project, nil
|
return project, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LoadProjectFromContent loads a Compose project from the given YAML content.
|
||||||
|
func LoadProjectFromContent(
|
||||||
|
ctx context.Context, content string, opts ...composecli.ProjectOptionsFn,
|
||||||
|
) (*types.Project, error) {
|
||||||
|
// Create a temporary directory for the compose file.
|
||||||
|
tmpDir, err := os.MkdirTemp("", "uncloud-compose-*")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("create temporary directory: %w", err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tmpDir)
|
||||||
|
|
||||||
|
// Write the YAML content to compose.yaml in the temporary directory.
|
||||||
|
composePath := filepath.Join(tmpDir, "compose.yaml")
|
||||||
|
if err := os.WriteFile(composePath, []byte(content), 0o644); err != nil {
|
||||||
|
return nil, fmt.Errorf("write compose file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return LoadProject(ctx, []string{composePath}, opts...)
|
||||||
|
}
|
||||||
|
|
||||||
// removeProjectPrefixFromNames removes the project name prefix from volume names.
|
// removeProjectPrefixFromNames removes the project name prefix from volume names.
|
||||||
func removeProjectPrefixFromNames(project *types.Project) {
|
func removeProjectPrefixFromNames(project *types.Project) {
|
||||||
prefix := project.Name + "_"
|
prefix := project.Name + "_"
|
||||||
|
|||||||
@@ -43,6 +43,8 @@ func ServiceSpecFromCompose(project *types.Project, serviceName string) (api.Ser
|
|||||||
|
|
||||||
spec := api.ServiceSpec{
|
spec := api.ServiceSpec{
|
||||||
Container: api.ContainerSpec{
|
Container: api.ContainerSpec{
|
||||||
|
CapAdd: service.CapAdd,
|
||||||
|
CapDrop: service.CapDrop,
|
||||||
Command: service.Command,
|
Command: service.Command,
|
||||||
Entrypoint: service.Entrypoint,
|
Entrypoint: service.Entrypoint,
|
||||||
Env: env,
|
Env: env,
|
||||||
@@ -51,6 +53,7 @@ func ServiceSpecFromCompose(project *types.Project, serviceName string) (api.Ser
|
|||||||
Privileged: service.Privileged,
|
Privileged: service.Privileged,
|
||||||
PullPolicy: pullPolicy,
|
PullPolicy: pullPolicy,
|
||||||
Resources: resourcesFromCompose(service),
|
Resources: resourcesFromCompose(service),
|
||||||
|
Sysctls: service.Sysctls,
|
||||||
User: service.User,
|
User: service.User,
|
||||||
},
|
},
|
||||||
Name: serviceName,
|
Name: serviceName,
|
||||||
@@ -249,7 +252,7 @@ func dockerVolumeSpecFromCompose(serviceVolume types.ServiceVolumeConfig, volume
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !volume.External {
|
if !volume.External {
|
||||||
if volume.Driver != "" {
|
if volume.Driver != "" || len(volume.DriverOpts) > 0 {
|
||||||
spec.VolumeOptions.Driver = &mount.Driver{
|
spec.VolumeOptions.Driver = &mount.Driver{
|
||||||
Name: volume.Driver,
|
Name: volume.Driver,
|
||||||
Options: volume.DriverOpts,
|
Options: volume.DriverOpts,
|
||||||
|
|||||||
@@ -3,13 +3,13 @@ package compose
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/compose-spec/compose-go/v2/loader"
|
composecli "github.com/compose-spec/compose-go/v2/cli"
|
||||||
"github.com/compose-spec/compose-go/v2/types"
|
|
||||||
"github.com/docker/docker/api/types/container"
|
"github.com/docker/docker/api/types/container"
|
||||||
"github.com/docker/docker/api/types/mount"
|
"github.com/docker/docker/api/types/mount"
|
||||||
"github.com/docker/go-units"
|
"github.com/docker/go-units"
|
||||||
@@ -20,57 +20,6 @@ import (
|
|||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
// loadProjectFromContent loads a compose project from YAML content.
|
|
||||||
// Keep the implementation in sync with LoadProject.
|
|
||||||
func loadProjectFromContent(t *testing.T, content string) (*types.Project, error) {
|
|
||||||
t.Helper()
|
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
configDetails := types.ConfigDetails{
|
|
||||||
ConfigFiles: []types.ConfigFile{
|
|
||||||
{
|
|
||||||
Filename: "docker-compose.yml",
|
|
||||||
Content: []byte(content),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
project, err := loader.LoadWithContext(ctx, configDetails, func(o *loader.Options) {
|
|
||||||
o.SetProjectName("test", true)
|
|
||||||
// Register our custom extensions
|
|
||||||
if o.KnownExtensions == nil {
|
|
||||||
o.KnownExtensions = map[string]any{}
|
|
||||||
}
|
|
||||||
o.KnownExtensions[CaddyExtensionKey] = Caddy{}
|
|
||||||
o.KnownExtensions[PortsExtensionKey] = PortsSource{}
|
|
||||||
o.KnownExtensions[MachinesExtensionKey] = MachinesSource{}
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
removeProjectPrefixFromNames(project)
|
|
||||||
// Apply extension transformations since we're not using LoadProject.
|
|
||||||
if project, err = transformServicesCaddyExtension(project); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if project, err = transformServicesPortsExtension(project); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate extension combinations after all transformations.
|
|
||||||
if err = validateServicesExtensions(project); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process image templates in services to expand Go template expressions using git repo state.
|
|
||||||
if project, err = ProcessImageTemplates(project); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return project, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestServiceSpecFromCompose(t *testing.T) {
|
func TestServiceSpecFromCompose(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
@@ -151,6 +100,8 @@ func TestServiceSpecFromCompose(t *testing.T) {
|
|||||||
Name: "test",
|
Name: "test",
|
||||||
Mode: api.ServiceModeReplicated,
|
Mode: api.ServiceModeReplicated,
|
||||||
Container: api.ContainerSpec{
|
Container: api.ContainerSpec{
|
||||||
|
CapAdd: []string{"NET_ADMIN"},
|
||||||
|
CapDrop: []string{"ALL"},
|
||||||
Command: []string{"nginx", "updated", "command"},
|
Command: []string{"nginx", "updated", "command"},
|
||||||
Entrypoint: []string{"/updated-docker-entrypoint.sh"},
|
Entrypoint: []string{"/updated-docker-entrypoint.sh"},
|
||||||
Env: map[string]string{
|
Env: map[string]string{
|
||||||
@@ -174,6 +125,9 @@ func TestServiceSpecFromCompose(t *testing.T) {
|
|||||||
Memory: 100 * units.MiB,
|
Memory: 100 * units.MiB,
|
||||||
MemoryReservation: 50 * units.MiB,
|
MemoryReservation: 50 * units.MiB,
|
||||||
},
|
},
|
||||||
|
Sysctls: map[string]string{
|
||||||
|
"net.ipv4.ip_forward": "1",
|
||||||
|
},
|
||||||
User: "nginx:nginx",
|
User: "nginx:nginx",
|
||||||
VolumeMounts: []api.VolumeMount{
|
VolumeMounts: []api.VolumeMount{
|
||||||
{
|
{
|
||||||
@@ -431,7 +385,15 @@ services:
|
|||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
project, err := loadProjectFromContent(t, tt.composeYAML)
|
// Get current working directory for relative path resolution to testdata directory.
|
||||||
|
wd, err := os.Getwd()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
project, err := LoadProjectFromContent(
|
||||||
|
context.Background(),
|
||||||
|
tt.composeYAML,
|
||||||
|
composecli.WithWorkingDirectory(wd),
|
||||||
|
)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
spec, err := ServiceSpecFromCompose(project, "web")
|
spec, err := ServiceSpecFromCompose(project, "web")
|
||||||
@@ -617,7 +579,7 @@ services:
|
|||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
project, err := loadProjectFromContent(t, tt.composeYAML)
|
project, err := LoadProjectFromContent(context.Background(), tt.composeYAML)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
spec, err := ServiceSpecFromCompose(project, "ai")
|
spec, err := ServiceSpecFromCompose(project, "ai")
|
||||||
@@ -629,6 +591,114 @@ services:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestServiceSpecFromCompose_VolumeDriverOpts(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
composeYAML string
|
||||||
|
expectedVolume api.VolumeSpec
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "volume with driver_opts only (no driver specified)",
|
||||||
|
composeYAML: `
|
||||||
|
services:
|
||||||
|
test:
|
||||||
|
image: nginx
|
||||||
|
volumes:
|
||||||
|
- nfsmount:/data/nfs
|
||||||
|
volumes:
|
||||||
|
nfsmount:
|
||||||
|
driver_opts:
|
||||||
|
type: "nfs"
|
||||||
|
o: "addr=192.168.1.100,nolock,soft"
|
||||||
|
device: ":/mnt/share"
|
||||||
|
`,
|
||||||
|
expectedVolume: api.VolumeSpec{
|
||||||
|
Name: "nfsmount",
|
||||||
|
Type: api.VolumeTypeVolume,
|
||||||
|
VolumeOptions: &api.VolumeOptions{
|
||||||
|
Name: "nfsmount",
|
||||||
|
Driver: &mount.Driver{
|
||||||
|
Options: map[string]string{
|
||||||
|
"type": "nfs",
|
||||||
|
"o": "addr=192.168.1.100,nolock,soft",
|
||||||
|
"device": ":/mnt/share",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "volume with driver and driver_opts",
|
||||||
|
composeYAML: `
|
||||||
|
services:
|
||||||
|
test:
|
||||||
|
image: nginx
|
||||||
|
volumes:
|
||||||
|
- nfsmount:/data/nfs
|
||||||
|
volumes:
|
||||||
|
nfsmount:
|
||||||
|
driver: local
|
||||||
|
driver_opts:
|
||||||
|
type: "nfs"
|
||||||
|
o: "addr=192.168.1.100,nolock,soft"
|
||||||
|
device: ":/mnt/share"
|
||||||
|
`,
|
||||||
|
expectedVolume: api.VolumeSpec{
|
||||||
|
Name: "nfsmount",
|
||||||
|
Type: api.VolumeTypeVolume,
|
||||||
|
VolumeOptions: &api.VolumeOptions{
|
||||||
|
Name: "nfsmount",
|
||||||
|
Driver: &mount.Driver{
|
||||||
|
Name: "local",
|
||||||
|
Options: map[string]string{
|
||||||
|
"type": "nfs",
|
||||||
|
"o": "addr=192.168.1.100,nolock,soft",
|
||||||
|
"device": ":/mnt/share",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "external volume ignores driver_opts",
|
||||||
|
composeYAML: `
|
||||||
|
services:
|
||||||
|
test:
|
||||||
|
image: nginx
|
||||||
|
volumes:
|
||||||
|
- extvolume:/data
|
||||||
|
volumes:
|
||||||
|
extvolume:
|
||||||
|
external: true
|
||||||
|
`,
|
||||||
|
expectedVolume: api.VolumeSpec{
|
||||||
|
Name: "extvolume",
|
||||||
|
Type: api.VolumeTypeVolume,
|
||||||
|
VolumeOptions: &api.VolumeOptions{
|
||||||
|
Name: "extvolume",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
project, err := LoadProjectFromContent(context.Background(), tt.composeYAML)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
spec, err := ServiceSpecFromCompose(project, "test")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.Len(t, spec.Volumes, 1, "expected exactly one volume")
|
||||||
|
actualVolume := spec.Volumes[0]
|
||||||
|
|
||||||
|
cmpOpts := cmp.Options{cmpopts.EquateEmpty()}
|
||||||
|
assert.True(t, cmp.Equal(actualVolume, tt.expectedVolume, cmpOpts...),
|
||||||
|
"Volume mismatch:\n%s", cmp.Diff(actualVolume, tt.expectedVolume, cmpOpts...))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestServiceSpecFromCompose_XMachinesPlacement(t *testing.T) {
|
func TestServiceSpecFromCompose_XMachinesPlacement(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
@@ -765,7 +835,7 @@ services:
|
|||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
project, err := loadProjectFromContent(t, tt.composeYAML)
|
project, err := LoadProjectFromContent(context.Background(), tt.composeYAML)
|
||||||
|
|
||||||
if tt.expectError {
|
if tt.expectError {
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
services:
|
services:
|
||||||
test:
|
test:
|
||||||
|
cap_add:
|
||||||
|
- NET_ADMIN
|
||||||
|
cap_drop:
|
||||||
|
- ALL
|
||||||
command: ["nginx", "updated", "command"]
|
command: ["nginx", "updated", "command"]
|
||||||
cpus: 0.5
|
cpus: 0.5
|
||||||
entrypoint: ["/updated-docker-entrypoint.sh"]
|
entrypoint: ["/updated-docker-entrypoint.sh"]
|
||||||
@@ -19,6 +23,8 @@ services:
|
|||||||
privileged: true
|
privileged: true
|
||||||
pull_policy: always
|
pull_policy: always
|
||||||
scale: 3
|
scale: 3
|
||||||
|
sysctls:
|
||||||
|
- net.ipv4.ip_forward=1
|
||||||
user: nginx:nginx
|
user: nginx:nginx
|
||||||
volumes:
|
volumes:
|
||||||
- /etc/passwd:/host/etc/passwd:ro
|
- /etc/passwd:/host/etc/passwd:ro
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package connector
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// defaultServiceConfig defines the default gRPC service configuration including retry policy for transient failures.
|
||||||
|
var defaultServiceConfig = mustMarshalJSON(map[string]any{
|
||||||
|
"methodConfig": []map[string]any{
|
||||||
|
{
|
||||||
|
"name": []map[string]string{{"service": ""}},
|
||||||
|
"retryPolicy": map[string]any{
|
||||||
|
"maxAttempts": 5, // 5 is the maximum allowed by gRPC
|
||||||
|
"initialBackoff": "0.5s",
|
||||||
|
"maxBackoff": "5s",
|
||||||
|
"backoffMultiplier": 2,
|
||||||
|
"retryableStatusCodes": []string{"UNAVAILABLE"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
func mustMarshalJSON(v any) string {
|
||||||
|
b, err := json.Marshal(v)
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Sprintf("failed to marshal service config: %v", err))
|
||||||
|
}
|
||||||
|
return string(b)
|
||||||
|
}
|
||||||
@@ -59,6 +59,7 @@ func (c *SSHConnector) Connect(ctx context.Context) (*grpc.ClientConn, error) {
|
|||||||
conn, err := grpc.NewClient(
|
conn, err := grpc.NewClient(
|
||||||
"unix://"+sockPath,
|
"unix://"+sockPath,
|
||||||
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
||||||
|
grpc.WithDefaultServiceConfig(defaultServiceConfig),
|
||||||
grpc.WithContextDialer(
|
grpc.WithContextDialer(
|
||||||
func(ctx context.Context, addr string) (net.Conn, error) {
|
func(ctx context.Context, addr string) (net.Conn, error) {
|
||||||
addr = strings.TrimPrefix(addr, "unix://")
|
addr = strings.TrimPrefix(addr, "unix://")
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ func (c *SSHCLIConnector) Connect(ctx context.Context) (*grpc.ClientConn, error)
|
|||||||
grpcConn, err := grpc.NewClient(
|
grpcConn, err := grpc.NewClient(
|
||||||
"passthrough:///", // Dummy target since we're using a custom dialer.
|
"passthrough:///", // Dummy target since we're using a custom dialer.
|
||||||
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
||||||
|
grpc.WithDefaultServiceConfig(defaultServiceConfig),
|
||||||
grpc.WithContextDialer(func(ctx context.Context, _ string) (net.Conn, error) {
|
grpc.WithContextDialer(func(ctx context.Context, _ string) (net.Conn, error) {
|
||||||
return c.conn, nil
|
return c.conn, nil
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ func (c *TCPConnector) Connect(_ context.Context) (*grpc.ClientConn, error) {
|
|||||||
conn, err := grpc.NewClient(
|
conn, err := grpc.NewClient(
|
||||||
c.apiAddr.String(),
|
c.apiAddr.String(),
|
||||||
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
||||||
|
grpc.WithDefaultServiceConfig(defaultServiceConfig),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("create machine API client: %w", err)
|
return nil, fmt.Errorf("create machine API client: %w", err)
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package connector
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"golang.org/x/net/proxy"
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
"google.golang.org/grpc/credentials/insecure"
|
||||||
|
)
|
||||||
|
|
||||||
|
// UnixConnector establishes a connection to the machine API through a unix domain socket.
|
||||||
|
type UnixConnector struct {
|
||||||
|
socketPath string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewUnixConnector(socketPath string) *UnixConnector {
|
||||||
|
return &UnixConnector{socketPath: socketPath}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *UnixConnector) Connect(_ context.Context) (*grpc.ClientConn, error) {
|
||||||
|
// gRPC uses "unix:path" syntax for unix sockets.
|
||||||
|
target := "unix:" + c.socketPath
|
||||||
|
|
||||||
|
conn, err := grpc.NewClient(
|
||||||
|
target,
|
||||||
|
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
||||||
|
grpc.WithDefaultServiceConfig(defaultServiceConfig),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("create machine API client: %w", err)
|
||||||
|
}
|
||||||
|
return conn, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *UnixConnector) Dialer() (proxy.ContextDialer, error) {
|
||||||
|
return nil, fmt.Errorf("proxy connections are not supported over a unix connection")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *UnixConnector) Close() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user