mirror of
https://github.com/psviderski/uncloud.git
synced 2026-08-26 19:13:34 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0b8495e964 | ||
|
|
6068e24f4f | ||
|
|
7861d56dff | ||
|
|
7bc69b9a9e | ||
|
|
38800924ed | ||
|
|
cf7e333579 | ||
|
|
769f5e47c3 | ||
|
|
6e1165ec57 | ||
|
|
8d16da596c | ||
|
|
d6c06828b3 | ||
|
|
fe93d91894 | ||
|
|
3f70018e7c | ||
|
|
70e9385032 | ||
|
|
db3016fe89 | ||
|
|
c56385602e | ||
|
|
fbbf839258 | ||
|
|
eee28e2ead | ||
|
|
ef57b62625 | ||
|
|
e854bbb3e1 | ||
|
|
938e7d05c0 | ||
|
|
6c2759f315 | ||
|
|
72bf34632f | ||
|
|
7db8bba6ea | ||
|
|
badb8ecd3c | ||
|
|
9413f1c0b1 | ||
|
|
d8818ba770 | ||
|
|
440215b811 | ||
|
|
02a418aafb | ||
|
|
33550be65e | ||
|
|
edad2429c5 | ||
|
|
b605a64fbe | ||
|
|
16c876d83f | ||
|
|
d6ff62159d | ||
|
|
fcf164dda1 | ||
|
|
e5957d9d6b | ||
|
|
143e68cc95 | ||
|
|
835c310987 | ||
|
|
5fce84d33c | ||
|
|
45497fd20c | ||
|
|
f08d9fe405 | ||
|
|
565a8d8366 | ||
|
|
343357adf9 | ||
|
|
8ee551ca8f | ||
|
|
948201cb6f | ||
|
|
f71fc57f34 | ||
|
|
1acb0ff110 | ||
|
|
03c2681792 | ||
|
|
f3cb6657ae | ||
|
|
4532b985d4 | ||
|
|
000a8a1d3e | ||
|
|
7705db2c4f | ||
|
|
590a1b231f | ||
|
|
8709ac0ed5 | ||
|
|
f8ebd94a79 | ||
|
|
87331a9265 | ||
|
|
2f5a2c6344 | ||
|
|
97e032cb7b | ||
|
|
410a95ec88 | ||
|
|
df4ab40e12 | ||
|
|
c6205d4f57 | ||
|
|
13dea92311 | ||
|
|
9100c22e7f | ||
|
|
6125553a97 | ||
|
|
c9452f1509 | ||
|
|
25b173128a | ||
|
|
4e40a93991 | ||
|
|
9fc54c9d64 | ||
|
|
cacfca7a88 | ||
|
|
b3885b0888 | ||
|
|
3ada89c4f8 | ||
|
|
20b2ca2ea5 | ||
|
|
1bbefdd0d2 | ||
|
|
f467fad84c |
+3
-2
@@ -27,8 +27,9 @@ go.work.sum
|
|||||||
.env
|
.env
|
||||||
.idea/
|
.idea/
|
||||||
|
|
||||||
data
|
/data/
|
||||||
dist/
|
/dist/
|
||||||
|
/uc
|
||||||
|
|
||||||
# Web
|
# Web
|
||||||
node_modules/
|
node_modules/
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ builds:
|
|||||||
goarch:
|
goarch:
|
||||||
- amd64
|
- amd64
|
||||||
- arm64
|
- arm64
|
||||||
|
ldflags:
|
||||||
|
- -s -w -X github.com/psviderski/uncloud/internal/version.version={{ .Version }}
|
||||||
|
|
||||||
- id: uncloudd
|
- id: uncloudd
|
||||||
main: ./cmd/uncloudd
|
main: ./cmd/uncloudd
|
||||||
@@ -35,6 +37,8 @@ builds:
|
|||||||
goarch:
|
goarch:
|
||||||
- amd64
|
- amd64
|
||||||
- arm64
|
- arm64
|
||||||
|
ldflags:
|
||||||
|
- -s -w -X github.com/psviderski/uncloud/internal/version.version={{ .Version }}
|
||||||
|
|
||||||
archives:
|
archives:
|
||||||
- id: uncloud
|
- id: uncloud
|
||||||
|
|||||||
@@ -31,11 +31,15 @@ reset-dev:
|
|||||||
ssh ubuntu@152.67.101.197 "sudo systemctl stop uncloud && sudo rm -rf /var/lib/uncloud"
|
ssh ubuntu@152.67.101.197 "sudo systemctl stop uncloud && sudo rm -rf /var/lib/uncloud"
|
||||||
|
|
||||||
demo-reset:
|
demo-reset:
|
||||||
rm -fv ~/.config/uncloud/config.toml
|
rm -fv ~/.config/uncloud/config.yaml
|
||||||
ssh ubuntu@152.67.101.197 "AUTO_CONFIRM=true sudo -E uncloud-uninstall && docker rmi caddy:2.9.1"
|
ssh ubuntu@152.67.101.197 "AUTO_CONFIRM=true sudo -E uncloud-uninstall && docker rmi caddy:2.9.1"
|
||||||
ssh root@5.223.45.199 "AUTO_CONFIRM=true sudo -E uncloud-uninstall"
|
ssh root@5.223.45.199 "AUTO_CONFIRM=true sudo -E uncloud-uninstall"
|
||||||
ssh spy@192.168.40.243 "AUTO_CONFIRM=true sudo -E uncloud-uninstall"
|
ssh spy@192.168.40.243 "AUTO_CONFIRM=true sudo -E uncloud-uninstall"
|
||||||
|
|
||||||
|
.PHONY: ucind-cluster
|
||||||
|
ucind-cluster:
|
||||||
|
go run ./cmd/ucind cluster rm && go run ./cmd/ucind cluster create -m 3
|
||||||
|
|
||||||
.PHONY: proto
|
.PHONY: proto
|
||||||
proto:
|
proto:
|
||||||
protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative \
|
protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative \
|
||||||
|
|||||||
+5
-4
@@ -3,12 +3,13 @@ package main
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/docker/docker/client"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/docker/docker/client"
|
||||||
"github.com/psviderski/uncloud/cmd/ucind/cluster"
|
"github.com/psviderski/uncloud/cmd/ucind/cluster"
|
||||||
"github.com/psviderski/uncloud/internal/ucind"
|
"github.com/psviderski/uncloud/internal/ucind"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
@@ -44,9 +45,9 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO: allow to override using UNCLOUD_CONFIG env var.
|
// TODO: allow to override using UNCLOUD_CONFIG env var.
|
||||||
cmd.PersistentFlags().StringVar(&configPath, "uncloud-config", "~/.config/uncloud/config.toml",
|
cmd.PersistentFlags().StringVar(&configPath, "uncloud-config", "~/.config/uncloud/config.yaml",
|
||||||
"path to the Uncloud configuration file.")
|
"path to the Uncloud configuration file.")
|
||||||
_ = cmd.MarkPersistentFlagFilename("uncloud-config", "toml")
|
_ = cmd.MarkPersistentFlagFilename("uncloud-config", "yaml", "yml")
|
||||||
|
|
||||||
cmd.AddCommand(
|
cmd.AddCommand(
|
||||||
cluster.NewRootCommand(),
|
cluster.NewRootCommand(),
|
||||||
|
|||||||
+19
-46
@@ -4,23 +4,23 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"maps"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/docker/cli/cli/streams"
|
"github.com/docker/cli/cli/streams"
|
||||||
"github.com/docker/compose/v2/pkg/progress"
|
"github.com/docker/compose/v2/pkg/progress"
|
||||||
"github.com/psviderski/uncloud/internal/cli"
|
"github.com/psviderski/uncloud/internal/cli"
|
||||||
"github.com/psviderski/uncloud/internal/machine/api/pb"
|
"github.com/psviderski/uncloud/internal/machine/api/pb"
|
||||||
"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/psviderski/uncloud/pkg/client/deploy"
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"maps"
|
|
||||||
"slices"
|
|
||||||
"strings"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type deployOptions struct {
|
type deployOptions struct {
|
||||||
image string
|
image string
|
||||||
machine string
|
machines []string
|
||||||
cluster string
|
context string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewDeployCommand() *cobra.Command {
|
func NewDeployCommand() *cobra.Command {
|
||||||
@@ -39,18 +39,19 @@ func NewDeployCommand() *cobra.Command {
|
|||||||
|
|
||||||
cmd.Flags().StringVar(&opts.image, "image", "",
|
cmd.Flags().StringVar(&opts.image, "image", "",
|
||||||
"Caddy Docker image to deploy. (default caddy:LATEST_VERSION)")
|
"Caddy Docker image to deploy. (default caddy:LATEST_VERSION)")
|
||||||
cmd.Flags().StringVarP(&opts.machine, "machine", "m", "",
|
cmd.Flags().StringSliceVarP(&opts.machines, "machine", "m", nil,
|
||||||
"Machine names to deploy to (comma-separated). (default is all machines)")
|
"Machine names to deploy to. Can be specified multiple times or as a comma-separated "+
|
||||||
|
"list of machine names. (default is all machines)")
|
||||||
cmd.Flags().StringVarP(
|
cmd.Flags().StringVarP(
|
||||||
&opts.cluster, "cluster", "c", "",
|
&opts.context, "context", "c", "",
|
||||||
"Name of the cluster to deploy to. (default is the current cluster)",
|
"Name of the cluster context to deploy to. (default is the current context)",
|
||||||
)
|
)
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
func runDeploy(ctx context.Context, uncli *cli.CLI, opts deployOptions) error {
|
func runDeploy(ctx context.Context, uncli *cli.CLI, opts deployOptions) error {
|
||||||
clusterClient, err := uncli.ConnectCluster(ctx, opts.cluster)
|
clusterClient, err := uncli.ConnectCluster(ctx, opts.context)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("connect to cluster: %w", err)
|
return fmt.Errorf("connect to cluster: %w", err)
|
||||||
}
|
}
|
||||||
@@ -87,16 +88,10 @@ func runDeploy(ctx context.Context, uncli *cli.CLI, opts deployOptions) error {
|
|||||||
fmt.Println()
|
fmt.Println()
|
||||||
fmt.Println("Preparing a deployment plan...")
|
fmt.Println("Preparing a deployment plan...")
|
||||||
|
|
||||||
var filter deploy.MachineFilter
|
placement := api.Placement{
|
||||||
if opts.machine != "" {
|
Machines: cli.ExpandCommaSeparatedValues(opts.machines),
|
||||||
machines := strings.Split(opts.machine, ",")
|
|
||||||
for i, m := range machines {
|
|
||||||
machines[i] = strings.TrimSpace(m)
|
|
||||||
}
|
|
||||||
filter = machineFilter(machines)
|
|
||||||
}
|
}
|
||||||
|
d, err := clusterClient.NewCaddyDeployment(opts.image, placement)
|
||||||
d, err := clusterClient.NewCaddyDeployment(opts.image, filter)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("create caddy deployment: %w", err)
|
return fmt.Errorf("create caddy deployment: %w", err)
|
||||||
}
|
}
|
||||||
@@ -107,31 +102,18 @@ func runDeploy(ctx context.Context, uncli *cli.CLI, opts deployOptions) error {
|
|||||||
|
|
||||||
plan, err := d.Plan(ctx)
|
plan, err := d.Plan(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, deploy.ErrNoMatchingMachines) {
|
|
||||||
return fmt.Errorf("no machines found matching: %s", opts.machine)
|
|
||||||
}
|
|
||||||
return fmt.Errorf("plan caddy deployment: %w", err)
|
return fmt.Errorf("plan caddy deployment: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(plan.Operations) == 0 {
|
if len(plan.Operations) == 0 {
|
||||||
if opts.machine != "" {
|
fmt.Printf("%s service is up to date.\n", client.CaddyServiceName)
|
||||||
fmt.Printf("%s service is up to date on selected machines.\n", client.CaddyServiceName)
|
|
||||||
} else {
|
|
||||||
fmt.Printf("%s service is up to date.\n", client.CaddyServiceName)
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
if svc.ID == "" {
|
if svc.ID == "" {
|
||||||
if opts.machine != "" {
|
if len(opts.machines) > 0 {
|
||||||
fmt.Println("This will run a Caddy container on selected machines.")
|
fmt.Println("This will run a Caddy container on each selected machine.")
|
||||||
} else {
|
} else {
|
||||||
fmt.Println("This will run a Caddy container on each machine.")
|
fmt.Println("This will run a Caddy container on each machine.")
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
if opts.machine != "" {
|
|
||||||
fmt.Println("This will perform a rolling update of Caddy containers on selected machines.")
|
|
||||||
} else {
|
|
||||||
fmt.Println("This will perform a rolling update of Caddy containers on each machine.")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialise a machine and container name resolver to properly format the plan output.
|
// Initialise a machine and container name resolver to properly format the plan output.
|
||||||
@@ -214,12 +196,3 @@ func UpdateDomainRecords(ctx context.Context, clusterClient *client.Client, prog
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func machineFilter(machines []string) deploy.MachineFilter {
|
|
||||||
if len(machines) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return func(m *pb.MachineInfo) bool {
|
|
||||||
return slices.Contains(machines, m.Name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
package context
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"maps"
|
||||||
|
"os"
|
||||||
|
"slices"
|
||||||
|
"text/tabwriter"
|
||||||
|
|
||||||
|
"github.com/psviderski/uncloud/internal/cli"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewListCommand() *cobra.Command {
|
||||||
|
var clusterContext string
|
||||||
|
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "ls",
|
||||||
|
Aliases: []string{"list"},
|
||||||
|
Short: "List available cluster contexts.",
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
uncli := cmd.Context().Value("cli").(*cli.CLI)
|
||||||
|
return list(uncli)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Flags().StringVarP(
|
||||||
|
&clusterContext, "context", "c", "",
|
||||||
|
"Name of the cluster context. (default is the current context)",
|
||||||
|
)
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func list(uncli *cli.CLI) error {
|
||||||
|
if uncli.Config == nil {
|
||||||
|
return fmt.Errorf("context management is not available: Uncloud configuration file is not being used")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(uncli.Config.Contexts) == 0 {
|
||||||
|
fmt.Println("No contexts found")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
contextNames := slices.Sorted(maps.Keys(uncli.Config.Contexts))
|
||||||
|
currentContext := uncli.Config.CurrentContext
|
||||||
|
|
||||||
|
tw := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', 0)
|
||||||
|
fmt.Fprintln(tw, "NAME\tCURRENT\tCONNECTIONS")
|
||||||
|
|
||||||
|
for _, name := range contextNames {
|
||||||
|
current := ""
|
||||||
|
if name == currentContext {
|
||||||
|
current = "*"
|
||||||
|
}
|
||||||
|
connCount := len(uncli.Config.Contexts[name].Connections)
|
||||||
|
fmt.Fprintf(tw, "%s\t%s\t%d\n", name, current, connCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
return tw.Flush()
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package context
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/psviderski/uncloud/internal/cli"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewRootCommand() *cobra.Command {
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "ctx",
|
||||||
|
Aliases: []string{"context"},
|
||||||
|
Short: "Switch between different cluster contexts. Contains subcommands to manage contexts.",
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
uncli := cmd.Context().Value("cli").(*cli.CLI)
|
||||||
|
return selectContext(uncli)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.AddCommand(
|
||||||
|
NewListCommand(),
|
||||||
|
NewUseCommand(),
|
||||||
|
)
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
package context
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"maps"
|
||||||
|
"slices"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/huh"
|
||||||
|
"github.com/psviderski/uncloud/internal/cli"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewUseCommand() *cobra.Command {
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "use [CONTEXT]",
|
||||||
|
Short: "Switch to a different cluster context.",
|
||||||
|
Long: "Switch to a different cluster context. If no context is provided, " +
|
||||||
|
"a list of available contexts will be displayed for selection.",
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
uncli := cmd.Context().Value("cli").(*cli.CLI)
|
||||||
|
|
||||||
|
if len(args) == 1 {
|
||||||
|
if err := uncli.SetCurrentContext(args[0]); err != nil {
|
||||||
|
return fmt.Errorf("failed to set the current cluster context to '%s': %w", args[0], err)
|
||||||
|
}
|
||||||
|
fmt.Printf("Current cluster context is now '%s'.\n", args[0])
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return selectContext(uncli)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func selectContext(uncli *cli.CLI) error {
|
||||||
|
if uncli.Config == nil {
|
||||||
|
return fmt.Errorf("context 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())
|
||||||
|
}
|
||||||
|
|
||||||
|
contextNames := slices.Sorted(maps.Keys(uncli.Config.Contexts))
|
||||||
|
|
||||||
|
var selected string
|
||||||
|
form := huh.NewForm(
|
||||||
|
huh.NewGroup(
|
||||||
|
huh.NewSelect[string]().
|
||||||
|
Title("Select a cluster context").
|
||||||
|
Options(buildContextOptions(contextNames, uncli.Config.CurrentContext)...).
|
||||||
|
Value(&selected),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
if err := form.Run(); err != nil {
|
||||||
|
return fmt.Errorf("select cluster context: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := uncli.SetCurrentContext(selected); err != nil {
|
||||||
|
return fmt.Errorf("set current cluster context: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Current cluster context is now '%s'.\n", selected)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildContextOptions(contexts []string, current string) []huh.Option[string] {
|
||||||
|
options := make([]huh.Option[string], len(contexts))
|
||||||
|
|
||||||
|
for i, ctx := range contexts {
|
||||||
|
opt := huh.NewOption(ctx, ctx)
|
||||||
|
if ctx == current {
|
||||||
|
opt.Key += " (current)"
|
||||||
|
opt = opt.Selected(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
options[i] = opt
|
||||||
|
}
|
||||||
|
|
||||||
|
return options
|
||||||
|
}
|
||||||
+59
-37
@@ -4,30 +4,32 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/compose-spec/compose-go/v2/types"
|
"strings"
|
||||||
|
|
||||||
|
composecli "github.com/compose-spec/compose-go/v2/cli"
|
||||||
"github.com/docker/compose/v2/pkg/progress"
|
"github.com/docker/compose/v2/pkg/progress"
|
||||||
"github.com/psviderski/uncloud/internal/cli"
|
"github.com/psviderski/uncloud/internal/cli"
|
||||||
"github.com/psviderski/uncloud/pkg/api"
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
|
"github.com/psviderski/uncloud/pkg/client"
|
||||||
"github.com/psviderski/uncloud/pkg/client/compose"
|
"github.com/psviderski/uncloud/pkg/client/compose"
|
||||||
"github.com/psviderski/uncloud/pkg/client/deploy"
|
"github.com/psviderski/uncloud/pkg/client/deploy"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"strings"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type deployOptions struct {
|
type deployOptions struct {
|
||||||
files []string
|
files []string
|
||||||
|
profiles []string
|
||||||
services []string
|
services []string
|
||||||
|
|
||||||
cluster string
|
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.
|
||||||
func NewDeployCommand() *cobra.Command {
|
func NewDeployCommand() *cobra.Command {
|
||||||
opts := deployOptions{}
|
opts := deployOptions{}
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "deploy [FLAGS] [SERVICE...]",
|
Use: "deploy [FLAGS] [SERVICE...]",
|
||||||
// TODO: remove WIP when the command is fully implemented.
|
Short: "Deploy services from a Compose file.",
|
||||||
Short: "WIP: Deploy services from a Compose file.",
|
|
||||||
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)
|
||||||
|
|
||||||
@@ -40,29 +42,46 @@ func NewDeployCommand() *cobra.Command {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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-ports-long.yaml)")
|
"One or more Compose files to deploy services from. (default compose.yaml)")
|
||||||
cmd.Flags().StringVarP(&opts.cluster, "cluster", "c", "",
|
cmd.Flags().StringSliceVarP(&opts.profiles, "profile", "p", nil,
|
||||||
"Name of the cluster to deploy to (default is the current cluster)")
|
"One or more Compose profiles to enable.")
|
||||||
|
cmd.Flags().StringVarP(&opts.context, "context", "c", "",
|
||||||
|
"Name of the cluster context to deploy to (default is the current context)")
|
||||||
|
// TODO: Consider adding a filter flag to specify which machines to deploy to but keep the rest running.
|
||||||
|
// Could be useful to test a new version on a subset of machines before rolling out to all.
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// projectOpts returns the project options for the Compose file(s).
|
||||||
|
func projectOpts(opts deployOptions) []composecli.ProjectOptionsFn {
|
||||||
|
projectOpts := []composecli.ProjectOptionsFn{}
|
||||||
|
|
||||||
|
if len(opts.profiles) > 0 {
|
||||||
|
projectOpts = append(projectOpts, composecli.WithDefaultProfiles(opts.profiles...))
|
||||||
|
}
|
||||||
|
|
||||||
|
return projectOpts
|
||||||
|
}
|
||||||
|
|
||||||
// runDeploy parses the Compose file(s) and deploys the services.
|
// runDeploy parses the Compose file(s) and deploys the services.
|
||||||
func runDeploy(ctx context.Context, uncli *cli.CLI, opts deployOptions) error {
|
func runDeploy(ctx context.Context, uncli *cli.CLI, opts deployOptions) error {
|
||||||
project, err := compose.LoadProject(ctx, opts.files)
|
projectOpts := projectOpts(opts)
|
||||||
|
|
||||||
|
project, err := compose.LoadProject(ctx, opts.files, projectOpts...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("load compose file(s): %w", err)
|
return fmt.Errorf("load compose file(s): %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(opts.services) > 0 {
|
if len(opts.services) > 0 {
|
||||||
// TODO: handle dependencies properly.
|
// Includes service dependencies by default. This is the default docker compose behavior.
|
||||||
project, err = project.WithSelectedServices(opts.services, types.IgnoreDependencies)
|
project, err = project.WithSelectedServices(opts.services)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("select services: %w", err)
|
return fmt.Errorf("select services: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
clusterClient, err := uncli.ConnectCluster(ctx, opts.cluster)
|
clusterClient, err := uncli.ConnectCluster(ctx, opts.context)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("connect to cluster: %w", err)
|
return fmt.Errorf("connect to cluster: %w", err)
|
||||||
}
|
}
|
||||||
@@ -84,30 +103,8 @@ func runDeploy(ctx context.Context, uncli *cli.CLI, opts deployOptions) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println("Deployment plan:")
|
fmt.Println("Deployment plan:")
|
||||||
|
if err = printPlan(ctx, clusterClient, plan); err != nil {
|
||||||
for _, op := range plan.Operations {
|
return fmt.Errorf("print deployment plan: %w", err)
|
||||||
svcPlan, ok := op.(*deploy.Plan)
|
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf("expected service Plan, got: %T", op)
|
|
||||||
}
|
|
||||||
|
|
||||||
svc, err := clusterClient.InspectService(ctx, svcPlan.ServiceID)
|
|
||||||
if err != nil {
|
|
||||||
if !errors.Is(err, api.ErrNotFound) {
|
|
||||||
return fmt.Errorf("inspect service: %w", err)
|
|
||||||
}
|
|
||||||
fmt.Printf("- Run service [name=%s]\n", svcPlan.ServiceName)
|
|
||||||
} else {
|
|
||||||
fmt.Printf("- Update service [name=%s]\n", svc.Name)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialise a machine and container name resolver to properly format the service plan output.
|
|
||||||
resolver, err := clusterClient.ServiceOperationNameResolver(ctx, svc)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("create machine and container name resolver for service operations: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Println(indent(svcPlan.Format(resolver), " "))
|
|
||||||
}
|
}
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
|
|
||||||
@@ -128,6 +125,31 @@ func runDeploy(ctx context.Context, uncli *cli.CLI, opts deployOptions) error {
|
|||||||
}, uncli.ProgressOut(), "Deploying services")
|
}, uncli.ProgressOut(), "Deploying services")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func printPlan(ctx context.Context, cli *client.Client, plan deploy.SequenceOperation) error {
|
||||||
|
for _, op := range plan.Operations {
|
||||||
|
svcPlan, ok := op.(*deploy.Plan)
|
||||||
|
if !ok {
|
||||||
|
fmt.Println("- " + op.Format(nil))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
svc, err := cli.InspectService(ctx, svcPlan.ServiceID)
|
||||||
|
if err != nil && !errors.Is(err, api.ErrNotFound) {
|
||||||
|
return fmt.Errorf("inspect service: %w", err)
|
||||||
|
}
|
||||||
|
// Initialise a machine and container name resolver to properly format the service plan output.
|
||||||
|
resolver, err := cli.ServiceOperationNameResolver(ctx, svc)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("create machine and container name resolver for service operations: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("- Deploy service [name=%s]\n", svcPlan.ServiceName)
|
||||||
|
fmt.Println(indent(svcPlan.Format(resolver), " "))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func indent(text, prefix string) string {
|
func indent(text, prefix string) string {
|
||||||
lines := strings.Split(text, "\n")
|
lines := strings.Split(text, "\n")
|
||||||
for i, line := range lines {
|
for i, line := range lines {
|
||||||
|
|||||||
@@ -4,15 +4,16 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/psviderski/uncloud/internal/cli"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"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"
|
||||||
"github.com/psviderski/uncloud/internal/cli"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type releaseOptions struct {
|
type releaseOptions struct {
|
||||||
cluster string
|
context string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewReleaseCommand() *cobra.Command {
|
func NewReleaseCommand() *cobra.Command {
|
||||||
@@ -28,15 +29,15 @@ func NewReleaseCommand() *cobra.Command {
|
|||||||
}
|
}
|
||||||
|
|
||||||
cmd.Flags().StringVarP(
|
cmd.Flags().StringVarP(
|
||||||
&opts.cluster, "cluster", "c", "",
|
&opts.context, "context", "c", "",
|
||||||
"Name of the cluster. (default is the current cluster)",
|
"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, opts releaseOptions) error {
|
||||||
client, err := uncli.ConnectCluster(ctx, opts.cluster)
|
client, err := uncli.ConnectCluster(ctx, opts.context)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("connect to cluster: %w", err)
|
return fmt.Errorf("connect to cluster: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"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"
|
||||||
"github.com/psviderski/uncloud/internal/machine/api/pb"
|
"github.com/psviderski/uncloud/internal/machine/api/pb"
|
||||||
@@ -18,7 +19,7 @@ const DefaultUncloudDNSAPIEndpoint = "https://dns.uncloud.run/v1"
|
|||||||
|
|
||||||
type reserveOptions struct {
|
type reserveOptions struct {
|
||||||
endpoint string
|
endpoint string
|
||||||
cluster string
|
context string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewReserveCommand() *cobra.Command {
|
func NewReserveCommand() *cobra.Command {
|
||||||
@@ -36,15 +37,15 @@ 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(
|
cmd.Flags().StringVarP(
|
||||||
&opts.cluster, "cluster", "c", "",
|
&opts.context, "context", "c", "",
|
||||||
"Name of the cluster. (default is the current cluster)",
|
"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.cluster)
|
clusterClient, err := uncli.ConnectCluster(ctx, opts.context)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("connect to cluster: %w", err)
|
return fmt.Errorf("connect to cluster: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,13 +4,14 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/psviderski/uncloud/internal/cli"
|
"github.com/psviderski/uncloud/internal/cli"
|
||||||
"github.com/psviderski/uncloud/pkg/api"
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
type showOptions struct {
|
type showOptions struct {
|
||||||
cluster string
|
context string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewShowCommand() *cobra.Command {
|
func NewShowCommand() *cobra.Command {
|
||||||
@@ -26,15 +27,15 @@ func NewShowCommand() *cobra.Command {
|
|||||||
}
|
}
|
||||||
|
|
||||||
cmd.Flags().StringVarP(
|
cmd.Flags().StringVarP(
|
||||||
&opts.cluster, "cluster", "c", "",
|
&opts.context, "context", "c", "",
|
||||||
"Name of the cluster. (default is the current cluster)",
|
"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, opts showOptions) error {
|
||||||
clusterClient, err := uncli.ConnectCluster(ctx, opts.cluster)
|
clusterClient, err := uncli.ConnectCluster(ctx, opts.context)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("connect to cluster: %w", err)
|
return fmt.Errorf("connect to cluster: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
+24
-19
@@ -4,20 +4,19 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/netip"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/cenkalti/backoff/v4"
|
"github.com/cenkalti/backoff/v4"
|
||||||
"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"
|
||||||
"github.com/psviderski/uncloud/internal/cli/config"
|
"github.com/psviderski/uncloud/internal/cli/config"
|
||||||
"github.com/psviderski/uncloud/internal/machine/api/pb"
|
|
||||||
"github.com/psviderski/uncloud/pkg/api"
|
"github.com/psviderski/uncloud/pkg/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/codes"
|
||||||
"google.golang.org/grpc/status"
|
"google.golang.org/grpc/status"
|
||||||
"google.golang.org/protobuf/types/known/emptypb"
|
|
||||||
"net/netip"
|
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type addOptions struct {
|
type addOptions struct {
|
||||||
@@ -25,7 +24,8 @@ type addOptions struct {
|
|||||||
noCaddy bool
|
noCaddy bool
|
||||||
publicIP string
|
publicIP string
|
||||||
sshKey string
|
sshKey string
|
||||||
cluster string
|
context string
|
||||||
|
version string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAddCommand() *cobra.Command {
|
func NewAddCommand() *cobra.Command {
|
||||||
@@ -65,10 +65,15 @@ func NewAddCommand() *cobra.Command {
|
|||||||
&opts.sshKey, "ssh-key", "i", "",
|
&opts.sshKey, "ssh-key", "i", "",
|
||||||
"path to SSH private key for SSH remote login. (default ~/.ssh/id_*)",
|
"path to SSH private key for SSH remote login. (default ~/.ssh/id_*)",
|
||||||
)
|
)
|
||||||
cmd.Flags().StringVarP(
|
cmd.Flags().StringVar(
|
||||||
&opts.cluster, "cluster", "c", "",
|
&opts.version, "version", "latest",
|
||||||
"Name of the cluster to add the machine to. (default is the current cluster)",
|
"Version of the Uncloud daemon to install on the machine.",
|
||||||
)
|
)
|
||||||
|
cmd.Flags().StringVarP(
|
||||||
|
&opts.context, "context", "c", "",
|
||||||
|
"Name of the cluster context to add the machine to. (default is the current context)",
|
||||||
|
)
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,7 +92,13 @@ func add(ctx context.Context, uncli *cli.CLI, remoteMachine cli.RemoteMachine, o
|
|||||||
publicIP = &ip
|
publicIP = &ip
|
||||||
}
|
}
|
||||||
|
|
||||||
machineClient, err := uncli.AddMachine(ctx, remoteMachine, opts.cluster, opts.name, publicIP)
|
machineClient, err := uncli.AddMachine(ctx, cli.AddMachineOptions{
|
||||||
|
Context: opts.context,
|
||||||
|
MachineName: opts.name,
|
||||||
|
PublicIP: publicIP,
|
||||||
|
RemoteMachine: remoteMachine,
|
||||||
|
Version: opts.version,
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -104,14 +115,6 @@ func add(ctx context.Context, uncli *cli.CLI, remoteMachine cli.RemoteMachine, o
|
|||||||
return fmt.Errorf("wait for cluster to be initialised on machine: %w", err)
|
return fmt.Errorf("wait for cluster to be initialised on machine: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Inspect the added machine to get its ID to create a filter for the Caddy deployment.
|
|
||||||
minfo, err := machineClient.Inspect(ctx, &emptypb.Empty{})
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("inspect machine: %w", err)
|
|
||||||
}
|
|
||||||
filter := func(m *pb.MachineInfo) bool {
|
|
||||||
return m.Id == minfo.Id
|
|
||||||
}
|
|
||||||
// Deploy a Caddy service container to the added machine. If caddy service is already deployed on other machines,
|
// 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. Otherwise, use the latest version.
|
||||||
caddyImage := ""
|
caddyImage := ""
|
||||||
@@ -136,7 +139,9 @@ func add(ctx context.Context, uncli *cli.CLI, remoteMachine cli.RemoteMachine, o
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
d, err := machineClient.NewCaddyDeployment(caddyImage, filter)
|
// TODO: scale the existing Caddy service to the new machine instead of running a new deployment
|
||||||
|
// that may cause a small downtime.
|
||||||
|
d, err := machineClient.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)
|
||||||
}
|
}
|
||||||
@@ -162,7 +167,7 @@ func waitClusterInitialised(ctx context.Context, client *client.Client) error {
|
|||||||
), ctx)
|
), ctx)
|
||||||
|
|
||||||
check := func() error {
|
check := func() error {
|
||||||
_, err := client.ListMachines(ctx)
|
_, err := client.ListMachines(ctx, nil)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ package machine
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/netip"
|
||||||
|
|
||||||
"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"
|
||||||
@@ -10,8 +12,8 @@ import (
|
|||||||
"github.com/psviderski/uncloud/internal/cli/config"
|
"github.com/psviderski/uncloud/internal/cli/config"
|
||||||
"github.com/psviderski/uncloud/internal/machine/api/pb"
|
"github.com/psviderski/uncloud/internal/machine/api/pb"
|
||||||
"github.com/psviderski/uncloud/internal/machine/cluster"
|
"github.com/psviderski/uncloud/internal/machine/cluster"
|
||||||
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"net/netip"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type initOptions struct {
|
type initOptions struct {
|
||||||
@@ -22,15 +24,17 @@ type initOptions struct {
|
|||||||
noDNS bool
|
noDNS bool
|
||||||
publicIP string
|
publicIP string
|
||||||
sshKey string
|
sshKey string
|
||||||
cluster string
|
version string
|
||||||
|
context string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewInitCommand() *cobra.Command {
|
func NewInitCommand() *cobra.Command {
|
||||||
opts := initOptions{}
|
opts := initOptions{}
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "init [USER@HOST:PORT]",
|
Use: "init [USER@HOST:PORT]",
|
||||||
Short: "Initialise a new cluster that consists of the local or remote machine.",
|
Short: "Initialise a new cluster with a remote machine as the first member.",
|
||||||
// TODO: include usage examples of initialising a local and remote machine.
|
// TODO: include usage examples of initialising a remote 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 {
|
||||||
uncli := cmd.Context().Value("cli").(*cli.CLI)
|
uncli := cmd.Context().Value("cli").(*cli.CLI)
|
||||||
@@ -79,9 +83,13 @@ func NewInitCommand() *cobra.Command {
|
|||||||
&opts.sshKey, "ssh-key", "i", "",
|
&opts.sshKey, "ssh-key", "i", "",
|
||||||
"Path to SSH private key for SSH remote login. (default ~/.ssh/id_*)",
|
"Path to SSH private key for SSH remote login. (default ~/.ssh/id_*)",
|
||||||
)
|
)
|
||||||
|
cmd.Flags().StringVar(
|
||||||
|
&opts.version, "version", "latest",
|
||||||
|
"Version of the Uncloud daemon to install on the machine.",
|
||||||
|
)
|
||||||
cmd.Flags().StringVarP(
|
cmd.Flags().StringVarP(
|
||||||
&opts.cluster, "cluster", "c", "",
|
&opts.context, "context", "c", "default",
|
||||||
"Name of the cluster in the local config if initialising a remote machine.",
|
"Name of the created context for the initialised cluster in the Uncloud config.",
|
||||||
)
|
)
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
@@ -106,8 +114,15 @@ func initCluster(ctx context.Context, uncli *cli.CLI, remoteMachine *cli.RemoteM
|
|||||||
}
|
}
|
||||||
publicIP = &ip
|
publicIP = &ip
|
||||||
}
|
}
|
||||||
|
|
||||||
client, err := uncli.InitCluster(ctx, remoteMachine, opts.cluster, opts.name, netPrefix, publicIP)
|
client, err := uncli.InitCluster(ctx, cli.InitClusterOptions{
|
||||||
|
Context: opts.context,
|
||||||
|
MachineName: opts.name,
|
||||||
|
Network: netPrefix,
|
||||||
|
PublicIP: publicIP,
|
||||||
|
RemoteMachine: remoteMachine,
|
||||||
|
Version: opts.version,
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -132,7 +147,7 @@ func initCluster(ctx context.Context, uncli *cli.CLI, remoteMachine *cli.RemoteM
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !opts.noCaddy {
|
if !opts.noCaddy {
|
||||||
d, err := client.NewCaddyDeployment("", nil)
|
d, err := client.NewCaddyDeployment("", api.Placement{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("create caddy deployment: %w", err)
|
return fmt.Errorf("create caddy deployment: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,29 +3,30 @@ package machine
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"text/tabwriter"
|
"text/tabwriter"
|
||||||
|
|
||||||
"github.com/psviderski/uncloud/internal/cli"
|
"github.com/psviderski/uncloud/internal/cli"
|
||||||
"github.com/psviderski/uncloud/internal/machine/network"
|
"github.com/psviderski/uncloud/internal/machine/network"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewListCommand() *cobra.Command {
|
func NewListCommand() *cobra.Command {
|
||||||
var cluster string
|
var clusterContext 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, cluster)
|
return list(cmd.Context(), uncli, clusterContext)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
cmd.Flags().StringVarP(
|
cmd.Flags().StringVarP(
|
||||||
&cluster, "cluster", "c", "",
|
&clusterContext, "context", "c", "",
|
||||||
"Name of the cluster. (default is the current cluster)",
|
"Name of the cluster context. (default is the current context)",
|
||||||
)
|
)
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
@@ -37,7 +38,7 @@ func list(ctx context.Context, uncli *cli.CLI, clusterName string) error {
|
|||||||
}
|
}
|
||||||
defer client.Close()
|
defer client.Close()
|
||||||
|
|
||||||
machines, err := client.ListMachines(ctx)
|
machines, err := client.ListMachines(ctx, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("list machines: %w", err)
|
return fmt.Errorf("list machines: %w", err)
|
||||||
}
|
}
|
||||||
+11
-4
@@ -7,12 +7,15 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/psviderski/uncloud/cmd/uncloud/caddy"
|
"github.com/psviderski/uncloud/cmd/uncloud/caddy"
|
||||||
|
cmdcontext "github.com/psviderski/uncloud/cmd/uncloud/context"
|
||||||
"github.com/psviderski/uncloud/cmd/uncloud/dns"
|
"github.com/psviderski/uncloud/cmd/uncloud/dns"
|
||||||
"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/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"
|
||||||
|
"github.com/psviderski/uncloud/internal/version"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -26,6 +29,7 @@ func main() {
|
|||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "uncloud",
|
Use: "uncloud",
|
||||||
Short: "A CLI tool for managing Uncloud resources such as clusters, machines, and services.",
|
Short: "A CLI tool for managing Uncloud resources such as clusters, machines, and services.",
|
||||||
|
Version: version.String(),
|
||||||
SilenceUsage: true,
|
SilenceUsage: true,
|
||||||
SilenceErrors: true,
|
SilenceErrors: true,
|
||||||
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
|
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
|
||||||
@@ -37,7 +41,7 @@ func main() {
|
|||||||
return fmt.Errorf("parse TCP address: %w", err)
|
return fmt.Errorf("parse TCP address: %w", err)
|
||||||
}
|
}
|
||||||
conn = &config.MachineConnection{
|
conn = &config.MachineConnection{
|
||||||
TCP: addrPort,
|
TCP: &addrPort,
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
dest := opts.connect
|
dest := opts.connect
|
||||||
@@ -53,7 +57,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)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("initialize CLI: %w", err)
|
return fmt.Errorf("initialise CLI: %w", err)
|
||||||
}
|
}
|
||||||
cmd.SetContext(context.WithValue(cmd.Context(), "cli", uncli))
|
cmd.SetContext(context.WithValue(cmd.Context(), "cli", uncli))
|
||||||
return nil
|
return nil
|
||||||
@@ -64,13 +68,15 @@ func main() {
|
|||||||
"Connect to a remote cluster machine without using the Uncloud configuration file.\n"+
|
"Connect to a remote cluster machine without using the Uncloud configuration file.\n"+
|
||||||
"Format: [ssh://]user@host[:port] or tcp://host:port")
|
"Format: [ssh://]user@host[:port] or tcp://host:port")
|
||||||
// TODO: allow to override using UNCLOUD_CONFIG env var.
|
// TODO: allow to override using UNCLOUD_CONFIG env var.
|
||||||
cmd.PersistentFlags().StringVar(&opts.configPath, "uncloud-config", "~/.config/uncloud/config.toml",
|
cmd.PersistentFlags().StringVar(&opts.configPath, "uncloud-config", "~/.config/uncloud/config.yaml",
|
||||||
"Path to the Uncloud configuration file.")
|
"Path to the Uncloud configuration file.")
|
||||||
_ = cmd.MarkPersistentFlagFilename("uncloud-config", "toml")
|
_ = cmd.MarkPersistentFlagFilename("uncloud-config", "yaml", "yml")
|
||||||
|
// TODO: make --context a global flag and pass it as a value of the command context.
|
||||||
|
|
||||||
cmd.AddCommand(
|
cmd.AddCommand(
|
||||||
NewDeployCommand(),
|
NewDeployCommand(),
|
||||||
caddy.NewRootCommand(),
|
caddy.NewRootCommand(),
|
||||||
|
cmdcontext.NewRootCommand(),
|
||||||
dns.NewRootCommand(),
|
dns.NewRootCommand(),
|
||||||
machine.NewRootCommand(),
|
machine.NewRootCommand(),
|
||||||
service.NewRootCommand(),
|
service.NewRootCommand(),
|
||||||
@@ -79,6 +85,7 @@ func main() {
|
|||||||
service.NewRmCommand(),
|
service.NewRmCommand(),
|
||||||
service.NewRunCommand(),
|
service.NewRunCommand(),
|
||||||
service.NewScaleCommand(),
|
service.NewScaleCommand(),
|
||||||
|
volume.NewRootCommand(),
|
||||||
)
|
)
|
||||||
cobra.CheckErr(cmd.Execute())
|
cobra.CheckErr(cmd.Execute())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,14 +3,15 @@ package service
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/docker/docker/pkg/stringid"
|
|
||||||
"github.com/docker/go-units"
|
|
||||||
"os"
|
"os"
|
||||||
"text/tabwriter"
|
"text/tabwriter"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/docker/docker/pkg/stringid"
|
||||||
|
"github.com/docker/go-units"
|
||||||
|
|
||||||
"github.com/psviderski/uncloud/internal/cli"
|
"github.com/psviderski/uncloud/internal/cli"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
type inspectOptions struct {
|
type inspectOptions struct {
|
||||||
@@ -31,8 +32,8 @@ func NewInspectCommand() *cobra.Command {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
cmd.Flags().StringVarP(
|
cmd.Flags().StringVarP(
|
||||||
&opts.cluster, "cluster", "c", "",
|
&opts.cluster, "context", "c", "",
|
||||||
"Name of the cluster. (default is the current cluster)",
|
"Name of the cluster context. (default is the current context)",
|
||||||
)
|
)
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
@@ -49,7 +50,7 @@ func inspect(ctx context.Context, uncli *cli.CLI, opts inspectOptions) error {
|
|||||||
return fmt.Errorf("inspect service: %w", err)
|
return fmt.Errorf("inspect service: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
machines, err := client.ListMachines(ctx)
|
machines, err := client.ListMachines(ctx, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("list machines: %w", err)
|
return fmt.Errorf("list machines: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,14 +3,16 @@ package service
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/psviderski/uncloud/internal/cli"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"text/tabwriter"
|
"text/tabwriter"
|
||||||
|
|
||||||
|
"github.com/psviderski/uncloud/internal/cli"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewListCommand() *cobra.Command {
|
func NewListCommand() *cobra.Command {
|
||||||
|
// TODO(lhf): rename to context
|
||||||
var cluster string
|
var cluster string
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "ls",
|
Use: "ls",
|
||||||
@@ -22,8 +24,8 @@ func NewListCommand() *cobra.Command {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
cmd.Flags().StringVarP(
|
cmd.Flags().StringVarP(
|
||||||
&cluster, "cluster", "c", "",
|
&cluster, "context", "c", "",
|
||||||
"Name of the cluster. (default is the current cluster)",
|
"Name of the cluster context. (default is the current context)",
|
||||||
)
|
)
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
@@ -3,11 +3,10 @@ package service
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/docker/cli/cli/streams"
|
|
||||||
"github.com/docker/compose/v2/pkg/progress"
|
"github.com/docker/compose/v2/pkg/progress"
|
||||||
"github.com/psviderski/uncloud/internal/cli"
|
"github.com/psviderski/uncloud/internal/cli"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"os"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type rmOptions struct {
|
type rmOptions struct {
|
||||||
@@ -29,8 +28,8 @@ func NewRmCommand() *cobra.Command {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
cmd.Flags().StringVarP(
|
cmd.Flags().StringVarP(
|
||||||
&opts.cluster, "cluster", "c", "",
|
&opts.cluster, "context", "c", "",
|
||||||
"Name of the cluster. (default is the current cluster)",
|
"Name of the cluster context. (default is the current context)",
|
||||||
)
|
)
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
@@ -48,7 +47,7 @@ func rm(ctx context.Context, uncli *cli.CLI, opts rmOptions) error {
|
|||||||
return fmt.Errorf("remove service '%s': %w", s, err)
|
return fmt.Errorf("remove service '%s': %w", s, err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}, streams.NewOut(os.Stdout), "Removing service "+s)
|
}, uncli.ProgressOut(), "Removing service "+s)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -6,8 +6,9 @@ import (
|
|||||||
|
|
||||||
func NewRootCommand() *cobra.Command {
|
func NewRootCommand() *cobra.Command {
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "service",
|
Use: "service",
|
||||||
Short: "Manage services in an Uncloud cluster.",
|
Aliases: []string{"svc"},
|
||||||
|
Short: "Manage services in an Uncloud cluster.",
|
||||||
}
|
}
|
||||||
cmd.AddCommand(
|
cmd.AddCommand(
|
||||||
NewInspectCommand(),
|
NewInspectCommand(),
|
||||||
|
|||||||
+256
-79
@@ -3,26 +3,35 @@ package service
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
dockeropts "github.com/docker/cli/opts"
|
||||||
|
"github.com/docker/compose/v2/pkg/progress"
|
||||||
|
"github.com/docker/docker/daemon/names"
|
||||||
"github.com/psviderski/uncloud/internal/cli"
|
"github.com/psviderski/uncloud/internal/cli"
|
||||||
"github.com/psviderski/uncloud/internal/machine/api/pb"
|
"github.com/psviderski/uncloud/internal/secret"
|
||||||
"github.com/psviderski/uncloud/pkg/api"
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
"github.com/psviderski/uncloud/pkg/client/deploy"
|
"github.com/psviderski/uncloud/pkg/client/deploy"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"slices"
|
|
||||||
"strings"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type runOptions struct {
|
type runOptions struct {
|
||||||
command []string
|
command []string
|
||||||
|
cpu dockeropts.NanoCPUs
|
||||||
entrypoint string
|
entrypoint string
|
||||||
entrypointChanged bool
|
entrypointChanged bool
|
||||||
|
env []string
|
||||||
image string
|
image string
|
||||||
machines []string
|
machines []string
|
||||||
|
memory dockeropts.MemBytes
|
||||||
mode string
|
mode string
|
||||||
name string
|
name string
|
||||||
|
privileged bool
|
||||||
publish []string
|
publish []string
|
||||||
pull string
|
pull string
|
||||||
replicas uint
|
replicas uint
|
||||||
|
user string
|
||||||
volumes []string
|
volumes []string
|
||||||
|
|
||||||
cluster string
|
cluster string
|
||||||
@@ -48,22 +57,34 @@ func NewRunCommand() *cobra.Command {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cmd.Flags().VarP(&opts.cpu, "cpu", "",
|
||||||
|
"Maximum number of CPU cores a service container can use. Fractional values are allowed: "+
|
||||||
|
"0.5 for half a core or 2.25 for two and a quarter cores.")
|
||||||
cmd.Flags().StringVar(&opts.entrypoint, "entrypoint", "",
|
cmd.Flags().StringVar(&opts.entrypoint, "entrypoint", "",
|
||||||
"Overwrite the default ENTRYPOINT of the image. Pass an empty string \"\" to reset it.")
|
"Overwrite the default ENTRYPOINT of the image. Pass an empty string \"\" to reset it.")
|
||||||
|
cmd.Flags().StringSliceVarP(&opts.env, "env", "e", nil,
|
||||||
|
"Set an environment variable for service containers. Can be specified multiple times.\n"+
|
||||||
|
"Format: VAR=value or just VAR to use the value from the local environment.")
|
||||||
cmd.Flags().StringVar(&opts.mode, "mode", api.ServiceModeReplicated,
|
cmd.Flags().StringVar(&opts.mode, "mode", api.ServiceModeReplicated,
|
||||||
fmt.Sprintf("Replication mode of the service: either '%s' (a specified number of containers across "+
|
fmt.Sprintf("Replication mode of the service: either '%s' (a specified number of containers across "+
|
||||||
"the machines) or '%s' (one container on every machine).",
|
"the machines) or '%s' (one container on every machine).",
|
||||||
api.ServiceModeReplicated, api.ServiceModeGlobal))
|
api.ServiceModeReplicated, api.ServiceModeGlobal))
|
||||||
cmd.Flags().StringSliceVarP(&opts.machines, "machine", "m", nil,
|
cmd.Flags().StringSliceVarP(&opts.machines, "machine", "m", nil,
|
||||||
"Placement constraint by machine name, limiting which machines the service can run on. Can be specified "+
|
"Placement constraint by machine names, limiting which machines the service can run on. Can be specified "+
|
||||||
"multiple times or as a comma-separated list of machine names. (default is any suitable machine)")
|
"multiple times or as a comma-separated list of machine names. (default is any suitable machine)")
|
||||||
|
cmd.Flags().VarP(&opts.memory, "memory", "",
|
||||||
|
"Maximum amount of memory a service container can use. Value is a positive integer with optional unit suffix "+
|
||||||
|
"(b, k, m, g). Default unit is bytes if no suffix specified.\n"+
|
||||||
|
"Examples: 1073741824, 1024m, 1g (all equal 1 gibibyte)")
|
||||||
cmd.Flags().StringVarP(&opts.name, "name", "n", "",
|
cmd.Flags().StringVarP(&opts.name, "name", "n", "",
|
||||||
"Assign a name to the service. A random name is generated if not specified.")
|
"Assign a name to the service. A random name is generated if not specified.")
|
||||||
|
cmd.Flags().BoolVar(&opts.privileged, "privileged", false,
|
||||||
|
"Give extended privileges to service containers. This is a security risk and should be used with caution.")
|
||||||
cmd.Flags().StringSliceVarP(&opts.publish, "publish", "p", nil,
|
cmd.Flags().StringSliceVarP(&opts.publish, "publish", "p", nil,
|
||||||
"Publish a service port to make it accessible outside the cluster. Can be specified multiple times.\n"+
|
"Publish a service port to make it accessible outside the cluster. Can be specified multiple times.\n"+
|
||||||
"Format: [hostname:][load_balancer_port:]container_port[/protocol] or [host_ip:]:host_port:container_port[/protocol]@host\n"+
|
"Format: [hostname:][load_balancer_port:]container_port[/protocol] or [host_ip:]:host_port:container_port[/protocol]@host\n"+
|
||||||
"Supported protocols: tcp, udp, http, https (default is tcp). If a hostname for http(s) port is not specified,\n"+
|
"Supported protocols: tcp, udp, http, https (default is tcp). If a hostname for http(s) port is not specified\n"+
|
||||||
"service-name.cluster-domain will be used as the hostname.\n"+
|
"and a cluster domain is reserved, service-name.cluster-domain will be used as the hostname.\n"+
|
||||||
"Examples:\n"+
|
"Examples:\n"+
|
||||||
" -p 8080/https Publish port 8080 as HTTPS via load balancer with default service-name.cluster-domain hostname\n"+
|
" -p 8080/https Publish port 8080 as HTTPS via load balancer with default service-name.cluster-domain hostname\n"+
|
||||||
" -p app.example.com:8080/https Publish port 8080 as HTTPS via load balancer with custom hostname\n"+
|
" -p app.example.com:8080/https Publish port 8080 as HTTPS via load balancer with custom hostname\n"+
|
||||||
@@ -74,86 +95,30 @@ func NewRunCommand() *cobra.Command {
|
|||||||
api.PullPolicyAlways, api.PullPolicyMissing, api.PullPolicyNever))
|
api.PullPolicyAlways, api.PullPolicyMissing, api.PullPolicyNever))
|
||||||
cmd.Flags().UintVar(&opts.replicas, "replicas", 1,
|
cmd.Flags().UintVar(&opts.replicas, "replicas", 1,
|
||||||
"Number of containers to run for the service. Only valid for a replicated service.")
|
"Number of containers to run for the service. Only valid for a replicated service.")
|
||||||
|
cmd.Flags().StringVarP(&opts.user, "user", "u", "",
|
||||||
|
"User name or UID and optionally group name or GID used for running the command inside service containers.\n"+
|
||||||
|
"Format: USER[:GROUP] or UID[:GID]. If not specified, the user is set to the default user of the image.")
|
||||||
cmd.Flags().StringSliceVarP(&opts.volumes, "volume", "v", nil,
|
cmd.Flags().StringSliceVarP(&opts.volumes, "volume", "v", nil,
|
||||||
"Bind mount a host file or directory into a service container using the format "+
|
"Mount a data volume or host path into service containers. Service containers will be scheduled on the machine(s) where\n"+
|
||||||
"/host/path:/container/path[:ro]. Can be specified multiple times.")
|
"the volume is located. Can be specified multiple times.\n"+
|
||||||
|
"Format: volume_name:/container/path[:ro|volume-nocopy] or /host/path:/container/path[:ro]\n"+
|
||||||
|
"Examples:\n"+
|
||||||
|
" -v postgres-data:/var/lib/postgresql/data Mount volume 'postgres-data' to /var/lib/postgresql/data 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")
|
||||||
|
|
||||||
cmd.Flags().StringVarP(
|
cmd.Flags().StringVarP(
|
||||||
&opts.cluster, "cluster", "c", "",
|
&opts.cluster, "context", "c", "",
|
||||||
"Name of the cluster to run the service in. (default is the current cluster)",
|
"Name of the cluster context to run the service in. (default is the current context)",
|
||||||
)
|
)
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
func run(ctx context.Context, uncli *cli.CLI, opts runOptions) error {
|
func run(ctx context.Context, uncli *cli.CLI, opts runOptions) error {
|
||||||
switch opts.mode {
|
spec, err := prepareServiceSpec(opts)
|
||||||
case api.ServiceModeReplicated, api.ServiceModeGlobal:
|
if err != nil {
|
||||||
default:
|
return err
|
||||||
return fmt.Errorf("invalid replication mode: '%s'", opts.mode)
|
|
||||||
}
|
|
||||||
|
|
||||||
switch opts.pull {
|
|
||||||
case api.PullPolicyAlways, api.PullPolicyMissing, api.PullPolicyNever:
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("invalid pull policy: '%s'", opts.pull)
|
|
||||||
}
|
|
||||||
|
|
||||||
var machineFilter deploy.MachineFilter
|
|
||||||
if len(opts.machines) > 0 {
|
|
||||||
var machines []string
|
|
||||||
for _, value := range opts.machines {
|
|
||||||
if value == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
mlist := strings.Split(value, ",")
|
|
||||||
for _, m := range mlist {
|
|
||||||
if m = strings.TrimSpace(m); m != "" {
|
|
||||||
machines = append(machines, m)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(machines) > 0 {
|
|
||||||
machineFilter = func(m *pb.MachineInfo) bool {
|
|
||||||
return slices.Contains(machines, m.Name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ports := make([]api.PortSpec, len(opts.publish))
|
|
||||||
for i, publishPort := range opts.publish {
|
|
||||||
port, err := api.ParsePortSpec(publishPort)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("invalid service port '%s': %w", publishPort, err)
|
|
||||||
}
|
|
||||||
ports[i] = port
|
|
||||||
}
|
|
||||||
// TODO: parse and validate opts.volumes to fail fast if invalid.
|
|
||||||
|
|
||||||
spec := api.ServiceSpec{
|
|
||||||
Container: api.ContainerSpec{
|
|
||||||
Command: opts.command,
|
|
||||||
Image: opts.image,
|
|
||||||
PullPolicy: opts.pull,
|
|
||||||
Volumes: opts.volumes,
|
|
||||||
},
|
|
||||||
Mode: opts.mode,
|
|
||||||
Name: opts.name,
|
|
||||||
Ports: ports,
|
|
||||||
Replicas: opts.replicas,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Overwrite the default ENTRYPOINT of the image or reset it if an empty string is passed.
|
|
||||||
if opts.entrypoint != "" {
|
|
||||||
spec.Container.Entrypoint = []string{opts.entrypoint}
|
|
||||||
} else if opts.entrypointChanged {
|
|
||||||
spec.Container.Entrypoint = []string{""}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := spec.Validate(); err != nil {
|
|
||||||
return fmt.Errorf("invalid service configuration: %w", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
clusterClient, err := uncli.ConnectCluster(ctx, opts.cluster)
|
clusterClient, err := uncli.ConnectCluster(ctx, opts.cluster)
|
||||||
@@ -162,9 +127,17 @@ func run(ctx context.Context, uncli *cli.CLI, opts runOptions) error {
|
|||||||
}
|
}
|
||||||
defer clusterClient.Close()
|
defer clusterClient.Close()
|
||||||
|
|
||||||
resp, err := clusterClient.RunService(ctx, spec, machineFilter)
|
var resp api.RunServiceResponse
|
||||||
|
err = progress.RunWithTitle(ctx, func(ctx context.Context) error {
|
||||||
|
resp, err = clusterClient.RunService(ctx, spec)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("run service: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}, uncli.ProgressOut(), fmt.Sprintf("Running service %s (%s mode)", spec.Name, spec.Mode))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("run service: %w", err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
svc, err := clusterClient.InspectService(ctx, resp.ID)
|
svc, err := clusterClient.InspectService(ctx, resp.ID)
|
||||||
@@ -183,3 +156,207 @@ func run(ctx context.Context, uncli *cli.CLI, opts runOptions) error {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func prepareServiceSpec(opts runOptions) (api.ServiceSpec, error) {
|
||||||
|
var spec api.ServiceSpec
|
||||||
|
|
||||||
|
env, err := parseEnv(opts.env)
|
||||||
|
if err != nil {
|
||||||
|
return spec, err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch opts.mode {
|
||||||
|
case api.ServiceModeReplicated, api.ServiceModeGlobal:
|
||||||
|
default:
|
||||||
|
return spec, fmt.Errorf("invalid replication mode: '%s'", opts.mode)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch opts.pull {
|
||||||
|
case api.PullPolicyAlways, api.PullPolicyMissing, api.PullPolicyNever:
|
||||||
|
default:
|
||||||
|
return spec, fmt.Errorf("invalid pull policy: '%s'", opts.pull)
|
||||||
|
}
|
||||||
|
|
||||||
|
ports := make([]api.PortSpec, len(opts.publish))
|
||||||
|
for i, publishPort := range opts.publish {
|
||||||
|
port, err := api.ParsePortSpec(publishPort)
|
||||||
|
if err != nil {
|
||||||
|
return spec, fmt.Errorf("invalid service port '%s': %w", publishPort, err)
|
||||||
|
}
|
||||||
|
ports[i] = port
|
||||||
|
}
|
||||||
|
|
||||||
|
volumes, mounts, err := parseVolumeFlags(opts.volumes)
|
||||||
|
if err != nil {
|
||||||
|
return spec, err
|
||||||
|
}
|
||||||
|
|
||||||
|
placement := api.Placement{
|
||||||
|
Machines: cli.ExpandCommaSeparatedValues(opts.machines),
|
||||||
|
}
|
||||||
|
|
||||||
|
spec = api.ServiceSpec{
|
||||||
|
Container: api.ContainerSpec{
|
||||||
|
Command: opts.command,
|
||||||
|
Env: env,
|
||||||
|
Image: opts.image,
|
||||||
|
Privileged: opts.privileged,
|
||||||
|
PullPolicy: opts.pull,
|
||||||
|
Resources: api.ContainerResources{
|
||||||
|
CPU: opts.cpu.Value(),
|
||||||
|
Memory: opts.memory.Value(),
|
||||||
|
},
|
||||||
|
User: opts.user,
|
||||||
|
VolumeMounts: mounts,
|
||||||
|
},
|
||||||
|
Mode: opts.mode,
|
||||||
|
Name: opts.name,
|
||||||
|
Placement: placement,
|
||||||
|
Ports: ports,
|
||||||
|
Replicas: opts.replicas,
|
||||||
|
Volumes: volumes,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Overwrite the default ENTRYPOINT of the image or reset it if an empty string is passed.
|
||||||
|
if opts.entrypoint != "" {
|
||||||
|
spec.Container.Entrypoint = []string{opts.entrypoint}
|
||||||
|
} else if opts.entrypointChanged {
|
||||||
|
spec.Container.Entrypoint = []string{""}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = spec.Validate(); err != nil {
|
||||||
|
return spec, fmt.Errorf("invalid service configuration: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate a service name if not specified to be able to include it in the progress title.
|
||||||
|
if spec.Name == "" {
|
||||||
|
spec.Name, err = deploy.GenerateServiceName(spec.Container.Image)
|
||||||
|
if err != nil {
|
||||||
|
return spec, fmt.Errorf("generate service name: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return spec, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseEnv parses the environment variables from the command line arguments.
|
||||||
|
// It supports two formats: "VAR=value" or just "VAR" to use the value from the local environment.
|
||||||
|
func parseEnv(env []string) (api.EnvVars, error) {
|
||||||
|
envVars := make(api.EnvVars)
|
||||||
|
for _, e := range env {
|
||||||
|
key, value, hasValue := strings.Cut(e, "=")
|
||||||
|
if key == "" {
|
||||||
|
return nil, fmt.Errorf("invalid environment variable: '%s'", e)
|
||||||
|
}
|
||||||
|
|
||||||
|
if hasValue {
|
||||||
|
envVars[key] = value
|
||||||
|
} else {
|
||||||
|
if localEnvValue, ok := os.LookupEnv(key); ok {
|
||||||
|
envVars[key] = localEnvValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return envVars, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseVolumeFlags parses volume flag values in Docker CLI format and returns VolumeSpecs and VolumeMounts.
|
||||||
|
// It handles both named volumes (volume_name:/container/path[:ro|volume-nocopy])
|
||||||
|
// and bind mounts (/host/path:/container/path[:ro]).
|
||||||
|
func parseVolumeFlags(volumes []string) ([]api.VolumeSpec, []api.VolumeMount, error) {
|
||||||
|
specs := make([]api.VolumeSpec, 0, len(volumes))
|
||||||
|
mounts := make([]api.VolumeMount, 0, len(volumes))
|
||||||
|
|
||||||
|
// Track volume names to avoid duplicate specs.
|
||||||
|
seenVolumes := make(map[string]struct{})
|
||||||
|
|
||||||
|
for _, vol := range volumes {
|
||||||
|
spec, mount, err := parseVolumeFlagValue(vol)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("invalid volume mount '%s': %w", vol, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := seenVolumes[spec.Name]; !ok {
|
||||||
|
specs = append(specs, spec)
|
||||||
|
seenVolumes[spec.Name] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
mounts = append(mounts, mount)
|
||||||
|
}
|
||||||
|
|
||||||
|
return specs, mounts, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseVolumeFlagValue(volume string) (api.VolumeSpec, api.VolumeMount, error) {
|
||||||
|
var spec api.VolumeSpec
|
||||||
|
var mount api.VolumeMount
|
||||||
|
|
||||||
|
parts := strings.Split(volume, ":")
|
||||||
|
switch len(parts) {
|
||||||
|
case 1:
|
||||||
|
return spec, mount, fmt.Errorf("invalid format, must contain at least one separator ':'")
|
||||||
|
case 2, 3:
|
||||||
|
// Format: (volume_name|/host/path):/container/path[:opts]
|
||||||
|
if !strings.HasPrefix(parts[1], "/") {
|
||||||
|
return spec, mount, fmt.Errorf("invalid container mount path: '%s', must be absolute path", parts[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
mount.ContainerPath = parts[1]
|
||||||
|
volumeNoCopy := false
|
||||||
|
|
||||||
|
if len(parts) == 3 {
|
||||||
|
opts := strings.Split(parts[2], ",")
|
||||||
|
for _, opt := range opts {
|
||||||
|
switch opt {
|
||||||
|
case "ro", "readonly":
|
||||||
|
mount.ReadOnly = true
|
||||||
|
case "volume-nocopy":
|
||||||
|
volumeNoCopy = true
|
||||||
|
default:
|
||||||
|
return spec, mount, fmt.Errorf("invalid option: '%s'", opt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasPrefix(parts[0], "/") {
|
||||||
|
// Host path bind mount: /host/path:/container/path
|
||||||
|
suffix, err := secret.RandomAlphaNumeric(4)
|
||||||
|
if err != nil {
|
||||||
|
return spec, mount, fmt.Errorf("generate random suffix: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
spec = api.VolumeSpec{
|
||||||
|
Name: "bind-" + suffix,
|
||||||
|
Type: api.VolumeTypeBind,
|
||||||
|
BindOptions: &api.BindOptions{
|
||||||
|
HostPath: parts[0],
|
||||||
|
CreateHostPath: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Named volume mount: volume_name:/container/path
|
||||||
|
volumeName := parts[0]
|
||||||
|
if !names.RestrictedNamePattern.MatchString(volumeName) {
|
||||||
|
return spec, mount, fmt.Errorf("volume name '%s' includes invalid characters, only '%s' are allowed. "+
|
||||||
|
"If you intended to pass a host directory or file, use absolute path",
|
||||||
|
volumeName, names.RestrictedNameChars)
|
||||||
|
}
|
||||||
|
|
||||||
|
spec = api.VolumeSpec{
|
||||||
|
Name: volumeName,
|
||||||
|
Type: api.VolumeTypeVolume,
|
||||||
|
VolumeOptions: &api.VolumeOptions{
|
||||||
|
Name: volumeName,
|
||||||
|
NoCopy: volumeNoCopy,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mount.VolumeName = spec.Name
|
||||||
|
default:
|
||||||
|
return spec, mount, fmt.Errorf("invalid format, must container at most 2 separators ':'")
|
||||||
|
}
|
||||||
|
|
||||||
|
return spec, mount, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -40,8 +40,8 @@ func NewScaleCommand() *cobra.Command {
|
|||||||
}
|
}
|
||||||
|
|
||||||
cmd.Flags().StringVarP(
|
cmd.Flags().StringVarP(
|
||||||
&opts.cluster, "cluster", "c", "",
|
&opts.cluster, "context", "c", "",
|
||||||
"Name of the cluster. (default is the current cluster)",
|
"Name of the cluster context. (default is the current context)",
|
||||||
)
|
)
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
@@ -143,7 +143,7 @@ func confirm() (bool, error) {
|
|||||||
Negative("No").
|
Negative("No").
|
||||||
Value(&confirmed),
|
Value(&confirmed),
|
||||||
),
|
),
|
||||||
)
|
).WithAccessible(true)
|
||||||
if err := form.Run(); err != nil {
|
if err := form.Run(); err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,148 @@
|
|||||||
|
package volume
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/huh"
|
||||||
|
"github.com/docker/docker/api/types/volume"
|
||||||
|
"github.com/psviderski/uncloud/internal/cli"
|
||||||
|
"github.com/psviderski/uncloud/internal/machine/api/pb"
|
||||||
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
type createOptions struct {
|
||||||
|
driver string
|
||||||
|
driverOpts []string
|
||||||
|
labels []string
|
||||||
|
machine string
|
||||||
|
context string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCreateCommand() *cobra.Command {
|
||||||
|
opts := createOptions{}
|
||||||
|
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "create [FLAGS] VOLUME_NAME",
|
||||||
|
Short: "Create a volume on a specific machine.",
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
uncli := cmd.Context().Value("cli").(*cli.CLI)
|
||||||
|
opts.driver = strings.TrimSpace(opts.driver)
|
||||||
|
|
||||||
|
volumeName := args[0]
|
||||||
|
if volumeName == "" {
|
||||||
|
return fmt.Errorf("volume name is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
return create(cmd.Context(), uncli, volumeName, opts)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Flags().StringVarP(&opts.driver, "driver", "d", "local",
|
||||||
|
"Volume driver to use.")
|
||||||
|
cmd.Flags().StringSliceVarP(&opts.driverOpts, "opt", "o", nil,
|
||||||
|
"Driver specific options in the form of 'key=value' pairs. Can be specified multiple times.")
|
||||||
|
cmd.Flags().StringSliceVarP(&opts.labels, "label", "l", nil,
|
||||||
|
"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", "",
|
||||||
|
"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
|
||||||
|
}
|
||||||
|
|
||||||
|
func create(ctx context.Context, uncli *cli.CLI, name string, opts createOptions) error {
|
||||||
|
client, err := uncli.ConnectCluster(ctx, opts.context)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("connect to cluster: %w", err)
|
||||||
|
}
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
// Parse driver options.
|
||||||
|
driverOpts := make(map[string]string)
|
||||||
|
for _, opt := range opts.driverOpts {
|
||||||
|
k, v, ok := strings.Cut(opt, "=")
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("invalid driver option format: '%s' (expected key=value)", opt)
|
||||||
|
}
|
||||||
|
driverOpts[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse labels.
|
||||||
|
labels := make(map[string]string)
|
||||||
|
for _, label := range opts.labels {
|
||||||
|
k, v, ok := strings.Cut(label, "=")
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("invalid label format: '%s' (expected key=value)", label)
|
||||||
|
}
|
||||||
|
labels[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
// List machines and filter by the specified machine name or ID.
|
||||||
|
// If no machine is specified, prompt the user to select one.
|
||||||
|
machines, err := client.ListMachines(ctx, nil)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("list machines: %w", err)
|
||||||
|
}
|
||||||
|
var selectedMachine *pb.MachineInfo
|
||||||
|
|
||||||
|
if opts.machine == "" {
|
||||||
|
if len(machines) == 1 {
|
||||||
|
selectedMachine = machines[0].Machine
|
||||||
|
} else {
|
||||||
|
if selectedMachine, err = promptSelectMachine(ctx, machines); err != nil {
|
||||||
|
return fmt.Errorf("select machine: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
m := machines.FindByNameOrID(opts.machine)
|
||||||
|
if m == nil {
|
||||||
|
return fmt.Errorf("machine '%s' not found", opts.machine)
|
||||||
|
}
|
||||||
|
selectedMachine = m.Machine
|
||||||
|
}
|
||||||
|
|
||||||
|
createOpts := volume.CreateOptions{
|
||||||
|
Name: name,
|
||||||
|
Driver: opts.driver,
|
||||||
|
DriverOpts: driverOpts,
|
||||||
|
Labels: labels,
|
||||||
|
}
|
||||||
|
vol, err := client.CreateVolume(ctx, selectedMachine.Id, createOpts)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("create volume '%s' on machine '%s': %w", name, selectedMachine.Name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Volume '%s' created on machine '%s'.\n", vol.Volume.Name, vol.MachineName)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func promptSelectMachine(ctx context.Context, machines api.MachineMembersList) (*pb.MachineInfo, error) {
|
||||||
|
options := make([]huh.Option[*pb.MachineInfo], len(machines))
|
||||||
|
for i, m := range machines {
|
||||||
|
options[i] = huh.NewOption(m.Machine.Name, m.Machine)
|
||||||
|
}
|
||||||
|
slices.SortFunc(options, func(a, b huh.Option[*pb.MachineInfo]) int {
|
||||||
|
return strings.Compare(a.Key, b.Key)
|
||||||
|
})
|
||||||
|
|
||||||
|
var selected *pb.MachineInfo
|
||||||
|
form := huh.NewForm(
|
||||||
|
huh.NewGroup(
|
||||||
|
huh.NewSelect[*pb.MachineInfo]().
|
||||||
|
Title("Select a machine to create the volume on (or specify with --machine flag)").
|
||||||
|
Options(options...).
|
||||||
|
Value(&selected),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
if err := form.RunWithContext(ctx); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return selected, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
package volume
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/psviderski/uncloud/internal/cli"
|
||||||
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
type inspectOptions struct {
|
||||||
|
machine string
|
||||||
|
context string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewInspectCommand() *cobra.Command {
|
||||||
|
opts := inspectOptions{}
|
||||||
|
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "inspect [FLAGS] VOLUME_NAME",
|
||||||
|
Short: "Display detailed information on a volume.",
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
uncli := cmd.Context().Value("cli").(*cli.CLI)
|
||||||
|
return inspect(cmd.Context(), uncli, args[0], opts)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Flags().StringVarP(&opts.machine, "machine", "m", "",
|
||||||
|
"Name or ID of the machine where the volume is located. "+
|
||||||
|
"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
|
||||||
|
}
|
||||||
|
|
||||||
|
func inspect(ctx context.Context, uncli *cli.CLI, name string, opts inspectOptions) error {
|
||||||
|
client, err := uncli.ConnectCluster(ctx, opts.context)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("connect to cluster: %w", err)
|
||||||
|
}
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
filter := &api.VolumeFilter{
|
||||||
|
Names: []string{name},
|
||||||
|
}
|
||||||
|
if opts.machine != "" {
|
||||||
|
filter.Machines = []string{opts.machine}
|
||||||
|
}
|
||||||
|
|
||||||
|
volumes, err := client.ListVolumes(ctx, filter)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("list volumes: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(volumes) == 0 {
|
||||||
|
if opts.machine != "" {
|
||||||
|
return fmt.Errorf("volume '%s' not found on machine '%s'", name, opts.machine)
|
||||||
|
}
|
||||||
|
return fmt.Errorf("volume '%s' not found on any machine", name)
|
||||||
|
}
|
||||||
|
if len(volumes) > 1 {
|
||||||
|
fmt.Printf("Volume '%s' found on multiple machines:\n", name)
|
||||||
|
for _, v := range volumes {
|
||||||
|
fmt.Printf(" • %s\n", v.MachineName)
|
||||||
|
}
|
||||||
|
return errors.New("specify --machine flag to choose which machine to use")
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := json.MarshalIndent(volumes[0], "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("marshal volume: %w", err)
|
||||||
|
}
|
||||||
|
fmt.Println(string(data))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
package volume
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
"text/tabwriter"
|
||||||
|
|
||||||
|
"github.com/psviderski/uncloud/internal/cli"
|
||||||
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
type listOptions struct {
|
||||||
|
machines []string
|
||||||
|
quiet bool
|
||||||
|
context string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewListCommand() *cobra.Command {
|
||||||
|
opts := listOptions{}
|
||||||
|
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "ls [FLAGS]",
|
||||||
|
Aliases: []string{"list"},
|
||||||
|
Short: "List volumes across all machines in the cluster.",
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
uncli := cmd.Context().Value("cli").(*cli.CLI)
|
||||||
|
return list(cmd.Context(), uncli, opts)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Flags().StringSliceVarP(&opts.machines, "machine", "m", nil,
|
||||||
|
"Filter volumes by machine name or ID. Can be specified multiple times or as a comma-separated list. "+
|
||||||
|
"(default is include all machines)")
|
||||||
|
cmd.Flags().BoolVarP(&opts.quiet, "quiet", "q", false,
|
||||||
|
"Only display volume names.")
|
||||||
|
|
||||||
|
cmd.Flags().StringVarP(&opts.context, "context", "c", "",
|
||||||
|
"Name of the cluster context. (default is the current context)")
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func list(ctx context.Context, uncli *cli.CLI, opts listOptions) error {
|
||||||
|
client, err := uncli.ConnectCluster(ctx, opts.context)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("connect to cluster: %w", err)
|
||||||
|
}
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
// Apply machine filter if specified.
|
||||||
|
var filter *api.VolumeFilter
|
||||||
|
if len(opts.machines) > 0 {
|
||||||
|
machines := cli.ExpandCommaSeparatedValues(opts.machines)
|
||||||
|
filter = &api.VolumeFilter{
|
||||||
|
Machines: machines,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
volumes, err := client.ListVolumes(ctx, filter)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("list volumes: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(volumes) == 0 {
|
||||||
|
if !opts.quiet {
|
||||||
|
fmt.Println("No volumes found.")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort the volumes by name first, then by machine name.
|
||||||
|
slices.SortFunc(volumes, func(a, b api.MachineVolume) int {
|
||||||
|
cmp := strings.Compare(a.Volume.Name, b.Volume.Name)
|
||||||
|
if cmp != 0 {
|
||||||
|
return cmp
|
||||||
|
}
|
||||||
|
return strings.Compare(a.MachineName, b.MachineName)
|
||||||
|
})
|
||||||
|
|
||||||
|
// If quiet mode, just print volume names.
|
||||||
|
if opts.quiet {
|
||||||
|
for _, v := range volumes {
|
||||||
|
fmt.Println(v.Volume.Name)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print the volumes in a table format.
|
||||||
|
tw := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', 0)
|
||||||
|
fmt.Fprintln(tw, "NAME\tDRIVER\tMACHINE")
|
||||||
|
|
||||||
|
for _, v := range volumes {
|
||||||
|
fmt.Fprintf(tw, "%s\t%s\t%s\n",
|
||||||
|
v.Volume.Name,
|
||||||
|
v.Volume.Driver,
|
||||||
|
v.MachineName,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return tw.Flush()
|
||||||
|
}
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
package volume
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/psviderski/uncloud/internal/cli"
|
||||||
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
type removeOptions struct {
|
||||||
|
force bool
|
||||||
|
machines []string
|
||||||
|
yes bool
|
||||||
|
context string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRemoveCommand() *cobra.Command {
|
||||||
|
opts := removeOptions{}
|
||||||
|
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "rm [FLAGS] VOLUME_NAME [VOLUME_NAME...]",
|
||||||
|
Aliases: []string{"remove", "delete"},
|
||||||
|
Short: "Remove one or more volumes.",
|
||||||
|
Long: "Remove one or more volumes. You cannot remove a volume that is in use by a container.",
|
||||||
|
Args: cobra.MinimumNArgs(1),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
uncli := cmd.Context().Value("cli").(*cli.CLI)
|
||||||
|
return remove(cmd.Context(), uncli, args, opts)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Flags().BoolVarP(&opts.force, "force", "f", false,
|
||||||
|
"Force the removal of one or more volumes.")
|
||||||
|
cmd.Flags().StringSliceVarP(&opts.machines, "machine", "m", nil,
|
||||||
|
"Name or ID of the machine to remove one or more volumes from. "+
|
||||||
|
"Can be specified multiple times or as a comma-separated list.\n"+
|
||||||
|
"If not specified, the found volume(s) will be removed from all machines.")
|
||||||
|
cmd.Flags().BoolVarP(&opts.yes, "yes", "y", false,
|
||||||
|
"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
|
||||||
|
}
|
||||||
|
|
||||||
|
func remove(ctx context.Context, uncli *cli.CLI, names []string, opts removeOptions) error {
|
||||||
|
client, err := uncli.ConnectCluster(ctx, opts.context)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("connect to cluster: %w", err)
|
||||||
|
}
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
filter := &api.VolumeFilter{
|
||||||
|
Names: names,
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(opts.machines) > 0 {
|
||||||
|
machines := cli.ExpandCommaSeparatedValues(opts.machines)
|
||||||
|
filter.Machines = machines
|
||||||
|
}
|
||||||
|
|
||||||
|
volumes, err := client.ListVolumes(ctx, filter)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("list volumes: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(volumes) == 0 {
|
||||||
|
if len(names) == 1 {
|
||||||
|
return fmt.Errorf("volume '%s' not found", names[0])
|
||||||
|
}
|
||||||
|
return fmt.Errorf("no volumes found matching the specified names")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Confirm removal if not using --yes flag.
|
||||||
|
if !opts.yes {
|
||||||
|
fmt.Println("The following volumes will be removed:")
|
||||||
|
for _, v := range volumes {
|
||||||
|
fmt.Printf(" • '%s' on machine '%s'\n", v.Volume.Name, v.MachineName)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println()
|
||||||
|
confirmed, err := cli.Confirm()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("confirm removal: %w", err)
|
||||||
|
}
|
||||||
|
if !confirmed {
|
||||||
|
fmt.Println("Cancelled. No volumes were removed.")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the volumes one by one collecting errors.
|
||||||
|
var removeErr error
|
||||||
|
for _, v := range volumes {
|
||||||
|
if err = client.RemoveVolume(ctx, v.MachineID, v.Volume.Name, opts.force); err != nil {
|
||||||
|
if !errors.Is(err, api.ErrNotFound) {
|
||||||
|
removeErr = errors.Join(removeErr, fmt.Errorf("failed to remove volume '%s' on machine '%s': %w",
|
||||||
|
v.Volume.Name, v.MachineName, err))
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Volume '%s' removed from machine '%s'.\n", v.Volume.Name, v.MachineName)
|
||||||
|
}
|
||||||
|
|
||||||
|
return removeErr
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package volume
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewRootCommand() *cobra.Command {
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "volume",
|
||||||
|
Short: "Manage volumes in an Uncloud cluster.",
|
||||||
|
}
|
||||||
|
cmd.AddCommand(
|
||||||
|
NewCreateCommand(),
|
||||||
|
NewInspectCommand(),
|
||||||
|
NewListCommand(),
|
||||||
|
NewRemoveCommand(),
|
||||||
|
)
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
@@ -2,14 +2,16 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
"github.com/psviderski/uncloud/internal/daemon"
|
"github.com/psviderski/uncloud/internal/daemon"
|
||||||
"github.com/psviderski/uncloud/internal/log"
|
"github.com/psviderski/uncloud/internal/log"
|
||||||
"github.com/psviderski/uncloud/internal/machine"
|
"github.com/psviderski/uncloud/internal/machine"
|
||||||
|
"github.com/psviderski/uncloud/internal/version"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
@@ -22,6 +24,7 @@ func main() {
|
|||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "uncloudd",
|
Use: "uncloudd",
|
||||||
Short: "Uncloud machine daemon.",
|
Short: "Uncloud machine daemon.",
|
||||||
|
Version: version.String(),
|
||||||
SilenceUsage: true,
|
SilenceUsage: true,
|
||||||
SilenceErrors: true,
|
SilenceErrors: true,
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
|||||||
+1
-1
@@ -16,7 +16,7 @@ In addition, any ports for any workloads you want to expose will need to be open
|
|||||||
|
|
||||||
### Configuration
|
### Configuration
|
||||||
|
|
||||||
Uncloud stores its configuration in `~/.config/uncloud/config.toml`. If you wish to reinitialise a cluster, simply remove it from this config.
|
Uncloud stores its configuration in `~/.config/uncloud/config.yaml`. If you wish to reinitialise a cluster, simply remove it from this config.
|
||||||
|
|
||||||
### Initialisation
|
### Initialisation
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ require (
|
|||||||
github.com/caddyserver/caddy/v2 v2.8.4
|
github.com/caddyserver/caddy/v2 v2.8.4
|
||||||
github.com/cenkalti/backoff/v4 v4.3.0
|
github.com/cenkalti/backoff/v4 v4.3.0
|
||||||
github.com/charmbracelet/huh v0.6.0
|
github.com/charmbracelet/huh v0.6.0
|
||||||
|
github.com/charmbracelet/lipgloss v0.13.0
|
||||||
github.com/compose-spec/compose-go/v2 v2.4.5
|
github.com/compose-spec/compose-go/v2 v2.4.5
|
||||||
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf
|
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf
|
||||||
github.com/deckarep/golang-set/v2 v2.8.0
|
github.com/deckarep/golang-set/v2 v2.8.0
|
||||||
@@ -21,6 +22,8 @@ require (
|
|||||||
github.com/docker/docker v27.4.0-rc.2+incompatible
|
github.com/docker/docker v27.4.0-rc.2+incompatible
|
||||||
github.com/docker/go-connections v0.5.0
|
github.com/docker/go-connections v0.5.0
|
||||||
github.com/docker/go-units v0.5.0
|
github.com/docker/go-units v0.5.0
|
||||||
|
github.com/goccy/go-yaml v1.17.1
|
||||||
|
github.com/google/go-cmp v0.7.0
|
||||||
github.com/google/go-containerregistry v0.20.2
|
github.com/google/go-containerregistry v0.20.2
|
||||||
github.com/hashicorp/memberlist v0.5.1
|
github.com/hashicorp/memberlist v0.5.1
|
||||||
github.com/hashicorp/serf v0.10.1
|
github.com/hashicorp/serf v0.10.1
|
||||||
@@ -33,6 +36,7 @@ require (
|
|||||||
github.com/ipfs/go-log/v2 v2.5.1
|
github.com/ipfs/go-log/v2 v2.5.1
|
||||||
github.com/jmoiron/sqlx v1.4.0
|
github.com/jmoiron/sqlx v1.4.0
|
||||||
github.com/lmittmann/tint v1.0.5
|
github.com/lmittmann/tint v1.0.5
|
||||||
|
github.com/miekg/dns v1.1.65
|
||||||
github.com/opencontainers/go-digest v1.0.0
|
github.com/opencontainers/go-digest v1.0.0
|
||||||
github.com/opencontainers/image-spec v1.1.0
|
github.com/opencontainers/image-spec v1.1.0
|
||||||
github.com/siderolabs/discovery-api v0.1.4
|
github.com/siderolabs/discovery-api v0.1.4
|
||||||
@@ -43,9 +47,9 @@ require (
|
|||||||
github.com/vishvananda/netlink v1.3.0
|
github.com/vishvananda/netlink v1.3.0
|
||||||
go.uber.org/zap v1.27.0
|
go.uber.org/zap v1.27.0
|
||||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba
|
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba
|
||||||
golang.org/x/crypto v0.32.0
|
golang.org/x/crypto v0.33.0
|
||||||
golang.org/x/net v0.34.0
|
golang.org/x/net v0.35.0
|
||||||
golang.org/x/sync v0.10.0
|
golang.org/x/sync v0.11.0
|
||||||
golang.org/x/sys v0.31.0
|
golang.org/x/sys v0.31.0
|
||||||
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173
|
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173
|
||||||
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6
|
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6
|
||||||
@@ -82,7 +86,6 @@ require (
|
|||||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
github.com/charmbracelet/bubbles v0.20.0 // indirect
|
github.com/charmbracelet/bubbles v0.20.0 // indirect
|
||||||
github.com/charmbracelet/bubbletea v1.1.1 // indirect
|
github.com/charmbracelet/bubbletea v1.1.1 // indirect
|
||||||
github.com/charmbracelet/lipgloss v0.13.0 // indirect
|
|
||||||
github.com/charmbracelet/x/ansi v0.3.2 // indirect
|
github.com/charmbracelet/x/ansi v0.3.2 // indirect
|
||||||
github.com/charmbracelet/x/exp/strings v0.0.0-20240919170804-a4978c8e603a // indirect
|
github.com/charmbracelet/x/exp/strings v0.0.0-20240919170804-a4978c8e603a // indirect
|
||||||
github.com/charmbracelet/x/term v0.2.0 // indirect
|
github.com/charmbracelet/x/term v0.2.0 // indirect
|
||||||
@@ -135,7 +138,6 @@ require (
|
|||||||
github.com/google/btree v1.1.2 // indirect
|
github.com/google/btree v1.1.2 // indirect
|
||||||
github.com/google/cel-go v0.20.1 // indirect
|
github.com/google/cel-go v0.20.1 // indirect
|
||||||
github.com/google/flatbuffers v24.3.25+incompatible // indirect
|
github.com/google/flatbuffers v24.3.25+incompatible // indirect
|
||||||
github.com/google/go-cmp v0.6.0 // indirect
|
|
||||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e // indirect
|
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e // indirect
|
||||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
|
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
@@ -196,7 +198,6 @@ require (
|
|||||||
github.com/mdlayher/socket v0.5.1 // indirect
|
github.com/mdlayher/socket v0.5.1 // indirect
|
||||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
|
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
|
||||||
github.com/mholt/acmez/v2 v2.0.3 // indirect
|
github.com/mholt/acmez/v2 v2.0.3 // indirect
|
||||||
github.com/miekg/dns v1.1.62 // indirect
|
|
||||||
github.com/miekg/pkcs11 v1.1.1 // indirect
|
github.com/miekg/pkcs11 v1.1.1 // indirect
|
||||||
github.com/minio/sha256-simd v1.0.1 // indirect
|
github.com/minio/sha256-simd v1.0.1 // indirect
|
||||||
github.com/mitchellh/cli v1.1.5 // indirect
|
github.com/mitchellh/cli v1.1.5 // indirect
|
||||||
@@ -302,11 +303,11 @@ require (
|
|||||||
go.uber.org/zap/exp v0.2.0 // indirect
|
go.uber.org/zap/exp v0.2.0 // indirect
|
||||||
golang.org/x/crypto/x509roots/fallback v0.0.0-20240507223354-67b13616a595 // indirect
|
golang.org/x/crypto/x509roots/fallback v0.0.0-20240507223354-67b13616a595 // indirect
|
||||||
golang.org/x/exp v0.0.0-20241215155358-4a5509556b9e // indirect
|
golang.org/x/exp v0.0.0-20241215155358-4a5509556b9e // indirect
|
||||||
golang.org/x/mod v0.22.0 // indirect
|
golang.org/x/mod v0.23.0 // indirect
|
||||||
golang.org/x/term v0.28.0 // indirect
|
golang.org/x/term v0.29.0 // indirect
|
||||||
golang.org/x/text v0.21.0 // indirect
|
golang.org/x/text v0.22.0 // indirect
|
||||||
golang.org/x/time v0.8.0 // indirect
|
golang.org/x/time v0.8.0 // indirect
|
||||||
golang.org/x/tools v0.29.0 // indirect
|
golang.org/x/tools v0.30.0 // indirect
|
||||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
|
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
|
||||||
google.golang.org/genproto v0.0.0-20240401170217-c3f982113cda // indirect
|
google.golang.org/genproto v0.0.0-20240401170217-c3f982113cda // indirect
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 // indirect
|
google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 // indirect
|
||||||
|
|||||||
@@ -343,6 +343,8 @@ github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZ
|
|||||||
github.com/go-viper/mapstructure/v2 v2.0.0 h1:dhn8MZ1gZ0mzeodTG3jt5Vj/o87xZKuNAprG2mQfMfc=
|
github.com/go-viper/mapstructure/v2 v2.0.0 h1:dhn8MZ1gZ0mzeodTG3jt5Vj/o87xZKuNAprG2mQfMfc=
|
||||||
github.com/go-viper/mapstructure/v2 v2.0.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
github.com/go-viper/mapstructure/v2 v2.0.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||||
github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0=
|
github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0=
|
||||||
|
github.com/goccy/go-yaml v1.17.1 h1:LI34wktB2xEE3ONG/2Ar54+/HJVBriAGJ55PHls4YuY=
|
||||||
|
github.com/goccy/go-yaml v1.17.1/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||||
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
|
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
|
||||||
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||||
github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E=
|
github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E=
|
||||||
@@ -397,8 +399,8 @@ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
|||||||
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
github.com/google/go-containerregistry v0.20.2 h1:B1wPJ1SN/S7pB+ZAimcciVD+r+yV/l/DSArMxlbwseo=
|
github.com/google/go-containerregistry v0.20.2 h1:B1wPJ1SN/S7pB+ZAimcciVD+r+yV/l/DSArMxlbwseo=
|
||||||
github.com/google/go-containerregistry v0.20.2/go.mod h1:z38EKdKh4h7IP2gSfUUqEvalZBqs6AoLeWfUy34nQC8=
|
github.com/google/go-containerregistry v0.20.2/go.mod h1:z38EKdKh4h7IP2gSfUUqEvalZBqs6AoLeWfUy34nQC8=
|
||||||
github.com/google/go-tpm v0.9.0 h1:sQF6YqWMi+SCXpsmS3fd21oPy/vSddwZry4JnmltHVk=
|
github.com/google/go-tpm v0.9.0 h1:sQF6YqWMi+SCXpsmS3fd21oPy/vSddwZry4JnmltHVk=
|
||||||
@@ -719,8 +721,8 @@ github.com/mholt/acmez/v2 v2.0.3 h1:CgDBlEwg3QBp6s45tPQmFIBrkRIkBT4rW4orMM6p4sw=
|
|||||||
github.com/mholt/acmez/v2 v2.0.3/go.mod h1:pQ1ysaDeGrIMvJ9dfJMk5kJNkn7L2sb3UhyrX6Q91cw=
|
github.com/mholt/acmez/v2 v2.0.3/go.mod h1:pQ1ysaDeGrIMvJ9dfJMk5kJNkn7L2sb3UhyrX6Q91cw=
|
||||||
github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
|
github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
|
||||||
github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI=
|
github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI=
|
||||||
github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ=
|
github.com/miekg/dns v1.1.65 h1:0+tIPHzUW0GCge7IiK3guGP57VAw7hoPDfApjkMD1Fc=
|
||||||
github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ=
|
github.com/miekg/dns v1.1.65/go.mod h1:Dzw9769uoKVaLuODMDZz9M6ynFU6Em65csPuoi8G0ck=
|
||||||
github.com/miekg/pkcs11 v1.0.2/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs=
|
github.com/miekg/pkcs11 v1.0.2/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs=
|
||||||
github.com/miekg/pkcs11 v1.1.1 h1:Ugu9pdy6vAYku5DEpVWVFPYnzV+bxB+iRdbuFSu7TvU=
|
github.com/miekg/pkcs11 v1.1.1 h1:Ugu9pdy6vAYku5DEpVWVFPYnzV+bxB+iRdbuFSu7TvU=
|
||||||
github.com/miekg/pkcs11 v1.1.1/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs=
|
github.com/miekg/pkcs11 v1.1.1/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs=
|
||||||
@@ -1194,8 +1196,8 @@ golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5y
|
|||||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
|
golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
|
||||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||||
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
|
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
|
||||||
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
|
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
|
||||||
golang.org/x/crypto/x509roots/fallback v0.0.0-20240507223354-67b13616a595 h1:TgSqweA595vD0Zt86JzLv3Pb/syKg8gd5KMGGbJPYFw=
|
golang.org/x/crypto/x509roots/fallback v0.0.0-20240507223354-67b13616a595 h1:TgSqweA595vD0Zt86JzLv3Pb/syKg8gd5KMGGbJPYFw=
|
||||||
golang.org/x/crypto/x509roots/fallback v0.0.0-20240507223354-67b13616a595/go.mod h1:kNa9WdvYnzFwC79zRpLRMJbdEFlhyM5RPFBBZp/wWH8=
|
golang.org/x/crypto/x509roots/fallback v0.0.0-20240507223354-67b13616a595/go.mod h1:kNa9WdvYnzFwC79zRpLRMJbdEFlhyM5RPFBBZp/wWH8=
|
||||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
@@ -1212,8 +1214,8 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
|||||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4=
|
golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM=
|
||||||
golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
|
golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
|
||||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
@@ -1235,8 +1237,8 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug
|
|||||||
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
|
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
|
||||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||||
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
|
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
|
||||||
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
|
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
|
||||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||||
golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE=
|
golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE=
|
||||||
golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||||
@@ -1250,8 +1252,8 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ
|
|||||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
|
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
|
||||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
@@ -1308,8 +1310,8 @@ golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
|
|||||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||||
golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg=
|
golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
|
||||||
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
|
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
@@ -1320,8 +1322,8 @@ golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
|||||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||||
golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg=
|
golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg=
|
||||||
golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
@@ -1343,8 +1345,8 @@ golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4f
|
|||||||
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||||
golang.org/x/tools v0.29.0 h1:Xx0h3TtM9rzQpQuR4dKLrdglAmCEN5Oi+P74JdhdzXE=
|
golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY=
|
||||||
golang.org/x/tools v0.29.0/go.mod h1:KMQVMRsVxU6nHCFXrBPhDB8XncLNLM0lIy/F14RP588=
|
golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY=
|
||||||
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
|||||||
+147
-107
@@ -4,6 +4,10 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/netip"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/huh"
|
||||||
"github.com/docker/cli/cli/streams"
|
"github.com/docker/cli/cli/streams"
|
||||||
"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"
|
||||||
@@ -13,17 +17,15 @@ 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/psviderski/uncloud/pkg/client/connector"
|
"github.com/psviderski/uncloud/pkg/client/connector"
|
||||||
"net/netip"
|
"google.golang.org/grpc/codes"
|
||||||
"os"
|
"google.golang.org/grpc/status"
|
||||||
|
|
||||||
"github.com/charmbracelet/huh"
|
|
||||||
"google.golang.org/protobuf/types/known/emptypb"
|
"google.golang.org/protobuf/types/known/emptypb"
|
||||||
)
|
)
|
||||||
|
|
||||||
const defaultClusterName = "default"
|
const defaultContextName = "default"
|
||||||
|
|
||||||
type CLI struct {
|
type CLI struct {
|
||||||
config *config.Config
|
Config *config.Config
|
||||||
conn *config.MachineConnection
|
conn *config.MachineConnection
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,65 +43,72 @@ func New(configPath string, conn *config.MachineConnection) (*CLI, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return &CLI{
|
return &CLI{
|
||||||
config: cfg,
|
Config: cfg,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cli *CLI) CreateCluster(name string) error {
|
func (cli *CLI) CreateContext(name string) error {
|
||||||
if _, ok := cli.config.Clusters[name]; ok {
|
if _, ok := cli.Config.Contexts[name]; ok {
|
||||||
return fmt.Errorf("cluster %q already exists", name)
|
return fmt.Errorf("context '%s' already exists", name)
|
||||||
}
|
}
|
||||||
cli.config.Clusters[name] = &config.Cluster{
|
cli.Config.Contexts[name] = &config.Context{
|
||||||
Name: name,
|
Name: name,
|
||||||
}
|
}
|
||||||
return cli.config.Save()
|
return cli.Config.Save()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cli *CLI) SetCurrentCluster(name string) error {
|
func (cli *CLI) SetCurrentContext(name string) error {
|
||||||
if _, ok := cli.config.Clusters[name]; !ok {
|
if _, ok := cli.Config.Contexts[name]; !ok {
|
||||||
return api.ErrNotFound
|
return api.ErrNotFound
|
||||||
}
|
}
|
||||||
cli.config.CurrentCluster = name
|
cli.Config.CurrentContext = name
|
||||||
return cli.config.Save()
|
return cli.Config.Save()
|
||||||
}
|
}
|
||||||
|
|
||||||
// ConnectCluster connects to a cluster using the given cluster name or the current cluster if not specified.
|
// ConnectCluster connects to a cluster using the given context name 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, clusterName string) (*client.Client, error) {
|
func (cli *CLI) ConnectCluster(ctx context.Context, contextName string) (*client.Client, error) {
|
||||||
if cli.conn != nil {
|
if cli.conn != nil {
|
||||||
return connectCluster(ctx, *cli.conn)
|
return connectCluster(ctx, *cli.conn)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(cli.config.Clusters) == 0 {
|
if len(cli.Config.Contexts) == 0 {
|
||||||
return nil, errors.New(
|
return nil, fmt.Errorf(
|
||||||
"no clusters found in the Uncloud config. " +
|
"no cluster contexts found in the Uncloud config (%s). "+
|
||||||
"Please initialise a cluster with `uncloud machine init` first",
|
"Please initialise a cluster with 'uncloud machine init' first",
|
||||||
|
cli.Config.Path(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if clusterName == "" {
|
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.CurrentCluster == "" {
|
if cli.Config.CurrentContext == "" {
|
||||||
return nil, errors.New(
|
|
||||||
"the current cluster is not set in the Uncloud config. " +
|
|
||||||
"Please specify a cluster with the --cluster flag or set current_cluster in the config",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if _, ok := cli.config.Clusters[cli.config.CurrentCluster]; !ok {
|
|
||||||
return nil, fmt.Errorf(
|
return nil, fmt.Errorf(
|
||||||
"current cluster %q not found in the config. "+
|
"the current cluster context is not set in the Uncloud config (%s). "+
|
||||||
"Please specify a cluster with the --cluster flag or update current_cluster in the config",
|
"Please specify the context with the '--context' flag or set 'current_context' in the config",
|
||||||
cli.config.CurrentCluster,
|
cli.Config.Path(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
clusterName = cli.config.CurrentCluster
|
if _, ok := cli.Config.Contexts[cli.Config.CurrentContext]; !ok {
|
||||||
|
return nil, fmt.Errorf(
|
||||||
|
"current cluster context '%s' not found in the Uncloud config (%s). "+
|
||||||
|
"Please specify the context with the '--context' flag or update 'current_context' in the config",
|
||||||
|
cli.Config.CurrentContext,
|
||||||
|
cli.Config.Path(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
contextName = cli.Config.CurrentContext
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg, ok := cli.config.Clusters[clusterName]
|
cfg, ok := cli.Config.Contexts[contextName]
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, fmt.Errorf("cluster %q not found in the config", clusterName)
|
return nil, fmt.Errorf("cluster context '%s' not found in the Uncloud config (%s)",
|
||||||
|
contextName, cli.Config.Path())
|
||||||
}
|
}
|
||||||
if len(cfg.Connections) == 0 {
|
if len(cfg.Connections) == 0 {
|
||||||
return nil, fmt.Errorf("no connection configurations found for cluster %q in the config", clusterName)
|
return nil, fmt.Errorf(
|
||||||
|
"no connection configurations found for cluster context '%s' in the Uncloud config (%s)",
|
||||||
|
contextName, cli.Config.Path(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: iterate over all connections and try to connect to the cluster using the first successful connection.
|
// TODO: iterate over all connections and try to connect to the cluster using the first successful connection.
|
||||||
@@ -107,7 +116,7 @@ func (cli *CLI) ConnectCluster(ctx context.Context, clusterName string) (*client
|
|||||||
|
|
||||||
c, err := connectCluster(ctx, conn)
|
c, err := connectCluster(ctx, conn)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.New("no valid connection configuration found for the cluster")
|
return nil, fmt.Errorf("connect to cluster (context '%s'): %w", contextName, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return c, nil
|
return c, nil
|
||||||
@@ -129,46 +138,42 @@ func connectCluster(ctx context.Context, conn config.MachineConnection) (*client
|
|||||||
KeyPath: keyPath,
|
KeyPath: keyPath,
|
||||||
}
|
}
|
||||||
return client.New(ctx, connector.NewSSHConnector(sshConfig))
|
return client.New(ctx, connector.NewSSHConnector(sshConfig))
|
||||||
} else if conn.TCP.IsValid() {
|
} else if conn.TCP != nil && conn.TCP.IsValid() {
|
||||||
return client.New(ctx, connector.NewTCPConnector(conn.TCP))
|
return client.New(ctx, connector.NewTCPConnector(*conn.TCP))
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, errors.New("connection configuration is invalid")
|
return nil, errors.New("connection configuration is invalid")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type InitClusterOptions struct {
|
||||||
|
Context string
|
||||||
|
MachineName string
|
||||||
|
Network netip.Prefix
|
||||||
|
PublicIP *netip.Addr
|
||||||
|
RemoteMachine *RemoteMachine
|
||||||
|
Version string
|
||||||
|
}
|
||||||
|
|
||||||
// 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.
|
||||||
// The client should be closed after use by the caller.
|
// The client should be closed after use by the caller.
|
||||||
func (cli *CLI) InitCluster(
|
func (cli *CLI) InitCluster(ctx context.Context, opts InitClusterOptions) (*client.Client, error) {
|
||||||
ctx context.Context,
|
if opts.RemoteMachine != nil {
|
||||||
remoteMachine *RemoteMachine,
|
return cli.initRemoteMachine(ctx, opts)
|
||||||
clusterName,
|
|
||||||
machineName string,
|
|
||||||
netPrefix netip.Prefix,
|
|
||||||
publicIP *netip.Addr,
|
|
||||||
) (*client.Client, error) {
|
|
||||||
if remoteMachine != nil {
|
|
||||||
return cli.initRemoteMachine(ctx, *remoteMachine, clusterName, machineName, netPrefix, publicIP)
|
|
||||||
}
|
}
|
||||||
// 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")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cli *CLI) initRemoteMachine(
|
func (cli *CLI) initRemoteMachine(ctx context.Context, opts InitClusterOptions) (*client.Client, error) {
|
||||||
ctx context.Context,
|
contextName := opts.Context
|
||||||
remoteMachine RemoteMachine,
|
if contextName == "" {
|
||||||
clusterName,
|
contextName = defaultContextName
|
||||||
machineName string,
|
|
||||||
netPrefix netip.Prefix,
|
|
||||||
publicIP *netip.Addr,
|
|
||||||
) (*client.Client, error) {
|
|
||||||
if clusterName == "" {
|
|
||||||
clusterName = defaultClusterName
|
|
||||||
}
|
}
|
||||||
if _, ok := cli.config.Clusters[clusterName]; ok {
|
if _, ok := cli.Config.Contexts[contextName]; ok {
|
||||||
return nil, fmt.Errorf("cluster %q already exists", clusterName)
|
return nil, fmt.Errorf("cluster context '%s' already exists", contextName)
|
||||||
}
|
}
|
||||||
|
|
||||||
machineClient, err := cli.provisionRemoteMachine(ctx, remoteMachine)
|
machineClient, err := cli.provisionRemoteMachine(ctx, *opts.RemoteMachine, opts.Version)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -190,13 +195,24 @@ func (cli *CLI) initRemoteMachine(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
req := &pb.InitClusterRequest{
|
// Check machine meets all necessary system requirements before proceeding.
|
||||||
MachineName: machineName,
|
checkResp, err := machineClient.CheckPrerequisites(ctx, &emptypb.Empty{})
|
||||||
Network: pb.NewIPPrefix(netPrefix),
|
// TODO(lhf): remove Unimplemented check when v0.9.0 is released.
|
||||||
|
if err != nil {
|
||||||
|
if status.Convert(err).Code() != codes.Unimplemented {
|
||||||
|
return nil, fmt.Errorf("check machine prerequisites: %w", err)
|
||||||
|
}
|
||||||
|
} else if !checkResp.Satisfied {
|
||||||
|
return nil, fmt.Errorf("machine prerequisites not satisfied: %s", checkResp.Error)
|
||||||
}
|
}
|
||||||
if publicIP != nil {
|
|
||||||
if publicIP.IsValid() {
|
req := &pb.InitClusterRequest{
|
||||||
req.PublicIpConfig = &pb.InitClusterRequest_PublicIp{PublicIp: pb.NewIP(*publicIP)}
|
MachineName: opts.MachineName,
|
||||||
|
Network: pb.NewIPPrefix(opts.Network),
|
||||||
|
}
|
||||||
|
if opts.PublicIP != nil {
|
||||||
|
if opts.PublicIP.IsValid() {
|
||||||
|
req.PublicIpConfig = &pb.InitClusterRequest_PublicIp{PublicIp: pb.NewIP(*opts.PublicIP)}
|
||||||
} else {
|
} else {
|
||||||
// Invalid or in other words zero IP means to automatically detect the public IP.
|
// Invalid or in other words zero IP means to automatically detect the public IP.
|
||||||
req.PublicIpConfig = &pb.InitClusterRequest_PublicIpAuto{PublicIpAuto: true}
|
req.PublicIpConfig = &pb.InitClusterRequest_PublicIpAuto{PublicIpAuto: true}
|
||||||
@@ -207,42 +223,50 @@ func (cli *CLI) initRemoteMachine(
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("init cluster: %w", err)
|
return nil, fmt.Errorf("init cluster: %w", err)
|
||||||
}
|
}
|
||||||
fmt.Printf("Cluster %q initialised with machine %q\n", clusterName, resp.Machine.Name)
|
fmt.Printf("Cluster initialised with machine '%s' and saved as context '%s' in your local config (%s)\n",
|
||||||
|
resp.Machine.Name, contextName, cli.Config.Path())
|
||||||
if err = cli.CreateCluster(clusterName); err != nil {
|
if err = cli.CreateContext(contextName); err != nil {
|
||||||
return nil, fmt.Errorf("save cluster to config: %w", err)
|
return nil, fmt.Errorf("save cluster context to config: %w", err)
|
||||||
}
|
}
|
||||||
// Set the current cluster to the just created one if it is the only cluster in the config.
|
if err = cli.SetCurrentContext(contextName); err != nil {
|
||||||
if len(cli.config.Clusters) == 1 {
|
return nil, fmt.Errorf("set current cluster context: %w", err)
|
||||||
if err = cli.SetCurrentCluster(clusterName); err != nil {
|
|
||||||
return nil, fmt.Errorf("set current cluster: %w", err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
fmt.Printf("Current cluster context is now '%s'.\n", contextName)
|
||||||
|
|
||||||
// Save the machine's SSH connection details in the cluster config.
|
// Save the machine's SSH connection details in the context config.
|
||||||
connCfg := config.MachineConnection{
|
connCfg := config.MachineConnection{
|
||||||
SSH: config.NewSSHDestination(remoteMachine.User, remoteMachine.Host, remoteMachine.Port),
|
SSH: config.NewSSHDestination(opts.RemoteMachine.User, opts.RemoteMachine.Host, opts.RemoteMachine.Port),
|
||||||
SSHKeyFile: remoteMachine.KeyPath,
|
SSHKeyFile: opts.RemoteMachine.KeyPath,
|
||||||
}
|
}
|
||||||
cli.config.Clusters[clusterName].Connections = append(cli.config.Clusters[clusterName].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 {
|
||||||
return nil, fmt.Errorf("save config: %w", err)
|
return nil, fmt.Errorf("save config: %w", err)
|
||||||
}
|
}
|
||||||
return machineClient, nil
|
return machineClient, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AddMachineOptions struct {
|
||||||
|
Context string
|
||||||
|
MachineName string
|
||||||
|
PublicIP *netip.Addr
|
||||||
|
RemoteMachine RemoteMachine
|
||||||
|
Version string
|
||||||
|
}
|
||||||
|
|
||||||
// AddMachine provisions a remote machine and adds it to the cluster. It returns a client to interact with the machine
|
// AddMachine provisions a remote machine and adds it to the cluster. It returns a client to interact with the machine
|
||||||
// which should be closed after use by the caller.
|
// which should be closed after use by the caller.
|
||||||
func (cli *CLI) AddMachine(
|
func (cli *CLI) AddMachine(ctx context.Context, opts AddMachineOptions) (*client.Client, error) {
|
||||||
ctx context.Context, remoteMachine RemoteMachine, clusterName, machineName string, publicIP *netip.Addr,
|
contextName := opts.Context
|
||||||
) (*client.Client, error) {
|
if contextName == "" {
|
||||||
c, err := cli.ConnectCluster(ctx, clusterName)
|
contextName = cli.Config.CurrentContext
|
||||||
|
}
|
||||||
|
c, err := cli.ConnectCluster(ctx, contextName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("connect to cluster: %w", err)
|
return nil, fmt.Errorf("connect to cluster (context '%s'): %w", contextName, err)
|
||||||
}
|
}
|
||||||
defer c.Close()
|
defer c.Close()
|
||||||
|
|
||||||
machineClient, err := cli.provisionRemoteMachine(ctx, remoteMachine)
|
machineClient, err := cli.provisionRemoteMachine(ctx, opts.RemoteMachine, opts.Version)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -263,6 +287,17 @@ func (cli *CLI) AddMachine(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check machine meets all necessary system requirements before proceeding.
|
||||||
|
checkResp, err := machineClient.CheckPrerequisites(ctx, &emptypb.Empty{})
|
||||||
|
// TODO(lhf): remove Unimplemented check when v0.9.0 is released.
|
||||||
|
if err != nil {
|
||||||
|
if status.Convert(err).Code() != codes.Unimplemented {
|
||||||
|
return nil, fmt.Errorf("check machine prerequisites: %w", err)
|
||||||
|
}
|
||||||
|
} else if !checkResp.Satisfied {
|
||||||
|
return nil, fmt.Errorf("machine prerequisites not satisfied: %s", checkResp.Error)
|
||||||
|
}
|
||||||
|
|
||||||
tokenResp, err := machineClient.Token(ctx, &emptypb.Empty{})
|
tokenResp, err := machineClient.Token(ctx, &emptypb.Empty{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("get remote machine token: %w", err)
|
return nil, fmt.Errorf("get remote machine token: %w", err)
|
||||||
@@ -278,15 +313,15 @@ func (cli *CLI) AddMachine(
|
|||||||
endpoints[i] = pb.NewIPPort(addrPort)
|
endpoints[i] = pb.NewIPPort(addrPort)
|
||||||
}
|
}
|
||||||
addReq := &pb.AddMachineRequest{
|
addReq := &pb.AddMachineRequest{
|
||||||
Name: machineName,
|
Name: opts.MachineName,
|
||||||
Network: &pb.NetworkConfig{
|
Network: &pb.NetworkConfig{
|
||||||
Endpoints: endpoints,
|
Endpoints: endpoints,
|
||||||
PublicKey: token.PublicKey,
|
PublicKey: token.PublicKey,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
if publicIP != nil {
|
if opts.PublicIP != nil {
|
||||||
if publicIP.IsValid() {
|
if opts.PublicIP.IsValid() {
|
||||||
addReq.PublicIp = pb.NewIP(*publicIP)
|
addReq.PublicIp = pb.NewIP(*opts.PublicIP)
|
||||||
} else if token.PublicIP.IsValid() {
|
} else if token.PublicIP.IsValid() {
|
||||||
// Invalid or in other words zero IP means to use an automatically detected public IP from the token.
|
// Invalid or in other words zero IP means to use an automatically detected public IP from the token.
|
||||||
addReq.PublicIp = pb.NewIP(token.PublicIP)
|
addReq.PublicIp = pb.NewIP(token.PublicIP)
|
||||||
@@ -295,11 +330,11 @@ func (cli *CLI) AddMachine(
|
|||||||
|
|
||||||
addResp, err := c.AddMachine(ctx, addReq)
|
addResp, err := c.AddMachine(ctx, addReq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("add machine to cluster: %w", err)
|
return nil, fmt.Errorf("add machine to cluster (context '%s'): %w", contextName, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// List other machines in the cluster to include them in the join request.
|
// List other machines in the cluster to include them in the join request.
|
||||||
machines, err := c.ListMachines(ctx)
|
machines, err := c.ListMachines(ctx, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("list cluster machines: %w", err)
|
return nil, fmt.Errorf("list cluster machines: %w", err)
|
||||||
}
|
}
|
||||||
@@ -319,18 +354,19 @@ func (cli *CLI) AddMachine(
|
|||||||
return nil, fmt.Errorf("join cluster: %w", err)
|
return nil, fmt.Errorf("join cluster: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("Machine %q added to cluster\n", addResp.Machine.Name)
|
// TODO: fix empty context name when using the current context (contextName == "").
|
||||||
|
fmt.Printf("Machine '%s' added to the cluster (context '%s').\n", addResp.Machine.Name, contextName)
|
||||||
|
|
||||||
// Save the machine's SSH connection details in the cluster config.
|
// Save the machine's SSH connection details in the context config.
|
||||||
connCfg := config.MachineConnection{
|
connCfg := config.MachineConnection{
|
||||||
SSH: config.NewSSHDestination(remoteMachine.User, remoteMachine.Host, remoteMachine.Port),
|
SSH: config.NewSSHDestination(opts.RemoteMachine.User, opts.RemoteMachine.Host, opts.RemoteMachine.Port),
|
||||||
SSHKeyFile: remoteMachine.KeyPath,
|
SSHKeyFile: opts.RemoteMachine.KeyPath,
|
||||||
}
|
}
|
||||||
if clusterName == "" {
|
if contextName == "" {
|
||||||
clusterName = cli.config.CurrentCluster
|
contextName = cli.Config.CurrentContext
|
||||||
}
|
}
|
||||||
cli.config.Clusters[clusterName].Connections = append(cli.config.Clusters[clusterName].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 {
|
||||||
return nil, fmt.Errorf("save config: %w", err)
|
return nil, fmt.Errorf("save config: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -339,7 +375,10 @@ func (cli *CLI) AddMachine(
|
|||||||
|
|
||||||
// provisionRemoteMachine installs the Uncloud daemon and dependencies on the remote machine over SSH and returns
|
// provisionRemoteMachine installs the Uncloud daemon and dependencies on the remote machine over SSH and returns
|
||||||
// a machine API client to interact with the machine. The client should be closed after use by the caller.
|
// a machine API client to interact with the machine. The client should be closed after use by the caller.
|
||||||
func (cli *CLI) provisionRemoteMachine(ctx context.Context, remoteMachine RemoteMachine) (*client.Client, error) {
|
// The version parameter specifies the version of the Uncloud daemon to install. If empty, the latest version is used.
|
||||||
|
func (cli *CLI) provisionRemoteMachine(
|
||||||
|
ctx context.Context, remoteMachine RemoteMachine, version string,
|
||||||
|
) (*client.Client, error) {
|
||||||
// Provision the remote machine by installing the Uncloud daemon and dependencies over SSH.
|
// Provision the remote machine by installing the Uncloud daemon and dependencies over SSH.
|
||||||
sshClient, err := sshexec.Connect(remoteMachine.User, remoteMachine.Host, remoteMachine.Port, remoteMachine.KeyPath)
|
sshClient, err := sshexec.Connect(remoteMachine.User, remoteMachine.Host, remoteMachine.Port, remoteMachine.KeyPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -350,7 +389,7 @@ func (cli *CLI) provisionRemoteMachine(ctx context.Context, remoteMachine Remote
|
|||||||
}
|
}
|
||||||
exec := sshexec.NewRemote(sshClient)
|
exec := sshexec.NewRemote(sshClient)
|
||||||
// Install and run the Uncloud daemon and dependencies on the remote machine.
|
// Install and run the Uncloud daemon and dependencies on the remote machine.
|
||||||
if err = provisionMachine(ctx, exec); err != nil {
|
if err = provisionMachine(ctx, exec, version); err != nil {
|
||||||
return nil, fmt.Errorf("provision machine: %w", err)
|
return nil, fmt.Errorf("provision machine: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -387,7 +426,7 @@ func (cli *CLI) promptResetMachine() error {
|
|||||||
Negative("No").
|
Negative("No").
|
||||||
Value(&confirm),
|
Value(&confirm),
|
||||||
),
|
),
|
||||||
)
|
).WithAccessible(true)
|
||||||
if err := form.Run(); err != nil {
|
if err := form.Run(); err != nil {
|
||||||
return fmt.Errorf("prompt user to confirm: %w", err)
|
return fmt.Errorf("prompt user to confirm: %w", err)
|
||||||
}
|
}
|
||||||
@@ -396,7 +435,8 @@ func (cli *CLI) promptResetMachine() error {
|
|||||||
return fmt.Errorf("remote machine is already initialised as a cluster member")
|
return fmt.Errorf("remote machine is already initialised as a cluster member")
|
||||||
}
|
}
|
||||||
// TODO: implement resetting the remote machine.
|
// TODO: implement resetting the remote machine.
|
||||||
return fmt.Errorf("resetting the remote machine is not implemented yet")
|
return fmt.Errorf("resetting the remote machine is not implemented yet. " +
|
||||||
|
"Please manually run 'uncloud-uninstall' on the remote machine to fully uninstall Uncloud from it")
|
||||||
}
|
}
|
||||||
|
|
||||||
// ProgressOut returns an output stream for progress writer.
|
// ProgressOut returns an output stream for progress writer.
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
package config
|
|
||||||
|
|
||||||
type Cluster struct {
|
|
||||||
Name string `toml:"-"`
|
|
||||||
Connections []MachineConnection `toml:"connections"`
|
|
||||||
}
|
|
||||||
@@ -2,14 +2,15 @@ package config
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/BurntSushi/toml"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/goccy/go-yaml"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Clusters map[string]*Cluster `toml:"clusters"`
|
CurrentContext string `yaml:"current_context"`
|
||||||
CurrentCluster string `toml:"current_cluster"`
|
Contexts map[string]*Context `yaml:"contexts"`
|
||||||
|
|
||||||
// path is the file path config is read from.
|
// path is the file path config is read from.
|
||||||
path string
|
path string
|
||||||
@@ -18,10 +19,10 @@ type Config struct {
|
|||||||
func NewFromFile(path string) (*Config, error) {
|
func NewFromFile(path string) (*Config, error) {
|
||||||
_, err := os.Stat(path)
|
_, err := os.Stat(path)
|
||||||
if err != nil && !os.IsNotExist(err) {
|
if err != nil && !os.IsNotExist(err) {
|
||||||
return nil, fmt.Errorf("check file permissions %q: %w", path, err)
|
return nil, fmt.Errorf("check file permissions '%s': %w", path, err)
|
||||||
}
|
}
|
||||||
c := &Config{
|
c := &Config{
|
||||||
Clusters: map[string]*Cluster{},
|
Contexts: map[string]*Context{},
|
||||||
path: path,
|
path: path,
|
||||||
}
|
}
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
@@ -34,30 +35,37 @@ func NewFromFile(path string) (*Config, error) {
|
|||||||
return c, nil
|
return c, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Config) Path() string {
|
||||||
|
return c.path
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Config) Read() error {
|
func (c *Config) Read() error {
|
||||||
_, err := toml.DecodeFile(c.path, c)
|
data, err := os.ReadFile(c.path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("read config file %q: %w", c.path, err)
|
return fmt.Errorf("read config file '%s': %w", c.path, err)
|
||||||
}
|
}
|
||||||
|
if err = yaml.Unmarshal(data, c); err != nil {
|
||||||
|
return fmt.Errorf("parse config file '%s': %s", c.path, yaml.FormatError(err, true, true))
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Config) Save() error {
|
func (c *Config) Save() error {
|
||||||
dir, _ := filepath.Split(c.path)
|
dir, _ := filepath.Split(c.path)
|
||||||
if err := os.MkdirAll(dir, 0700); err != nil {
|
if err := os.MkdirAll(dir, 0700); err != nil {
|
||||||
return fmt.Errorf("create config directory %q: %w", dir, err)
|
return fmt.Errorf("create config directory '%s': %w", dir, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
f, err := os.OpenFile(c.path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
|
f, err := os.OpenFile(c.path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("write config file %q: %w", c.path, err)
|
return fmt.Errorf("write config file '%s': %w", c.path, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
encoder := toml.NewEncoder(f)
|
encoder := yaml.NewEncoder(f, yaml.Indent(2), yaml.IndentSequence(true))
|
||||||
encoder.Indent = ""
|
|
||||||
if err = encoder.Encode(c); err != nil {
|
if err = encoder.Encode(c); err != nil {
|
||||||
_ = f.Close()
|
_ = f.Close()
|
||||||
return fmt.Errorf("encode config file %q: %w", c.path, err)
|
return fmt.Errorf("encode config file '%s': %w", c.path, err)
|
||||||
}
|
}
|
||||||
return f.Close()
|
return f.Close()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"net/netip"
|
"net/netip"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/psviderski/uncloud/internal/secret"
|
"github.com/psviderski/uncloud/internal/secret"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -14,11 +15,13 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type MachineConnection struct {
|
type MachineConnection struct {
|
||||||
SSH SSHDestination `toml:"ssh,omitempty"`
|
SSH SSHDestination `yaml:"ssh,omitempty"`
|
||||||
SSHKeyFile string `toml:"ssh_key_file,omitempty"`
|
SSHKeyFile string `yaml:"ssh_key_file,omitempty"`
|
||||||
TCP netip.AddrPort `toml:"tcp,omitempty"`
|
// TCP is the address and port of the machine's API server.
|
||||||
Host string `toml:"host,omitempty"`
|
// The pointer is used to omit the field when not set. Otherwise, yaml marshalling includes an empty object.
|
||||||
PublicKey secret.Secret `toml:"public_key,omitempty"`
|
TCP *netip.AddrPort `yaml:"tcp,omitempty"`
|
||||||
|
Host string `yaml:"host,omitempty"`
|
||||||
|
PublicKey secret.Secret `yaml:"public_key,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// SSHDestination represents an SSH destination string in the canonical form of "user@host:port".
|
// SSHDestination represents an SSH destination string in the canonical form of "user@host:port".
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
type Context struct {
|
||||||
|
Name string `yaml:"-"`
|
||||||
|
Connections []MachineConnection `yaml:"connections"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ExpandCommaSeparatedValues takes a slice of strings and expands any comma-separated values into individual elements.
|
||||||
|
func ExpandCommaSeparatedValues(values []string) []string {
|
||||||
|
if len(values) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var expanded []string
|
||||||
|
for _, value := range values {
|
||||||
|
for _, v := range strings.Split(value, ",") {
|
||||||
|
if v = strings.TrimSpace(v); v != "" {
|
||||||
|
expanded = append(expanded, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return expanded
|
||||||
|
}
|
||||||
+19
-15
@@ -4,6 +4,8 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/psviderski/uncloud/internal/sshexec"
|
"github.com/psviderski/uncloud/internal/sshexec"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -17,36 +19,38 @@ type RemoteMachine struct {
|
|||||||
KeyPath string
|
KeyPath string
|
||||||
}
|
}
|
||||||
|
|
||||||
func installCmd(user string) string {
|
func installCmd(user string, version string) string {
|
||||||
sudoPrefix := "sudo"
|
sudoPrefix := ""
|
||||||
|
var env []string
|
||||||
|
|
||||||
// Add the SSH user (non-root) to the uncloud group to allow access to the Uncloud daemon unix socket.
|
// Add the SSH user (non-root) to the uncloud group to allow access to the Uncloud daemon unix socket.
|
||||||
env := "UNCLOUD_GROUP_ADD_USER=" + user
|
if user != "root" {
|
||||||
|
sudoPrefix = "sudo"
|
||||||
curlBashCmd := fmt.Sprintf(
|
env = append(env, "UNCLOUD_GROUP_ADD_USER="+sshexec.Quote(user))
|
||||||
"curl -fsSL %s | %s %s bash", sshexec.Quote(installScriptURL), sudoPrefix, sshexec.Quote(env),
|
|
||||||
)
|
|
||||||
|
|
||||||
if user == "root" {
|
|
||||||
curlBashCmd = fmt.Sprintf(
|
|
||||||
"curl -fsSL %s | bash", sshexec.Quote(installScriptURL),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
if version != "" {
|
||||||
|
env = append(env, "UNCLOUD_VERSION="+sshexec.Quote(version))
|
||||||
|
}
|
||||||
|
|
||||||
|
envCmd := strings.Join(env, " ")
|
||||||
|
curlBashCmd := fmt.Sprintf("curl -fsSL %s | %s %s bash", sshexec.Quote(installScriptURL), sudoPrefix, envCmd)
|
||||||
|
|
||||||
return curlBashCmd
|
return curlBashCmd
|
||||||
}
|
}
|
||||||
|
|
||||||
// provisionMachine provisions the remote machine by downloading the Uncloud install script from GitHub and running it.
|
// provisionMachine provisions the remote machine by downloading the Uncloud install script from GitHub and running it.
|
||||||
func provisionMachine(ctx context.Context, exec sshexec.Executor) error {
|
// If version is specified, it will be passed to the install script as UNCLOUD_VERSION environment variable.
|
||||||
|
func provisionMachine(ctx context.Context, exec sshexec.Executor, version string) error {
|
||||||
user, err := exec.Run(ctx, "whoami")
|
user, err := exec.Run(ctx, "whoami")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("run whoami: %w", err)
|
return fmt.Errorf("run whoami: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
installCmd := installCmd(user)
|
cmd := installCmd(user, version)
|
||||||
|
|
||||||
fmt.Println("Downloading Uncloud install script:", installScriptURL)
|
fmt.Println("Downloading Uncloud install script:", installScriptURL)
|
||||||
|
|
||||||
cmd := sshexec.QuoteCommand("bash", "-c", "set -o pipefail; "+installCmd)
|
cmd = sshexec.QuoteCommand("bash", "-c", "set -o pipefail; "+cmd)
|
||||||
if err = exec.Stream(ctx, cmd, os.Stdout, os.Stderr); err != nil {
|
if err = exec.Stream(ctx, cmd, os.Stdout, os.Stderr); err != nil {
|
||||||
return fmt.Errorf("download and run install script: %w", err)
|
return fmt.Errorf("download and run install script: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,12 +8,29 @@ import (
|
|||||||
|
|
||||||
func TestInstallCmd(t *testing.T) {
|
func TestInstallCmd(t *testing.T) {
|
||||||
t.Run("root", func(t *testing.T) {
|
t.Run("root", func(t *testing.T) {
|
||||||
cmd := installCmd("root")
|
cmd := installCmd("root", "")
|
||||||
assert.NotContains(t, cmd, "sudo")
|
assert.NotContains(t, cmd, "sudo")
|
||||||
|
assert.NotContains(t, cmd, "UNCLOUD_GROUP_ADD_USER")
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test with version
|
||||||
|
t.Run("root with version", func(t *testing.T) {
|
||||||
|
cmd := installCmd("root", "v1.2.3")
|
||||||
|
assert.NotContains(t, cmd, "sudo")
|
||||||
|
assert.NotContains(t, cmd, "UNCLOUD_GROUP_ADD_USER")
|
||||||
|
assert.Contains(t, cmd, "UNCLOUD_VERSION=v1.2.3")
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("nonroot", func(t *testing.T) {
|
t.Run("nonroot", func(t *testing.T) {
|
||||||
cmd := installCmd("nonroot")
|
cmd := installCmd("nonroot", "")
|
||||||
assert.Contains(t, cmd, "sudo")
|
assert.Contains(t, cmd, "sudo")
|
||||||
|
assert.Contains(t, cmd, "UNCLOUD_GROUP_ADD_USER=nonroot")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("nonroot with version", func(t *testing.T) {
|
||||||
|
cmd := installCmd("nonroot", "v1.2.3")
|
||||||
|
assert.Contains(t, cmd, "sudo")
|
||||||
|
assert.Contains(t, cmd, "UNCLOUD_GROUP_ADD_USER=nonroot")
|
||||||
|
assert.Contains(t, cmd, "UNCLOUD_VERSION=v1.2.3")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ func Confirm() (bool, error) {
|
|||||||
Negative("No").
|
Negative("No").
|
||||||
Value(&confirmed),
|
Value(&confirmed),
|
||||||
),
|
),
|
||||||
)
|
).WithAccessible(true)
|
||||||
if err := form.Run(); err != nil {
|
if err := form.Run(); err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-2
@@ -45,11 +45,19 @@ func (h *SlogTextHandler) Enabled(ctx context.Context, level slog.Level) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *SlogTextHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
|
func (h *SlogTextHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
|
||||||
return h.TextHandler.WithAttrs(attrs)
|
return &SlogTextHandler{
|
||||||
|
TextHandler: h.TextHandler.WithAttrs(attrs).(*slog.TextHandler),
|
||||||
|
opts: h.opts,
|
||||||
|
w: h.w,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *SlogTextHandler) WithGroup(name string) slog.Handler {
|
func (h *SlogTextHandler) WithGroup(name string) slog.Handler {
|
||||||
return h.TextHandler.WithGroup(name)
|
return &SlogTextHandler{
|
||||||
|
TextHandler: h.TextHandler.WithGroup(name).(*slog.TextHandler),
|
||||||
|
opts: h.opts,
|
||||||
|
w: h.w,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *SlogTextHandler) Handle(ctx context.Context, r slog.Record) error {
|
func (h *SlogTextHandler) Handle(ctx context.Context, r slog.Record) error {
|
||||||
|
|||||||
@@ -982,6 +982,309 @@ func (x *RemoteImage) GetManifest() []byte {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type CreateVolumeRequest struct {
|
||||||
|
state protoimpl.MessageState
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
|
||||||
|
// JSON serialised volume.CreateOptions.
|
||||||
|
Options []byte `protobuf:"bytes,1,opt,name=options,proto3" json:"options,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *CreateVolumeRequest) Reset() {
|
||||||
|
*x = CreateVolumeRequest{}
|
||||||
|
if protoimpl.UnsafeEnabled {
|
||||||
|
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[18]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *CreateVolumeRequest) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*CreateVolumeRequest) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *CreateVolumeRequest) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[18]
|
||||||
|
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 CreateVolumeRequest.ProtoReflect.Descriptor instead.
|
||||||
|
func (*CreateVolumeRequest) Descriptor() ([]byte, []int) {
|
||||||
|
return file_internal_machine_api_pb_docker_proto_rawDescGZIP(), []int{18}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *CreateVolumeRequest) GetOptions() []byte {
|
||||||
|
if x != nil {
|
||||||
|
return x.Options
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateVolumeResponse struct {
|
||||||
|
state protoimpl.MessageState
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
|
||||||
|
// JSON serialised volume.Volume.
|
||||||
|
Volume []byte `protobuf:"bytes,1,opt,name=volume,proto3" json:"volume,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *CreateVolumeResponse) Reset() {
|
||||||
|
*x = CreateVolumeResponse{}
|
||||||
|
if protoimpl.UnsafeEnabled {
|
||||||
|
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[19]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *CreateVolumeResponse) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*CreateVolumeResponse) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *CreateVolumeResponse) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[19]
|
||||||
|
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 CreateVolumeResponse.ProtoReflect.Descriptor instead.
|
||||||
|
func (*CreateVolumeResponse) Descriptor() ([]byte, []int) {
|
||||||
|
return file_internal_machine_api_pb_docker_proto_rawDescGZIP(), []int{19}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *CreateVolumeResponse) GetVolume() []byte {
|
||||||
|
if x != nil {
|
||||||
|
return x.Volume
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListVolumesRequest struct {
|
||||||
|
state protoimpl.MessageState
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
|
||||||
|
// JSON serialised volume.ListOptions.
|
||||||
|
Options []byte `protobuf:"bytes,1,opt,name=options,proto3" json:"options,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ListVolumesRequest) Reset() {
|
||||||
|
*x = ListVolumesRequest{}
|
||||||
|
if protoimpl.UnsafeEnabled {
|
||||||
|
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[20]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ListVolumesRequest) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*ListVolumesRequest) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *ListVolumesRequest) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[20]
|
||||||
|
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 ListVolumesRequest.ProtoReflect.Descriptor instead.
|
||||||
|
func (*ListVolumesRequest) Descriptor() ([]byte, []int) {
|
||||||
|
return file_internal_machine_api_pb_docker_proto_rawDescGZIP(), []int{20}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ListVolumesRequest) GetOptions() []byte {
|
||||||
|
if x != nil {
|
||||||
|
return x.Options
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListVolumesResponse struct {
|
||||||
|
state protoimpl.MessageState
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
|
||||||
|
// Must contain only one repeated messages field to allow broadcasting ListVolumes requests to multiple machines.
|
||||||
|
Messages []*MachineVolumes `protobuf:"bytes,1,rep,name=messages,proto3" json:"messages,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ListVolumesResponse) Reset() {
|
||||||
|
*x = ListVolumesResponse{}
|
||||||
|
if protoimpl.UnsafeEnabled {
|
||||||
|
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[21]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ListVolumesResponse) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*ListVolumesResponse) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *ListVolumesResponse) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[21]
|
||||||
|
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 ListVolumesResponse.ProtoReflect.Descriptor instead.
|
||||||
|
func (*ListVolumesResponse) Descriptor() ([]byte, []int) {
|
||||||
|
return file_internal_machine_api_pb_docker_proto_rawDescGZIP(), []int{21}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ListVolumesResponse) GetMessages() []*MachineVolumes {
|
||||||
|
if x != nil {
|
||||||
|
return x.Messages
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type MachineVolumes struct {
|
||||||
|
state protoimpl.MessageState
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
|
||||||
|
Metadata *Metadata `protobuf:"bytes,1,opt,name=metadata,proto3" json:"metadata,omitempty"`
|
||||||
|
// JSON serialised volume.ListResponse.
|
||||||
|
Response []byte `protobuf:"bytes,2,opt,name=response,proto3" json:"response,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *MachineVolumes) Reset() {
|
||||||
|
*x = MachineVolumes{}
|
||||||
|
if protoimpl.UnsafeEnabled {
|
||||||
|
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[22]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *MachineVolumes) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*MachineVolumes) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *MachineVolumes) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[22]
|
||||||
|
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 MachineVolumes.ProtoReflect.Descriptor instead.
|
||||||
|
func (*MachineVolumes) Descriptor() ([]byte, []int) {
|
||||||
|
return file_internal_machine_api_pb_docker_proto_rawDescGZIP(), []int{22}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *MachineVolumes) GetMetadata() *Metadata {
|
||||||
|
if x != nil {
|
||||||
|
return x.Metadata
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *MachineVolumes) GetResponse() []byte {
|
||||||
|
if x != nil {
|
||||||
|
return x.Response
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type RemoveVolumeRequest struct {
|
||||||
|
state protoimpl.MessageState
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
|
||||||
|
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
|
||||||
|
Force bool `protobuf:"varint,2,opt,name=force,proto3" json:"force,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *RemoveVolumeRequest) Reset() {
|
||||||
|
*x = RemoveVolumeRequest{}
|
||||||
|
if protoimpl.UnsafeEnabled {
|
||||||
|
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[23]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *RemoveVolumeRequest) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*RemoveVolumeRequest) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *RemoveVolumeRequest) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[23]
|
||||||
|
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 RemoveVolumeRequest.ProtoReflect.Descriptor instead.
|
||||||
|
func (*RemoveVolumeRequest) Descriptor() ([]byte, []int) {
|
||||||
|
return file_internal_machine_api_pb_docker_proto_rawDescGZIP(), []int{23}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *RemoveVolumeRequest) GetId() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Id
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *RemoveVolumeRequest) GetForce() bool {
|
||||||
|
if x != nil {
|
||||||
|
return x.Force
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
type CreateServiceContainerRequest struct {
|
type CreateServiceContainerRequest struct {
|
||||||
state protoimpl.MessageState
|
state protoimpl.MessageState
|
||||||
sizeCache protoimpl.SizeCache
|
sizeCache protoimpl.SizeCache
|
||||||
@@ -996,7 +1299,7 @@ type CreateServiceContainerRequest struct {
|
|||||||
func (x *CreateServiceContainerRequest) Reset() {
|
func (x *CreateServiceContainerRequest) Reset() {
|
||||||
*x = CreateServiceContainerRequest{}
|
*x = CreateServiceContainerRequest{}
|
||||||
if protoimpl.UnsafeEnabled {
|
if protoimpl.UnsafeEnabled {
|
||||||
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[18]
|
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[24]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@@ -1009,7 +1312,7 @@ func (x *CreateServiceContainerRequest) String() string {
|
|||||||
func (*CreateServiceContainerRequest) ProtoMessage() {}
|
func (*CreateServiceContainerRequest) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *CreateServiceContainerRequest) ProtoReflect() protoreflect.Message {
|
func (x *CreateServiceContainerRequest) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[18]
|
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[24]
|
||||||
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 {
|
||||||
@@ -1022,7 +1325,7 @@ func (x *CreateServiceContainerRequest) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use CreateServiceContainerRequest.ProtoReflect.Descriptor instead.
|
// Deprecated: Use CreateServiceContainerRequest.ProtoReflect.Descriptor instead.
|
||||||
func (*CreateServiceContainerRequest) Descriptor() ([]byte, []int) {
|
func (*CreateServiceContainerRequest) Descriptor() ([]byte, []int) {
|
||||||
return file_internal_machine_api_pb_docker_proto_rawDescGZIP(), []int{18}
|
return file_internal_machine_api_pb_docker_proto_rawDescGZIP(), []int{24}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *CreateServiceContainerRequest) GetServiceId() string {
|
func (x *CreateServiceContainerRequest) GetServiceId() string {
|
||||||
@@ -1060,7 +1363,7 @@ type ServiceContainer struct {
|
|||||||
func (x *ServiceContainer) Reset() {
|
func (x *ServiceContainer) Reset() {
|
||||||
*x = ServiceContainer{}
|
*x = ServiceContainer{}
|
||||||
if protoimpl.UnsafeEnabled {
|
if protoimpl.UnsafeEnabled {
|
||||||
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[19]
|
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[25]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@@ -1073,7 +1376,7 @@ func (x *ServiceContainer) String() string {
|
|||||||
func (*ServiceContainer) ProtoMessage() {}
|
func (*ServiceContainer) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *ServiceContainer) ProtoReflect() protoreflect.Message {
|
func (x *ServiceContainer) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[19]
|
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[25]
|
||||||
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 {
|
||||||
@@ -1086,7 +1389,7 @@ func (x *ServiceContainer) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use ServiceContainer.ProtoReflect.Descriptor instead.
|
// Deprecated: Use ServiceContainer.ProtoReflect.Descriptor instead.
|
||||||
func (*ServiceContainer) Descriptor() ([]byte, []int) {
|
func (*ServiceContainer) Descriptor() ([]byte, []int) {
|
||||||
return file_internal_machine_api_pb_docker_proto_rawDescGZIP(), []int{19}
|
return file_internal_machine_api_pb_docker_proto_rawDescGZIP(), []int{25}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *ServiceContainer) GetContainer() []byte {
|
func (x *ServiceContainer) GetContainer() []byte {
|
||||||
@@ -1116,7 +1419,7 @@ type ListServiceContainersRequest struct {
|
|||||||
func (x *ListServiceContainersRequest) Reset() {
|
func (x *ListServiceContainersRequest) Reset() {
|
||||||
*x = ListServiceContainersRequest{}
|
*x = ListServiceContainersRequest{}
|
||||||
if protoimpl.UnsafeEnabled {
|
if protoimpl.UnsafeEnabled {
|
||||||
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[20]
|
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[26]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@@ -1129,7 +1432,7 @@ func (x *ListServiceContainersRequest) String() string {
|
|||||||
func (*ListServiceContainersRequest) ProtoMessage() {}
|
func (*ListServiceContainersRequest) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *ListServiceContainersRequest) ProtoReflect() protoreflect.Message {
|
func (x *ListServiceContainersRequest) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[20]
|
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[26]
|
||||||
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 {
|
||||||
@@ -1142,7 +1445,7 @@ func (x *ListServiceContainersRequest) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use ListServiceContainersRequest.ProtoReflect.Descriptor instead.
|
// Deprecated: Use ListServiceContainersRequest.ProtoReflect.Descriptor instead.
|
||||||
func (*ListServiceContainersRequest) Descriptor() ([]byte, []int) {
|
func (*ListServiceContainersRequest) Descriptor() ([]byte, []int) {
|
||||||
return file_internal_machine_api_pb_docker_proto_rawDescGZIP(), []int{20}
|
return file_internal_machine_api_pb_docker_proto_rawDescGZIP(), []int{26}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *ListServiceContainersRequest) GetServiceId() string {
|
func (x *ListServiceContainersRequest) GetServiceId() string {
|
||||||
@@ -1172,7 +1475,7 @@ type ListServiceContainersResponse struct {
|
|||||||
func (x *ListServiceContainersResponse) Reset() {
|
func (x *ListServiceContainersResponse) Reset() {
|
||||||
*x = ListServiceContainersResponse{}
|
*x = ListServiceContainersResponse{}
|
||||||
if protoimpl.UnsafeEnabled {
|
if protoimpl.UnsafeEnabled {
|
||||||
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[21]
|
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[27]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@@ -1185,7 +1488,7 @@ func (x *ListServiceContainersResponse) String() string {
|
|||||||
func (*ListServiceContainersResponse) ProtoMessage() {}
|
func (*ListServiceContainersResponse) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *ListServiceContainersResponse) ProtoReflect() protoreflect.Message {
|
func (x *ListServiceContainersResponse) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[21]
|
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[27]
|
||||||
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 {
|
||||||
@@ -1198,7 +1501,7 @@ func (x *ListServiceContainersResponse) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use ListServiceContainersResponse.ProtoReflect.Descriptor instead.
|
// Deprecated: Use ListServiceContainersResponse.ProtoReflect.Descriptor instead.
|
||||||
func (*ListServiceContainersResponse) Descriptor() ([]byte, []int) {
|
func (*ListServiceContainersResponse) Descriptor() ([]byte, []int) {
|
||||||
return file_internal_machine_api_pb_docker_proto_rawDescGZIP(), []int{21}
|
return file_internal_machine_api_pb_docker_proto_rawDescGZIP(), []int{27}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *ListServiceContainersResponse) GetMessages() []*MachineServiceContainers {
|
func (x *ListServiceContainersResponse) GetMessages() []*MachineServiceContainers {
|
||||||
@@ -1220,7 +1523,7 @@ type MachineServiceContainers struct {
|
|||||||
func (x *MachineServiceContainers) Reset() {
|
func (x *MachineServiceContainers) Reset() {
|
||||||
*x = MachineServiceContainers{}
|
*x = MachineServiceContainers{}
|
||||||
if protoimpl.UnsafeEnabled {
|
if protoimpl.UnsafeEnabled {
|
||||||
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[22]
|
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[28]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@@ -1233,7 +1536,7 @@ func (x *MachineServiceContainers) String() string {
|
|||||||
func (*MachineServiceContainers) ProtoMessage() {}
|
func (*MachineServiceContainers) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *MachineServiceContainers) ProtoReflect() protoreflect.Message {
|
func (x *MachineServiceContainers) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[22]
|
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[28]
|
||||||
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 {
|
||||||
@@ -1246,7 +1549,7 @@ func (x *MachineServiceContainers) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use MachineServiceContainers.ProtoReflect.Descriptor instead.
|
// Deprecated: Use MachineServiceContainers.ProtoReflect.Descriptor instead.
|
||||||
func (*MachineServiceContainers) Descriptor() ([]byte, []int) {
|
func (*MachineServiceContainers) Descriptor() ([]byte, []int) {
|
||||||
return file_internal_machine_api_pb_docker_proto_rawDescGZIP(), []int{22}
|
return file_internal_machine_api_pb_docker_proto_rawDescGZIP(), []int{28}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *MachineServiceContainers) GetMetadata() *Metadata {
|
func (x *MachineServiceContainers) GetMetadata() *Metadata {
|
||||||
@@ -1352,108 +1655,143 @@ var file_internal_machine_api_pb_docker_proto_rawDesc = []byte{
|
|||||||
0x0a, 0x09, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28,
|
0x0a, 0x09, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28,
|
||||||
0x09, 0x52, 0x09, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x12, 0x1a, 0x0a, 0x08,
|
0x09, 0x52, 0x09, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x12, 0x1a, 0x0a, 0x08,
|
||||||
0x6d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08,
|
0x6d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08,
|
||||||
0x6d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x22, 0x88, 0x01, 0x0a, 0x1d, 0x43, 0x72, 0x65,
|
0x6d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x22, 0x2f, 0x0a, 0x13, 0x43, 0x72, 0x65, 0x61,
|
||||||
0x61, 0x74, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69,
|
0x74, 0x65, 0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12,
|
||||||
0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x65,
|
0x18, 0x0a, 0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c,
|
||||||
0x72, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09,
|
0x52, 0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x2e, 0x0a, 0x14, 0x43, 0x72, 0x65,
|
||||||
0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x49, 0x64, 0x12, 0x21, 0x0a, 0x0c, 0x73, 0x65, 0x72,
|
0x61, 0x74, 0x65, 0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
|
||||||
0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x70, 0x65, 0x63, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52,
|
0x65, 0x12, 0x16, 0x0a, 0x06, 0x76, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28,
|
||||||
0x0b, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x53, 0x70, 0x65, 0x63, 0x12, 0x25, 0x0a, 0x0e,
|
0x0c, 0x52, 0x06, 0x76, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x22, 0x2e, 0x0a, 0x12, 0x4c, 0x69, 0x73,
|
||||||
0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03,
|
0x74, 0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12,
|
||||||
0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x4e,
|
0x18, 0x0a, 0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c,
|
||||||
0x61, 0x6d, 0x65, 0x22, 0x53, 0x0a, 0x10, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f,
|
0x52, 0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x46, 0x0a, 0x13, 0x4c, 0x69, 0x73,
|
||||||
0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x1c, 0x0a, 0x09, 0x63, 0x6f, 0x6e, 0x74, 0x61,
|
0x74, 0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
|
||||||
0x69, 0x6e, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x63, 0x6f, 0x6e, 0x74,
|
0x12, 0x2f, 0x0a, 0x08, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03,
|
||||||
0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x21, 0x0a, 0x0c, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65,
|
0x28, 0x0b, 0x32, 0x13, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65,
|
||||||
0x5f, 0x73, 0x70, 0x65, 0x63, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0b, 0x73, 0x65, 0x72,
|
0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x73, 0x52, 0x08, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65,
|
||||||
0x76, 0x69, 0x63, 0x65, 0x53, 0x70, 0x65, 0x63, 0x22, 0x57, 0x0a, 0x1c, 0x4c, 0x69, 0x73, 0x74,
|
0x73, 0x22, 0x57, 0x0a, 0x0e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x56, 0x6f, 0x6c, 0x75,
|
||||||
0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72,
|
0x6d, 0x65, 0x73, 0x12, 0x29, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18,
|
||||||
0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x65, 0x72, 0x76,
|
0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d, 0x65, 0x74, 0x61,
|
||||||
0x69, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x65,
|
0x64, 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x1a,
|
||||||
0x72, 0x76, 0x69, 0x63, 0x65, 0x49, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f,
|
0x0a, 0x08, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c,
|
||||||
0x6e, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e,
|
0x52, 0x08, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x3b, 0x0a, 0x13, 0x52, 0x65,
|
||||||
0x73, 0x22, 0x5a, 0x0a, 0x1d, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65,
|
0x6d, 0x6f, 0x76, 0x65, 0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
|
||||||
0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
|
0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69,
|
||||||
0x73, 0x65, 0x12, 0x39, 0x0a, 0x08, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x18, 0x01,
|
0x64, 0x12, 0x14, 0x0a, 0x05, 0x66, 0x6f, 0x72, 0x63, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08,
|
||||||
0x20, 0x03, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69,
|
0x52, 0x05, 0x66, 0x6f, 0x72, 0x63, 0x65, 0x22, 0x88, 0x01, 0x0a, 0x1d, 0x43, 0x72, 0x65, 0x61,
|
||||||
0x6e, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e,
|
0x74, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e,
|
||||||
0x65, 0x72, 0x73, 0x52, 0x08, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x22, 0x7c, 0x0a,
|
0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x65, 0x72,
|
||||||
0x18, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43,
|
0x76, 0x69, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73,
|
||||||
0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x12, 0x29, 0x0a, 0x08, 0x6d, 0x65, 0x74,
|
0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x49, 0x64, 0x12, 0x21, 0x0a, 0x0c, 0x73, 0x65, 0x72, 0x76,
|
||||||
0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x61, 0x70,
|
0x69, 0x63, 0x65, 0x5f, 0x73, 0x70, 0x65, 0x63, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0b,
|
||||||
0x69, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61,
|
0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x53, 0x70, 0x65, 0x63, 0x12, 0x25, 0x0a, 0x0e, 0x63,
|
||||||
0x64, 0x61, 0x74, 0x61, 0x12, 0x35, 0x0a, 0x0a, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65,
|
0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20,
|
||||||
0x72, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x53,
|
0x01, 0x28, 0x09, 0x52, 0x0d, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x4e, 0x61,
|
||||||
0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52,
|
0x6d, 0x65, 0x22, 0x53, 0x0a, 0x10, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e,
|
||||||
0x0a, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x32, 0xf3, 0x07, 0x0a, 0x06,
|
0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x1c, 0x0a, 0x09, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69,
|
||||||
0x44, 0x6f, 0x63, 0x6b, 0x65, 0x72, 0x12, 0x4c, 0x0a, 0x0f, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65,
|
0x6e, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x63, 0x6f, 0x6e, 0x74, 0x61,
|
||||||
0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x1b, 0x2e, 0x61, 0x70, 0x69, 0x2e,
|
0x69, 0x6e, 0x65, 0x72, 0x12, 0x21, 0x0a, 0x0c, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x5f,
|
||||||
0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52,
|
0x73, 0x70, 0x65, 0x63, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0b, 0x73, 0x65, 0x72, 0x76,
|
||||||
0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x72, 0x65,
|
0x69, 0x63, 0x65, 0x53, 0x70, 0x65, 0x63, 0x22, 0x57, 0x0a, 0x1c, 0x4c, 0x69, 0x73, 0x74, 0x53,
|
||||||
0x61, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70,
|
0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73,
|
||||||
0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4f, 0x0a, 0x10, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x43,
|
0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x65, 0x72, 0x76, 0x69,
|
||||||
0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x1c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49,
|
0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x65, 0x72,
|
||||||
0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52,
|
0x76, 0x69, 0x63, 0x65, 0x49, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e,
|
||||||
0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x73,
|
0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73,
|
||||||
0x70, 0x65, 0x63, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x73,
|
0x22, 0x5a, 0x0a, 0x1d, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43,
|
||||||
0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x44, 0x0a, 0x0e, 0x53, 0x74, 0x61, 0x72, 0x74, 0x43, 0x6f,
|
0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
|
||||||
0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x1a, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x53, 0x74,
|
0x65, 0x12, 0x39, 0x0a, 0x08, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x18, 0x01, 0x20,
|
||||||
0x61, 0x72, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75,
|
0x03, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e,
|
||||||
0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f,
|
0x65, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65,
|
||||||
0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x42, 0x0a, 0x0d, 0x53,
|
0x72, 0x73, 0x52, 0x08, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x22, 0x7c, 0x0a, 0x18,
|
||||||
0x74, 0x6f, 0x70, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x19, 0x2e, 0x61,
|
0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f,
|
||||||
0x70, 0x69, 0x2e, 0x53, 0x74, 0x6f, 0x70, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72,
|
0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x12, 0x29, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61,
|
||||||
0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65,
|
0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x61, 0x70, 0x69,
|
||||||
0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12,
|
0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64,
|
||||||
0x49, 0x0a, 0x0e, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72,
|
0x61, 0x74, 0x61, 0x12, 0x35, 0x0a, 0x0a, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72,
|
||||||
0x73, 0x12, 0x1a, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x74,
|
0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x53, 0x65,
|
||||||
0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e,
|
0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x0a,
|
||||||
0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65,
|
0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x32, 0xbc, 0x09, 0x0a, 0x06, 0x44,
|
||||||
0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x46, 0x0a, 0x0f, 0x52, 0x65,
|
0x6f, 0x63, 0x6b, 0x65, 0x72, 0x12, 0x4c, 0x0a, 0x0f, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x43,
|
||||||
0x6d, 0x6f, 0x76, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x1b, 0x2e,
|
0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x1b, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43,
|
||||||
0x61, 0x70, 0x69, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69,
|
0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65,
|
||||||
0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f,
|
0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x72, 0x65, 0x61,
|
||||||
0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70,
|
0x74, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f,
|
||||||
0x74, 0x79, 0x12, 0x36, 0x0a, 0x09, 0x50, 0x75, 0x6c, 0x6c, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x12,
|
0x6e, 0x73, 0x65, 0x12, 0x4f, 0x0a, 0x10, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x43, 0x6f,
|
||||||
0x15, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x50, 0x75, 0x6c, 0x6c, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x52,
|
0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x1c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e,
|
||||||
0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x10, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4a, 0x53, 0x4f,
|
0x73, 0x70, 0x65, 0x63, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65,
|
||||||
0x4e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x30, 0x01, 0x12, 0x43, 0x0a, 0x0c, 0x49, 0x6e,
|
0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x73, 0x70,
|
||||||
0x73, 0x70, 0x65, 0x63, 0x74, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x12, 0x18, 0x2e, 0x61, 0x70, 0x69,
|
0x65, 0x63, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70,
|
||||||
0x2e, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71,
|
0x6f, 0x6e, 0x73, 0x65, 0x12, 0x44, 0x0a, 0x0e, 0x53, 0x74, 0x61, 0x72, 0x74, 0x43, 0x6f, 0x6e,
|
||||||
0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x73, 0x70, 0x65,
|
0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x1a, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x53, 0x74, 0x61,
|
||||||
0x63, 0x74, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12,
|
0x72, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65,
|
||||||
0x55, 0x0a, 0x12, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65,
|
0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74,
|
||||||
0x49, 0x6d, 0x61, 0x67, 0x65, 0x12, 0x1e, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x73, 0x70,
|
0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x42, 0x0a, 0x0d, 0x53, 0x74,
|
||||||
0x65, 0x63, 0x74, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x52, 0x65,
|
0x6f, 0x70, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x19, 0x2e, 0x61, 0x70,
|
||||||
0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x73, 0x70,
|
0x69, 0x2e, 0x53, 0x74, 0x6f, 0x70, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52,
|
||||||
0x65, 0x63, 0x74, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x52, 0x65,
|
0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e,
|
||||||
0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x5a, 0x0a, 0x16, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65,
|
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x49,
|
||||||
0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72,
|
0x0a, 0x0e, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73,
|
||||||
0x12, 0x22, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x53, 0x65, 0x72,
|
0x12, 0x1a, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61,
|
||||||
0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71,
|
0x69, 0x6e, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x61,
|
||||||
0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74,
|
0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72,
|
||||||
0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
|
0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x46, 0x0a, 0x0f, 0x52, 0x65, 0x6d,
|
||||||
0x73, 0x65, 0x12, 0x4e, 0x0a, 0x17, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72,
|
0x6f, 0x76, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x1b, 0x2e, 0x61,
|
||||||
0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x1c, 0x2e,
|
|
||||||
0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61,
|
|
||||||
0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x15, 0x2e, 0x61, 0x70,
|
|
||||||
0x69, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e,
|
|
||||||
0x65, 0x72, 0x12, 0x5e, 0x0a, 0x15, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63,
|
|
||||||
0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x12, 0x21, 0x2e, 0x61, 0x70,
|
|
||||||
0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e,
|
|
||||||
0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22,
|
|
||||||
0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65,
|
|
||||||
0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
|
|
||||||
0x73, 0x65, 0x12, 0x4d, 0x0a, 0x16, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x53, 0x65, 0x72, 0x76,
|
|
||||||
0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x1b, 0x2e, 0x61,
|
|
||||||
0x70, 0x69, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e,
|
0x70, 0x69, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e,
|
||||||
0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67,
|
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,
|
0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74,
|
||||||
0x79, 0x42, 0x37, 0x5a, 0x35, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f,
|
0x79, 0x12, 0x36, 0x0a, 0x09, 0x50, 0x75, 0x6c, 0x6c, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x12, 0x15,
|
||||||
0x70, 0x73, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x6b, 0x69, 0x2f, 0x75, 0x6e, 0x63, 0x6c, 0x6f,
|
0x2e, 0x61, 0x70, 0x69, 0x2e, 0x50, 0x75, 0x6c, 0x6c, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x52, 0x65,
|
||||||
0x75, 0x64, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x6d, 0x61, 0x63, 0x68,
|
0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x10, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4a, 0x53, 0x4f, 0x4e,
|
||||||
0x69, 0x6e, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74,
|
0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x30, 0x01, 0x12, 0x43, 0x0a, 0x0c, 0x49, 0x6e, 0x73,
|
||||||
0x6f, 0x33,
|
0x70, 0x65, 0x63, 0x74, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x12, 0x18, 0x2e, 0x61, 0x70, 0x69, 0x2e,
|
||||||
|
0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75,
|
||||||
|
0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63,
|
||||||
|
0x74, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x55,
|
||||||
|
0x0a, 0x12, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x49,
|
||||||
|
0x6d, 0x61, 0x67, 0x65, 0x12, 0x1e, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x73, 0x70, 0x65,
|
||||||
|
0x63, 0x74, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71,
|
||||||
|
0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x73, 0x70, 0x65,
|
||||||
|
0x63, 0x74, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73,
|
||||||
|
0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x43, 0x0a, 0x0c, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x56,
|
||||||
|
0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x12, 0x18, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x72, 0x65, 0x61,
|
||||||
|
0x74, 0x65, 0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a,
|
||||||
|
0x19, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x56, 0x6f, 0x6c, 0x75,
|
||||||
|
0x6d, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x40, 0x0a, 0x0b, 0x4c, 0x69,
|
||||||
|
0x73, 0x74, 0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x73, 0x12, 0x17, 0x2e, 0x61, 0x70, 0x69, 0x2e,
|
||||||
|
0x4c, 0x69, 0x73, 0x74, 0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65,
|
||||||
|
0x73, 0x74, 0x1a, 0x18, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x56, 0x6f, 0x6c,
|
||||||
|
0x75, 0x6d, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x40, 0x0a, 0x0c,
|
||||||
|
0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x12, 0x18, 0x2e, 0x61,
|
||||||
|
0x70, 0x69, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 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, 0x5a,
|
||||||
|
0x0a, 0x16, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43,
|
||||||
|
0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x22, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43,
|
||||||
|
0x72, 0x65, 0x61, 0x74, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x74,
|
||||||
|
0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x61,
|
||||||
|
0x70, 0x69, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e,
|
||||||
|
0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4e, 0x0a, 0x17, 0x49, 0x6e,
|
||||||
|
0x73, 0x70, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x74,
|
||||||
|
0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x1c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x73, 0x70,
|
||||||
|
0x65, 0x63, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75,
|
||||||
|
0x65, 0x73, 0x74, 0x1a, 0x15, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63,
|
||||||
|
0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x5e, 0x0a, 0x15, 0x4c, 0x69,
|
||||||
|
0x73, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e,
|
||||||
|
0x65, 0x72, 0x73, 0x12, 0x21, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x65,
|
||||||
|
0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x52,
|
||||||
|
0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73,
|
||||||
|
0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65,
|
||||||
|
0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4d, 0x0a, 0x16, 0x52, 0x65,
|
||||||
|
0x6d, 0x6f, 0x76, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61,
|
||||||
|
0x69, 0x6e, 0x65, 0x72, 0x12, 0x1b, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x76,
|
||||||
|
0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
|
||||||
|
0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f,
|
||||||
|
0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 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 (
|
||||||
@@ -1468,7 +1806,7 @@ func file_internal_machine_api_pb_docker_proto_rawDescGZIP() []byte {
|
|||||||
return file_internal_machine_api_pb_docker_proto_rawDescData
|
return file_internal_machine_api_pb_docker_proto_rawDescData
|
||||||
}
|
}
|
||||||
|
|
||||||
var file_internal_machine_api_pb_docker_proto_msgTypes = make([]protoimpl.MessageInfo, 23)
|
var file_internal_machine_api_pb_docker_proto_msgTypes = make([]protoimpl.MessageInfo, 29)
|
||||||
var file_internal_machine_api_pb_docker_proto_goTypes = []any{
|
var file_internal_machine_api_pb_docker_proto_goTypes = []any{
|
||||||
(*CreateContainerRequest)(nil), // 0: api.CreateContainerRequest
|
(*CreateContainerRequest)(nil), // 0: api.CreateContainerRequest
|
||||||
(*CreateContainerResponse)(nil), // 1: api.CreateContainerResponse
|
(*CreateContainerResponse)(nil), // 1: api.CreateContainerResponse
|
||||||
@@ -1488,55 +1826,69 @@ var file_internal_machine_api_pb_docker_proto_goTypes = []any{
|
|||||||
(*InspectRemoteImageRequest)(nil), // 15: api.InspectRemoteImageRequest
|
(*InspectRemoteImageRequest)(nil), // 15: api.InspectRemoteImageRequest
|
||||||
(*InspectRemoteImageResponse)(nil), // 16: api.InspectRemoteImageResponse
|
(*InspectRemoteImageResponse)(nil), // 16: api.InspectRemoteImageResponse
|
||||||
(*RemoteImage)(nil), // 17: api.RemoteImage
|
(*RemoteImage)(nil), // 17: api.RemoteImage
|
||||||
(*CreateServiceContainerRequest)(nil), // 18: api.CreateServiceContainerRequest
|
(*CreateVolumeRequest)(nil), // 18: api.CreateVolumeRequest
|
||||||
(*ServiceContainer)(nil), // 19: api.ServiceContainer
|
(*CreateVolumeResponse)(nil), // 19: api.CreateVolumeResponse
|
||||||
(*ListServiceContainersRequest)(nil), // 20: api.ListServiceContainersRequest
|
(*ListVolumesRequest)(nil), // 20: api.ListVolumesRequest
|
||||||
(*ListServiceContainersResponse)(nil), // 21: api.ListServiceContainersResponse
|
(*ListVolumesResponse)(nil), // 21: api.ListVolumesResponse
|
||||||
(*MachineServiceContainers)(nil), // 22: api.MachineServiceContainers
|
(*MachineVolumes)(nil), // 22: api.MachineVolumes
|
||||||
(*Metadata)(nil), // 23: api.Metadata
|
(*RemoveVolumeRequest)(nil), // 23: api.RemoveVolumeRequest
|
||||||
(*emptypb.Empty)(nil), // 24: google.protobuf.Empty
|
(*CreateServiceContainerRequest)(nil), // 24: api.CreateServiceContainerRequest
|
||||||
|
(*ServiceContainer)(nil), // 25: api.ServiceContainer
|
||||||
|
(*ListServiceContainersRequest)(nil), // 26: api.ListServiceContainersRequest
|
||||||
|
(*ListServiceContainersResponse)(nil), // 27: api.ListServiceContainersResponse
|
||||||
|
(*MachineServiceContainers)(nil), // 28: api.MachineServiceContainers
|
||||||
|
(*Metadata)(nil), // 29: api.Metadata
|
||||||
|
(*emptypb.Empty)(nil), // 30: google.protobuf.Empty
|
||||||
}
|
}
|
||||||
var file_internal_machine_api_pb_docker_proto_depIdxs = []int32{
|
var file_internal_machine_api_pb_docker_proto_depIdxs = []int32{
|
||||||
8, // 0: api.ListContainersResponse.messages:type_name -> api.MachineContainers
|
8, // 0: api.ListContainersResponse.messages:type_name -> api.MachineContainers
|
||||||
23, // 1: api.MachineContainers.metadata:type_name -> api.Metadata
|
29, // 1: api.MachineContainers.metadata:type_name -> api.Metadata
|
||||||
14, // 2: api.InspectImageResponse.messages:type_name -> api.Image
|
14, // 2: api.InspectImageResponse.messages:type_name -> api.Image
|
||||||
23, // 3: api.Image.metadata:type_name -> api.Metadata
|
29, // 3: api.Image.metadata:type_name -> api.Metadata
|
||||||
17, // 4: api.InspectRemoteImageResponse.messages:type_name -> api.RemoteImage
|
17, // 4: api.InspectRemoteImageResponse.messages:type_name -> api.RemoteImage
|
||||||
23, // 5: api.RemoteImage.metadata:type_name -> api.Metadata
|
29, // 5: api.RemoteImage.metadata:type_name -> api.Metadata
|
||||||
22, // 6: api.ListServiceContainersResponse.messages:type_name -> api.MachineServiceContainers
|
22, // 6: api.ListVolumesResponse.messages:type_name -> api.MachineVolumes
|
||||||
23, // 7: api.MachineServiceContainers.metadata:type_name -> api.Metadata
|
29, // 7: api.MachineVolumes.metadata:type_name -> api.Metadata
|
||||||
19, // 8: api.MachineServiceContainers.containers:type_name -> api.ServiceContainer
|
28, // 8: api.ListServiceContainersResponse.messages:type_name -> api.MachineServiceContainers
|
||||||
0, // 9: api.Docker.CreateContainer:input_type -> api.CreateContainerRequest
|
29, // 9: api.MachineServiceContainers.metadata:type_name -> api.Metadata
|
||||||
2, // 10: api.Docker.InspectContainer:input_type -> api.InspectContainerRequest
|
25, // 10: api.MachineServiceContainers.containers:type_name -> api.ServiceContainer
|
||||||
4, // 11: api.Docker.StartContainer:input_type -> api.StartContainerRequest
|
0, // 11: api.Docker.CreateContainer:input_type -> api.CreateContainerRequest
|
||||||
5, // 12: api.Docker.StopContainer:input_type -> api.StopContainerRequest
|
2, // 12: api.Docker.InspectContainer:input_type -> api.InspectContainerRequest
|
||||||
6, // 13: api.Docker.ListContainers:input_type -> api.ListContainersRequest
|
4, // 13: api.Docker.StartContainer:input_type -> api.StartContainerRequest
|
||||||
9, // 14: api.Docker.RemoveContainer:input_type -> api.RemoveContainerRequest
|
5, // 14: api.Docker.StopContainer:input_type -> api.StopContainerRequest
|
||||||
10, // 15: api.Docker.PullImage:input_type -> api.PullImageRequest
|
6, // 15: api.Docker.ListContainers:input_type -> api.ListContainersRequest
|
||||||
12, // 16: api.Docker.InspectImage:input_type -> api.InspectImageRequest
|
9, // 16: api.Docker.RemoveContainer:input_type -> api.RemoveContainerRequest
|
||||||
15, // 17: api.Docker.InspectRemoteImage:input_type -> api.InspectRemoteImageRequest
|
10, // 17: api.Docker.PullImage:input_type -> api.PullImageRequest
|
||||||
18, // 18: api.Docker.CreateServiceContainer:input_type -> api.CreateServiceContainerRequest
|
12, // 18: api.Docker.InspectImage:input_type -> api.InspectImageRequest
|
||||||
2, // 19: api.Docker.InspectServiceContainer:input_type -> api.InspectContainerRequest
|
15, // 19: api.Docker.InspectRemoteImage:input_type -> api.InspectRemoteImageRequest
|
||||||
20, // 20: api.Docker.ListServiceContainers:input_type -> api.ListServiceContainersRequest
|
18, // 20: api.Docker.CreateVolume:input_type -> api.CreateVolumeRequest
|
||||||
9, // 21: api.Docker.RemoveServiceContainer:input_type -> api.RemoveContainerRequest
|
20, // 21: api.Docker.ListVolumes:input_type -> api.ListVolumesRequest
|
||||||
1, // 22: api.Docker.CreateContainer:output_type -> api.CreateContainerResponse
|
23, // 22: api.Docker.RemoveVolume:input_type -> api.RemoveVolumeRequest
|
||||||
3, // 23: api.Docker.InspectContainer:output_type -> api.InspectContainerResponse
|
24, // 23: api.Docker.CreateServiceContainer:input_type -> api.CreateServiceContainerRequest
|
||||||
24, // 24: api.Docker.StartContainer:output_type -> google.protobuf.Empty
|
2, // 24: api.Docker.InspectServiceContainer:input_type -> api.InspectContainerRequest
|
||||||
24, // 25: api.Docker.StopContainer:output_type -> google.protobuf.Empty
|
26, // 25: api.Docker.ListServiceContainers:input_type -> api.ListServiceContainersRequest
|
||||||
7, // 26: api.Docker.ListContainers:output_type -> api.ListContainersResponse
|
9, // 26: api.Docker.RemoveServiceContainer:input_type -> api.RemoveContainerRequest
|
||||||
24, // 27: api.Docker.RemoveContainer:output_type -> google.protobuf.Empty
|
1, // 27: api.Docker.CreateContainer:output_type -> api.CreateContainerResponse
|
||||||
11, // 28: api.Docker.PullImage:output_type -> api.JSONMessage
|
3, // 28: api.Docker.InspectContainer:output_type -> api.InspectContainerResponse
|
||||||
13, // 29: api.Docker.InspectImage:output_type -> api.InspectImageResponse
|
30, // 29: api.Docker.StartContainer:output_type -> google.protobuf.Empty
|
||||||
16, // 30: api.Docker.InspectRemoteImage:output_type -> api.InspectRemoteImageResponse
|
30, // 30: api.Docker.StopContainer:output_type -> google.protobuf.Empty
|
||||||
1, // 31: api.Docker.CreateServiceContainer:output_type -> api.CreateContainerResponse
|
7, // 31: api.Docker.ListContainers:output_type -> api.ListContainersResponse
|
||||||
19, // 32: api.Docker.InspectServiceContainer:output_type -> api.ServiceContainer
|
30, // 32: api.Docker.RemoveContainer:output_type -> google.protobuf.Empty
|
||||||
21, // 33: api.Docker.ListServiceContainers:output_type -> api.ListServiceContainersResponse
|
11, // 33: api.Docker.PullImage:output_type -> api.JSONMessage
|
||||||
24, // 34: api.Docker.RemoveServiceContainer:output_type -> google.protobuf.Empty
|
13, // 34: api.Docker.InspectImage:output_type -> api.InspectImageResponse
|
||||||
22, // [22:35] is the sub-list for method output_type
|
16, // 35: api.Docker.InspectRemoteImage:output_type -> api.InspectRemoteImageResponse
|
||||||
9, // [9:22] is the sub-list for method input_type
|
19, // 36: api.Docker.CreateVolume:output_type -> api.CreateVolumeResponse
|
||||||
9, // [9:9] is the sub-list for extension type_name
|
21, // 37: api.Docker.ListVolumes:output_type -> api.ListVolumesResponse
|
||||||
9, // [9:9] is the sub-list for extension extendee
|
30, // 38: api.Docker.RemoveVolume:output_type -> google.protobuf.Empty
|
||||||
0, // [0:9] is the sub-list for field type_name
|
1, // 39: api.Docker.CreateServiceContainer:output_type -> api.CreateContainerResponse
|
||||||
|
25, // 40: api.Docker.InspectServiceContainer:output_type -> api.ServiceContainer
|
||||||
|
27, // 41: api.Docker.ListServiceContainers:output_type -> api.ListServiceContainersResponse
|
||||||
|
30, // 42: api.Docker.RemoveServiceContainer:output_type -> google.protobuf.Empty
|
||||||
|
27, // [27:43] is the sub-list for method output_type
|
||||||
|
11, // [11:27] is the sub-list for method input_type
|
||||||
|
11, // [11:11] is the sub-list for extension type_name
|
||||||
|
11, // [11:11] is the sub-list for extension extendee
|
||||||
|
0, // [0:11] is the sub-list for field type_name
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() { file_internal_machine_api_pb_docker_proto_init() }
|
func init() { file_internal_machine_api_pb_docker_proto_init() }
|
||||||
@@ -1763,7 +2115,7 @@ func file_internal_machine_api_pb_docker_proto_init() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
file_internal_machine_api_pb_docker_proto_msgTypes[18].Exporter = func(v any, i int) any {
|
file_internal_machine_api_pb_docker_proto_msgTypes[18].Exporter = func(v any, i int) any {
|
||||||
switch v := v.(*CreateServiceContainerRequest); i {
|
switch v := v.(*CreateVolumeRequest); i {
|
||||||
case 0:
|
case 0:
|
||||||
return &v.state
|
return &v.state
|
||||||
case 1:
|
case 1:
|
||||||
@@ -1775,7 +2127,7 @@ func file_internal_machine_api_pb_docker_proto_init() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
file_internal_machine_api_pb_docker_proto_msgTypes[19].Exporter = func(v any, i int) any {
|
file_internal_machine_api_pb_docker_proto_msgTypes[19].Exporter = func(v any, i int) any {
|
||||||
switch v := v.(*ServiceContainer); i {
|
switch v := v.(*CreateVolumeResponse); i {
|
||||||
case 0:
|
case 0:
|
||||||
return &v.state
|
return &v.state
|
||||||
case 1:
|
case 1:
|
||||||
@@ -1787,7 +2139,7 @@ func file_internal_machine_api_pb_docker_proto_init() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
file_internal_machine_api_pb_docker_proto_msgTypes[20].Exporter = func(v any, i int) any {
|
file_internal_machine_api_pb_docker_proto_msgTypes[20].Exporter = func(v any, i int) any {
|
||||||
switch v := v.(*ListServiceContainersRequest); i {
|
switch v := v.(*ListVolumesRequest); i {
|
||||||
case 0:
|
case 0:
|
||||||
return &v.state
|
return &v.state
|
||||||
case 1:
|
case 1:
|
||||||
@@ -1799,7 +2151,7 @@ func file_internal_machine_api_pb_docker_proto_init() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
file_internal_machine_api_pb_docker_proto_msgTypes[21].Exporter = func(v any, i int) any {
|
file_internal_machine_api_pb_docker_proto_msgTypes[21].Exporter = func(v any, i int) any {
|
||||||
switch v := v.(*ListServiceContainersResponse); i {
|
switch v := v.(*ListVolumesResponse); i {
|
||||||
case 0:
|
case 0:
|
||||||
return &v.state
|
return &v.state
|
||||||
case 1:
|
case 1:
|
||||||
@@ -1811,6 +2163,78 @@ func file_internal_machine_api_pb_docker_proto_init() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
file_internal_machine_api_pb_docker_proto_msgTypes[22].Exporter = func(v any, i int) any {
|
file_internal_machine_api_pb_docker_proto_msgTypes[22].Exporter = func(v any, i int) any {
|
||||||
|
switch v := v.(*MachineVolumes); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file_internal_machine_api_pb_docker_proto_msgTypes[23].Exporter = func(v any, i int) any {
|
||||||
|
switch v := v.(*RemoveVolumeRequest); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file_internal_machine_api_pb_docker_proto_msgTypes[24].Exporter = func(v any, i int) any {
|
||||||
|
switch v := v.(*CreateServiceContainerRequest); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file_internal_machine_api_pb_docker_proto_msgTypes[25].Exporter = func(v any, i int) any {
|
||||||
|
switch v := v.(*ServiceContainer); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file_internal_machine_api_pb_docker_proto_msgTypes[26].Exporter = func(v any, i int) any {
|
||||||
|
switch v := v.(*ListServiceContainersRequest); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file_internal_machine_api_pb_docker_proto_msgTypes[27].Exporter = func(v any, i int) any {
|
||||||
|
switch v := v.(*ListServiceContainersResponse); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file_internal_machine_api_pb_docker_proto_msgTypes[28].Exporter = func(v any, i int) any {
|
||||||
switch v := v.(*MachineServiceContainers); i {
|
switch v := v.(*MachineServiceContainers); i {
|
||||||
case 0:
|
case 0:
|
||||||
return &v.state
|
return &v.state
|
||||||
@@ -1829,7 +2253,7 @@ func file_internal_machine_api_pb_docker_proto_init() {
|
|||||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||||
RawDescriptor: file_internal_machine_api_pb_docker_proto_rawDesc,
|
RawDescriptor: file_internal_machine_api_pb_docker_proto_rawDesc,
|
||||||
NumEnums: 0,
|
NumEnums: 0,
|
||||||
NumMessages: 23,
|
NumMessages: 29,
|
||||||
NumExtensions: 0,
|
NumExtensions: 0,
|
||||||
NumServices: 1,
|
NumServices: 1,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -14,12 +14,17 @@ service Docker {
|
|||||||
rpc StopContainer(StopContainerRequest) returns (google.protobuf.Empty);
|
rpc StopContainer(StopContainerRequest) returns (google.protobuf.Empty);
|
||||||
rpc ListContainers(ListContainersRequest) returns (ListContainersResponse);
|
rpc ListContainers(ListContainersRequest) returns (ListContainersResponse);
|
||||||
rpc RemoveContainer(RemoveContainerRequest) returns (google.protobuf.Empty);
|
rpc RemoveContainer(RemoveContainerRequest) returns (google.protobuf.Empty);
|
||||||
|
|
||||||
rpc PullImage(PullImageRequest) returns (stream JSONMessage);
|
rpc PullImage(PullImageRequest) returns (stream JSONMessage);
|
||||||
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
|
||||||
// Docker auth credentials if necessary.
|
// Docker auth credentials if necessary.
|
||||||
rpc InspectRemoteImage(InspectRemoteImageRequest) returns (InspectRemoteImageResponse);
|
rpc InspectRemoteImage(InspectRemoteImageRequest) returns (InspectRemoteImageResponse);
|
||||||
|
|
||||||
|
rpc CreateVolume(CreateVolumeRequest) returns (CreateVolumeResponse);
|
||||||
|
rpc ListVolumes(ListVolumesRequest) returns (ListVolumesResponse);
|
||||||
|
rpc RemoveVolume(RemoveVolumeRequest) returns (google.protobuf.Empty);
|
||||||
|
|
||||||
rpc CreateServiceContainer(CreateServiceContainerRequest) returns (CreateContainerResponse);
|
rpc CreateServiceContainer(CreateServiceContainerRequest) returns (CreateContainerResponse);
|
||||||
rpc InspectServiceContainer(InspectContainerRequest) returns (ServiceContainer);
|
rpc InspectServiceContainer(InspectContainerRequest) returns (ServiceContainer);
|
||||||
rpc ListServiceContainers(ListServiceContainersRequest) returns (ListServiceContainersResponse);
|
rpc ListServiceContainers(ListServiceContainersRequest) returns (ListServiceContainersResponse);
|
||||||
@@ -129,6 +134,37 @@ message RemoteImage {
|
|||||||
bytes manifest = 3;
|
bytes manifest = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message CreateVolumeRequest {
|
||||||
|
// JSON serialised volume.CreateOptions.
|
||||||
|
bytes options = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message CreateVolumeResponse {
|
||||||
|
// JSON serialised volume.Volume.
|
||||||
|
bytes volume = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ListVolumesRequest {
|
||||||
|
// JSON serialised volume.ListOptions.
|
||||||
|
bytes options = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ListVolumesResponse {
|
||||||
|
// Must contain only one repeated messages field to allow broadcasting ListVolumes requests to multiple machines.
|
||||||
|
repeated MachineVolumes messages = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message MachineVolumes {
|
||||||
|
Metadata metadata = 1;
|
||||||
|
// JSON serialised volume.ListResponse.
|
||||||
|
bytes response = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message RemoveVolumeRequest {
|
||||||
|
string id = 1;
|
||||||
|
bool force = 2;
|
||||||
|
}
|
||||||
|
|
||||||
message CreateServiceContainerRequest {
|
message CreateServiceContainerRequest {
|
||||||
string service_id = 1;
|
string service_id = 1;
|
||||||
// JSON serialised api.ServiceSpec.
|
// JSON serialised api.ServiceSpec.
|
||||||
|
|||||||
@@ -29,6 +29,9 @@ const (
|
|||||||
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"
|
||||||
|
Docker_CreateVolume_FullMethodName = "/api.Docker/CreateVolume"
|
||||||
|
Docker_ListVolumes_FullMethodName = "/api.Docker/ListVolumes"
|
||||||
|
Docker_RemoveVolume_FullMethodName = "/api.Docker/RemoveVolume"
|
||||||
Docker_CreateServiceContainer_FullMethodName = "/api.Docker/CreateServiceContainer"
|
Docker_CreateServiceContainer_FullMethodName = "/api.Docker/CreateServiceContainer"
|
||||||
Docker_InspectServiceContainer_FullMethodName = "/api.Docker/InspectServiceContainer"
|
Docker_InspectServiceContainer_FullMethodName = "/api.Docker/InspectServiceContainer"
|
||||||
Docker_ListServiceContainers_FullMethodName = "/api.Docker/ListServiceContainers"
|
Docker_ListServiceContainers_FullMethodName = "/api.Docker/ListServiceContainers"
|
||||||
@@ -50,6 +53,9 @@ type DockerClient interface {
|
|||||||
// 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
|
||||||
// Docker auth credentials if necessary.
|
// Docker auth credentials if necessary.
|
||||||
InspectRemoteImage(ctx context.Context, in *InspectRemoteImageRequest, opts ...grpc.CallOption) (*InspectRemoteImageResponse, error)
|
InspectRemoteImage(ctx context.Context, in *InspectRemoteImageRequest, opts ...grpc.CallOption) (*InspectRemoteImageResponse, error)
|
||||||
|
CreateVolume(ctx context.Context, in *CreateVolumeRequest, opts ...grpc.CallOption) (*CreateVolumeResponse, error)
|
||||||
|
ListVolumes(ctx context.Context, in *ListVolumesRequest, opts ...grpc.CallOption) (*ListVolumesResponse, error)
|
||||||
|
RemoveVolume(ctx context.Context, in *RemoveVolumeRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
|
||||||
CreateServiceContainer(ctx context.Context, in *CreateServiceContainerRequest, opts ...grpc.CallOption) (*CreateContainerResponse, error)
|
CreateServiceContainer(ctx context.Context, in *CreateServiceContainerRequest, opts ...grpc.CallOption) (*CreateContainerResponse, error)
|
||||||
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)
|
||||||
@@ -163,6 +169,36 @@ func (c *dockerClient) InspectRemoteImage(ctx context.Context, in *InspectRemote
|
|||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *dockerClient) CreateVolume(ctx context.Context, in *CreateVolumeRequest, opts ...grpc.CallOption) (*CreateVolumeResponse, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(CreateVolumeResponse)
|
||||||
|
err := c.cc.Invoke(ctx, Docker_CreateVolume_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *dockerClient) ListVolumes(ctx context.Context, in *ListVolumesRequest, opts ...grpc.CallOption) (*ListVolumesResponse, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(ListVolumesResponse)
|
||||||
|
err := c.cc.Invoke(ctx, Docker_ListVolumes_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *dockerClient) RemoveVolume(ctx context.Context, in *RemoveVolumeRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(emptypb.Empty)
|
||||||
|
err := c.cc.Invoke(ctx, Docker_RemoveVolume_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (c *dockerClient) CreateServiceContainer(ctx context.Context, in *CreateServiceContainerRequest, opts ...grpc.CallOption) (*CreateContainerResponse, error) {
|
func (c *dockerClient) CreateServiceContainer(ctx context.Context, in *CreateServiceContainerRequest, opts ...grpc.CallOption) (*CreateContainerResponse, error) {
|
||||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
out := new(CreateContainerResponse)
|
out := new(CreateContainerResponse)
|
||||||
@@ -218,6 +254,9 @@ type DockerServer interface {
|
|||||||
// 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
|
||||||
// Docker auth credentials if necessary.
|
// Docker auth credentials if necessary.
|
||||||
InspectRemoteImage(context.Context, *InspectRemoteImageRequest) (*InspectRemoteImageResponse, error)
|
InspectRemoteImage(context.Context, *InspectRemoteImageRequest) (*InspectRemoteImageResponse, error)
|
||||||
|
CreateVolume(context.Context, *CreateVolumeRequest) (*CreateVolumeResponse, error)
|
||||||
|
ListVolumes(context.Context, *ListVolumesRequest) (*ListVolumesResponse, error)
|
||||||
|
RemoveVolume(context.Context, *RemoveVolumeRequest) (*emptypb.Empty, error)
|
||||||
CreateServiceContainer(context.Context, *CreateServiceContainerRequest) (*CreateContainerResponse, error)
|
CreateServiceContainer(context.Context, *CreateServiceContainerRequest) (*CreateContainerResponse, error)
|
||||||
InspectServiceContainer(context.Context, *InspectContainerRequest) (*ServiceContainer, error)
|
InspectServiceContainer(context.Context, *InspectContainerRequest) (*ServiceContainer, error)
|
||||||
ListServiceContainers(context.Context, *ListServiceContainersRequest) (*ListServiceContainersResponse, error)
|
ListServiceContainers(context.Context, *ListServiceContainersRequest) (*ListServiceContainersResponse, error)
|
||||||
@@ -259,6 +298,15 @@ func (UnimplementedDockerServer) InspectImage(context.Context, *InspectImageRequ
|
|||||||
func (UnimplementedDockerServer) InspectRemoteImage(context.Context, *InspectRemoteImageRequest) (*InspectRemoteImageResponse, error) {
|
func (UnimplementedDockerServer) InspectRemoteImage(context.Context, *InspectRemoteImageRequest) (*InspectRemoteImageResponse, error) {
|
||||||
return nil, status.Errorf(codes.Unimplemented, "method InspectRemoteImage not implemented")
|
return nil, status.Errorf(codes.Unimplemented, "method InspectRemoteImage not implemented")
|
||||||
}
|
}
|
||||||
|
func (UnimplementedDockerServer) CreateVolume(context.Context, *CreateVolumeRequest) (*CreateVolumeResponse, error) {
|
||||||
|
return nil, status.Errorf(codes.Unimplemented, "method CreateVolume not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedDockerServer) ListVolumes(context.Context, *ListVolumesRequest) (*ListVolumesResponse, error) {
|
||||||
|
return nil, status.Errorf(codes.Unimplemented, "method ListVolumes not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedDockerServer) RemoveVolume(context.Context, *RemoveVolumeRequest) (*emptypb.Empty, error) {
|
||||||
|
return nil, status.Errorf(codes.Unimplemented, "method RemoveVolume not implemented")
|
||||||
|
}
|
||||||
func (UnimplementedDockerServer) CreateServiceContainer(context.Context, *CreateServiceContainerRequest) (*CreateContainerResponse, error) {
|
func (UnimplementedDockerServer) CreateServiceContainer(context.Context, *CreateServiceContainerRequest) (*CreateContainerResponse, error) {
|
||||||
return nil, status.Errorf(codes.Unimplemented, "method CreateServiceContainer not implemented")
|
return nil, status.Errorf(codes.Unimplemented, "method CreateServiceContainer not implemented")
|
||||||
}
|
}
|
||||||
@@ -447,6 +495,60 @@ func _Docker_InspectRemoteImage_Handler(srv interface{}, ctx context.Context, de
|
|||||||
return interceptor(ctx, in, info, handler)
|
return interceptor(ctx, in, info, handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func _Docker_CreateVolume_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(CreateVolumeRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(DockerServer).CreateVolume(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: Docker_CreateVolume_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(DockerServer).CreateVolume(ctx, req.(*CreateVolumeRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _Docker_ListVolumes_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(ListVolumesRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(DockerServer).ListVolumes(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: Docker_ListVolumes_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(DockerServer).ListVolumes(ctx, req.(*ListVolumesRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _Docker_RemoveVolume_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(RemoveVolumeRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(DockerServer).RemoveVolume(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: Docker_RemoveVolume_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(DockerServer).RemoveVolume(ctx, req.(*RemoveVolumeRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
func _Docker_CreateServiceContainer_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
func _Docker_CreateServiceContainer_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
in := new(CreateServiceContainerRequest)
|
in := new(CreateServiceContainerRequest)
|
||||||
if err := dec(in); err != nil {
|
if err := dec(in); err != nil {
|
||||||
@@ -558,6 +660,18 @@ var Docker_ServiceDesc = grpc.ServiceDesc{
|
|||||||
MethodName: "InspectRemoteImage",
|
MethodName: "InspectRemoteImage",
|
||||||
Handler: _Docker_InspectRemoteImage_Handler,
|
Handler: _Docker_InspectRemoteImage_Handler,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
MethodName: "CreateVolume",
|
||||||
|
Handler: _Docker_CreateVolume_Handler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MethodName: "ListVolumes",
|
||||||
|
Handler: _Docker_ListVolumes_Handler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MethodName: "RemoveVolume",
|
||||||
|
Handler: _Docker_RemoveVolume_Handler,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
MethodName: "CreateServiceContainer",
|
MethodName: "CreateServiceContainer",
|
||||||
Handler: _Docker_CreateServiceContainer_Handler,
|
Handler: _Docker_CreateServiceContainer_Handler,
|
||||||
|
|||||||
@@ -21,6 +21,63 @@ const (
|
|||||||
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type CheckPrerequisitesResponse struct {
|
||||||
|
state protoimpl.MessageState
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
|
||||||
|
// Overall status of the checks.
|
||||||
|
Satisfied bool `protobuf:"varint,1,opt,name=satisfied,proto3" json:"satisfied,omitempty"`
|
||||||
|
// Error message if not satisfied (empty if all checks pass).
|
||||||
|
Error string `protobuf:"bytes,2,opt,name=error,proto3" json:"error,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *CheckPrerequisitesResponse) Reset() {
|
||||||
|
*x = CheckPrerequisitesResponse{}
|
||||||
|
if protoimpl.UnsafeEnabled {
|
||||||
|
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[0]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *CheckPrerequisitesResponse) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*CheckPrerequisitesResponse) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *CheckPrerequisitesResponse) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[0]
|
||||||
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use CheckPrerequisitesResponse.ProtoReflect.Descriptor instead.
|
||||||
|
func (*CheckPrerequisitesResponse) Descriptor() ([]byte, []int) {
|
||||||
|
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{0}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *CheckPrerequisitesResponse) GetSatisfied() bool {
|
||||||
|
if x != nil {
|
||||||
|
return x.Satisfied
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *CheckPrerequisitesResponse) GetError() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Error
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
type MachineInfo struct {
|
type MachineInfo struct {
|
||||||
state protoimpl.MessageState
|
state protoimpl.MessageState
|
||||||
sizeCache protoimpl.SizeCache
|
sizeCache protoimpl.SizeCache
|
||||||
@@ -35,7 +92,7 @@ type MachineInfo struct {
|
|||||||
func (x *MachineInfo) Reset() {
|
func (x *MachineInfo) Reset() {
|
||||||
*x = MachineInfo{}
|
*x = MachineInfo{}
|
||||||
if protoimpl.UnsafeEnabled {
|
if protoimpl.UnsafeEnabled {
|
||||||
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[0]
|
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[1]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@@ -48,7 +105,7 @@ func (x *MachineInfo) String() string {
|
|||||||
func (*MachineInfo) ProtoMessage() {}
|
func (*MachineInfo) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *MachineInfo) ProtoReflect() protoreflect.Message {
|
func (x *MachineInfo) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[0]
|
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[1]
|
||||||
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 {
|
||||||
@@ -61,7 +118,7 @@ func (x *MachineInfo) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use MachineInfo.ProtoReflect.Descriptor instead.
|
// Deprecated: Use MachineInfo.ProtoReflect.Descriptor instead.
|
||||||
func (*MachineInfo) Descriptor() ([]byte, []int) {
|
func (*MachineInfo) Descriptor() ([]byte, []int) {
|
||||||
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{0}
|
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{1}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *MachineInfo) GetId() string {
|
func (x *MachineInfo) GetId() string {
|
||||||
@@ -106,7 +163,7 @@ type NetworkConfig struct {
|
|||||||
func (x *NetworkConfig) Reset() {
|
func (x *NetworkConfig) Reset() {
|
||||||
*x = NetworkConfig{}
|
*x = NetworkConfig{}
|
||||||
if protoimpl.UnsafeEnabled {
|
if protoimpl.UnsafeEnabled {
|
||||||
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[1]
|
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[2]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@@ -119,7 +176,7 @@ func (x *NetworkConfig) String() string {
|
|||||||
func (*NetworkConfig) ProtoMessage() {}
|
func (*NetworkConfig) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *NetworkConfig) ProtoReflect() protoreflect.Message {
|
func (x *NetworkConfig) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[1]
|
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[2]
|
||||||
if protoimpl.UnsafeEnabled && x != nil {
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
@@ -132,7 +189,7 @@ func (x *NetworkConfig) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use NetworkConfig.ProtoReflect.Descriptor instead.
|
// Deprecated: Use NetworkConfig.ProtoReflect.Descriptor instead.
|
||||||
func (*NetworkConfig) Descriptor() ([]byte, []int) {
|
func (*NetworkConfig) Descriptor() ([]byte, []int) {
|
||||||
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{1}
|
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{2}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *NetworkConfig) GetSubnet() *IPPrefix {
|
func (x *NetworkConfig) GetSubnet() *IPPrefix {
|
||||||
@@ -180,7 +237,7 @@ type InitClusterRequest struct {
|
|||||||
func (x *InitClusterRequest) Reset() {
|
func (x *InitClusterRequest) Reset() {
|
||||||
*x = InitClusterRequest{}
|
*x = InitClusterRequest{}
|
||||||
if protoimpl.UnsafeEnabled {
|
if protoimpl.UnsafeEnabled {
|
||||||
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[2]
|
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[3]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@@ -193,7 +250,7 @@ func (x *InitClusterRequest) String() string {
|
|||||||
func (*InitClusterRequest) ProtoMessage() {}
|
func (*InitClusterRequest) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *InitClusterRequest) ProtoReflect() protoreflect.Message {
|
func (x *InitClusterRequest) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[2]
|
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[3]
|
||||||
if protoimpl.UnsafeEnabled && x != nil {
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
@@ -206,7 +263,7 @@ func (x *InitClusterRequest) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use InitClusterRequest.ProtoReflect.Descriptor instead.
|
// Deprecated: Use InitClusterRequest.ProtoReflect.Descriptor instead.
|
||||||
func (*InitClusterRequest) Descriptor() ([]byte, []int) {
|
func (*InitClusterRequest) Descriptor() ([]byte, []int) {
|
||||||
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{2}
|
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{3}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *InitClusterRequest) GetMachineName() string {
|
func (x *InitClusterRequest) GetMachineName() string {
|
||||||
@@ -271,7 +328,7 @@ type InitClusterResponse struct {
|
|||||||
func (x *InitClusterResponse) Reset() {
|
func (x *InitClusterResponse) Reset() {
|
||||||
*x = InitClusterResponse{}
|
*x = InitClusterResponse{}
|
||||||
if protoimpl.UnsafeEnabled {
|
if protoimpl.UnsafeEnabled {
|
||||||
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[3]
|
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[4]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@@ -284,7 +341,7 @@ func (x *InitClusterResponse) String() string {
|
|||||||
func (*InitClusterResponse) ProtoMessage() {}
|
func (*InitClusterResponse) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *InitClusterResponse) ProtoReflect() protoreflect.Message {
|
func (x *InitClusterResponse) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[3]
|
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[4]
|
||||||
if protoimpl.UnsafeEnabled && x != nil {
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
@@ -297,7 +354,7 @@ func (x *InitClusterResponse) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use InitClusterResponse.ProtoReflect.Descriptor instead.
|
// Deprecated: Use InitClusterResponse.ProtoReflect.Descriptor instead.
|
||||||
func (*InitClusterResponse) Descriptor() ([]byte, []int) {
|
func (*InitClusterResponse) Descriptor() ([]byte, []int) {
|
||||||
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{3}
|
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{4}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *InitClusterResponse) GetMachine() *MachineInfo {
|
func (x *InitClusterResponse) GetMachine() *MachineInfo {
|
||||||
@@ -319,7 +376,7 @@ type JoinClusterRequest struct {
|
|||||||
func (x *JoinClusterRequest) Reset() {
|
func (x *JoinClusterRequest) Reset() {
|
||||||
*x = JoinClusterRequest{}
|
*x = JoinClusterRequest{}
|
||||||
if protoimpl.UnsafeEnabled {
|
if protoimpl.UnsafeEnabled {
|
||||||
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[4]
|
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[5]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@@ -332,7 +389,7 @@ func (x *JoinClusterRequest) String() string {
|
|||||||
func (*JoinClusterRequest) ProtoMessage() {}
|
func (*JoinClusterRequest) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *JoinClusterRequest) ProtoReflect() protoreflect.Message {
|
func (x *JoinClusterRequest) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[4]
|
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[5]
|
||||||
if protoimpl.UnsafeEnabled && x != nil {
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
@@ -345,7 +402,7 @@ func (x *JoinClusterRequest) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use JoinClusterRequest.ProtoReflect.Descriptor instead.
|
// Deprecated: Use JoinClusterRequest.ProtoReflect.Descriptor instead.
|
||||||
func (*JoinClusterRequest) Descriptor() ([]byte, []int) {
|
func (*JoinClusterRequest) Descriptor() ([]byte, []int) {
|
||||||
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{4}
|
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{5}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *JoinClusterRequest) GetMachine() *MachineInfo {
|
func (x *JoinClusterRequest) GetMachine() *MachineInfo {
|
||||||
@@ -373,7 +430,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[5]
|
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[6]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@@ -386,7 +443,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[5]
|
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[6]
|
||||||
if protoimpl.UnsafeEnabled && x != nil {
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
@@ -399,7 +456,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{5}
|
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{6}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *TokenResponse) GetToken() string {
|
func (x *TokenResponse) GetToken() string {
|
||||||
@@ -423,7 +480,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[6]
|
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[7]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@@ -436,7 +493,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[6]
|
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[7]
|
||||||
if protoimpl.UnsafeEnabled && x != nil {
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
@@ -449,7 +506,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{6}
|
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{7}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *Service) GetId() string {
|
func (x *Service) GetId() string {
|
||||||
@@ -491,7 +548,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[7]
|
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)
|
||||||
}
|
}
|
||||||
@@ -504,7 +561,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[7]
|
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 {
|
||||||
@@ -517,7 +574,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{7}
|
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{8}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *InspectServiceRequest) GetId() string {
|
func (x *InspectServiceRequest) GetId() string {
|
||||||
@@ -538,7 +595,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[8]
|
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)
|
||||||
}
|
}
|
||||||
@@ -551,7 +608,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[8]
|
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 {
|
||||||
@@ -564,7 +621,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{8}
|
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{9}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *InspectServiceResponse) GetService() *Service {
|
func (x *InspectServiceResponse) GetService() *Service {
|
||||||
@@ -587,7 +644,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[9]
|
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)
|
||||||
}
|
}
|
||||||
@@ -600,7 +657,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[9]
|
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 {
|
||||||
@@ -613,7 +670,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{6, 0}
|
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{7, 0}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *Service_Container) GetMachineId() string {
|
func (x *Service_Container) GetMachineId() string {
|
||||||
@@ -640,96 +697,106 @@ var file_internal_machine_api_pb_machine_proto_rawDesc = []byte{
|
|||||||
0x70, 0x74, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x24, 0x69, 0x6e, 0x74, 0x65, 0x72,
|
0x70, 0x74, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x24, 0x69, 0x6e, 0x74, 0x65, 0x72,
|
||||||
0x6e, 0x61, 0x6c, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f,
|
0x6e, 0x61, 0x6c, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f,
|
||||||
0x70, 0x62, 0x2f, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22,
|
0x70, 0x62, 0x2f, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22,
|
||||||
0x85, 0x01, 0x0a, 0x0b, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x12,
|
0x50, 0x0a, 0x1a, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x50, 0x72, 0x65, 0x72, 0x65, 0x71, 0x75, 0x69,
|
||||||
0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12,
|
0x73, 0x69, 0x74, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1c, 0x0a,
|
||||||
0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e,
|
0x09, 0x73, 0x61, 0x74, 0x69, 0x73, 0x66, 0x69, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08,
|
||||||
0x61, 0x6d, 0x65, 0x12, 0x2c, 0x0a, 0x07, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x18, 0x03,
|
0x52, 0x09, 0x73, 0x61, 0x74, 0x69, 0x73, 0x66, 0x69, 0x65, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x65,
|
||||||
0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4e, 0x65, 0x74, 0x77, 0x6f,
|
0x72, 0x72, 0x6f, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f,
|
||||||
0x72, 0x6b, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x07, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72,
|
0x72, 0x22, 0x85, 0x01, 0x0a, 0x0b, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66,
|
||||||
0x6b, 0x12, 0x24, 0x0a, 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x69, 0x70, 0x18, 0x04,
|
0x6f, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69,
|
||||||
0x20, 0x01, 0x28, 0x0b, 0x32, 0x07, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x50, 0x52, 0x08, 0x70,
|
0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52,
|
||||||
0x75, 0x62, 0x6c, 0x69, 0x63, 0x49, 0x70, 0x22, 0xae, 0x01, 0x0a, 0x0d, 0x4e, 0x65, 0x74, 0x77,
|
0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x2c, 0x0a, 0x07, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b,
|
||||||
0x6f, 0x72, 0x6b, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x25, 0x0a, 0x06, 0x73, 0x75, 0x62,
|
0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4e, 0x65, 0x74,
|
||||||
0x6e, 0x65, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x61, 0x70, 0x69, 0x2e,
|
0x77, 0x6f, 0x72, 0x6b, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x07, 0x6e, 0x65, 0x74, 0x77,
|
||||||
0x49, 0x50, 0x50, 0x72, 0x65, 0x66, 0x69, 0x78, 0x52, 0x06, 0x73, 0x75, 0x62, 0x6e, 0x65, 0x74,
|
0x6f, 0x72, 0x6b, 0x12, 0x24, 0x0a, 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x69, 0x70,
|
||||||
0x12, 0x2c, 0x0a, 0x0d, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x5f, 0x69,
|
0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x07, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x50, 0x52,
|
||||||
0x70, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x07, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x50,
|
0x08, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x49, 0x70, 0x22, 0xae, 0x01, 0x0a, 0x0d, 0x4e, 0x65,
|
||||||
0x52, 0x0c, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x49, 0x70, 0x12, 0x29,
|
0x74, 0x77, 0x6f, 0x72, 0x6b, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x25, 0x0a, 0x06, 0x73,
|
||||||
0x0a, 0x09, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28,
|
0x75, 0x62, 0x6e, 0x65, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x61, 0x70,
|
||||||
0x0b, 0x32, 0x0b, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x50, 0x50, 0x6f, 0x72, 0x74, 0x52, 0x09,
|
0x69, 0x2e, 0x49, 0x50, 0x50, 0x72, 0x65, 0x66, 0x69, 0x78, 0x52, 0x06, 0x73, 0x75, 0x62, 0x6e,
|
||||||
0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x70, 0x75, 0x62,
|
0x65, 0x74, 0x12, 0x2c, 0x0a, 0x0d, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74,
|
||||||
0x6c, 0x69, 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x70,
|
0x5f, 0x69, 0x70, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x07, 0x2e, 0x61, 0x70, 0x69, 0x2e,
|
||||||
0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x22, 0xc3, 0x01, 0x0a, 0x12, 0x49, 0x6e, 0x69,
|
0x49, 0x50, 0x52, 0x0c, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x49, 0x70,
|
||||||
0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12,
|
0x12, 0x29, 0x0a, 0x09, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x18, 0x03, 0x20,
|
||||||
0x20, 0x0a, 0x0b, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x01,
|
0x03, 0x28, 0x0b, 0x32, 0x0b, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x50, 0x50, 0x6f, 0x72, 0x74,
|
||||||
0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x4e, 0x61, 0x6d,
|
0x52, 0x09, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x70,
|
||||||
0x65, 0x12, 0x27, 0x0a, 0x07, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x18, 0x02, 0x20, 0x01,
|
0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52,
|
||||||
0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x50, 0x50, 0x72, 0x65, 0x66, 0x69,
|
0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x22, 0xc3, 0x01, 0x0a, 0x12, 0x49,
|
||||||
0x78, 0x52, 0x07, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x12, 0x26, 0x0a, 0x09, 0x70, 0x75,
|
0x6e, 0x69, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
|
||||||
0x62, 0x6c, 0x69, 0x63, 0x5f, 0x69, 0x70, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x07, 0x2e,
|
0x74, 0x12, 0x20, 0x0a, 0x0b, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x4e, 0x61, 0x6d, 0x65,
|
||||||
0x61, 0x70, 0x69, 0x2e, 0x49, 0x50, 0x48, 0x00, 0x52, 0x08, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63,
|
0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x4e,
|
||||||
0x49, 0x70, 0x12, 0x26, 0x0a, 0x0e, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x69, 0x70, 0x5f,
|
0x61, 0x6d, 0x65, 0x12, 0x27, 0x0a, 0x07, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x18, 0x02,
|
||||||
0x61, 0x75, 0x74, 0x6f, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x48, 0x00, 0x52, 0x0c, 0x70, 0x75,
|
0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x50, 0x50, 0x72, 0x65,
|
||||||
0x62, 0x6c, 0x69, 0x63, 0x49, 0x70, 0x41, 0x75, 0x74, 0x6f, 0x42, 0x12, 0x0a, 0x10, 0x70, 0x75,
|
0x66, 0x69, 0x78, 0x52, 0x07, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x12, 0x26, 0x0a, 0x09,
|
||||||
0x62, 0x6c, 0x69, 0x63, 0x5f, 0x69, 0x70, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0x41,
|
0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x69, 0x70, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32,
|
||||||
0x0a, 0x13, 0x49, 0x6e, 0x69, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x73,
|
0x07, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x50, 0x48, 0x00, 0x52, 0x08, 0x70, 0x75, 0x62, 0x6c,
|
||||||
0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2a, 0x0a, 0x07, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65,
|
0x69, 0x63, 0x49, 0x70, 0x12, 0x26, 0x0a, 0x0e, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x69,
|
||||||
0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d, 0x61, 0x63,
|
0x70, 0x5f, 0x61, 0x75, 0x74, 0x6f, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x48, 0x00, 0x52, 0x0c,
|
||||||
0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x07, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e,
|
0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x49, 0x70, 0x41, 0x75, 0x74, 0x6f, 0x42, 0x12, 0x0a, 0x10,
|
||||||
0x65, 0x22, 0x79, 0x0a, 0x12, 0x4a, 0x6f, 0x69, 0x6e, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72,
|
0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x69, 0x70, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67,
|
||||||
0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2a, 0x0a, 0x07, 0x6d, 0x61, 0x63, 0x68, 0x69,
|
0x22, 0x41, 0x0a, 0x13, 0x49, 0x6e, 0x69, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x52,
|
||||||
|
0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2a, 0x0a, 0x07, 0x6d, 0x61, 0x63, 0x68, 0x69,
|
||||||
0x6e, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d,
|
0x6e, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d,
|
||||||
0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x07, 0x6d, 0x61, 0x63, 0x68,
|
0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x07, 0x6d, 0x61, 0x63, 0x68,
|
||||||
0x69, 0x6e, 0x65, 0x12, 0x37, 0x0a, 0x0e, 0x6f, 0x74, 0x68, 0x65, 0x72, 0x5f, 0x6d, 0x61, 0x63,
|
0x69, 0x6e, 0x65, 0x22, 0x79, 0x0a, 0x12, 0x4a, 0x6f, 0x69, 0x6e, 0x43, 0x6c, 0x75, 0x73, 0x74,
|
||||||
0x68, 0x69, 0x6e, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x61, 0x70,
|
0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2a, 0x0a, 0x07, 0x6d, 0x61, 0x63,
|
||||||
0x69, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x0d, 0x6f,
|
0x68, 0x69, 0x6e, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x61, 0x70, 0x69,
|
||||||
0x74, 0x68, 0x65, 0x72, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x22, 0x25, 0x0a, 0x0d,
|
0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x07, 0x6d, 0x61,
|
||||||
0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x14, 0x0a,
|
0x63, 0x68, 0x69, 0x6e, 0x65, 0x12, 0x37, 0x0a, 0x0e, 0x6f, 0x74, 0x68, 0x65, 0x72, 0x5f, 0x6d,
|
||||||
0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x6f,
|
0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x10, 0x2e,
|
||||||
0x6b, 0x65, 0x6e, 0x22, 0xc3, 0x01, 0x0a, 0x07, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12,
|
0x61, 0x70, 0x69, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52,
|
||||||
0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12,
|
0x0d, 0x6f, 0x74, 0x68, 0x65, 0x72, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x22, 0x25,
|
||||||
0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e,
|
0x0a, 0x0d, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12,
|
||||||
0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28,
|
0x14, 0x0a, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05,
|
||||||
0x09, 0x52, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x12, 0x36, 0x0a, 0x0a, 0x63, 0x6f, 0x6e, 0x74, 0x61,
|
0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0xc3, 0x01, 0x0a, 0x07, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63,
|
||||||
0x69, 0x6e, 0x65, 0x72, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x61, 0x70,
|
0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69,
|
||||||
0x69, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69,
|
0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52,
|
||||||
0x6e, 0x65, 0x72, 0x52, 0x0a, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x1a,
|
0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x18, 0x03, 0x20,
|
||||||
0x48, 0x0a, 0x09, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x1d, 0x0a, 0x0a,
|
0x01, 0x28, 0x09, 0x52, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x12, 0x36, 0x0a, 0x0a, 0x63, 0x6f, 0x6e,
|
||||||
0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
|
0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e,
|
||||||
0x52, 0x09, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x64, 0x12, 0x1c, 0x0a, 0x09, 0x63,
|
0x61, 0x70, 0x69, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x43, 0x6f, 0x6e, 0x74,
|
||||||
0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09,
|
0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x0a, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72,
|
||||||
0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x22, 0x27, 0x0a, 0x15, 0x49, 0x6e, 0x73,
|
0x73, 0x1a, 0x48, 0x0a, 0x09, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x1d,
|
||||||
0x70, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65,
|
0x0a, 0x0a, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01,
|
||||||
0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02,
|
0x28, 0x09, 0x52, 0x09, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x64, 0x12, 0x1c, 0x0a,
|
||||||
0x69, 0x64, 0x22, 0x40, 0x0a, 0x16, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72,
|
0x09, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c,
|
||||||
0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x26, 0x0a, 0x07,
|
0x52, 0x09, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x22, 0x27, 0x0a, 0x15, 0x49,
|
||||||
0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0c, 0x2e,
|
0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x71,
|
||||||
0x61, 0x70, 0x69, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x07, 0x73, 0x65, 0x72,
|
0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
|
||||||
0x76, 0x69, 0x63, 0x65, 0x32, 0xc0, 0x02, 0x0a, 0x07, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65,
|
0x52, 0x02, 0x69, 0x64, 0x22, 0x40, 0x0a, 0x16, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53,
|
||||||
0x12, 0x40, 0x0a, 0x0b, 0x49, 0x6e, 0x69, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x12,
|
0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x26,
|
||||||
0x17, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x69, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65,
|
0x0a, 0x07, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32,
|
||||||
0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49,
|
0x0c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x07, 0x73,
|
||||||
0x6e, 0x69, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
|
0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x32, 0x8f, 0x03, 0x0a, 0x07, 0x4d, 0x61, 0x63, 0x68, 0x69,
|
||||||
0x73, 0x65, 0x12, 0x3e, 0x0a, 0x0b, 0x4a, 0x6f, 0x69, 0x6e, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65,
|
0x6e, 0x65, 0x12, 0x4d, 0x0a, 0x12, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x50, 0x72, 0x65, 0x72, 0x65,
|
||||||
0x72, 0x12, 0x17, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4a, 0x6f, 0x69, 0x6e, 0x43, 0x6c, 0x75, 0x73,
|
0x71, 0x75, 0x69, 0x73, 0x69, 0x74, 0x65, 0x73, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c,
|
||||||
0x74, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f,
|
0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79,
|
||||||
0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70,
|
0x1a, 0x1f, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x50, 0x72, 0x65, 0x72,
|
||||||
0x74, 0x79, 0x12, 0x33, 0x0a, 0x05, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x16, 0x2e, 0x67, 0x6f,
|
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,
|
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,
|
0x70, 0x74, 0x79, 0x12, 0x33, 0x0a, 0x05, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x16, 0x2e, 0x67,
|
||||||
0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x33, 0x0a, 0x07, 0x49, 0x6e, 0x73, 0x70, 0x65,
|
0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45,
|
||||||
0x63, 0x74, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74,
|
0x6d, 0x70, 0x74, 0x79, 0x1a, 0x12, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x54, 0x6f, 0x6b, 0x65, 0x6e,
|
||||||
0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x10, 0x2e, 0x61, 0x70, 0x69,
|
0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x33, 0x0a, 0x07, 0x49, 0x6e, 0x73, 0x70,
|
||||||
0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x49, 0x0a, 0x0e,
|
0x65, 0x63, 0x74, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f,
|
||||||
0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x1a,
|
0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x10, 0x2e, 0x61, 0x70,
|
||||||
0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72, 0x76,
|
0x69, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x49, 0x0a,
|
||||||
0x69, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x61, 0x70, 0x69,
|
0x0e, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12,
|
||||||
0x2e, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52,
|
0x1a, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72,
|
||||||
0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x37, 0x5a, 0x35, 0x67, 0x69, 0x74, 0x68, 0x75,
|
0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x61, 0x70,
|
||||||
0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x70, 0x73, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x6b, 0x69,
|
0x69, 0x2e, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65,
|
||||||
0x2f, 0x75, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61,
|
0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x37, 0x5a, 0x35, 0x67, 0x69, 0x74, 0x68,
|
||||||
0x6c, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x62,
|
0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x70, 0x73, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x6b,
|
||||||
0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
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 (
|
||||||
@@ -744,48 +811,51 @@ 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, 10)
|
var file_internal_machine_api_pb_machine_proto_msgTypes = make([]protoimpl.MessageInfo, 11)
|
||||||
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
|
(*CheckPrerequisitesResponse)(nil), // 0: api.CheckPrerequisitesResponse
|
||||||
(*NetworkConfig)(nil), // 1: api.NetworkConfig
|
(*MachineInfo)(nil), // 1: api.MachineInfo
|
||||||
(*InitClusterRequest)(nil), // 2: api.InitClusterRequest
|
(*NetworkConfig)(nil), // 2: api.NetworkConfig
|
||||||
(*InitClusterResponse)(nil), // 3: api.InitClusterResponse
|
(*InitClusterRequest)(nil), // 3: api.InitClusterRequest
|
||||||
(*JoinClusterRequest)(nil), // 4: api.JoinClusterRequest
|
(*InitClusterResponse)(nil), // 4: api.InitClusterResponse
|
||||||
(*TokenResponse)(nil), // 5: api.TokenResponse
|
(*JoinClusterRequest)(nil), // 5: api.JoinClusterRequest
|
||||||
(*Service)(nil), // 6: api.Service
|
(*TokenResponse)(nil), // 6: api.TokenResponse
|
||||||
(*InspectServiceRequest)(nil), // 7: api.InspectServiceRequest
|
(*Service)(nil), // 7: api.Service
|
||||||
(*InspectServiceResponse)(nil), // 8: api.InspectServiceResponse
|
(*InspectServiceRequest)(nil), // 8: api.InspectServiceRequest
|
||||||
(*Service_Container)(nil), // 9: api.Service.Container
|
(*InspectServiceResponse)(nil), // 9: api.InspectServiceResponse
|
||||||
(*IP)(nil), // 10: api.IP
|
(*Service_Container)(nil), // 10: api.Service.Container
|
||||||
(*IPPrefix)(nil), // 11: api.IPPrefix
|
(*IP)(nil), // 11: api.IP
|
||||||
(*IPPort)(nil), // 12: api.IPPort
|
(*IPPrefix)(nil), // 12: api.IPPrefix
|
||||||
(*emptypb.Empty)(nil), // 13: google.protobuf.Empty
|
(*IPPort)(nil), // 13: api.IPPort
|
||||||
|
(*emptypb.Empty)(nil), // 14: 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
|
2, // 0: api.MachineInfo.network:type_name -> api.NetworkConfig
|
||||||
10, // 1: api.MachineInfo.public_ip:type_name -> api.IP
|
11, // 1: api.MachineInfo.public_ip:type_name -> api.IP
|
||||||
11, // 2: api.NetworkConfig.subnet:type_name -> api.IPPrefix
|
12, // 2: api.NetworkConfig.subnet:type_name -> api.IPPrefix
|
||||||
10, // 3: api.NetworkConfig.management_ip:type_name -> api.IP
|
11, // 3: api.NetworkConfig.management_ip:type_name -> api.IP
|
||||||
12, // 4: api.NetworkConfig.endpoints:type_name -> api.IPPort
|
13, // 4: api.NetworkConfig.endpoints:type_name -> api.IPPort
|
||||||
11, // 5: api.InitClusterRequest.network:type_name -> api.IPPrefix
|
12, // 5: api.InitClusterRequest.network:type_name -> api.IPPrefix
|
||||||
10, // 6: api.InitClusterRequest.public_ip:type_name -> api.IP
|
11, // 6: api.InitClusterRequest.public_ip:type_name -> api.IP
|
||||||
0, // 7: api.InitClusterResponse.machine:type_name -> api.MachineInfo
|
1, // 7: api.InitClusterResponse.machine:type_name -> api.MachineInfo
|
||||||
0, // 8: api.JoinClusterRequest.machine:type_name -> api.MachineInfo
|
1, // 8: api.JoinClusterRequest.machine:type_name -> api.MachineInfo
|
||||||
0, // 9: api.JoinClusterRequest.other_machines:type_name -> api.MachineInfo
|
1, // 9: api.JoinClusterRequest.other_machines:type_name -> api.MachineInfo
|
||||||
9, // 10: api.Service.containers:type_name -> api.Service.Container
|
10, // 10: api.Service.containers:type_name -> api.Service.Container
|
||||||
6, // 11: api.InspectServiceResponse.service:type_name -> api.Service
|
7, // 11: api.InspectServiceResponse.service:type_name -> api.Service
|
||||||
2, // 12: api.Machine.InitCluster:input_type -> api.InitClusterRequest
|
14, // 12: api.Machine.CheckPrerequisites:input_type -> google.protobuf.Empty
|
||||||
4, // 13: api.Machine.JoinCluster:input_type -> api.JoinClusterRequest
|
3, // 13: api.Machine.InitCluster:input_type -> api.InitClusterRequest
|
||||||
13, // 14: api.Machine.Token:input_type -> google.protobuf.Empty
|
5, // 14: api.Machine.JoinCluster:input_type -> api.JoinClusterRequest
|
||||||
13, // 15: api.Machine.Inspect:input_type -> google.protobuf.Empty
|
14, // 15: api.Machine.Token:input_type -> google.protobuf.Empty
|
||||||
7, // 16: api.Machine.InspectService:input_type -> api.InspectServiceRequest
|
14, // 16: api.Machine.Inspect:input_type -> google.protobuf.Empty
|
||||||
3, // 17: api.Machine.InitCluster:output_type -> api.InitClusterResponse
|
8, // 17: api.Machine.InspectService:input_type -> api.InspectServiceRequest
|
||||||
13, // 18: api.Machine.JoinCluster:output_type -> google.protobuf.Empty
|
0, // 18: api.Machine.CheckPrerequisites:output_type -> api.CheckPrerequisitesResponse
|
||||||
5, // 19: api.Machine.Token:output_type -> api.TokenResponse
|
4, // 19: api.Machine.InitCluster:output_type -> api.InitClusterResponse
|
||||||
0, // 20: api.Machine.Inspect:output_type -> api.MachineInfo
|
14, // 20: api.Machine.JoinCluster:output_type -> google.protobuf.Empty
|
||||||
8, // 21: api.Machine.InspectService:output_type -> api.InspectServiceResponse
|
6, // 21: api.Machine.Token:output_type -> api.TokenResponse
|
||||||
17, // [17:22] is the sub-list for method output_type
|
1, // 22: api.Machine.Inspect:output_type -> api.MachineInfo
|
||||||
12, // [12:17] is the sub-list for method input_type
|
9, // 23: api.Machine.InspectService:output_type -> api.InspectServiceResponse
|
||||||
|
18, // [18:24] is the sub-list for method output_type
|
||||||
|
12, // [12:18] is the sub-list for method input_type
|
||||||
12, // [12:12] is the sub-list for extension type_name
|
12, // [12:12] is the sub-list for extension type_name
|
||||||
12, // [12:12] is the sub-list for extension extendee
|
12, // [12:12] is the sub-list for extension extendee
|
||||||
0, // [0:12] is the sub-list for field type_name
|
0, // [0:12] is the sub-list for field type_name
|
||||||
@@ -799,7 +869,7 @@ func file_internal_machine_api_pb_machine_proto_init() {
|
|||||||
file_internal_machine_api_pb_common_proto_init()
|
file_internal_machine_api_pb_common_proto_init()
|
||||||
if !protoimpl.UnsafeEnabled {
|
if !protoimpl.UnsafeEnabled {
|
||||||
file_internal_machine_api_pb_machine_proto_msgTypes[0].Exporter = func(v any, i int) any {
|
file_internal_machine_api_pb_machine_proto_msgTypes[0].Exporter = func(v any, i int) any {
|
||||||
switch v := v.(*MachineInfo); i {
|
switch v := v.(*CheckPrerequisitesResponse); i {
|
||||||
case 0:
|
case 0:
|
||||||
return &v.state
|
return &v.state
|
||||||
case 1:
|
case 1:
|
||||||
@@ -811,7 +881,7 @@ func file_internal_machine_api_pb_machine_proto_init() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
file_internal_machine_api_pb_machine_proto_msgTypes[1].Exporter = func(v any, i int) any {
|
file_internal_machine_api_pb_machine_proto_msgTypes[1].Exporter = func(v any, i int) any {
|
||||||
switch v := v.(*NetworkConfig); i {
|
switch v := v.(*MachineInfo); i {
|
||||||
case 0:
|
case 0:
|
||||||
return &v.state
|
return &v.state
|
||||||
case 1:
|
case 1:
|
||||||
@@ -823,7 +893,7 @@ func file_internal_machine_api_pb_machine_proto_init() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
file_internal_machine_api_pb_machine_proto_msgTypes[2].Exporter = func(v any, i int) any {
|
file_internal_machine_api_pb_machine_proto_msgTypes[2].Exporter = func(v any, i int) any {
|
||||||
switch v := v.(*InitClusterRequest); i {
|
switch v := v.(*NetworkConfig); i {
|
||||||
case 0:
|
case 0:
|
||||||
return &v.state
|
return &v.state
|
||||||
case 1:
|
case 1:
|
||||||
@@ -835,7 +905,7 @@ func file_internal_machine_api_pb_machine_proto_init() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
file_internal_machine_api_pb_machine_proto_msgTypes[3].Exporter = func(v any, i int) any {
|
file_internal_machine_api_pb_machine_proto_msgTypes[3].Exporter = func(v any, i int) any {
|
||||||
switch v := v.(*InitClusterResponse); i {
|
switch v := v.(*InitClusterRequest); i {
|
||||||
case 0:
|
case 0:
|
||||||
return &v.state
|
return &v.state
|
||||||
case 1:
|
case 1:
|
||||||
@@ -847,7 +917,7 @@ func file_internal_machine_api_pb_machine_proto_init() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
file_internal_machine_api_pb_machine_proto_msgTypes[4].Exporter = func(v any, i int) any {
|
file_internal_machine_api_pb_machine_proto_msgTypes[4].Exporter = func(v any, i int) any {
|
||||||
switch v := v.(*JoinClusterRequest); i {
|
switch v := v.(*InitClusterResponse); i {
|
||||||
case 0:
|
case 0:
|
||||||
return &v.state
|
return &v.state
|
||||||
case 1:
|
case 1:
|
||||||
@@ -859,7 +929,7 @@ func file_internal_machine_api_pb_machine_proto_init() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
file_internal_machine_api_pb_machine_proto_msgTypes[5].Exporter = func(v any, i int) any {
|
file_internal_machine_api_pb_machine_proto_msgTypes[5].Exporter = func(v any, i int) any {
|
||||||
switch v := v.(*TokenResponse); i {
|
switch v := v.(*JoinClusterRequest); i {
|
||||||
case 0:
|
case 0:
|
||||||
return &v.state
|
return &v.state
|
||||||
case 1:
|
case 1:
|
||||||
@@ -871,7 +941,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.(*Service); i {
|
switch v := v.(*TokenResponse); i {
|
||||||
case 0:
|
case 0:
|
||||||
return &v.state
|
return &v.state
|
||||||
case 1:
|
case 1:
|
||||||
@@ -883,7 +953,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.(*InspectServiceRequest); i {
|
switch v := v.(*Service); i {
|
||||||
case 0:
|
case 0:
|
||||||
return &v.state
|
return &v.state
|
||||||
case 1:
|
case 1:
|
||||||
@@ -895,7 +965,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.(*InspectServiceResponse); i {
|
switch v := v.(*InspectServiceRequest); i {
|
||||||
case 0:
|
case 0:
|
||||||
return &v.state
|
return &v.state
|
||||||
case 1:
|
case 1:
|
||||||
@@ -907,6 +977,18 @@ 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.(*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[10].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
|
||||||
@@ -919,7 +1001,7 @@ func file_internal_machine_api_pb_machine_proto_init() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
file_internal_machine_api_pb_machine_proto_msgTypes[2].OneofWrappers = []any{
|
file_internal_machine_api_pb_machine_proto_msgTypes[3].OneofWrappers = []any{
|
||||||
(*InitClusterRequest_PublicIp)(nil),
|
(*InitClusterRequest_PublicIp)(nil),
|
||||||
(*InitClusterRequest_PublicIpAuto)(nil),
|
(*InitClusterRequest_PublicIpAuto)(nil),
|
||||||
}
|
}
|
||||||
@@ -929,7 +1011,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: 10,
|
NumMessages: 11,
|
||||||
NumExtensions: 0,
|
NumExtensions: 0,
|
||||||
NumServices: 1,
|
NumServices: 1,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import "google/protobuf/empty.proto";
|
|||||||
import "internal/machine/api/pb/common.proto";
|
import "internal/machine/api/pb/common.proto";
|
||||||
|
|
||||||
service Machine {
|
service Machine {
|
||||||
|
// CheckPrerequisites verifies if the machine meets all necessary system requirements to participate in the cluster.
|
||||||
|
rpc CheckPrerequisites(google.protobuf.Empty) returns (CheckPrerequisitesResponse);
|
||||||
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);
|
||||||
@@ -29,6 +31,13 @@ message NetworkConfig {
|
|||||||
bytes public_key = 4;
|
bytes public_key = 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message CheckPrerequisitesResponse {
|
||||||
|
// Overall status of the checks.
|
||||||
|
bool satisfied = 1;
|
||||||
|
// Error message if not satisfied (empty if all checks pass).
|
||||||
|
string error = 2;
|
||||||
|
}
|
||||||
|
|
||||||
message InitClusterRequest {
|
message InitClusterRequest {
|
||||||
string machineName = 1;
|
string machineName = 1;
|
||||||
IPPrefix network = 2;
|
IPPrefix network = 2;
|
||||||
|
|||||||
@@ -20,17 +20,19 @@ import (
|
|||||||
const _ = grpc.SupportPackageIsVersion9
|
const _ = grpc.SupportPackageIsVersion9
|
||||||
|
|
||||||
const (
|
const (
|
||||||
Machine_InitCluster_FullMethodName = "/api.Machine/InitCluster"
|
Machine_CheckPrerequisites_FullMethodName = "/api.Machine/CheckPrerequisites"
|
||||||
Machine_JoinCluster_FullMethodName = "/api.Machine/JoinCluster"
|
Machine_InitCluster_FullMethodName = "/api.Machine/InitCluster"
|
||||||
Machine_Token_FullMethodName = "/api.Machine/Token"
|
Machine_JoinCluster_FullMethodName = "/api.Machine/JoinCluster"
|
||||||
Machine_Inspect_FullMethodName = "/api.Machine/Inspect"
|
Machine_Token_FullMethodName = "/api.Machine/Token"
|
||||||
Machine_InspectService_FullMethodName = "/api.Machine/InspectService"
|
Machine_Inspect_FullMethodName = "/api.Machine/Inspect"
|
||||||
|
Machine_InspectService_FullMethodName = "/api.Machine/InspectService"
|
||||||
)
|
)
|
||||||
|
|
||||||
// MachineClient is the client API for Machine service.
|
// MachineClient is the client API for Machine service.
|
||||||
//
|
//
|
||||||
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
|
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
|
||||||
type MachineClient interface {
|
type MachineClient interface {
|
||||||
|
CheckPrerequisites(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*CheckPrerequisitesResponse, error)
|
||||||
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)
|
||||||
@@ -46,6 +48,16 @@ func NewMachineClient(cc grpc.ClientConnInterface) MachineClient {
|
|||||||
return &machineClient{cc}
|
return &machineClient{cc}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *machineClient) CheckPrerequisites(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*CheckPrerequisitesResponse, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(CheckPrerequisitesResponse)
|
||||||
|
err := c.cc.Invoke(ctx, Machine_CheckPrerequisites_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (c *machineClient) InitCluster(ctx context.Context, in *InitClusterRequest, opts ...grpc.CallOption) (*InitClusterResponse, error) {
|
func (c *machineClient) InitCluster(ctx context.Context, in *InitClusterRequest, opts ...grpc.CallOption) (*InitClusterResponse, error) {
|
||||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
out := new(InitClusterResponse)
|
out := new(InitClusterResponse)
|
||||||
@@ -100,6 +112,7 @@ func (c *machineClient) InspectService(ctx context.Context, in *InspectServiceRe
|
|||||||
// All implementations must embed UnimplementedMachineServer
|
// All implementations must embed UnimplementedMachineServer
|
||||||
// for forward compatibility.
|
// for forward compatibility.
|
||||||
type MachineServer interface {
|
type MachineServer interface {
|
||||||
|
CheckPrerequisites(context.Context, *emptypb.Empty) (*CheckPrerequisitesResponse, error)
|
||||||
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)
|
||||||
@@ -115,6 +128,9 @@ type MachineServer interface {
|
|||||||
// pointer dereference when methods are called.
|
// pointer dereference when methods are called.
|
||||||
type UnimplementedMachineServer struct{}
|
type UnimplementedMachineServer struct{}
|
||||||
|
|
||||||
|
func (UnimplementedMachineServer) CheckPrerequisites(context.Context, *emptypb.Empty) (*CheckPrerequisitesResponse, error) {
|
||||||
|
return nil, status.Errorf(codes.Unimplemented, "method CheckPrerequisites not implemented")
|
||||||
|
}
|
||||||
func (UnimplementedMachineServer) InitCluster(context.Context, *InitClusterRequest) (*InitClusterResponse, error) {
|
func (UnimplementedMachineServer) InitCluster(context.Context, *InitClusterRequest) (*InitClusterResponse, error) {
|
||||||
return nil, status.Errorf(codes.Unimplemented, "method InitCluster not implemented")
|
return nil, status.Errorf(codes.Unimplemented, "method InitCluster not implemented")
|
||||||
}
|
}
|
||||||
@@ -151,6 +167,24 @@ func RegisterMachineServer(s grpc.ServiceRegistrar, srv MachineServer) {
|
|||||||
s.RegisterService(&Machine_ServiceDesc, srv)
|
s.RegisterService(&Machine_ServiceDesc, srv)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func _Machine_CheckPrerequisites_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).CheckPrerequisites(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: Machine_CheckPrerequisites_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(MachineServer).CheckPrerequisites(ctx, req.(*emptypb.Empty))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
func _Machine_InitCluster_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
func _Machine_InitCluster_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
in := new(InitClusterRequest)
|
in := new(InitClusterRequest)
|
||||||
if err := dec(in); err != nil {
|
if err := dec(in); err != nil {
|
||||||
@@ -248,6 +282,10 @@ var Machine_ServiceDesc = grpc.ServiceDesc{
|
|||||||
ServiceName: "api.Machine",
|
ServiceName: "api.Machine",
|
||||||
HandlerType: (*MachineServer)(nil),
|
HandlerType: (*MachineServer)(nil),
|
||||||
Methods: []grpc.MethodDesc{
|
Methods: []grpc.MethodDesc{
|
||||||
|
{
|
||||||
|
MethodName: "CheckPrerequisites",
|
||||||
|
Handler: _Machine_CheckPrerequisites_Handler,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
MethodName: "InitCluster",
|
MethodName: "InitCluster",
|
||||||
Handler: _Machine_InitCluster_Handler,
|
Handler: _Machine_InitCluster_Handler,
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import (
|
|||||||
"github.com/caddyserver/caddy/v2/caddyconfig"
|
"github.com/caddyserver/caddy/v2/caddyconfig"
|
||||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp/reverseproxy"
|
"github.com/caddyserver/caddy/v2/modules/caddyhttp/reverseproxy"
|
||||||
"github.com/psviderski/uncloud/internal/machine/docker"
|
|
||||||
"github.com/psviderski/uncloud/pkg/api"
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -24,34 +23,38 @@ func GenerateConfig(containers []api.ServiceContainer, verifyResponse string) (*
|
|||||||
httpHostUpstreams := make(map[string][]string)
|
httpHostUpstreams := make(map[string][]string)
|
||||||
httpsHostUpstreams := make(map[string][]string)
|
httpsHostUpstreams := make(map[string][]string)
|
||||||
for _, ctr := range containers {
|
for _, ctr := range containers {
|
||||||
logger := slog.With("container", ctr.ID)
|
if !ctr.Healthy() {
|
||||||
network, ok := ctr.NetworkSettings.Networks[docker.NetworkName]
|
|
||||||
if !ok {
|
|
||||||
// Container is not connected to the uncloud Docker network (could be host network).
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if network.IPAddress == "" {
|
|
||||||
logger.Error("Container has no IPv4 address.")
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ip := ctr.UncloudNetworkIP()
|
||||||
|
if !ip.IsValid() {
|
||||||
|
// Container is not connected to the uncloud Docker network (could be host network).
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
log := slog.With("container", ctr.ID)
|
||||||
|
|
||||||
ports, err := ctr.ServicePorts()
|
ports, err := ctr.ServicePorts()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("Failed to parse service ports for container.", "err", err)
|
log.Error("Failed to parse service ports for container.", "err", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, port := range ports {
|
for _, port := range ports {
|
||||||
|
if port.Mode != api.PortModeIngress {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
switch port.Protocol {
|
switch port.Protocol {
|
||||||
case api.ProtocolHTTP:
|
case api.ProtocolHTTP:
|
||||||
upstream := net.JoinHostPort(network.IPAddress, strconv.Itoa(int(port.ContainerPort)))
|
upstream := net.JoinHostPort(ip.String(), strconv.Itoa(int(port.ContainerPort)))
|
||||||
httpHostUpstreams[port.Hostname] = append(httpHostUpstreams[port.Hostname], upstream)
|
httpHostUpstreams[port.Hostname] = append(httpHostUpstreams[port.Hostname], upstream)
|
||||||
case api.ProtocolHTTPS:
|
case api.ProtocolHTTPS:
|
||||||
upstream := net.JoinHostPort(network.IPAddress, strconv.Itoa(int(port.ContainerPort)))
|
upstream := net.JoinHostPort(ip.String(), strconv.Itoa(int(port.ContainerPort)))
|
||||||
httpsHostUpstreams[port.Hostname] = append(httpsHostUpstreams[port.Hostname], upstream)
|
httpsHostUpstreams[port.Hostname] = append(httpsHostUpstreams[port.Hostname], upstream)
|
||||||
default:
|
default:
|
||||||
// TODO: implement L4 ingress routing for TCP and UDP.
|
// TODO: implement L4 ingress routing for TCP and UDP.
|
||||||
logger.Error("Unsupported protocol for ingress port.", "port", port)
|
log.Error("Unsupported protocol for ingress port.", "port", port)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,25 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestGenerateConfig(t *testing.T) {
|
func TestGenerateConfig(t *testing.T) {
|
||||||
|
configWithoutServices := `{
|
||||||
|
"servers": {
|
||||||
|
"http": {
|
||||||
|
"listen": [":80"],
|
||||||
|
"routes": [{
|
||||||
|
"match": [{"path": ["/.uncloud-verify"]}],
|
||||||
|
"handle": [{
|
||||||
|
"body": "verification-response-body",
|
||||||
|
"handler": "static_response",
|
||||||
|
"status_code": 200
|
||||||
|
}]
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
"https": {
|
||||||
|
"listen": [":443"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
containers []api.ServiceContainer
|
containers []api.ServiceContainer
|
||||||
@@ -23,25 +42,8 @@ func TestGenerateConfig(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "empty containers",
|
name: "empty containers",
|
||||||
containers: []api.ServiceContainer{},
|
containers: []api.ServiceContainer{},
|
||||||
want: `{
|
want: configWithoutServices,
|
||||||
"servers": {
|
wantErr: false,
|
||||||
"http": {
|
|
||||||
"listen": [":80"],
|
|
||||||
"routes": [{
|
|
||||||
"match": [{"path": ["/.uncloud-verify"]}],
|
|
||||||
"handle": [{
|
|
||||||
"body": "verification-response-body",
|
|
||||||
"handler": "static_response",
|
|
||||||
"status_code": 200
|
|
||||||
}]
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
"https": {
|
|
||||||
"listen": [":443"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}`,
|
|
||||||
wantErr: false,
|
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -231,24 +233,7 @@ func TestGenerateConfig(t *testing.T) {
|
|||||||
containers: []api.ServiceContainer{
|
containers: []api.ServiceContainer{
|
||||||
newContainerWithoutNetwork("ignored.example.com:8080/http"),
|
newContainerWithoutNetwork("ignored.example.com:8080/http"),
|
||||||
},
|
},
|
||||||
want: `{
|
want: configWithoutServices,
|
||||||
"servers": {
|
|
||||||
"http": {
|
|
||||||
"listen": [":80"],
|
|
||||||
"routes": [{
|
|
||||||
"match": [{"path": ["/.uncloud-verify"]}],
|
|
||||||
"handle": [{
|
|
||||||
"body": "verification-response-body",
|
|
||||||
"handler": "static_response",
|
|
||||||
"status_code": 200
|
|
||||||
}]
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
"https": {
|
|
||||||
"listen": [":443"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}`,
|
|
||||||
wantErr: false,
|
wantErr: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -256,45 +241,63 @@ func TestGenerateConfig(t *testing.T) {
|
|||||||
containers: []api.ServiceContainer{
|
containers: []api.ServiceContainer{
|
||||||
newContainer("10.210.0.2", "invalid-port"),
|
newContainer("10.210.0.2", "invalid-port"),
|
||||||
},
|
},
|
||||||
want: `{
|
want: configWithoutServices,
|
||||||
"servers": {
|
|
||||||
"http": {
|
|
||||||
"listen": [":80"],
|
|
||||||
"routes": [{
|
|
||||||
"match": [{"path": ["/.uncloud-verify"]}],
|
|
||||||
"handle": [{
|
|
||||||
"body": "verification-response-body",
|
|
||||||
"handler": "static_response",
|
|
||||||
"status_code": 200
|
|
||||||
}]
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
"https": {
|
|
||||||
"listen": [":443"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}`,
|
|
||||||
wantErr: false,
|
wantErr: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "containers with unsupported protocols ignored",
|
name: "containers with unsupported protocols and host mode ignored",
|
||||||
containers: []api.ServiceContainer{
|
containers: []api.ServiceContainer{
|
||||||
newContainer("10.210.0.2", "5000/tcp"),
|
newContainer("10.210.0.2", "5000/tcp"),
|
||||||
newContainer("10.210.0.3", "5000/udp"),
|
newContainer("10.210.0.3", "5000/udp"),
|
||||||
newContainer("10.210.0.4", "80:8080/tcp@host"),
|
newContainer("10.210.0.4", "80:8080/tcp@host"),
|
||||||
},
|
},
|
||||||
|
want: configWithoutServices,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "restarting container ignored",
|
||||||
|
containers: []api.ServiceContainer{
|
||||||
|
newRestartingContainer("10.210.0.2", "app.example.com:8080/http"),
|
||||||
|
},
|
||||||
|
want: configWithoutServices,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "stopped container ignored",
|
||||||
|
containers: []api.ServiceContainer{
|
||||||
|
newStoppedContainer("10.210.0.2", "app.example.com:8080/http"),
|
||||||
|
},
|
||||||
|
want: configWithoutServices,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "mix of running, restarting, and stopped containers",
|
||||||
|
containers: []api.ServiceContainer{
|
||||||
|
newContainer("10.210.0.2", "app.example.com:8080/http"),
|
||||||
|
newRestartingContainer("10.210.0.3", "app.example.com:8080/http"),
|
||||||
|
newStoppedContainer("10.210.0.4", "app.example.com:8080/http"),
|
||||||
|
},
|
||||||
want: `{
|
want: `{
|
||||||
"servers": {
|
"servers": {
|
||||||
"http": {
|
"http": {
|
||||||
"listen": [":80"],
|
"listen": [":80"],
|
||||||
"routes": [{
|
"routes": [
|
||||||
"match": [{"path": ["/.uncloud-verify"]}],
|
{
|
||||||
"handle": [{
|
"match": [{"host": ["app.example.com"]}],
|
||||||
"body": "verification-response-body",
|
"handle": [{
|
||||||
"handler": "static_response",
|
"handler": "reverse_proxy",
|
||||||
"status_code": 200
|
"upstreams": [{"dial": "10.210.0.2:8080"}]
|
||||||
}]
|
}]
|
||||||
}]
|
},
|
||||||
|
{
|
||||||
|
"match": [{"path": ["/.uncloud-verify"]}],
|
||||||
|
"handle": [{
|
||||||
|
"body": "verification-response-body",
|
||||||
|
"handler": "static_response",
|
||||||
|
"status_code": 200
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"https": {
|
"https": {
|
||||||
"listen": [":443"]
|
"listen": [":443"]
|
||||||
@@ -326,7 +329,11 @@ func TestGenerateConfig(t *testing.T) {
|
|||||||
func newContainer(ip string, ports ...string) api.ServiceContainer {
|
func newContainer(ip string, ports ...string) api.ServiceContainer {
|
||||||
portsLabel := strings.Join(ports, ",")
|
portsLabel := strings.Join(ports, ",")
|
||||||
return api.ServiceContainer{Container: api.Container{ContainerJSON: types.ContainerJSON{
|
return api.ServiceContainer{Container: api.Container{ContainerJSON: types.ContainerJSON{
|
||||||
ContainerJSONBase: &types.ContainerJSONBase{},
|
ContainerJSONBase: &types.ContainerJSONBase{
|
||||||
|
State: &types.ContainerState{
|
||||||
|
Running: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
NetworkSettings: &types.NetworkSettings{
|
NetworkSettings: &types.NetworkSettings{
|
||||||
Networks: map[string]*network.EndpointSettings{
|
Networks: map[string]*network.EndpointSettings{
|
||||||
docker.NetworkName: {
|
docker.NetworkName: {
|
||||||
@@ -345,7 +352,11 @@ func newContainer(ip string, ports ...string) api.ServiceContainer {
|
|||||||
func newContainerWithoutNetwork(ports ...string) api.ServiceContainer {
|
func newContainerWithoutNetwork(ports ...string) api.ServiceContainer {
|
||||||
portsLabel := strings.Join(ports, ",")
|
portsLabel := strings.Join(ports, ",")
|
||||||
return api.ServiceContainer{Container: api.Container{ContainerJSON: types.ContainerJSON{
|
return api.ServiceContainer{Container: api.Container{ContainerJSON: types.ContainerJSON{
|
||||||
ContainerJSONBase: &types.ContainerJSONBase{},
|
ContainerJSONBase: &types.ContainerJSONBase{
|
||||||
|
State: &types.ContainerState{
|
||||||
|
Running: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
NetworkSettings: &types.NetworkSettings{
|
NetworkSettings: &types.NetworkSettings{
|
||||||
Networks: map[string]*network.EndpointSettings{
|
Networks: map[string]*network.EndpointSettings{
|
||||||
"other-network": {
|
"other-network": {
|
||||||
@@ -360,3 +371,15 @@ func newContainerWithoutNetwork(ports ...string) api.ServiceContainer {
|
|||||||
},
|
},
|
||||||
}}}
|
}}}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func newRestartingContainer(ip string, ports ...string) api.ServiceContainer {
|
||||||
|
ctr := newContainer(ip, ports...)
|
||||||
|
ctr.Container.State.Restarting = true
|
||||||
|
return ctr
|
||||||
|
}
|
||||||
|
|
||||||
|
func newStoppedContainer(ip string, ports ...string) api.ServiceContainer {
|
||||||
|
ctr := newContainer(ip, ports...)
|
||||||
|
ctr.Container.State.Running = false
|
||||||
|
return ctr
|
||||||
|
}
|
||||||
|
|||||||
@@ -36,16 +36,11 @@ func NewDB(path string) (*sqlx.DB, error) {
|
|||||||
schema := `
|
schema := `
|
||||||
CREATE TABLE IF NOT EXISTS containers (
|
CREATE TABLE IF NOT EXISTS containers (
|
||||||
id TEXT NOT NULL PRIMARY KEY,
|
id TEXT NOT NULL PRIMARY KEY,
|
||||||
service_id TEXT NOT NULL,
|
|
||||||
service_name TEXT AS (json_extract(service_spec, '$.Name')),
|
|
||||||
service_spec TEXT NOT NULL CHECK (json_valid(service_spec)),
|
service_spec TEXT NOT NULL CHECK (json_valid(service_spec)),
|
||||||
-- 'subsecond' modifier is used to store timestamps with millisecond precision.
|
-- 'subsecond' modifier is used to store timestamps with millisecond precision.
|
||||||
created_at TIMESTAMP NOT NULL DEFAULT (datetime('subsecond')),
|
created_at TIMESTAMP NOT NULL DEFAULT (datetime('subsecond')),
|
||||||
updated_at TIMESTAMP NOT NULL DEFAULT (datetime('subsecond'))
|
updated_at TIMESTAMP NOT NULL DEFAULT (datetime('subsecond'))
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_containers_service_id ON containers (service_id);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_containers_service_name ON containers (service_name);
|
|
||||||
`
|
`
|
||||||
|
|
||||||
if _, err = db.Exec(schema); err != nil {
|
if _, err = db.Exec(schema); err != nil {
|
||||||
|
|||||||
@@ -0,0 +1,126 @@
|
|||||||
|
package dns
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"net/netip"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/psviderski/uncloud/internal/machine/store"
|
||||||
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ClusterResolver implements Resolver by tracking containers in the cluster and resolving service names
|
||||||
|
// to their IP addresses.
|
||||||
|
type ClusterResolver struct {
|
||||||
|
store *store.Store
|
||||||
|
// serviceIPs maps service names to container IPs.
|
||||||
|
serviceIPs map[string][]netip.Addr
|
||||||
|
// mu protects the serviceIPs map.
|
||||||
|
mu sync.RWMutex
|
||||||
|
// lastUpdate tracks when records were last updated.
|
||||||
|
lastUpdate time.Time
|
||||||
|
log *slog.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewClusterResolver creates a new cluster resolver using the cluster store.
|
||||||
|
func NewClusterResolver(store *store.Store) *ClusterResolver {
|
||||||
|
return &ClusterResolver{
|
||||||
|
store: store,
|
||||||
|
serviceIPs: make(map[string][]netip.Addr),
|
||||||
|
log: slog.With("component", "dns-resolver"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run starts watching for container changes and updates DNS records accordingly.
|
||||||
|
func (r *ClusterResolver) Run(ctx context.Context) error {
|
||||||
|
containers, changes, err := r.store.SubscribeContainers(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("subscribe to container changes: %w", err)
|
||||||
|
}
|
||||||
|
r.log.Info("Subscribed to container changes in the cluster to keep DNS records updated.")
|
||||||
|
|
||||||
|
// TODO: implement machine membership check using Corrossion Admin client to filter available containers.
|
||||||
|
r.updateServiceIPs(containers)
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case _, ok := <-changes:
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("containers subscription failed")
|
||||||
|
}
|
||||||
|
r.log.Debug("Cluster containers changed, updating DNS records.")
|
||||||
|
|
||||||
|
containers, err = r.store.ListContainers(ctx, store.ListOptions{})
|
||||||
|
if err != nil {
|
||||||
|
r.log.Error("Failed to list containers.", "err", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: implement machine membership check using Corrossion Admin client to filter available containers.
|
||||||
|
r.updateServiceIPs(containers)
|
||||||
|
case <-ctx.Done():
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// updateServiceIPs processes container records and updates the serviceIPs map.
|
||||||
|
func (r *ClusterResolver) updateServiceIPs(containers []store.ContainerRecord) {
|
||||||
|
newServiceIPs := make(map[string][]netip.Addr, len(r.serviceIPs))
|
||||||
|
|
||||||
|
containersCount := 0
|
||||||
|
for _, record := range containers {
|
||||||
|
if !record.Container.Healthy() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
ip := record.Container.UncloudNetworkIP()
|
||||||
|
if !ip.IsValid() {
|
||||||
|
// Container is not connected to the uncloud Docker network (could be host network).
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
ctr := api.ServiceContainer{Container: record.Container}
|
||||||
|
if ctr.ServiceID() == "" || ctr.ServiceName() == "" {
|
||||||
|
// Container is not part of a service, skip it.
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: remove normalisation after implementing service name validation:
|
||||||
|
//.https://github.com/psviderski/uncloud/issues/53
|
||||||
|
serviceName := strings.ToLower(ctr.ServiceName())
|
||||||
|
|
||||||
|
newServiceIPs[serviceName] = append(newServiceIPs[serviceName], ip)
|
||||||
|
// Also add the service ID as a valid lookup.
|
||||||
|
newServiceIPs[ctr.ServiceID()] = append(newServiceIPs[ctr.ServiceID()], ip)
|
||||||
|
containersCount++
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the serviceIPs map atomically.
|
||||||
|
r.mu.Lock()
|
||||||
|
r.serviceIPs = newServiceIPs
|
||||||
|
r.mu.Unlock()
|
||||||
|
|
||||||
|
r.log.Debug("DNS records updated.", "services", len(newServiceIPs)/2, "containers", containersCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve returns IP addresses of the service containers.
|
||||||
|
func (r *ClusterResolver) Resolve(serviceName string) []netip.Addr {
|
||||||
|
r.mu.RLock()
|
||||||
|
defer r.mu.RUnlock()
|
||||||
|
|
||||||
|
ips, ok := r.serviceIPs[serviceName]
|
||||||
|
if !ok || len(ips) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return a copy of the IPs slice to prevent modification of the original.
|
||||||
|
ipsCopy := make([]netip.Addr, len(ips))
|
||||||
|
copy(ipsCopy, ips)
|
||||||
|
|
||||||
|
return ipsCopy
|
||||||
|
}
|
||||||
@@ -0,0 +1,342 @@
|
|||||||
|
package dns
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"math/rand/v2"
|
||||||
|
"net"
|
||||||
|
"net/netip"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/miekg/dns"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// InternalDomain is the cluster internal domain for service discovery. All DNS queries ending with this suffix
|
||||||
|
// will be resolved using the internal DNS server.
|
||||||
|
InternalDomain = "internal."
|
||||||
|
// Port is the standard DNS port.
|
||||||
|
Port = 53
|
||||||
|
// maxConcurrentForwards is the maximum number of concurrent forwarded queries to upstream DNS servers.
|
||||||
|
// 1024 is the default used by the Docker internal DNS server.
|
||||||
|
maxConcurrentForwards = 1024
|
||||||
|
// forwardingTimeout is the timeout for forwarding a DNS query to an upstream server.
|
||||||
|
forwardingTimeout = 3 * time.Second
|
||||||
|
)
|
||||||
|
|
||||||
|
// Resolver is an interface for resolving service names to IP addresses.
|
||||||
|
type Resolver interface {
|
||||||
|
// Resolve returns a list of IP addresses of the service containers.
|
||||||
|
// An empty list is returned if no service is found.
|
||||||
|
Resolve(serviceName string) []netip.Addr
|
||||||
|
}
|
||||||
|
|
||||||
|
// Server is an embedded internal DNS server for service discovery and forwarding external queries
|
||||||
|
// to upstream DNS servers.
|
||||||
|
type Server struct {
|
||||||
|
listenAddr netip.Addr
|
||||||
|
resolver Resolver
|
||||||
|
upstreamServers []netip.AddrPort
|
||||||
|
|
||||||
|
udpServer *dns.Server
|
||||||
|
tcpServer *dns.Server
|
||||||
|
inProgressReqs sync.WaitGroup
|
||||||
|
forwardSemaphore chan struct{}
|
||||||
|
log *slog.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewServer creates a new DNS server with the given configuration.
|
||||||
|
// If upstreams is nil, nameservers from /etc/resolv.conf will be used. An empty upstreams list means to only resolve
|
||||||
|
// internal DNS queries and not forward any external queries.
|
||||||
|
func NewServer(listenAddr netip.Addr, resolver Resolver, upstreams []netip.AddrPort) (*Server, error) {
|
||||||
|
if !listenAddr.IsValid() {
|
||||||
|
return nil, fmt.Errorf("invalid listen address: %s", listenAddr)
|
||||||
|
}
|
||||||
|
if resolver == nil {
|
||||||
|
return nil, fmt.Errorf("resolver must be provided")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load upstreams from /etc/resolv.conf and set fallback servers only if upstreams is not provided.
|
||||||
|
if upstreams == nil {
|
||||||
|
if nameservers, err := parseNameserversFromResolvConf(); err != nil {
|
||||||
|
slog.Warn("Failed to parse nameservers from /etc/resolv.conf.", "err", err)
|
||||||
|
} else {
|
||||||
|
for _, ns := range nameservers {
|
||||||
|
if ns.Compare(listenAddr) == 0 {
|
||||||
|
// Skip this internal DNS server address if it has been configured in /etc/resolv.conf
|
||||||
|
// to not create a forwarding loop.
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
upstreams = append(upstreams, netip.AddrPortFrom(ns, Port))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to common public DNS servers if no nameservers were found in /etc/resolv.conf.
|
||||||
|
if upstreams == nil {
|
||||||
|
upstreams = []netip.AddrPort{
|
||||||
|
netip.AddrPortFrom(netip.MustParseAddr("1.1.1.1"), Port), // Cloudflare DNS
|
||||||
|
netip.AddrPortFrom(netip.MustParseAddr("8.8.8.8"), Port), // Google DNS
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Server{
|
||||||
|
listenAddr: listenAddr,
|
||||||
|
resolver: resolver,
|
||||||
|
upstreamServers: upstreams,
|
||||||
|
forwardSemaphore: make(chan struct{}, maxConcurrentForwards),
|
||||||
|
log: slog.With("component", "dns-server"),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run starts the DNS server listening on both UDP and TCP ports. The server on TCP is not critical so it won't return
|
||||||
|
// an error if it fails to start. The server will run until the context is canceled or an error occurs.
|
||||||
|
func (s *Server) Run(ctx context.Context) error {
|
||||||
|
addr := net.JoinHostPort(s.listenAddr.String(), strconv.Itoa(Port))
|
||||||
|
s.udpServer = &dns.Server{
|
||||||
|
Addr: addr,
|
||||||
|
Net: "udp",
|
||||||
|
Handler: dns.HandlerFunc(s.handleRequest),
|
||||||
|
}
|
||||||
|
s.tcpServer = &dns.Server{
|
||||||
|
Addr: addr,
|
||||||
|
Net: "tcp",
|
||||||
|
Handler: dns.HandlerFunc(s.handleRequest),
|
||||||
|
}
|
||||||
|
|
||||||
|
errCh := make(chan error, 1) // Buffer size 1 is for UDP server error only.
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
s.log.Info("Starting DNS server on UDP port.", "addr", addr, "upstreams", s.upstreamServers)
|
||||||
|
if err := s.udpServer.ListenAndServe(); err != nil {
|
||||||
|
errCh <- fmt.Errorf("listen and serve on %s/udp: %w", addr, err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
s.log.Info("Starting DNS server on TCP port.", "addr", addr, "upstreams", s.upstreamServers)
|
||||||
|
if err := s.tcpServer.ListenAndServe(); err != nil {
|
||||||
|
// TCP server is not critical, so log the error and continue.
|
||||||
|
slog.Warn("Failed to listen and serve DNS server on TCP port. "+
|
||||||
|
"TCP server is not critical and will be ignored.", "addr", addr, "err", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case err := <-errCh:
|
||||||
|
// Stop the servers if the UDP one fails.
|
||||||
|
s.stop()
|
||||||
|
return err
|
||||||
|
case <-ctx.Done():
|
||||||
|
s.log.Info("Stopping DNS server.")
|
||||||
|
return s.stop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// stop gracefully shuts down the DNS server.
|
||||||
|
func (s *Server) stop() error {
|
||||||
|
var udpErr, tcpErr error
|
||||||
|
|
||||||
|
if s.udpServer != nil {
|
||||||
|
udpErr = s.udpServer.Shutdown()
|
||||||
|
}
|
||||||
|
if s.tcpServer != nil {
|
||||||
|
tcpErr = s.tcpServer.Shutdown()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for all in-progress requests to finish.
|
||||||
|
s.inProgressReqs.Wait()
|
||||||
|
|
||||||
|
if udpErr != nil {
|
||||||
|
return fmt.Errorf("shutdown DNS server (UDP): %w", udpErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
if tcpErr != nil {
|
||||||
|
return fmt.Errorf("shutdown DNS server (TCP): %w", tcpErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleRequest processes a DNS query and returns an appropriate response.
|
||||||
|
func (s *Server) handleRequest(w dns.ResponseWriter, req *dns.Msg) {
|
||||||
|
s.inProgressReqs.Add(1)
|
||||||
|
defer s.inProgressReqs.Done()
|
||||||
|
|
||||||
|
if len(req.Question) == 0 {
|
||||||
|
resp := new(dns.Msg).SetRcode(req, dns.RcodeFormatError)
|
||||||
|
s.reply(w, req, resp)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// While the original DNS RFCs allow multiple questions, in practice it never works. So handle only the first one.
|
||||||
|
q := req.Question[0]
|
||||||
|
log := s.log.With("name", q.Name, "type", dns.TypeToString[q.Qtype])
|
||||||
|
log.Debug("Received DNS query.")
|
||||||
|
|
||||||
|
if !dns.IsSubDomain(InternalDomain, dns.CanonicalName(q.Name)) {
|
||||||
|
log.Debug("Forwarding non-internal DNS query to upstream DNS servers.")
|
||||||
|
|
||||||
|
// Use the same transport for the forwarded request as the original request.
|
||||||
|
resp, err := s.forwardRequest(req, w.LocalAddr().Network())
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Failed to forward DNS query.", "err", err)
|
||||||
|
resp = new(dns.Msg).SetRcode(req, dns.RcodeServerFailure)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.reply(w, req, resp)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle the query for the internal domain.
|
||||||
|
resp := new(dns.Msg).SetReply(req)
|
||||||
|
resp.Authoritative = true
|
||||||
|
resp.RecursionAvailable = true
|
||||||
|
|
||||||
|
switch q.Qtype {
|
||||||
|
case dns.TypeA:
|
||||||
|
records := s.handleAQuery(q.Name)
|
||||||
|
if len(records) > 0 {
|
||||||
|
log.Debug("Found A records for internal DNS query.", "count", len(records))
|
||||||
|
resp.Answer = append(resp.Answer, records...)
|
||||||
|
} else {
|
||||||
|
log.Debug("No records found for internal DNS query.")
|
||||||
|
resp.SetRcode(req, dns.RcodeNameError)
|
||||||
|
}
|
||||||
|
// TODO: Handle other query types (SRV, TXT, etc.) as needed.
|
||||||
|
}
|
||||||
|
|
||||||
|
// Truncate the response if it exceeds the maximum size for the transport protocol.
|
||||||
|
maxSize := dns.MinMsgSize
|
||||||
|
if w.LocalAddr().Network() == "tcp" {
|
||||||
|
maxSize = dns.MaxMsgSize
|
||||||
|
} else {
|
||||||
|
// Retrieve the UDP buffer size from the EDNS0 record if present.
|
||||||
|
if opt := req.IsEdns0(); opt != nil {
|
||||||
|
if udpSize := int(opt.UDPSize()); udpSize > maxSize {
|
||||||
|
maxSize = udpSize
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resp.Truncate(maxSize)
|
||||||
|
|
||||||
|
s.reply(w, req, resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) reply(w dns.ResponseWriter, req *dns.Msg, resp *dns.Msg) error {
|
||||||
|
if err := w.WriteMsg(resp); err != nil {
|
||||||
|
s.log.Error("Failed to write DNS response.", "err", err, "msg", resp)
|
||||||
|
// It may fail due to a malformed response message, e.g. exceeding the maximum size. In that case,
|
||||||
|
// attempt to send a server failure response instead so the client doesn't have to wait for a timeout.
|
||||||
|
if resp.Rcode != dns.RcodeServerFailure {
|
||||||
|
resp = new(dns.Msg).SetRcode(req, dns.RcodeServerFailure)
|
||||||
|
if err2 := w.WriteMsg(resp); err2 != nil {
|
||||||
|
s.log.Error("Failed to write DNS error response.", "err", err2, "msg", resp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// forwardRequest forwards a DNS query to system DNS servers
|
||||||
|
func (s *Server) forwardRequest(req *dns.Msg, proto string) (*dns.Msg, error) {
|
||||||
|
if len(s.upstreamServers) == 0 {
|
||||||
|
return nil, errors.New("no upstream DNS servers configured")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply concurrency control for forwarded queries.
|
||||||
|
select {
|
||||||
|
case s.forwardSemaphore <- struct{}{}:
|
||||||
|
defer func() {
|
||||||
|
<-s.forwardSemaphore
|
||||||
|
}()
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("too many concurrent forwarded queries (max: %d)", maxConcurrentForwards)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create DNS client with timeout and same transport as original request
|
||||||
|
client := &dns.Client{
|
||||||
|
Net: proto,
|
||||||
|
Timeout: forwardingTimeout,
|
||||||
|
}
|
||||||
|
|
||||||
|
var lastErr error
|
||||||
|
for _, server := range s.upstreamServers {
|
||||||
|
resp, _, err := client.Exchange(req, server.String())
|
||||||
|
if err == nil {
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
lastErr = err
|
||||||
|
s.log.Debug("Failed to forward DNS query to upstream server.", "server", server, "err", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, lastErr
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleAQuery processes an A query for the internal domain and returns A records for the requested name.
|
||||||
|
// The internal domain suffix is already stripped from the name. An empty list is returned if no records are found.
|
||||||
|
func (s *Server) handleAQuery(name string) []dns.RR {
|
||||||
|
serviceName := trimInternalDomain(name)
|
||||||
|
ips := s.resolver.Resolve(serviceName)
|
||||||
|
if len(ips) == 0 {
|
||||||
|
s.log.Debug("Failed to resolve service name.", "service", serviceName)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
s.log.Debug("Resolved service name.", "service", serviceName, "ips", ips)
|
||||||
|
|
||||||
|
if len(ips) > 1 {
|
||||||
|
// TODO: sort by proximity to the requesting container/machine. For now, just shuffle the IPs.
|
||||||
|
rand.Shuffle(len(ips), func(i, j int) {
|
||||||
|
ips[i], ips[j] = ips[j], ips[i]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create A records for each IP.
|
||||||
|
records := make([]dns.RR, 0, len(ips))
|
||||||
|
for _, ip := range ips {
|
||||||
|
records = append(records, &dns.A{
|
||||||
|
Hdr: dns.RR_Header{
|
||||||
|
Name: name,
|
||||||
|
Rrtype: dns.TypeA,
|
||||||
|
Class: dns.ClassINET,
|
||||||
|
// TODO: should we increate the TTL to some reasonably small value like 5-30 seconds to allow
|
||||||
|
// at least some caching?
|
||||||
|
Ttl: 0,
|
||||||
|
},
|
||||||
|
A: net.ParseIP(ip.String()),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return records
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseNameserversFromResolvConf parses the nameservers from /etc/resolv.conf.
|
||||||
|
func parseNameserversFromResolvConf() ([]netip.Addr, error) {
|
||||||
|
config, err := dns.ClientConfigFromFile("/etc/resolv.conf")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var servers []netip.Addr
|
||||||
|
for _, server := range config.Servers {
|
||||||
|
if addr, err := netip.ParseAddr(server); err == nil {
|
||||||
|
servers = append(servers, addr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return servers, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func trimInternalDomain(name string) string {
|
||||||
|
name = dns.CanonicalName(name)
|
||||||
|
if !dns.IsSubDomain(InternalDomain, name) {
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.TrimSuffix(name, "."+InternalDomain)
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"github.com/docker/docker/api/types"
|
"github.com/docker/docker/api/types"
|
||||||
"github.com/docker/docker/api/types/container"
|
"github.com/docker/docker/api/types/container"
|
||||||
"github.com/docker/docker/api/types/network"
|
"github.com/docker/docker/api/types/network"
|
||||||
|
"github.com/docker/docker/api/types/volume"
|
||||||
"github.com/docker/docker/errdefs"
|
"github.com/docker/docker/errdefs"
|
||||||
"github.com/docker/docker/pkg/jsonmessage"
|
"github.com/docker/docker/pkg/jsonmessage"
|
||||||
regtypes "github.com/google/go-containerregistry/pkg/v1/types"
|
regtypes "github.com/google/go-containerregistry/pkg/v1/types"
|
||||||
@@ -341,6 +342,75 @@ func parseRemoteImageMessage(msg *pb.RemoteImage) (api.MachineRemoteImage, error
|
|||||||
return mri, nil
|
return mri, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CreateVolume creates a new volume with the given options.
|
||||||
|
func (c *Client) CreateVolume(ctx context.Context, opts volume.CreateOptions) (volume.Volume, error) {
|
||||||
|
var vol volume.Volume
|
||||||
|
|
||||||
|
optsBytes, err := json.Marshal(opts)
|
||||||
|
if err != nil {
|
||||||
|
return vol, fmt.Errorf("marshal options: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := c.grpcClient.CreateVolume(ctx, &pb.CreateVolumeRequest{Options: optsBytes})
|
||||||
|
if err != nil {
|
||||||
|
return vol, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = json.Unmarshal(resp.Volume, &vol); err != nil {
|
||||||
|
return vol, fmt.Errorf("unmarshal volume: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return vol, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MachineVolumes represents a volume list response from a machine.
|
||||||
|
type MachineVolumes struct {
|
||||||
|
Metadata *pb.Metadata
|
||||||
|
Response volume.ListResponse
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListVolumes returns a list of all volumes matching the filter.
|
||||||
|
func (c *Client) ListVolumes(ctx context.Context, opts volume.ListOptions) ([]MachineVolumes, error) {
|
||||||
|
optsBytes, err := json.Marshal(opts)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("marshal options: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := c.grpcClient.ListVolumes(ctx, &pb.ListVolumesRequest{Options: optsBytes})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
machineVolumes := make([]MachineVolumes, len(resp.Messages))
|
||||||
|
for i, msg := range resp.Messages {
|
||||||
|
machineVolumes[i].Metadata = msg.Metadata
|
||||||
|
if msg.Metadata != nil && msg.Metadata.Error != "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = json.Unmarshal(msg.Response, &machineVolumes[i].Response); err != nil {
|
||||||
|
return nil, fmt.Errorf("unmarshal response: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return machineVolumes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveVolume removes a volume with the given ID.
|
||||||
|
func (c *Client) RemoveVolume(ctx context.Context, id string, force bool) error {
|
||||||
|
_, err := c.grpcClient.RemoveVolume(ctx, &pb.RemoveVolumeRequest{
|
||||||
|
Id: id,
|
||||||
|
Force: force,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
if status.Convert(err).Code() == codes.NotFound {
|
||||||
|
return errdefs.NotFound(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// CreateServiceContainer creates a new container for the service with the given specifications.
|
// CreateServiceContainer creates a new container for the service with the given specifications.
|
||||||
func (c *Client) CreateServiceContainer(
|
func (c *Client) CreateServiceContainer(
|
||||||
ctx context.Context, serviceID string, spec api.ServiceSpec, containerName string,
|
ctx context.Context, serviceID string, spec api.ServiceSpec, containerName string,
|
||||||
|
|||||||
@@ -8,7 +8,9 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
|
"net/netip"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -17,7 +19,9 @@ import (
|
|||||||
"github.com/docker/docker/api/types/container"
|
"github.com/docker/docker/api/types/container"
|
||||||
"github.com/docker/docker/api/types/filters"
|
"github.com/docker/docker/api/types/filters"
|
||||||
"github.com/docker/docker/api/types/image"
|
"github.com/docker/docker/api/types/image"
|
||||||
|
"github.com/docker/docker/api/types/mount"
|
||||||
"github.com/docker/docker/api/types/network"
|
"github.com/docker/docker/api/types/network"
|
||||||
|
"github.com/docker/docker/api/types/volume"
|
||||||
"github.com/docker/docker/client"
|
"github.com/docker/docker/client"
|
||||||
"github.com/docker/go-connections/nat"
|
"github.com/docker/go-connections/nat"
|
||||||
"github.com/google/go-containerregistry/pkg/authn"
|
"github.com/google/go-containerregistry/pkg/authn"
|
||||||
@@ -27,6 +31,7 @@ import (
|
|||||||
"github.com/opencontainers/go-digest"
|
"github.com/opencontainers/go-digest"
|
||||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||||
"github.com/psviderski/uncloud/internal/machine/api/pb"
|
"github.com/psviderski/uncloud/internal/machine/api/pb"
|
||||||
|
"github.com/psviderski/uncloud/internal/machine/dns"
|
||||||
"github.com/psviderski/uncloud/internal/secret"
|
"github.com/psviderski/uncloud/internal/secret"
|
||||||
"github.com/psviderski/uncloud/pkg/api"
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
@@ -42,13 +47,17 @@ type Server struct {
|
|||||||
pb.UnimplementedDockerServer
|
pb.UnimplementedDockerServer
|
||||||
client *client.Client
|
client *client.Client
|
||||||
db *sqlx.DB
|
db *sqlx.DB
|
||||||
|
// internalDNSIP is a function that returns the IP address of the internal DNS server. It may return an empty
|
||||||
|
// address if the address is unknown (e.g. when the machine is not initialised yet).
|
||||||
|
internalDNSIP func() netip.Addr
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewServer creates a new Docker gRPC server with the provided Docker client.
|
// NewServer creates a new Docker gRPC server with the provided Docker client.
|
||||||
func NewServer(cli *client.Client, db *sqlx.DB) *Server {
|
func NewServer(cli *client.Client, db *sqlx.DB, internalDNSIP func() netip.Addr) *Server {
|
||||||
return &Server{
|
return &Server{
|
||||||
client: cli,
|
client: cli,
|
||||||
db: db,
|
db: db,
|
||||||
|
internalDNSIP: internalDNSIP,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -329,6 +338,80 @@ func (s *Server) InspectRemoteImage(
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CreateVolume creates a new volume with the given options.
|
||||||
|
func (s *Server) CreateVolume(ctx context.Context, req *pb.CreateVolumeRequest) (*pb.CreateVolumeResponse, error) {
|
||||||
|
var opts volume.CreateOptions
|
||||||
|
if err := json.Unmarshal(req.Options, &opts); err != nil {
|
||||||
|
return nil, status.Errorf(codes.InvalidArgument, "unmarshal options: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
vol, err := s.client.VolumeCreate(ctx, opts)
|
||||||
|
if err != nil {
|
||||||
|
return nil, status.Error(codes.Internal, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
volBytes, err := json.Marshal(vol)
|
||||||
|
if err != nil {
|
||||||
|
return nil, status.Errorf(codes.Internal, "marshal volume: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &pb.CreateVolumeResponse{Volume: volBytes}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListVolumes returns a list of all volumes matching the filter.
|
||||||
|
func (s *Server) ListVolumes(ctx context.Context, req *pb.ListVolumesRequest) (*pb.ListVolumesResponse, error) {
|
||||||
|
var opts volume.ListOptions
|
||||||
|
if len(req.Options) > 0 {
|
||||||
|
if err := json.Unmarshal(req.Options, &opts); err != nil {
|
||||||
|
return nil, status.Errorf(codes.InvalidArgument, "unmarshal options: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle filters separately because they implement custom JSON unmarshalling.
|
||||||
|
var raw map[string]json.RawMessage
|
||||||
|
if err := json.Unmarshal(req.Options, &raw); err != nil {
|
||||||
|
return nil, status.Errorf(codes.InvalidArgument, "unmarshal options to raw map: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if filtersBytes, ok := raw["Filters"]; ok {
|
||||||
|
args, err := filters.FromJSON(string(filtersBytes))
|
||||||
|
if err != nil {
|
||||||
|
return nil, status.Errorf(codes.InvalidArgument, "unmarshal filters: %v", err)
|
||||||
|
}
|
||||||
|
opts.Filters = args
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := s.client.VolumeList(ctx, opts)
|
||||||
|
if err != nil {
|
||||||
|
return nil, status.Error(codes.Internal, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
respBytes, err := json.Marshal(resp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, status.Errorf(codes.Internal, "marshal response: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &pb.ListVolumesResponse{
|
||||||
|
Messages: []*pb.MachineVolumes{
|
||||||
|
{
|
||||||
|
Response: respBytes,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveVolume removes a volume with the given ID.
|
||||||
|
func (s *Server) RemoveVolume(ctx context.Context, req *pb.RemoveVolumeRequest) (*emptypb.Empty, error) {
|
||||||
|
if err := s.client.VolumeRemove(ctx, req.Id, req.Force); err != nil {
|
||||||
|
if client.IsErrNotFound(err) {
|
||||||
|
return nil, status.Error(codes.NotFound, err.Error())
|
||||||
|
}
|
||||||
|
return nil, status.Error(codes.Internal, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return &emptypb.Empty{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
// CreateServiceContainer creates a new container for the service with the given specifications.
|
// CreateServiceContainer creates a new container for the service with the given specifications.
|
||||||
func (s *Server) CreateServiceContainer(
|
func (s *Server) CreateServiceContainer(
|
||||||
ctx context.Context, req *pb.CreateServiceContainerRequest,
|
ctx context.Context, req *pb.CreateServiceContainerRequest,
|
||||||
@@ -357,6 +440,7 @@ func (s *Server) CreateServiceContainer(
|
|||||||
|
|
||||||
config := &container.Config{
|
config := &container.Config{
|
||||||
Cmd: spec.Container.Command,
|
Cmd: spec.Container.Command,
|
||||||
|
Env: spec.Container.Env.ToSlice(),
|
||||||
Entrypoint: spec.Container.Entrypoint,
|
Entrypoint: spec.Container.Entrypoint,
|
||||||
Hostname: containerName,
|
Hostname: containerName,
|
||||||
Image: spec.Container.Image,
|
Image: spec.Container.Image,
|
||||||
@@ -366,6 +450,7 @@ func (s *Server) CreateServiceContainer(
|
|||||||
api.LabelServiceMode: spec.Mode,
|
api.LabelServiceMode: spec.Mode,
|
||||||
api.LabelManaged: "",
|
api.LabelManaged: "",
|
||||||
},
|
},
|
||||||
|
User: spec.Container.User,
|
||||||
}
|
}
|
||||||
if spec.Mode == "" {
|
if spec.Mode == "" {
|
||||||
config.Labels[api.LabelServiceMode] = api.ServiceModeReplicated
|
config.Labels[api.LabelServiceMode] = api.ServiceModeReplicated
|
||||||
@@ -385,6 +470,14 @@ func (s *Server) CreateServiceContainer(
|
|||||||
config.Labels[api.LabelServicePorts] = strings.Join(encodedPorts, ",")
|
config.Labels[api.LabelServicePorts] = strings.Join(encodedPorts, ",")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mounts, err := ToDockerMounts(spec.Volumes, spec.Container.VolumeMounts)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err = s.verifyDockerVolumesExist(ctx, mounts); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
portBindings := make(nat.PortMap)
|
portBindings := make(nat.PortMap)
|
||||||
for _, p := range spec.Ports {
|
for _, p := range spec.Ports {
|
||||||
if p.Mode != api.PortModeHost {
|
if p.Mode != api.PortModeHost {
|
||||||
@@ -403,13 +496,38 @@ func (s *Server) CreateServiceContainer(
|
|||||||
hostConfig := &container.HostConfig{
|
hostConfig := &container.HostConfig{
|
||||||
Binds: spec.Container.Volumes,
|
Binds: spec.Container.Volumes,
|
||||||
Init: spec.Container.Init,
|
Init: spec.Container.Init,
|
||||||
|
Mounts: mounts,
|
||||||
PortBindings: portBindings,
|
PortBindings: portBindings,
|
||||||
|
Privileged: spec.Container.Privileged,
|
||||||
|
Resources: container.Resources{
|
||||||
|
NanoCPUs: spec.Container.Resources.CPU,
|
||||||
|
Memory: spec.Container.Resources.Memory,
|
||||||
|
MemoryReservation: spec.Container.Resources.MemoryReservation,
|
||||||
|
},
|
||||||
// Always restart service containers if they exit or a machine restarts.
|
// Always restart service containers if they exit or a machine restarts.
|
||||||
// For one-off containers and batch jobs we plan to use a different service type/mode.
|
// For one-off containers and batch jobs we plan to use a different service type/mode.
|
||||||
RestartPolicy: container.RestartPolicy{
|
RestartPolicy: container.RestartPolicy{
|
||||||
Name: container.RestartPolicyAlways,
|
Name: container.RestartPolicyAlways,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Configure the container to use the internal DNS server if it's available.
|
||||||
|
dnsIP := s.internalDNSIP()
|
||||||
|
if dnsIP.IsValid() {
|
||||||
|
hostConfig.DNS = []string{dnsIP.String()}
|
||||||
|
// Optimize DNS resolution for service discovery by appending the search domain to names without a dot.
|
||||||
|
// For example, the first attempt for "my-service" will be "my-service.internal".
|
||||||
|
hostConfig.DNSOptions = []string{"ndots:1"}
|
||||||
|
hostConfig.DNSSearch = []string{dns.InternalDomain}
|
||||||
|
}
|
||||||
|
|
||||||
|
if spec.Container.LogDriver != nil {
|
||||||
|
hostConfig.LogConfig = container.LogConfig{
|
||||||
|
Type: spec.Container.LogDriver.Name,
|
||||||
|
Config: spec.Container.LogDriver.Options,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
networkConfig := &network.NetworkingConfig{
|
networkConfig := &network.NetworkingConfig{
|
||||||
EndpointsConfig: map[string]*network.EndpointSettings{
|
EndpointsConfig: map[string]*network.EndpointSettings{
|
||||||
NetworkName: {},
|
NetworkName: {},
|
||||||
@@ -440,9 +558,8 @@ func (s *Server) CreateServiceContainer(
|
|||||||
return nil, status.Errorf(codes.Internal, "marshal service spec: %v", err)
|
return nil, status.Errorf(codes.Internal, "marshal service spec: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err = s.db.ExecContext(ctx,
|
if _, err = s.db.ExecContext(ctx, `INSERT INTO containers (id, service_spec) VALUES ($1, $2)`,
|
||||||
`INSERT INTO containers (id, service_id, service_spec) VALUES ($1, $2, $3)`,
|
resp.ID, string(specBytes)); err != nil {
|
||||||
resp.ID, req.ServiceId, string(specBytes)); err != nil {
|
|
||||||
removeContainer()
|
removeContainer()
|
||||||
return nil, status.Errorf(codes.Internal, "store container in database: %v", err)
|
return nil, status.Errorf(codes.Internal, "store container in database: %v", err)
|
||||||
}
|
}
|
||||||
@@ -450,6 +567,101 @@ func (s *Server) CreateServiceContainer(
|
|||||||
return &pb.CreateContainerResponse{Response: respBytes}, nil
|
return &pb.CreateContainerResponse{Response: respBytes}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ToDockerMounts(volumes []api.VolumeSpec, mounts []api.VolumeMount) ([]mount.Mount, error) {
|
||||||
|
normalisedVolumes := make([]api.VolumeSpec, len(volumes))
|
||||||
|
for i, v := range volumes {
|
||||||
|
normalisedVolumes[i] = v.SetDefaults()
|
||||||
|
}
|
||||||
|
|
||||||
|
dockerMounts := make([]mount.Mount, 0, len(mounts))
|
||||||
|
for _, m := range mounts {
|
||||||
|
idx := slices.IndexFunc(normalisedVolumes, func(v api.VolumeSpec) bool {
|
||||||
|
return v.Name == m.VolumeName
|
||||||
|
})
|
||||||
|
if idx == -1 {
|
||||||
|
return nil, fmt.Errorf("volume mount references a volume that doesn't exist in the volumes spec: '%s'",
|
||||||
|
m.VolumeName)
|
||||||
|
}
|
||||||
|
|
||||||
|
vol := normalisedVolumes[idx]
|
||||||
|
if err := vol.Validate(); err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid volume: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
dm := mount.Mount{
|
||||||
|
Type: mount.Type(vol.Type),
|
||||||
|
Target: m.ContainerPath,
|
||||||
|
ReadOnly: m.ReadOnly,
|
||||||
|
}
|
||||||
|
|
||||||
|
switch vol.Type {
|
||||||
|
case api.VolumeTypeBind:
|
||||||
|
dm.Source = vol.BindOptions.HostPath
|
||||||
|
dm.BindOptions = toDockerBindOptions(vol.BindOptions)
|
||||||
|
case api.VolumeTypeVolume:
|
||||||
|
dm.Source = vol.DockerVolumeName()
|
||||||
|
dm.VolumeOptions = &mount.VolumeOptions{
|
||||||
|
NoCopy: vol.VolumeOptions.NoCopy,
|
||||||
|
Labels: vol.VolumeOptions.Labels,
|
||||||
|
Subpath: vol.VolumeOptions.SubPath,
|
||||||
|
DriverConfig: vol.VolumeOptions.Driver,
|
||||||
|
}
|
||||||
|
case api.VolumeTypeTmpfs:
|
||||||
|
dm.TmpfsOptions = vol.TmpfsOptions
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unsupported volume type: '%s'", vol.Type)
|
||||||
|
}
|
||||||
|
|
||||||
|
dockerMounts = append(dockerMounts, dm)
|
||||||
|
}
|
||||||
|
|
||||||
|
return dockerMounts, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func toDockerBindOptions(opts *api.BindOptions) *mount.BindOptions {
|
||||||
|
if opts == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
dockerOpts := &mount.BindOptions{
|
||||||
|
Propagation: opts.Propagation,
|
||||||
|
CreateMountpoint: opts.CreateHostPath,
|
||||||
|
}
|
||||||
|
|
||||||
|
switch opts.Recursive {
|
||||||
|
case "disabled":
|
||||||
|
dockerOpts.NonRecursive = true
|
||||||
|
case "writable":
|
||||||
|
dockerOpts.ReadOnlyNonRecursive = true
|
||||||
|
case "readonly":
|
||||||
|
dockerOpts.ReadOnlyForceRecursive = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return dockerOpts
|
||||||
|
}
|
||||||
|
|
||||||
|
// verifyDockerVolumesExist checks if the Docker named volumes referenced in the mounts exist on the machine.
|
||||||
|
func (s *Server) verifyDockerVolumesExist(ctx context.Context, mounts []mount.Mount) error {
|
||||||
|
for _, m := range mounts {
|
||||||
|
if m.Type != mount.TypeVolume {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: non-local volume drivers should likely be handled differently (needs proper investigation).
|
||||||
|
if _, err := s.client.VolumeInspect(ctx, m.Source); err != nil {
|
||||||
|
if client.IsErrNotFound(err) {
|
||||||
|
return status.Errorf(codes.NotFound, "volume '%s' not found", m.Source)
|
||||||
|
}
|
||||||
|
return status.Errorf(codes.Internal, "inspect volume '%s': %v", m.Source, err.Error())
|
||||||
|
}
|
||||||
|
// TODO: check if the volume driver and options are the same as in the mount and fail if not.
|
||||||
|
// Should we even ignore driver-specific options in the volume spec for externally managed volumes?
|
||||||
|
// Instead, just inspect the existing volume and construct the mount from it.
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// InspectServiceContainer returns the container information and service specification that was used to create the
|
// InspectServiceContainer returns the container information and service specification that was used to create the
|
||||||
// container with the given ID.
|
// container with the given ID.
|
||||||
func (s *Server) InspectServiceContainer(
|
func (s *Server) InspectServiceContainer(
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import (
|
|||||||
"github.com/psviderski/uncloud/internal/machine/caddyfile"
|
"github.com/psviderski/uncloud/internal/machine/caddyfile"
|
||||||
"github.com/psviderski/uncloud/internal/machine/cluster"
|
"github.com/psviderski/uncloud/internal/machine/cluster"
|
||||||
"github.com/psviderski/uncloud/internal/machine/corroservice"
|
"github.com/psviderski/uncloud/internal/machine/corroservice"
|
||||||
|
"github.com/psviderski/uncloud/internal/machine/dns"
|
||||||
machinedocker "github.com/psviderski/uncloud/internal/machine/docker"
|
machinedocker "github.com/psviderski/uncloud/internal/machine/docker"
|
||||||
"github.com/psviderski/uncloud/internal/machine/network"
|
"github.com/psviderski/uncloud/internal/machine/network"
|
||||||
"github.com/psviderski/uncloud/internal/machine/store"
|
"github.com/psviderski/uncloud/internal/machine/store"
|
||||||
@@ -62,6 +63,8 @@ type Config struct {
|
|||||||
// CaddyConfigPath specifies where the machine generates the Caddy reverse proxy configuration file for routing
|
// CaddyConfigPath specifies where the machine generates the Caddy reverse proxy configuration file for routing
|
||||||
// external traffic to service containers across the internal network. Default is DataDir/caddy/caddy.json.
|
// external traffic to service containers across the internal network. Default is DataDir/caddy/caddy.json.
|
||||||
CaddyConfigPath string
|
CaddyConfigPath string
|
||||||
|
// DNSUpstreams specifies the upstream DNS servers for the embedded internal DNS server.
|
||||||
|
DNSUpstreams []netip.AddrPort
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetDefaults returns a new Config with default values set where not provided.
|
// SetDefaults returns a new Config with default values set where not provided.
|
||||||
@@ -217,7 +220,6 @@ func NewMachine(config *Config) (*Machine, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("init machine database: %w", err)
|
return nil, fmt.Errorf("init machine database: %w", err)
|
||||||
}
|
}
|
||||||
dockerServer := machinedocker.NewServer(dockerCli, db)
|
|
||||||
|
|
||||||
// Init a local gRPC proxy server that proxies requests to the local or remote machine API servers.
|
// Init a local gRPC proxy server that proxies requests to the local or remote machine API servers.
|
||||||
proxyDirector := apiproxy.NewDirector(config.MachineSockPath, APIPort)
|
proxyDirector := apiproxy.NewDirector(config.MachineSockPath, APIPort)
|
||||||
@@ -235,11 +237,16 @@ func NewMachine(config *Config) (*Machine, error) {
|
|||||||
initialised: make(chan struct{}, 1),
|
initialised: make(chan struct{}, 1),
|
||||||
store: corroStore,
|
store: corroStore,
|
||||||
cluster: c,
|
cluster: c,
|
||||||
docker: dockerServer,
|
|
||||||
localProxyServer: localProxyServer,
|
localProxyServer: localProxyServer,
|
||||||
proxyDirector: proxyDirector,
|
proxyDirector: proxyDirector,
|
||||||
}
|
}
|
||||||
m.localMachineServer = newGRPCServer(m, c, dockerServer)
|
|
||||||
|
// Machine IP will only be available after the machine is initialised as a cluster member so wrap it in a function.
|
||||||
|
internalDNSIP := func() netip.Addr {
|
||||||
|
return m.IP()
|
||||||
|
}
|
||||||
|
m.docker = machinedocker.NewServer(dockerCli, db, internalDNSIP)
|
||||||
|
m.localMachineServer = newGRPCServer(m, c, m.docker)
|
||||||
|
|
||||||
if m.Initialised() {
|
if m.Initialised() {
|
||||||
m.initialised <- struct{}{}
|
m.initialised <- struct{}{}
|
||||||
@@ -270,6 +277,15 @@ func (m *Machine) Initialised() bool {
|
|||||||
return m.state.ID != ""
|
return m.state.ID != ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IP returns the machine IPv4 address in the cluster network which is the first address in the machine subnet.
|
||||||
|
func (m *Machine) IP() netip.Addr {
|
||||||
|
if !m.Initialised() {
|
||||||
|
return netip.Addr{}
|
||||||
|
}
|
||||||
|
|
||||||
|
return network.MachineIP(m.state.Network.Subnet)
|
||||||
|
}
|
||||||
|
|
||||||
func (m *Machine) Run(ctx context.Context) error {
|
func (m *Machine) Run(ctx context.Context) error {
|
||||||
// Docker dependency is essential for the machine to function. Block until it's ready.
|
// Docker dependency is essential for the machine to function. Block until it's ready.
|
||||||
if err := docker.WaitDaemonReady(ctx, m.config.DockerClient); err != nil {
|
if err := docker.WaitDaemonReady(ctx, m.config.DockerClient); err != nil {
|
||||||
@@ -373,6 +389,12 @@ func (m *Machine) Run(ctx context.Context) error {
|
|||||||
return fmt.Errorf("create Caddyfile controller: %w", err)
|
return fmt.Errorf("create Caddyfile controller: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dnsResolver := dns.NewClusterResolver(m.store)
|
||||||
|
dnsServer, err := dns.NewServer(m.IP(), dnsResolver, m.config.DNSUpstreams)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("create embedded DNS server: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
ctrl, err = newNetworkController(
|
ctrl, err = newNetworkController(
|
||||||
m.state,
|
m.state,
|
||||||
m.store,
|
m.store,
|
||||||
@@ -380,6 +402,8 @@ func (m *Machine) Run(ctx context.Context) error {
|
|||||||
m.config.CorrosionService,
|
m.config.CorrosionService,
|
||||||
m.config.DockerClient,
|
m.config.DockerClient,
|
||||||
caddyfileCtrl,
|
caddyfileCtrl,
|
||||||
|
dnsServer,
|
||||||
|
dnsResolver,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("initialise network controller: %w", err)
|
return fmt.Errorf("initialise network controller: %w", err)
|
||||||
@@ -520,6 +544,38 @@ func (m *Machine) configureCorrosion() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CheckPrerequisites verifies if the machine meets all necessary system requirements to participate in the cluster.
|
||||||
|
func (m *Machine) CheckPrerequisites(ctx context.Context, _ *emptypb.Empty) (*pb.CheckPrerequisitesResponse, error) {
|
||||||
|
// Check DNS port (UDP) availability.
|
||||||
|
if err := checkDNSPortAvailable(); err != nil {
|
||||||
|
return &pb.CheckPrerequisitesResponse{
|
||||||
|
Satisfied: false,
|
||||||
|
Error: err.Error(),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &pb.CheckPrerequisitesResponse{
|
||||||
|
Satisfied: true,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkDNSPortAvailable verifies that DNS port 53/udp is available for Uncloud's embedded DNS service.
|
||||||
|
func checkDNSPortAvailable() error {
|
||||||
|
addr := &net.UDPAddr{
|
||||||
|
IP: net.IPv4(127, 0, 0, 210), // Use a unique loopback address to avoid conflicts.
|
||||||
|
Port: dns.Port,
|
||||||
|
}
|
||||||
|
conn, err := net.ListenUDP("udp", addr)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("DNS port %d/udp is already in use by another service: %w. Uncloud needs this port "+
|
||||||
|
"to run the embedded internal DNS service on WireGuard interface 'uncloud'. Please reconfigure "+
|
||||||
|
"any DNS servers (like dnsmasq, systemd-resolved, or named) that might be listening on all network "+
|
||||||
|
"interfaces (0.0.0.0) on the machine and try again", dns.Port, err)
|
||||||
|
}
|
||||||
|
conn.Close()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// InitCluster initialises a new cluster on the local machine with the provided network configuration.
|
// InitCluster initialises a new cluster on the local machine with the provided network configuration.
|
||||||
func (m *Machine) InitCluster(ctx context.Context, req *pb.InitClusterRequest) (*pb.InitClusterResponse, error) {
|
func (m *Machine) InitCluster(ctx context.Context, req *pb.InitClusterRequest) (*pb.InitClusterResponse, error) {
|
||||||
if m.Initialised() {
|
if m.Initialised() {
|
||||||
|
|||||||
+78
-65
@@ -4,22 +4,24 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/cenkalti/backoff/v4"
|
|
||||||
"github.com/docker/docker/client"
|
|
||||||
"golang.org/x/sync/errgroup"
|
|
||||||
"google.golang.org/grpc"
|
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net"
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"slices"
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/cenkalti/backoff/v4"
|
||||||
|
"github.com/docker/docker/client"
|
||||||
"github.com/psviderski/uncloud/internal/machine/api/pb"
|
"github.com/psviderski/uncloud/internal/machine/api/pb"
|
||||||
"github.com/psviderski/uncloud/internal/machine/caddyfile"
|
"github.com/psviderski/uncloud/internal/machine/caddyfile"
|
||||||
"github.com/psviderski/uncloud/internal/machine/corroservice"
|
"github.com/psviderski/uncloud/internal/machine/corroservice"
|
||||||
|
"github.com/psviderski/uncloud/internal/machine/dns"
|
||||||
"github.com/psviderski/uncloud/internal/machine/docker"
|
"github.com/psviderski/uncloud/internal/machine/docker"
|
||||||
"github.com/psviderski/uncloud/internal/machine/network"
|
"github.com/psviderski/uncloud/internal/machine/network"
|
||||||
"github.com/psviderski/uncloud/internal/machine/store"
|
"github.com/psviderski/uncloud/internal/machine/store"
|
||||||
|
"golang.org/x/sync/errgroup"
|
||||||
|
"google.golang.org/grpc"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -38,8 +40,9 @@ type networkController struct {
|
|||||||
dockerCli *client.Client
|
dockerCli *client.Client
|
||||||
caddyfileCtrl *caddyfile.Controller
|
caddyfileCtrl *caddyfile.Controller
|
||||||
|
|
||||||
// TODO: DNS server/resolver listening on the machine IP, e.g. 10.210.0.1:53. It can't listen on 127.0.X.X
|
// dnsServer is the embedded internal DNS server for the cluster listening on the machine IP.
|
||||||
// like resolved does because it needs to be reachable from both the host and the containers.
|
dnsServer *dns.Server
|
||||||
|
dnsResolver *dns.ClusterResolver
|
||||||
}
|
}
|
||||||
|
|
||||||
func newNetworkController(
|
func newNetworkController(
|
||||||
@@ -49,6 +52,8 @@ func newNetworkController(
|
|||||||
corroService corroservice.Service,
|
corroService corroservice.Service,
|
||||||
dockerCli *client.Client,
|
dockerCli *client.Client,
|
||||||
caddyfileCtrl *caddyfile.Controller,
|
caddyfileCtrl *caddyfile.Controller,
|
||||||
|
dnsServer *dns.Server,
|
||||||
|
dnsResolver *dns.ClusterResolver,
|
||||||
) (
|
) (
|
||||||
*networkController, error,
|
*networkController, error,
|
||||||
) {
|
) {
|
||||||
@@ -68,6 +73,8 @@ func newNetworkController(
|
|||||||
corroService: corroService,
|
corroService: corroService,
|
||||||
dockerCli: dockerCli,
|
dockerCli: dockerCli,
|
||||||
caddyfileCtrl: caddyfileCtrl,
|
caddyfileCtrl: caddyfileCtrl,
|
||||||
|
dnsServer: dnsServer,
|
||||||
|
dnsResolver: dnsResolver,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,65 +117,73 @@ func (nc *networkController) Run(ctx context.Context) error {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
errGroup.Go(func() error {
|
||||||
|
slog.Info("Starting embedded DNS resolver.")
|
||||||
|
if err := nc.dnsResolver.Run(ctx); err != nil {
|
||||||
|
return fmt.Errorf("embedded DNS resolver failed: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
errGroup.Go(func() error {
|
||||||
|
slog.Info("Starting embedded DNS server.")
|
||||||
|
if err := nc.dnsServer.Run(ctx); err != nil {
|
||||||
|
return fmt.Errorf("embedded DNS server failed: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
// Setup Docker network and synchronise containers to the cluster store.
|
// Setup Docker network and synchronise containers to the cluster store.
|
||||||
errGroup.Go(
|
errGroup.Go(func() error {
|
||||||
func() error {
|
return nc.prepareAndWatchDocker(ctx)
|
||||||
return nc.prepareAndWatchDocker(ctx)
|
})
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
// Handle machine changes in the cluster. Handling machine and endpoint changes should be done
|
// Handle machine changes in the cluster. Handling machine and endpoint changes should be done
|
||||||
// in separate goroutines to avoid a deadlock when reconfiguring the network.
|
// in separate goroutines to avoid a deadlock when reconfiguring the network.
|
||||||
errGroup.Go(
|
errGroup.Go(func() error {
|
||||||
func() error {
|
if err := nc.handleMachineChanges(ctx); err != nil {
|
||||||
if err := nc.handleMachineChanges(ctx); err != nil {
|
return fmt.Errorf("handle new machines: %w", err)
|
||||||
return fmt.Errorf("handle new machines: %w", err)
|
}
|
||||||
}
|
return nil
|
||||||
return nil
|
})
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
// Watch for endpoint changes and update the machine state accordingly.
|
// Watch for endpoint changes and update the machine state accordingly.
|
||||||
errGroup.Go(
|
errGroup.Go(func() error {
|
||||||
func() error {
|
for {
|
||||||
for {
|
select {
|
||||||
select {
|
case e, ok := <-nc.endpointChanges:
|
||||||
case e, ok := <-nc.endpointChanges:
|
if !ok {
|
||||||
if !ok {
|
// The channel was closed, stop watching for changes.
|
||||||
// The channel was closed, stop watching for changes.
|
nc.endpointChanges = nil
|
||||||
nc.endpointChanges = nil
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
nc.state.mu.Lock()
|
|
||||||
for i := range nc.state.Network.Peers {
|
|
||||||
if nc.state.Network.Peers[i].PublicKey.Equal(e.PublicKey) {
|
|
||||||
nc.state.Network.Peers[i].Endpoint = &e.Endpoint
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err := nc.state.Save(); err != nil {
|
|
||||||
slog.Error("Failed to save machine state.", "err", err)
|
|
||||||
}
|
|
||||||
nc.state.mu.Unlock()
|
|
||||||
|
|
||||||
slog.Debug("Preserved endpoint change in the machine state.",
|
|
||||||
"public_key", e.PublicKey, "endpoint", e.Endpoint)
|
|
||||||
case <-ctx.Done():
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
errGroup.Go(
|
nc.state.mu.Lock()
|
||||||
func() error {
|
for i := range nc.state.Network.Peers {
|
||||||
if err := nc.wgnet.Run(ctx); err != nil {
|
if nc.state.Network.Peers[i].PublicKey.Equal(e.PublicKey) {
|
||||||
return fmt.Errorf("WireGuard network failed: %w", err)
|
nc.state.Network.Peers[i].Endpoint = &e.Endpoint
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := nc.state.Save(); err != nil {
|
||||||
|
slog.Error("Failed to save machine state.", "err", err)
|
||||||
|
}
|
||||||
|
nc.state.mu.Unlock()
|
||||||
|
|
||||||
|
slog.Debug("Preserved endpoint change in the machine state.",
|
||||||
|
"public_key", e.PublicKey, "endpoint", e.Endpoint)
|
||||||
|
case <-ctx.Done():
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
return nil
|
}
|
||||||
},
|
})
|
||||||
)
|
|
||||||
|
errGroup.Go(func() error {
|
||||||
|
if err := nc.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 Caddyfile controller.")
|
slog.Info("Starting Caddyfile controller.")
|
||||||
@@ -180,16 +195,14 @@ func (nc *networkController) Run(ctx context.Context) error {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Wait for the context to be done and stop the network API server.
|
// Wait for the context to be done and stop the network API server.
|
||||||
errGroup.Go(
|
errGroup.Go(func() error {
|
||||||
func() error {
|
<-ctx.Done()
|
||||||
<-ctx.Done()
|
slog.Info("Stopping network API server.")
|
||||||
slog.Info("Stopping network API server.")
|
// TODO: implement timeout for graceful shutdown.
|
||||||
// TODO: implement timeout for graceful shutdown.
|
nc.server.GracefulStop()
|
||||||
nc.server.GracefulStop()
|
slog.Info("Network API server stopped.")
|
||||||
slog.Info("Network API server stopped.")
|
return nil
|
||||||
return nil
|
})
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
return errGroup.Wait()
|
return errGroup.Wait()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,11 @@ package network
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/psviderski/uncloud/internal/secret"
|
|
||||||
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
|
||||||
"net"
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
|
|
||||||
|
"github.com/psviderski/uncloud/internal/secret"
|
||||||
|
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
@@ -13,7 +14,7 @@ type Config struct {
|
|||||||
// in the subnet. Other IP addresses are allocated to containers running on the machine.
|
// in the subnet. Other IP addresses are allocated to containers running on the machine.
|
||||||
Subnet netip.Prefix
|
Subnet netip.Prefix
|
||||||
// ManagementIP is the IPv6 address assigned to the machine within the WireGuard network. This address is used
|
// ManagementIP is the IPv6 address assigned to the machine within the WireGuard network. This address is used
|
||||||
// for cluster management traffic, such as gRPC communication with the machine API server and Serf gossip.
|
// for cluster management traffic, such as gRPC communication with the machine API server and Corrosion gossip.
|
||||||
ManagementIP netip.Addr
|
ManagementIP netip.Addr
|
||||||
PrivateKey secret.Secret
|
PrivateKey secret.Secret
|
||||||
PublicKey secret.Secret
|
PublicKey secret.Secret
|
||||||
@@ -23,7 +24,7 @@ type Config struct {
|
|||||||
type PeerConfig struct {
|
type PeerConfig struct {
|
||||||
Subnet *netip.Prefix `json:",omitempty"`
|
Subnet *netip.Prefix `json:",omitempty"`
|
||||||
// ManagementIP is the IPv6 address assigned to the peer within the WireGuard network. This address is used
|
// ManagementIP is the IPv6 address assigned to the peer within the WireGuard network. This address is used
|
||||||
// for cluster management traffic, such as gRPC communication with the machine API server and Serf gossip.
|
// for cluster management traffic, such as gRPC communication with the machine API server and Corrosion gossip.
|
||||||
ManagementIP netip.Addr
|
ManagementIP netip.Addr
|
||||||
Endpoint *netip.AddrPort `json:",omitempty"`
|
Endpoint *netip.AddrPort `json:",omitempty"`
|
||||||
AllEndpoints []netip.AddrPort `json:",omitempty"`
|
AllEndpoints []netip.AddrPort `json:",omitempty"`
|
||||||
|
|||||||
@@ -2,9 +2,10 @@ package network
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/psviderski/uncloud/internal/secret"
|
|
||||||
"net"
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
|
|
||||||
|
"github.com/psviderski/uncloud/internal/secret"
|
||||||
)
|
)
|
||||||
|
|
||||||
// MachineIP returns the IP address of the machine which is the first address in the subnet.
|
// MachineIP returns the IP address of the machine which is the first address in the subnet.
|
||||||
|
|||||||
@@ -4,17 +4,18 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/netip"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/cenkalti/backoff/v4"
|
"github.com/cenkalti/backoff/v4"
|
||||||
"github.com/docker/docker/api/types/container"
|
"github.com/docker/docker/api/types/container"
|
||||||
"github.com/docker/docker/api/types/filters"
|
"github.com/docker/docker/api/types/filters"
|
||||||
"github.com/docker/docker/api/types/network"
|
"github.com/docker/docker/api/types/network"
|
||||||
dockerclient "github.com/docker/docker/client"
|
dockerclient "github.com/docker/docker/client"
|
||||||
"google.golang.org/protobuf/types/known/emptypb"
|
|
||||||
"net/netip"
|
|
||||||
"time"
|
|
||||||
"github.com/psviderski/uncloud/internal/machine"
|
"github.com/psviderski/uncloud/internal/machine"
|
||||||
"github.com/psviderski/uncloud/internal/machine/api/pb"
|
"github.com/psviderski/uncloud/internal/machine/api/pb"
|
||||||
"github.com/psviderski/uncloud/internal/machine/cluster"
|
"github.com/psviderski/uncloud/internal/machine/cluster"
|
||||||
|
"google.golang.org/protobuf/types/known/emptypb"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -31,6 +32,29 @@ type CreateClusterOptions struct {
|
|||||||
Machines int
|
Machines int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Cluster) PopulateMachineIDs(ctx context.Context) error {
|
||||||
|
for i := range c.Machines {
|
||||||
|
if c.Machines[i].ID != "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
cli, err := c.Machines[i].Connect(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("connect to machine '%s': %w", c.Machines[i].Name, err)
|
||||||
|
}
|
||||||
|
//goland:noinspection GoDeferInLoop
|
||||||
|
defer cli.Close()
|
||||||
|
|
||||||
|
m, err := cli.Inspect(ctx, &emptypb.Empty{})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("inspect machine '%s': %w", c.Machines[i].Name, err)
|
||||||
|
}
|
||||||
|
c.Machines[i].ID = m.Id
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (p *Provisioner) CreateCluster(ctx context.Context, name string, opts CreateClusterOptions) (Cluster, error) {
|
func (p *Provisioner) CreateCluster(ctx context.Context, name string, opts CreateClusterOptions) (Cluster, error) {
|
||||||
var c Cluster
|
var c Cluster
|
||||||
|
|
||||||
@@ -74,12 +98,15 @@ func (p *Provisioner) CreateCluster(ctx context.Context, name string, opts Creat
|
|||||||
if err = p.initCluster(ctx, c.Machines); err != nil {
|
if err = p.initCluster(ctx, c.Machines); err != nil {
|
||||||
return c, err
|
return c, err
|
||||||
}
|
}
|
||||||
|
if err = c.PopulateMachineIDs(ctx); err != nil {
|
||||||
|
return c, fmt.Errorf("populate machine IDs: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
if p.configUpdater != nil {
|
if p.configUpdater != nil {
|
||||||
if err = p.configUpdater.AddCluster(c); err != nil {
|
if err = p.configUpdater.AddCluster(c); err != nil {
|
||||||
return c, fmt.Errorf("add cluster to Uncloud config: %w", err)
|
return c, fmt.Errorf("add cluster to Uncloud config: %w", err)
|
||||||
}
|
}
|
||||||
fmt.Printf("Cluster '%s' added to Uncloud config.\n", c.Name)
|
fmt.Printf("Cluster '%s' added to Uncloud config as the current context.\n", name)
|
||||||
}
|
}
|
||||||
|
|
||||||
return c, nil
|
return c, nil
|
||||||
@@ -89,7 +116,7 @@ func (p *Provisioner) initCluster(ctx context.Context, machines []Machine) error
|
|||||||
// Init a new cluster on the first machine.
|
// Init a new cluster on the first machine.
|
||||||
initMachine := machines[0]
|
initMachine := machines[0]
|
||||||
|
|
||||||
if err := p.WaitMachineReady(ctx, initMachine, 30*time.Second); err != nil {
|
if err := WaitMachineReady(ctx, initMachine, 30*time.Second); err != nil {
|
||||||
return fmt.Errorf("wait for machine %q to be ready: %w", initMachine.Name, err)
|
return fmt.Errorf("wait for machine %q to be ready: %w", initMachine.Name, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,7 +138,7 @@ func (p *Provisioner) initCluster(ctx context.Context, machines []Machine) error
|
|||||||
|
|
||||||
// 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 = p.WaitMachineReady(ctx, m, 5*time.Second); err != nil {
|
if err = WaitMachineReady(ctx, m, 5*time.Second); err != nil {
|
||||||
return fmt.Errorf("wait for machine %q to be ready: %w", m.Name, err)
|
return fmt.Errorf("wait for machine %q to be ready: %w", m.Name, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -215,13 +242,17 @@ func (p *Provisioner) InspectCluster(ctx context.Context, name string) (Cluster,
|
|||||||
c.Machines = append(c.Machines, m)
|
c.Machines = append(c.Machines, m)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err = c.PopulateMachineIDs(ctx); err != nil {
|
||||||
|
return c, fmt.Errorf("populate machine IDs: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
return c, nil
|
return c, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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 := p.WaitMachineReady(ctx, firstMachine, timeout); err != nil {
|
if err := WaitMachineReady(ctx, firstMachine, timeout); err != nil {
|
||||||
return fmt.Errorf("wait for machine '%s' to be ready: %w", firstMachine.Name, err)
|
return fmt.Errorf("wait for machine '%s' to be ready: %w", firstMachine.Name, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -238,7 +269,7 @@ func (p *Provisioner) WaitClusterReady(ctx context.Context, c Cluster, timeout t
|
|||||||
), ctx)
|
), ctx)
|
||||||
|
|
||||||
checkMachinesUp := func() error {
|
checkMachinesUp := func() error {
|
||||||
machines, err := cli.ListMachines(ctx)
|
machines, err := cli.ListMachines(ctx, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("list machines: %w", err)
|
return fmt.Errorf("list machines: %w", err)
|
||||||
}
|
}
|
||||||
@@ -278,7 +309,12 @@ func (p *Provisioner) RemoveCluster(ctx context.Context, name string) error {
|
|||||||
return fmt.Errorf("list Docker containers with cluster name '%s': %w", name, err)
|
return fmt.Errorf("list Docker containers with cluster name '%s': %w", name, err)
|
||||||
}
|
}
|
||||||
for _, c := range containers {
|
for _, c := range containers {
|
||||||
if err = p.dockerCli.ContainerRemove(ctx, c.ID, container.RemoveOptions{Force: true}); err != nil {
|
removeOpts := container.RemoveOptions{
|
||||||
|
// Remove anonymous volumes attached to the container (typically /var/lib/docker).
|
||||||
|
RemoveVolumes: true,
|
||||||
|
Force: true,
|
||||||
|
}
|
||||||
|
if err = p.dockerCli.ContainerRemove(ctx, c.ID, removeOpts); err != nil {
|
||||||
return fmt.Errorf("remove Docker container '%s': %w", c.ID, err)
|
return fmt.Errorf("remove Docker container '%s': %w", c.ID, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+13
-12
@@ -2,6 +2,7 @@ package ucind
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/psviderski/uncloud/internal/cli/config"
|
"github.com/psviderski/uncloud/internal/cli/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -19,22 +20,22 @@ func (u *ConfigUpdater) AddCluster(c Cluster) error {
|
|||||||
return fmt.Errorf("read Uncloud config: %w", err)
|
return fmt.Errorf("read Uncloud config: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, ok := cfg.Clusters[c.Name]; ok {
|
if _, ok := cfg.Contexts[c.Name]; ok {
|
||||||
return fmt.Errorf("cluster '%s' already exists", c.Name)
|
return fmt.Errorf("cluster context '%s' already exists", c.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
clusterCfg := &config.Cluster{
|
clusterCfg := &config.Context{
|
||||||
Name: c.Name,
|
Name: c.Name,
|
||||||
Connections: make([]config.MachineConnection, len(c.Machines)),
|
Connections: make([]config.MachineConnection, len(c.Machines)),
|
||||||
}
|
}
|
||||||
for i, m := range c.Machines {
|
for i, m := range c.Machines {
|
||||||
clusterCfg.Connections[i] = config.MachineConnection{
|
clusterCfg.Connections[i] = config.MachineConnection{
|
||||||
TCP: m.APIAddress,
|
TCP: &m.APIAddress,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg.Clusters[c.Name] = clusterCfg
|
cfg.Contexts[c.Name] = clusterCfg
|
||||||
cfg.CurrentCluster = c.Name
|
cfg.CurrentContext = c.Name
|
||||||
|
|
||||||
if err = cfg.Save(); err != nil {
|
if err = cfg.Save(); err != nil {
|
||||||
return fmt.Errorf("save config: %w", err)
|
return fmt.Errorf("save config: %w", err)
|
||||||
@@ -48,17 +49,17 @@ func (u *ConfigUpdater) RemoveCluster(name string) error {
|
|||||||
return fmt.Errorf("read Uncloud config: %w", err)
|
return fmt.Errorf("read Uncloud config: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, ok := cfg.Clusters[name]; !ok {
|
if _, ok := cfg.Contexts[name]; !ok {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
delete(cfg.Clusters, name)
|
delete(cfg.Contexts, name)
|
||||||
|
|
||||||
if cfg.CurrentCluster == name {
|
if cfg.CurrentContext == name {
|
||||||
cfg.CurrentCluster = ""
|
cfg.CurrentContext = ""
|
||||||
}
|
}
|
||||||
if _, ok := cfg.Clusters["default"]; ok {
|
if _, ok := cfg.Contexts["default"]; ok {
|
||||||
cfg.CurrentCluster = "default"
|
cfg.CurrentContext = "default"
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = cfg.Save(); err != nil {
|
if err = cfg.Save(); err != nil {
|
||||||
|
|||||||
@@ -4,6 +4,11 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"net/netip"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/cenkalti/backoff/v4"
|
"github.com/cenkalti/backoff/v4"
|
||||||
"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"
|
||||||
@@ -13,10 +18,6 @@ import (
|
|||||||
"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"
|
"google.golang.org/protobuf/types/known/emptypb"
|
||||||
"io"
|
|
||||||
"net"
|
|
||||||
"net/netip"
|
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -29,6 +30,7 @@ const (
|
|||||||
type Machine struct {
|
type Machine struct {
|
||||||
ClusterName string
|
ClusterName string
|
||||||
ContainerName string
|
ContainerName string
|
||||||
|
ID string
|
||||||
Name string
|
Name string
|
||||||
APIAddress netip.AddrPort
|
APIAddress netip.AddrPort
|
||||||
}
|
}
|
||||||
@@ -185,7 +187,7 @@ func randomMachineName() (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// WaitMachineReady waits for the machine API to respond.
|
// WaitMachineReady waits for the machine API to respond.
|
||||||
func (p *Provisioner) WaitMachineReady(ctx context.Context, m Machine, timeout time.Duration) error {
|
func WaitMachineReady(ctx context.Context, m Machine, timeout time.Duration) error {
|
||||||
cli, err := m.Connect(ctx)
|
cli, err := m.Connect(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("connect to machine over TCP '%s': %w", m.APIAddress, err)
|
return fmt.Errorf("connect to machine over TCP '%s': %w", m.APIAddress, err)
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package version
|
||||||
|
|
||||||
|
var version string
|
||||||
|
|
||||||
|
func String() string {
|
||||||
|
return version
|
||||||
|
}
|
||||||
+20
-6
@@ -6,6 +6,7 @@ import (
|
|||||||
"slices"
|
"slices"
|
||||||
|
|
||||||
"github.com/docker/docker/api/types/container"
|
"github.com/docker/docker/api/types/container"
|
||||||
|
"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"
|
"google.golang.org/grpc/metadata"
|
||||||
)
|
)
|
||||||
@@ -16,6 +17,7 @@ type Client interface {
|
|||||||
ImageClient
|
ImageClient
|
||||||
MachineClient
|
MachineClient
|
||||||
ServiceClient
|
ServiceClient
|
||||||
|
VolumeClient
|
||||||
}
|
}
|
||||||
|
|
||||||
type ContainerClient interface {
|
type ContainerClient interface {
|
||||||
@@ -39,29 +41,41 @@ type ImageClient interface {
|
|||||||
|
|
||||||
type MachineClient interface {
|
type MachineClient interface {
|
||||||
InspectMachine(ctx context.Context, id string) (*pb.MachineMember, error)
|
InspectMachine(ctx context.Context, id string) (*pb.MachineMember, error)
|
||||||
ListMachines(ctx context.Context) ([]*pb.MachineMember, error)
|
ListMachines(ctx context.Context, filter *MachineFilter) (MachineMembersList, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type ServiceClient interface {
|
type ServiceClient interface {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
type VolumeClient interface {
|
||||||
|
CreateVolume(ctx context.Context, machineNameOrID string, opts volume.CreateOptions) (MachineVolume, error)
|
||||||
|
ListVolumes(ctx context.Context, filter *VolumeFilter) ([]MachineVolume, error)
|
||||||
|
RemoveVolume(ctx context.Context, machineNameOrID, volumeName string, force bool) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// ProxyMachinesContext returns a new context that proxies gRPC requests to the specified machines.
|
// ProxyMachinesContext returns a new context that proxies gRPC requests to the specified machines.
|
||||||
// If namesOrIDs is nil, all machines are included.
|
// If namesOrIDs is nil, all machines are included.
|
||||||
func ProxyMachinesContext(ctx context.Context, cli MachineClient, namesOrIDs []string) (context.Context, error) {
|
func ProxyMachinesContext(
|
||||||
machines, err := cli.ListMachines(ctx)
|
ctx context.Context, cli MachineClient, namesOrIDs []string,
|
||||||
|
) (context.Context, MachineMembersList, error) {
|
||||||
|
machines, err := cli.ListMachines(ctx, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("list machines: %w", err)
|
return nil, nil, fmt.Errorf("list machines: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var proxiedMachines MachineMembersList
|
||||||
md := metadata.New(nil)
|
md := metadata.New(nil)
|
||||||
for _, m := range machines {
|
for _, m := range machines {
|
||||||
if namesOrIDs == nil ||
|
if len(namesOrIDs) == 0 ||
|
||||||
slices.Contains(namesOrIDs, m.Machine.Name) || slices.Contains(namesOrIDs, m.Machine.Id) {
|
slices.Contains(namesOrIDs, m.Machine.Name) || slices.Contains(namesOrIDs, m.Machine.Id) {
|
||||||
|
proxiedMachines = append(proxiedMachines, m)
|
||||||
machineIP, _ := m.Machine.Network.ManagementIp.ToAddr()
|
machineIP, _ := m.Machine.Network.ManagementIp.ToAddr()
|
||||||
md.Append("machines", machineIP.String())
|
md.Append("machines", machineIP.String())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return metadata.NewOutgoingContext(ctx, md), nil
|
return metadata.NewOutgoingContext(ctx, md), proxiedMachines, nil
|
||||||
}
|
}
|
||||||
|
|||||||
+20
-5
@@ -3,6 +3,7 @@ package api
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/netip"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -11,6 +12,9 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
// DockerNetworkName is the name of the Docker network used by uncloud. Keep the value in sync with NetworkName
|
||||||
|
// in internal/machine/docker/manager.go.
|
||||||
|
DockerNetworkName = "uncloud"
|
||||||
LabelManaged = "uncloud.managed"
|
LabelManaged = "uncloud.managed"
|
||||||
LabelServiceID = "uncloud.service.id"
|
LabelServiceID = "uncloud.service.id"
|
||||||
LabelServiceName = "uncloud.service.name"
|
LabelServiceName = "uncloud.service.name"
|
||||||
@@ -90,6 +94,22 @@ func (c *Container) HumanState() (string, error) {
|
|||||||
c.State.ExitCode, units.HumanDuration(time.Now().UTC().Sub(finishedAt))), nil
|
c.State.ExitCode, units.HumanDuration(time.Now().UTC().Sub(finishedAt))), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UncloudNetworkIP returns the IP address of the container in the uncloud Docker network.
|
||||||
|
func (c *Container) UncloudNetworkIP() netip.Addr {
|
||||||
|
network, ok := c.NetworkSettings.Networks[DockerNetworkName]
|
||||||
|
if !ok {
|
||||||
|
// Container is not connected to the uncloud Docker network (could be host network).
|
||||||
|
return netip.Addr{}
|
||||||
|
}
|
||||||
|
|
||||||
|
ip, err := netip.ParseAddr(network.IPAddress)
|
||||||
|
if err != nil {
|
||||||
|
return netip.Addr{}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ip
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Container) UnmarshalJSON(data []byte) error {
|
func (c *Container) UnmarshalJSON(data []byte) error {
|
||||||
// A temporary type that's identical to Container but doesn't have the UnmarshalJSON method.
|
// A temporary type that's identical to Container but doesn't have the UnmarshalJSON method.
|
||||||
type ContainerAlias Container
|
type ContainerAlias Container
|
||||||
@@ -110,11 +130,6 @@ type ServiceContainer struct {
|
|||||||
ServiceSpec ServiceSpec
|
ServiceSpec ServiceSpec
|
||||||
}
|
}
|
||||||
|
|
||||||
type MachineContainer struct {
|
|
||||||
MachineID string
|
|
||||||
Container Container
|
|
||||||
}
|
|
||||||
|
|
||||||
// ServiceID returns the ID of the service this container belongs to.
|
// ServiceID returns the ID of the service this container belongs to.
|
||||||
func (c *ServiceContainer) ServiceID() string {
|
func (c *ServiceContainer) ServiceID() string {
|
||||||
return c.Config.Labels[LabelServiceID]
|
return c.Config.Labels[LabelServiceID]
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import "github.com/psviderski/uncloud/internal/machine/api/pb"
|
||||||
|
|
||||||
|
// MachineFilter defines criteria to filter machines in ListMachines.
|
||||||
|
type MachineFilter struct {
|
||||||
|
// Available filters machines that are not DOWN.
|
||||||
|
Available bool
|
||||||
|
// NamesOrIDs filters machines by their names or IDs.
|
||||||
|
NamesOrIDs []string
|
||||||
|
}
|
||||||
|
|
||||||
|
type MachineMembersList []*pb.MachineMember
|
||||||
|
|
||||||
|
func (m MachineMembersList) FindByManagementIP(ip string) *pb.MachineMember {
|
||||||
|
for _, machine := range m {
|
||||||
|
addr, err := machine.Machine.Network.ManagementIp.ToAddr()
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if addr.String() == ip {
|
||||||
|
return machine
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m MachineMembersList) FindByNameOrID(nameOrID string) *pb.MachineMember {
|
||||||
|
for _, machine := range m {
|
||||||
|
if machine.Machine.Id == nameOrID || machine.Machine.Name == nameOrID {
|
||||||
|
return machine
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
// Placement defines the placement constraints for service containers.
|
||||||
|
type Placement struct {
|
||||||
|
// Machines is a list of machine names or IDs where service containers are allowed to be deployed.
|
||||||
|
// If empty, containers can be deployed to any available machine in the cluster.
|
||||||
|
Machines []string `json:",omitempty"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
const (
|
||||||
|
MilliCore = 1_000_000
|
||||||
|
Core = 1000 * MilliCore
|
||||||
|
)
|
||||||
|
|
||||||
|
type ContainerResources struct {
|
||||||
|
// CPU is the maximum amount of CPU nanocores (1000000000 = 1 CPU core) the container can use.
|
||||||
|
CPU int64
|
||||||
|
// Memory is the maximum amount of memory (in bytes) the container can use.
|
||||||
|
Memory int64
|
||||||
|
// MemoryReservation is the minimum amount of memory (in bytes) the container needs to run efficiently.
|
||||||
|
// TODO: implement a placement constraint that checks available memory on machines.
|
||||||
|
MemoryReservation int64
|
||||||
|
}
|
||||||
+122
-5
@@ -4,11 +4,12 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"maps"
|
"maps"
|
||||||
"reflect"
|
|
||||||
"regexp"
|
"regexp"
|
||||||
"slices"
|
"slices"
|
||||||
|
|
||||||
"github.com/distribution/reference"
|
"github.com/distribution/reference"
|
||||||
|
"github.com/google/go-cmp/cmp"
|
||||||
|
"github.com/google/go-cmp/cmp/cmpopts"
|
||||||
"github.com/psviderski/uncloud/internal/machine/api/pb"
|
"github.com/psviderski/uncloud/internal/machine/api/pb"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -42,10 +43,35 @@ type ServiceSpec struct {
|
|||||||
// Mode is the replication mode of the service. Default is ServiceModeReplicated if empty.
|
// Mode is the replication mode of the service. Default is ServiceModeReplicated if empty.
|
||||||
Mode string
|
Mode string
|
||||||
Name string
|
Name string
|
||||||
|
// Placement defines the placement constraints for the service.
|
||||||
|
Placement Placement
|
||||||
// Ports defines what service ports to publish to make the service accessible outside the cluster.
|
// Ports defines what service ports to publish to make the service accessible outside the cluster.
|
||||||
Ports []PortSpec
|
Ports []PortSpec
|
||||||
// Replicas is the number of containers to run for the service. Only valid for a replicated service.
|
// Replicas is the number of containers to run for the service. Only valid for a replicated service.
|
||||||
Replicas uint `json:",omitempty"`
|
Replicas uint `json:",omitempty"`
|
||||||
|
// Volumes is list of data volumes that can be mounted into the container.
|
||||||
|
Volumes []VolumeSpec
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ServiceSpec) Volume(name string) (VolumeSpec, bool) {
|
||||||
|
for _, v := range s.Volumes {
|
||||||
|
if v.Name == name {
|
||||||
|
return v, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return VolumeSpec{}, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// MountedDockerVolumes returns the list of volumes of VolumeTypeVolume type that are mounted into the container.
|
||||||
|
func (s *ServiceSpec) MountedDockerVolumes() []VolumeSpec {
|
||||||
|
volumes := make(map[string]VolumeSpec)
|
||||||
|
for _, m := range s.Container.VolumeMounts {
|
||||||
|
if v, ok := s.Volume(m.VolumeName); ok && v.Type == VolumeTypeVolume {
|
||||||
|
volumes[v.Name] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return slices.Collect(maps.Values(volumes))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ServiceSpec) SetDefaults() ServiceSpec {
|
func (s *ServiceSpec) SetDefaults() ServiceSpec {
|
||||||
@@ -60,6 +86,10 @@ func (s *ServiceSpec) SetDefaults() ServiceSpec {
|
|||||||
}
|
}
|
||||||
spec.Container = spec.Container.SetDefaults()
|
spec.Container = spec.Container.SetDefaults()
|
||||||
|
|
||||||
|
for i, v := range spec.Volumes {
|
||||||
|
spec.Volumes[i] = v.SetDefaults()
|
||||||
|
}
|
||||||
|
|
||||||
return spec
|
return spec
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,6 +115,26 @@ func (s *ServiceSpec) Validate() error {
|
|||||||
|
|
||||||
// TODO: validate there is no conflict between ports.
|
// TODO: validate there is no conflict between ports.
|
||||||
|
|
||||||
|
volumeNames := make(map[string]struct{})
|
||||||
|
for _, v := range s.Volumes {
|
||||||
|
if err := v.Validate(); err != nil {
|
||||||
|
return fmt.Errorf("invalid volume: %w", err)
|
||||||
|
}
|
||||||
|
if _, ok := volumeNames[v.Name]; ok {
|
||||||
|
return fmt.Errorf("duplicate volume name: '%s'", v.Name)
|
||||||
|
}
|
||||||
|
volumeNames[v.Name] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, m := range s.Container.VolumeMounts {
|
||||||
|
if !slices.ContainsFunc(s.Volumes, func(v VolumeSpec) bool {
|
||||||
|
return v.Name == m.VolumeName
|
||||||
|
}) {
|
||||||
|
return fmt.Errorf("volume mount references a volume that doesn't exist in the service spec: '%s'",
|
||||||
|
m.VolumeName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,6 +147,13 @@ func (s *ServiceSpec) Clone() ServiceSpec {
|
|||||||
}
|
}
|
||||||
spec.Container = s.Container.Clone()
|
spec.Container = s.Container.Clone()
|
||||||
|
|
||||||
|
if s.Volumes != nil {
|
||||||
|
spec.Volumes = make([]VolumeSpec, len(s.Volumes))
|
||||||
|
for i, v := range s.Volumes {
|
||||||
|
spec.Volumes[i] = v.Clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return spec
|
return spec
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,13 +164,27 @@ type ContainerSpec struct {
|
|||||||
Command []string
|
Command []string
|
||||||
// Entrypoint overrides the default ENTRYPOINT of the image.
|
// Entrypoint overrides the default ENTRYPOINT of the image.
|
||||||
Entrypoint []string
|
Entrypoint []string
|
||||||
Image string
|
// Env defines the environment variables to set inside the container.
|
||||||
|
Env EnvVars
|
||||||
|
Image string
|
||||||
// Run a custom init inside the container. If nil, use the daemon's configured settings.
|
// Run a custom init inside the container. If nil, use the daemon's configured settings.
|
||||||
Init *bool
|
Init *bool
|
||||||
|
// LogDriver overrides the default logging driver for the container. Each Docker daemon can have its own default.
|
||||||
|
LogDriver *LogDriver
|
||||||
|
// Privileged gives extended privileges to the container. This is a security risk and should be used with caution.
|
||||||
|
Privileged bool
|
||||||
// PullPolicy determines when to pull the image from the registry or use the image already available in the cluster.
|
// PullPolicy determines when to pull the image from the registry or use the image already available in the cluster.
|
||||||
// Default is PullPolicyMissing if empty.
|
// Default is PullPolicyMissing if empty.
|
||||||
PullPolicy string
|
PullPolicy string
|
||||||
// List of volumes to bind mount into the container.
|
// Resource allocation for the container.
|
||||||
|
Resources ContainerResources
|
||||||
|
// User overrides the default user of the image used to run the container. Format: user|UID[:group|GID].
|
||||||
|
User string
|
||||||
|
// VolumeMounts specifies how volumes are mounted into the container filesystem.
|
||||||
|
// Each mount references a volume defined in ServiceSpec.Volumes.
|
||||||
|
VolumeMounts []VolumeMount
|
||||||
|
// Volumes is list of data volumes to mount into the container.
|
||||||
|
// TODO(lhf): delete all usage, has been replaced with []VolumeMounts.
|
||||||
Volumes []string
|
Volumes []string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,7 +200,13 @@ func (s *ContainerSpec) SetDefaults() ContainerSpec {
|
|||||||
|
|
||||||
func (s *ContainerSpec) Validate() error {
|
func (s *ContainerSpec) Validate() error {
|
||||||
if _, err := reference.ParseDockerRef(s.Image); err != nil {
|
if _, err := reference.ParseDockerRef(s.Image); err != nil {
|
||||||
return fmt.Errorf("invalid image: %w", err)
|
return fmt.Errorf("invalid image '%s': %w", s.Image, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, m := range s.VolumeMounts {
|
||||||
|
if err := m.Validate(); err != nil {
|
||||||
|
return fmt.Errorf("invalid volume mount: %w", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -142,7 +219,10 @@ func (s *ContainerSpec) Equals(spec ContainerSpec) bool {
|
|||||||
slices.Sort(orig.Volumes)
|
slices.Sort(orig.Volumes)
|
||||||
slices.Sort(spec.Volumes)
|
slices.Sort(spec.Volumes)
|
||||||
|
|
||||||
return reflect.DeepEqual(orig, spec)
|
sortVolumeMounts(orig.VolumeMounts)
|
||||||
|
sortVolumeMounts(spec.VolumeMounts)
|
||||||
|
|
||||||
|
return cmp.Equal(orig, spec, cmpopts.EquateEmpty())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ContainerSpec) Clone() ContainerSpec {
|
func (s *ContainerSpec) Clone() ContainerSpec {
|
||||||
@@ -156,14 +236,51 @@ func (s *ContainerSpec) Clone() ContainerSpec {
|
|||||||
spec.Entrypoint = make([]string, len(s.Entrypoint))
|
spec.Entrypoint = make([]string, len(s.Entrypoint))
|
||||||
copy(spec.Entrypoint, s.Entrypoint)
|
copy(spec.Entrypoint, s.Entrypoint)
|
||||||
}
|
}
|
||||||
|
if s.LogDriver != nil {
|
||||||
|
logDriver := *s.LogDriver
|
||||||
|
if s.LogDriver.Options != nil {
|
||||||
|
logDriver.Options = maps.Clone(s.LogDriver.Options)
|
||||||
|
}
|
||||||
|
spec.LogDriver = &logDriver
|
||||||
|
}
|
||||||
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)
|
||||||
}
|
}
|
||||||
|
if s.VolumeMounts != nil {
|
||||||
|
spec.VolumeMounts = make([]VolumeMount, len(s.VolumeMounts))
|
||||||
|
copy(spec.VolumeMounts, s.VolumeMounts)
|
||||||
|
}
|
||||||
|
|
||||||
return spec
|
return spec
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type EnvVars map[string]string
|
||||||
|
|
||||||
|
// ToSlice converts the environment variables to a slice of strings in the format "key=value".
|
||||||
|
func (e EnvVars) ToSlice() []string {
|
||||||
|
env := make([]string, 0, len(e))
|
||||||
|
for k, v := range e {
|
||||||
|
if k == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
env = append(env, fmt.Sprintf("%s=%s", k, v))
|
||||||
|
}
|
||||||
|
return env
|
||||||
|
}
|
||||||
|
|
||||||
|
type LogDriver struct {
|
||||||
|
// Name of the logging driver to use.
|
||||||
|
Name string
|
||||||
|
// Options is the configuration options to pass to the logging driver.
|
||||||
|
Options map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
type RunServiceResponse struct {
|
||||||
|
ID string
|
||||||
|
Name string
|
||||||
|
}
|
||||||
|
|
||||||
type Service struct {
|
type Service struct {
|
||||||
ID string
|
ID string
|
||||||
Name string
|
Name string
|
||||||
|
|||||||
@@ -0,0 +1,287 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"slices"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/docker/docker/api/types/mount"
|
||||||
|
"github.com/docker/docker/api/types/volume"
|
||||||
|
"github.com/google/go-cmp/cmp"
|
||||||
|
"github.com/google/go-cmp/cmp/cmpopts"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// VolumeTypeBind is the type for mounting a host path.
|
||||||
|
VolumeTypeBind = "bind"
|
||||||
|
// VolumeTypeVolume is the type for mounting a named Docker volume.
|
||||||
|
VolumeTypeVolume = "volume"
|
||||||
|
// VolumeTypeTmpfs is the type for mounting a temporary file system stored in the host memory.
|
||||||
|
VolumeTypeTmpfs = "tmpfs"
|
||||||
|
|
||||||
|
// VolumeDriverLocal is the default volume driver for local named Docker volumes.
|
||||||
|
VolumeDriverLocal = "local"
|
||||||
|
)
|
||||||
|
|
||||||
|
// VolumeSpec defines a volume mount specification. As of April 2025, the volume must be created before deploying
|
||||||
|
// a service using it.
|
||||||
|
type VolumeSpec struct {
|
||||||
|
// Name is the volume name used to reference this volume in container mounts.
|
||||||
|
Name string
|
||||||
|
Type string
|
||||||
|
BindOptions *BindOptions `json:",omitempty"`
|
||||||
|
TmpfsOptions *mount.TmpfsOptions `json:",omitempty"`
|
||||||
|
VolumeOptions *VolumeOptions `json:",omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// BindOptions represents options for a bind volume.
|
||||||
|
type BindOptions struct {
|
||||||
|
// HostPath is the absolute path on the host filesystem.
|
||||||
|
HostPath string
|
||||||
|
// CreateHostPath indicates whether the host path should be created if it doesn't exist.
|
||||||
|
// If false, deployment will fail if the path doesn't exist.
|
||||||
|
CreateHostPath bool `json:",omitempty"`
|
||||||
|
Propagation mount.Propagation `json:",omitempty"`
|
||||||
|
Recursive string `json:",omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// VolumeOptions represents options for a named Docker volume.
|
||||||
|
type VolumeOptions struct {
|
||||||
|
// Driver specifies the volume driver and its options for volume creation.
|
||||||
|
// TODO: It seems we don't really need Driver and Labels if we only support externally managed volumes.
|
||||||
|
// However we may need them in the future if we add support for isolated container-scoped volumes.
|
||||||
|
Driver *mount.Driver `json:",omitempty"`
|
||||||
|
// Labels are key-value metadata to apply to the volume if creating a new volume.
|
||||||
|
Labels map[string]string `json:",omitempty"`
|
||||||
|
// Name of the named Docker volume to use. If not specified, defaults to the VolumeSpec.Name.
|
||||||
|
Name string `json:",omitempty"`
|
||||||
|
// NoCopy prevents automatic copying of data from the container mount path to the volume.
|
||||||
|
NoCopy bool `json:",omitempty"`
|
||||||
|
// SubPath is the path within the volume to mount instead of its root.
|
||||||
|
SubPath string `json:",omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *VolumeSpec) DockerVolumeName() string {
|
||||||
|
if v.Type != VolumeTypeVolume {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if v.VolumeOptions != nil && v.VolumeOptions.Name != "" {
|
||||||
|
return v.VolumeOptions.Name
|
||||||
|
}
|
||||||
|
return v.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *VolumeSpec) SetDefaults() VolumeSpec {
|
||||||
|
spec := v.Clone()
|
||||||
|
|
||||||
|
if spec.Type == VolumeTypeVolume {
|
||||||
|
if spec.VolumeOptions == nil {
|
||||||
|
spec.VolumeOptions = &VolumeOptions{}
|
||||||
|
}
|
||||||
|
if spec.VolumeOptions.Name == "" {
|
||||||
|
spec.VolumeOptions.Name = spec.Name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// TODO: set explicit default values for Propagation and Recursive for bind mounts?
|
||||||
|
|
||||||
|
return spec
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *VolumeSpec) Validate() error {
|
||||||
|
if v.Name == "" {
|
||||||
|
return fmt.Errorf("volume name must not be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
switch v.Type {
|
||||||
|
case VolumeTypeBind:
|
||||||
|
if v.BindOptions == nil {
|
||||||
|
return fmt.Errorf("bind volume must have bind options")
|
||||||
|
}
|
||||||
|
case VolumeTypeVolume, VolumeTypeTmpfs:
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("invalid volume type: '%s', must be one of '%s', '%s', '%s')",
|
||||||
|
v.Type, VolumeTypeBind, VolumeTypeVolume, VolumeTypeTmpfs)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *VolumeSpec) Equals(other VolumeSpec) bool {
|
||||||
|
vol := v.SetDefaults()
|
||||||
|
other = other.SetDefaults()
|
||||||
|
|
||||||
|
return cmp.Equal(vol, other, cmpopts.EquateEmpty())
|
||||||
|
}
|
||||||
|
|
||||||
|
// MatchesDockerVolume checks if this VolumeSpec is compatible with the given named Docker volume.
|
||||||
|
// In other words, it checks if the spec could be used to mount the volume.
|
||||||
|
func (v *VolumeSpec) MatchesDockerVolume(vol volume.Volume) bool {
|
||||||
|
if v.Type != VolumeTypeVolume {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
spec := v.SetDefaults()
|
||||||
|
|
||||||
|
if spec.DockerVolumeName() != vol.Name {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// The volume spec may not define the driver which means to use the default driver if creating a new volume
|
||||||
|
// or accept any driver when mounting an existing volume. If the driver is specified in the spec, the spec's
|
||||||
|
// driver and options must match the volume's driver and options.
|
||||||
|
if spec.VolumeOptions.Driver != nil {
|
||||||
|
volDriver := vol.Driver
|
||||||
|
if volDriver == "" {
|
||||||
|
volDriver = VolumeDriverLocal
|
||||||
|
}
|
||||||
|
|
||||||
|
if spec.VolumeOptions.Driver.Name != volDriver {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(spec.VolumeOptions.Driver.Options, vol.Options) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *VolumeSpec) Clone() VolumeSpec {
|
||||||
|
spec := *v
|
||||||
|
|
||||||
|
if v.BindOptions != nil {
|
||||||
|
opts := *v.BindOptions
|
||||||
|
spec.BindOptions = &opts
|
||||||
|
}
|
||||||
|
|
||||||
|
if v.VolumeOptions != nil {
|
||||||
|
opts := *v.VolumeOptions
|
||||||
|
if v.VolumeOptions.Driver != nil {
|
||||||
|
driver := *v.VolumeOptions.Driver
|
||||||
|
if driver.Options != nil {
|
||||||
|
driver.Options = make(map[string]string, len(v.VolumeOptions.Driver.Options))
|
||||||
|
for k, val := range v.VolumeOptions.Driver.Options {
|
||||||
|
driver.Options[k] = val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
opts.Driver = &driver
|
||||||
|
}
|
||||||
|
|
||||||
|
if opts.Labels != nil {
|
||||||
|
opts.Labels = make(map[string]string, len(v.VolumeOptions.Labels))
|
||||||
|
for k, val := range v.VolumeOptions.Labels {
|
||||||
|
opts.Labels[k] = val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
spec.VolumeOptions = &opts
|
||||||
|
}
|
||||||
|
|
||||||
|
if v.TmpfsOptions != nil {
|
||||||
|
opts := *v.TmpfsOptions
|
||||||
|
opts.Options = make([][]string, len(v.TmpfsOptions.Options))
|
||||||
|
for i, opt := range v.TmpfsOptions.Options {
|
||||||
|
opts.Options[i] = make([]string, len(opt))
|
||||||
|
copy(opts.Options[i], opt)
|
||||||
|
}
|
||||||
|
spec.TmpfsOptions = &opts
|
||||||
|
}
|
||||||
|
|
||||||
|
return spec
|
||||||
|
}
|
||||||
|
|
||||||
|
// VolumeMount defines how a volume is mounted into a container.
|
||||||
|
type VolumeMount struct {
|
||||||
|
// VolumeName references a volume defined in ServiceSpec.Volumes by its Name field.
|
||||||
|
VolumeName string
|
||||||
|
// ContainerPath is the absolute path where the volume is mounted in the container.
|
||||||
|
ContainerPath string
|
||||||
|
// ReadOnly indicates whether the volume should be mounted read-only.
|
||||||
|
// If false (default), the volume is mounted read-write.
|
||||||
|
ReadOnly bool `json:",omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *VolumeMount) Validate() error {
|
||||||
|
if m.VolumeName == "" {
|
||||||
|
return fmt.Errorf("volume name must not be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.HasPrefix(m.ContainerPath, "/") {
|
||||||
|
return fmt.Errorf("invalid container path: '%s', must be an absolute path in the container", m.ContainerPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func sortVolumeMounts(mounts []VolumeMount) {
|
||||||
|
sort.Slice(mounts, func(i, j int) bool {
|
||||||
|
if mounts[i].VolumeName != mounts[j].VolumeName {
|
||||||
|
return mounts[i].VolumeName < mounts[j].VolumeName
|
||||||
|
}
|
||||||
|
if mounts[i].ContainerPath != mounts[j].ContainerPath {
|
||||||
|
return mounts[i].ContainerPath < mounts[j].ContainerPath
|
||||||
|
}
|
||||||
|
if mounts[i].ReadOnly != mounts[j].ReadOnly {
|
||||||
|
if !mounts[i].ReadOnly {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// MachineVolume represents a volume on a specific machine.
|
||||||
|
type MachineVolume struct {
|
||||||
|
// MachineID is the ID of the machine where the volume exists.
|
||||||
|
MachineID string
|
||||||
|
// MachineName is the name of the machine where the volume exists.
|
||||||
|
MachineName string
|
||||||
|
// Volume is the Docker volume model.
|
||||||
|
Volume volume.Volume
|
||||||
|
}
|
||||||
|
|
||||||
|
// VolumeFilter defines criteria to filter volumes in ListVolumes.
|
||||||
|
type VolumeFilter struct {
|
||||||
|
// Driver filters volumes by storage driver name.
|
||||||
|
Driver string
|
||||||
|
// Labels filters volumes by label key-value pairs. Volumes must match all labels.
|
||||||
|
Labels map[string]string
|
||||||
|
// MachineIDs filters volumes to those on the specified machines (names or IDs).
|
||||||
|
Machines []string
|
||||||
|
// Names filters volumes by name. Volumes must match one of the names.
|
||||||
|
Names []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// MatchesFilter checks if a volume matches the specified filter criteria.
|
||||||
|
func (v *MachineVolume) MatchesFilter(filter *VolumeFilter) bool {
|
||||||
|
if filter == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
// Filter by name.
|
||||||
|
if len(filter.Names) > 0 && !slices.Contains(filter.Names, v.Volume.Name) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// Filter by driver.
|
||||||
|
if filter.Driver != "" && v.Volume.Driver != filter.Driver {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// Filter by labels.
|
||||||
|
for key, value := range filter.Labels {
|
||||||
|
labelValue, exists := v.Volume.Labels[key]
|
||||||
|
if !exists || labelValue != value {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Filter by machines.
|
||||||
|
if len(filter.Machines) > 0 {
|
||||||
|
if !slices.ContainsFunc(filter.Machines, func(nameOrID string) bool {
|
||||||
|
return v.MachineName == nameOrID || v.MachineID == nameOrID
|
||||||
|
}) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
+22
-6
@@ -2,13 +2,14 @@ package client
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
|
||||||
"github.com/Masterminds/semver"
|
"github.com/Masterminds/semver"
|
||||||
"github.com/distribution/reference"
|
"github.com/distribution/reference"
|
||||||
"github.com/google/go-containerregistry/pkg/name"
|
"github.com/google/go-containerregistry/pkg/name"
|
||||||
"github.com/google/go-containerregistry/pkg/v1/remote"
|
"github.com/google/go-containerregistry/pkg/v1/remote"
|
||||||
"github.com/psviderski/uncloud/pkg/api"
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
"github.com/psviderski/uncloud/pkg/client/deploy"
|
"github.com/psviderski/uncloud/pkg/client/deploy"
|
||||||
"regexp"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -22,7 +23,7 @@ var caddyImageTagRegex = regexp.MustCompile(`^2\.\d+\.\d+$`)
|
|||||||
// NewCaddyDeployment creates a new deployment for a Caddy reverse proxy service.
|
// NewCaddyDeployment creates a new deployment for a Caddy reverse proxy service.
|
||||||
// The service is deployed in global mode to all machines in the cluster. If the image is not provided, the latest
|
// The service is deployed in global mode to all machines in the cluster. If the image is not provided, the latest
|
||||||
// version of the official Caddy Docker image is used.
|
// version of the official Caddy Docker image is used.
|
||||||
func (cli *Client) NewCaddyDeployment(image string, filter deploy.MachineFilter) (*deploy.Deployment, error) {
|
func (cli *Client) NewCaddyDeployment(image string, placement api.Placement) (*deploy.Deployment, error) {
|
||||||
latest, err := LatestCaddyImage()
|
latest, err := LatestCaddyImage()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("look up latest Caddy image: %w", err)
|
return nil, fmt.Errorf("look up latest Caddy image: %w", err)
|
||||||
@@ -36,10 +37,16 @@ func (cli *Client) NewCaddyDeployment(image string, filter deploy.MachineFilter)
|
|||||||
Container: api.ContainerSpec{
|
Container: api.ContainerSpec{
|
||||||
Command: []string{"caddy", "run", "-c", "/config/caddy.json", "--watch"},
|
Command: []string{"caddy", "run", "-c", "/config/caddy.json", "--watch"},
|
||||||
Image: image,
|
Image: image,
|
||||||
Volumes: []string{"/var/lib/uncloud/caddy:/config"},
|
VolumeMounts: []api.VolumeMount{
|
||||||
|
{
|
||||||
|
VolumeName: "config",
|
||||||
|
ContainerPath: "/config",
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
Mode: api.ServiceModeGlobal,
|
Mode: api.ServiceModeGlobal,
|
||||||
Name: CaddyServiceName,
|
Name: CaddyServiceName,
|
||||||
|
Placement: placement,
|
||||||
Ports: []api.PortSpec{
|
Ports: []api.PortSpec{
|
||||||
{
|
{
|
||||||
PublishedPort: 80,
|
PublishedPort: 80,
|
||||||
@@ -54,9 +61,18 @@ func (cli *Client) NewCaddyDeployment(image string, filter deploy.MachineFilter)
|
|||||||
Mode: api.PortModeHost,
|
Mode: api.PortModeHost,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
Volumes: []api.VolumeSpec{
|
||||||
|
{
|
||||||
|
Name: "config",
|
||||||
|
Type: api.VolumeTypeBind,
|
||||||
|
BindOptions: &api.BindOptions{
|
||||||
|
HostPath: "/var/lib/uncloud/caddy",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
return cli.NewDeployment(spec, &deploy.RollingStrategy{MachineFilter: filter}), nil
|
return cli.NewDeployment(spec, nil), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// LatestCaddyImage returns the latest image of the official Caddy Docker image on Docker Hub.
|
// LatestCaddyImage returns the latest image of the official Caddy Docker image on Docker Hub.
|
||||||
|
|||||||
@@ -1,31 +0,0 @@
|
|||||||
package client
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"github.com/psviderski/uncloud/internal/machine/api/pb"
|
|
||||||
"github.com/psviderski/uncloud/pkg/api"
|
|
||||||
"google.golang.org/protobuf/types/known/emptypb"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (cli *Client) InspectMachine(ctx context.Context, id string) (*pb.MachineMember, error) {
|
|
||||||
machines, err := cli.ListMachines(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, m := range machines {
|
|
||||||
if m.Machine.Id == id || m.Machine.Name == id {
|
|
||||||
return m, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, api.ErrNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cli *Client) ListMachines(ctx context.Context) ([]*pb.MachineMember, error) {
|
|
||||||
resp, err := cli.ClusterClient.ListMachines(ctx, &emptypb.Empty{})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return resp.Machines, nil
|
|
||||||
}
|
|
||||||
+118
-36
@@ -4,10 +4,16 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"github.com/compose-spec/compose-go/v2/graph"
|
"github.com/compose-spec/compose-go/v2/graph"
|
||||||
"github.com/compose-spec/compose-go/v2/types"
|
"github.com/compose-spec/compose-go/v2/types"
|
||||||
|
"github.com/docker/docker/api/types/volume"
|
||||||
"github.com/psviderski/uncloud/pkg/api"
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
"github.com/psviderski/uncloud/pkg/client/deploy"
|
"github.com/psviderski/uncloud/pkg/client/deploy"
|
||||||
|
"github.com/psviderski/uncloud/pkg/client/deploy/scheduler"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Client interface {
|
type Client interface {
|
||||||
@@ -19,10 +25,16 @@ type Deployment struct {
|
|||||||
Client Client
|
Client Client
|
||||||
Project *types.Project
|
Project *types.Project
|
||||||
SpecResolver *deploy.ServiceSpecResolver
|
SpecResolver *deploy.ServiceSpecResolver
|
||||||
|
state *scheduler.ClusterState
|
||||||
plan *deploy.SequenceOperation
|
plan *deploy.SequenceOperation
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewDeployment(ctx context.Context, cli Client, project *types.Project) (*Deployment, error) {
|
func NewDeployment(ctx context.Context, cli Client, project *types.Project) (*Deployment, error) {
|
||||||
|
state, err := scheduler.InspectClusterState(ctx, cli)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("inspect cluster state: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
domain, err := cli.GetDomain(ctx)
|
domain, err := cli.GetDomain(ctx)
|
||||||
if err != nil && !errors.Is(err, api.ErrNotFound) {
|
if err != nil && !errors.Is(err, api.ErrNotFound) {
|
||||||
return nil, fmt.Errorf("get cluster domain: %w", err)
|
return nil, fmt.Errorf("get cluster domain: %w", err)
|
||||||
@@ -31,13 +43,13 @@ func NewDeployment(ctx context.Context, cli Client, project *types.Project) (*De
|
|||||||
resolver := &deploy.ServiceSpecResolver{
|
resolver := &deploy.ServiceSpecResolver{
|
||||||
// If the domain is not found (not reserved), an empty domain is used for the resolver.
|
// If the domain is not found (not reserved), an empty domain is used for the resolver.
|
||||||
ClusterDomain: domain,
|
ClusterDomain: domain,
|
||||||
// TODO: provide an image resolver.
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return &Deployment{
|
return &Deployment{
|
||||||
Client: cli,
|
Client: cli,
|
||||||
Project: project,
|
Project: project,
|
||||||
SpecResolver: resolver,
|
SpecResolver: resolver,
|
||||||
|
state: state,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,63 +57,133 @@ func (d *Deployment) Plan(ctx context.Context) (deploy.SequenceOperation, error)
|
|||||||
if d.plan != nil {
|
if d.plan != nil {
|
||||||
return *d.plan, nil
|
return *d.plan, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
plan := deploy.SequenceOperation{}
|
plan := deploy.SequenceOperation{}
|
||||||
|
|
||||||
|
// Generate service specs for all services in the project.
|
||||||
|
var serviceSpecs []api.ServiceSpec
|
||||||
|
var mu sync.Mutex
|
||||||
err := graph.InDependencyOrder(ctx, d.Project,
|
err := graph.InDependencyOrder(ctx, d.Project,
|
||||||
func(ctx context.Context, name string, _ types.ServiceConfig) error {
|
func(ctx context.Context, name string, _ types.ServiceConfig) error {
|
||||||
spec, err := d.ServiceSpec(name)
|
spec, err := d.ServiceSpec(name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("convert compose service '%s' to service spec: %w", name, err)
|
return err
|
||||||
}
|
}
|
||||||
|
// The graph is traversed concurrently, so we need to use a mutex to protect the shared slice.
|
||||||
// TODO: properly handle depends_on conditions in the service deployment plan as the first operation.
|
mu.Lock()
|
||||||
deployment := deploy.NewDeployment(d.Client, spec, nil)
|
serviceSpecs = append(serviceSpecs, spec)
|
||||||
if err != nil {
|
mu.Unlock()
|
||||||
return fmt.Errorf("create deployment for service '%s': %w", name, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
servicePlan, err := deployment.Plan(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("create deployment plan for service '%s': %w", name, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skip no-op (up-to-date) service plans.
|
|
||||||
if len(servicePlan.Operations) > 0 {
|
|
||||||
plan.Operations = append(plan.Operations, &servicePlan)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
d.plan = &plan
|
return plan, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return plan, err
|
// Check external volumes and plan the creation of missing volumes before deploying services.
|
||||||
|
volumeOps, err := d.planVolumes(serviceSpecs)
|
||||||
|
if err != nil {
|
||||||
|
return plan, err
|
||||||
|
}
|
||||||
|
for _, op := range volumeOps {
|
||||||
|
plan.Operations = append(plan.Operations, op)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, spec := range serviceSpecs {
|
||||||
|
// 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.
|
||||||
|
deployment := deploy.NewDeployment(d.Client, spec, &deploy.RollingStrategy{State: d.state})
|
||||||
|
servicePlan, err := deployment.Plan(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return plan, fmt.Errorf("create deployment plan for service '%s': %w", spec.Name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip no-op (up-to-date) service plans.
|
||||||
|
if len(servicePlan.Operations) > 0 {
|
||||||
|
plan.Operations = append(plan.Operations, &servicePlan)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
d.plan = &plan
|
||||||
|
return plan, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServiceSpec returns the service specification for the given compose service that is ready for deployment.
|
// ServiceSpec returns the service specification for the given compose service that is ready for deployment.
|
||||||
func (d *Deployment) ServiceSpec(name string) (api.ServiceSpec, error) {
|
func (d *Deployment) ServiceSpec(name string) (api.ServiceSpec, error) {
|
||||||
service, err := d.Project.GetService(name)
|
spec, err := ServiceSpecFromCompose(d.Project, name)
|
||||||
if err != nil {
|
|
||||||
return api.ServiceSpec{}, fmt.Errorf("get config for compose service '%s': %w", name, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
spec, err := ServiceSpecFromCompose(name, service)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return spec, fmt.Errorf("convert compose service '%s' to service spec: %w", name, err)
|
return spec, fmt.Errorf("convert compose service '%s' to service spec: %w", name, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: resolve the image to a digest and supported platforms using an image resolver that broadcasts requests
|
|
||||||
// to all machines in the cluster. If service.PullPolicy is "missing":
|
|
||||||
// - Broadcast request if any machine contains a particular image and resolve it to image@digest.
|
|
||||||
// - If not found, broadcast request to resolve an image using a registry, and resolve it to image@digest.
|
|
||||||
// TODO: configure placement filter based on the supported platforms of the image.
|
|
||||||
|
|
||||||
// TODO: maybe instantiate ImageResolver here based on PullPolicy of each service?
|
|
||||||
|
|
||||||
return spec, nil
|
return spec, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PlanVolumes checks if the external volumes exist and plans the creation of missing volumes.
|
||||||
|
func (d *Deployment) planVolumes(serviceSpecs []api.ServiceSpec) ([]*deploy.CreateVolumeOperation, error) {
|
||||||
|
if len(d.Project.Volumes) == 0 {
|
||||||
|
// No volumes to check or create.
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := d.checkExternalVolumesExist(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: The scheduler should ideally work with the resolved service specs to correctly identify eligible machines.
|
||||||
|
// Figure out where the best place to resolve the specs is.
|
||||||
|
volumeScheduler, err := scheduler.NewVolumeScheduler(d.state, serviceSpecs)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("init volume scheduler: %w", err)
|
||||||
|
}
|
||||||
|
scheduledVolumes, err := volumeScheduler.Schedule()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("schedule volumes: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate operations to create scheduled missing volumes.
|
||||||
|
var ops []*deploy.CreateVolumeOperation
|
||||||
|
for machineID, volumes := range scheduledVolumes {
|
||||||
|
for _, v := range volumes {
|
||||||
|
machineName := machineID
|
||||||
|
if m, ok := d.state.Machine(machineID); ok {
|
||||||
|
machineName = m.Info.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
ops = append(ops, &deploy.CreateVolumeOperation{
|
||||||
|
MachineID: machineID,
|
||||||
|
MachineName: machineName,
|
||||||
|
VolumeSpec: v,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ops, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkExternalVolumesExist checks that all external volumes exist in the cluster.
|
||||||
|
func (d *Deployment) checkExternalVolumesExist() error {
|
||||||
|
var externalNames []string
|
||||||
|
for _, v := range d.Project.Volumes {
|
||||||
|
if v.External {
|
||||||
|
externalNames = append(externalNames, v.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var notFound []string
|
||||||
|
for _, name := range externalNames {
|
||||||
|
if !slices.ContainsFunc(d.state.Machines, func(m *scheduler.Machine) bool {
|
||||||
|
return slices.ContainsFunc(m.Volumes, func(vol volume.Volume) bool {
|
||||||
|
return vol.Name == name
|
||||||
|
})
|
||||||
|
}) {
|
||||||
|
notFound = append(notFound, fmt.Sprintf("'%s'", name))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(notFound) > 0 {
|
||||||
|
return fmt.Errorf("external volumes not found: %s", strings.Join(notFound, ", "))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (d *Deployment) Run(ctx context.Context) error {
|
func (d *Deployment) Run(ctx context.Context) error {
|
||||||
plan, err := d.Plan(ctx)
|
plan, err := d.Plan(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -5,22 +5,31 @@ package compose
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
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"
|
||||||
)
|
)
|
||||||
|
|
||||||
func LoadProject(ctx context.Context, paths []string) (*types.Project, error) {
|
// FakeProjectName is a placeholder name for the project to be able to strip it from the resource names used as prefix.
|
||||||
options, err := composecli.NewProjectOptions(
|
const FakeProjectName = "f-a-k-e"
|
||||||
paths,
|
|
||||||
|
func LoadProject(ctx context.Context, paths []string, opts ...composecli.ProjectOptionsFn) (*types.Project, error) {
|
||||||
|
defaultOpts := []composecli.ProjectOptionsFn{
|
||||||
|
composecli.WithName(FakeProjectName),
|
||||||
// First apply os.Environment, always wins.
|
// First apply os.Environment, always wins.
|
||||||
composecli.WithOsEnv,
|
composecli.WithOsEnv,
|
||||||
// Read dot env file to populate project environment.
|
// Read dot env file to populate project environment.
|
||||||
composecli.WithDotEnv,
|
composecli.WithDotEnv,
|
||||||
// Get compose file path set by COMPOSE_FILE.
|
// Get compose file path set by COMPOSE_FILE.
|
||||||
composecli.WithConfigFileEnv,
|
composecli.WithConfigFileEnv,
|
||||||
// If none was selected, get default compose-ports-long.yaml file from current dir or parent folders.
|
// If none was selected, get default Compose file names from current or parent folders.
|
||||||
composecli.WithDefaultConfigPath,
|
composecli.WithDefaultConfigPath,
|
||||||
composecli.WithExtension(PortsExtensionKey, PortsSource{}),
|
composecli.WithExtension(PortsExtensionKey, PortsSource{}),
|
||||||
|
}
|
||||||
|
|
||||||
|
options, err := composecli.NewProjectOptions(
|
||||||
|
paths,
|
||||||
|
append(defaultOpts, opts...)...,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("create compose parser options: %w", err)
|
return nil, fmt.Errorf("create compose parser options: %w", err)
|
||||||
|
|||||||
@@ -2,11 +2,23 @@ package compose
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"maps"
|
||||||
|
"os"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/compose-spec/compose-go/v2/types"
|
"github.com/compose-spec/compose-go/v2/types"
|
||||||
|
"github.com/docker/docker/api/types/mount"
|
||||||
|
"github.com/opencontainers/go-digest"
|
||||||
"github.com/psviderski/uncloud/pkg/api"
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
func ServiceSpecFromCompose(name string, service types.ServiceConfig) (api.ServiceSpec, error) {
|
func ServiceSpecFromCompose(project *types.Project, serviceName string) (api.ServiceSpec, error) {
|
||||||
|
service, err := project.GetService(serviceName)
|
||||||
|
if err != nil {
|
||||||
|
return api.ServiceSpec{}, fmt.Errorf("get config for compose service '%s': %w", serviceName, err)
|
||||||
|
}
|
||||||
|
|
||||||
pullPolicy := ""
|
pullPolicy := ""
|
||||||
switch service.PullPolicy {
|
switch service.PullPolicy {
|
||||||
case types.PullPolicyAlways:
|
case types.PullPolicyAlways:
|
||||||
@@ -19,35 +31,211 @@ func ServiceSpecFromCompose(name string, service types.ServiceConfig) (api.Servi
|
|||||||
return api.ServiceSpec{}, fmt.Errorf("unsupported pull policy: '%s'", service.PullPolicy)
|
return api.ServiceSpec{}, fmt.Errorf("unsupported pull policy: '%s'", service.PullPolicy)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
env := make(map[string]string, len(service.Environment))
|
||||||
|
for k, v := range service.Environment {
|
||||||
|
if v == nil {
|
||||||
|
// nil value means the variable misses a value in the compose file, and it hasn't been resolved
|
||||||
|
// to a variable from the local environment running this code.
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
env[k] = *v
|
||||||
|
}
|
||||||
|
|
||||||
spec := api.ServiceSpec{
|
spec := api.ServiceSpec{
|
||||||
Container: api.ContainerSpec{
|
Container: api.ContainerSpec{
|
||||||
Command: service.Command,
|
Command: service.Command,
|
||||||
|
Entrypoint: service.Entrypoint,
|
||||||
|
Env: env,
|
||||||
Image: service.Image,
|
Image: service.Image,
|
||||||
Init: service.Init,
|
Init: service.Init,
|
||||||
|
Privileged: service.Privileged,
|
||||||
PullPolicy: pullPolicy,
|
PullPolicy: pullPolicy,
|
||||||
// TODO: env
|
Resources: resourcesFromCompose(service),
|
||||||
// TODO: volumes
|
User: service.User,
|
||||||
},
|
},
|
||||||
Name: name,
|
Name: serviceName,
|
||||||
|
Mode: api.ServiceModeReplicated,
|
||||||
|
// TODO: implement and map x-machines to Placement.
|
||||||
}
|
}
|
||||||
|
|
||||||
if ports, ok := service.Extensions[PortsExtensionKey].([]api.PortSpec); ok {
|
if ports, ok := service.Extensions[PortsExtensionKey].([]api.PortSpec); ok {
|
||||||
spec.Ports = ports
|
spec.Ports = ports
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Map LogDriver if specified
|
||||||
|
if service.Logging != nil && service.Logging.Driver != "" {
|
||||||
|
spec.Container.LogDriver = &api.LogDriver{
|
||||||
|
Name: service.Logging.Driver,
|
||||||
|
Options: service.Logging.Options,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if service.Scale != nil {
|
||||||
|
spec.Replicas = uint(*service.Scale)
|
||||||
|
}
|
||||||
|
|
||||||
if service.Deploy != nil {
|
if service.Deploy != nil {
|
||||||
switch service.Deploy.Mode {
|
switch service.Deploy.Mode {
|
||||||
case "global":
|
case "global":
|
||||||
spec.Mode = api.ServiceModeGlobal
|
spec.Mode = api.ServiceModeGlobal
|
||||||
case "", "replicated":
|
case "", "replicated":
|
||||||
spec.Mode = api.ServiceModeReplicated
|
|
||||||
if service.Deploy.Replicas != nil {
|
if service.Deploy.Replicas != nil {
|
||||||
spec.Replicas = uint(*service.Deploy.Replicas)
|
spec.Replicas = uint(*service.Deploy.Replicas)
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
return spec, fmt.Errorf("unsupported deploy mode: %s", service.Deploy.Mode)
|
return spec, fmt.Errorf("unsupported deploy mode: '%s'", service.Deploy.Mode)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: can service.tmpfs be handled as tmpfs volume mounts as well?
|
||||||
|
volumeSpecs, volumeMounts, err := volumeSpecsFromCompose(project.Volumes, service.Volumes)
|
||||||
|
if err != nil {
|
||||||
|
return spec, err
|
||||||
|
}
|
||||||
|
|
||||||
|
spec.Volumes = volumeSpecs
|
||||||
|
spec.Container.VolumeMounts = volumeMounts
|
||||||
|
|
||||||
return spec, nil
|
return spec, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func resourcesFromCompose(service types.ServiceConfig) api.ContainerResources {
|
||||||
|
resources := api.ContainerResources{
|
||||||
|
CPU: int64(service.CPUS * 1e9),
|
||||||
|
Memory: int64(service.MemLimit),
|
||||||
|
MemoryReservation: int64(service.MemReservation),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map resources from deploy section if specified.
|
||||||
|
if service.Deploy != nil {
|
||||||
|
if service.Deploy.Resources.Limits != nil {
|
||||||
|
if service.Deploy.Resources.Limits.NanoCPUs > 0 {
|
||||||
|
// It seems Limits.NanoCPUs is actually not nano CPUs but a CPU fraction.
|
||||||
|
resources.CPU = int64(service.Deploy.Resources.Limits.NanoCPUs * 1e9)
|
||||||
|
}
|
||||||
|
if service.Deploy.Resources.Limits.MemoryBytes > 0 {
|
||||||
|
resources.Memory = int64(service.Deploy.Resources.Limits.MemoryBytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if service.Deploy.Resources.Reservations != nil {
|
||||||
|
if service.Deploy.Resources.Reservations.MemoryBytes > 0 {
|
||||||
|
resources.MemoryReservation = int64(service.Deploy.Resources.Reservations.MemoryBytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return resources
|
||||||
|
}
|
||||||
|
|
||||||
|
func volumeSpecsFromCompose(
|
||||||
|
volumes types.Volumes, serviceVolumes []types.ServiceVolumeConfig,
|
||||||
|
) ([]api.VolumeSpec, []api.VolumeMount, error) {
|
||||||
|
volumeSpecs := make(map[string]api.VolumeSpec)
|
||||||
|
var volumeMounts []api.VolumeMount
|
||||||
|
|
||||||
|
for _, serviceVolume := range serviceVolumes {
|
||||||
|
var volSpec api.VolumeSpec
|
||||||
|
|
||||||
|
switch serviceVolume.Type {
|
||||||
|
case types.VolumeTypeBind:
|
||||||
|
volSpec = bindVolumeSpecFromCompose(serviceVolume)
|
||||||
|
case types.VolumeTypeVolume:
|
||||||
|
volSpec = dockerVolumeSpecFromCompose(serviceVolume, volumes[serviceVolume.Source])
|
||||||
|
case types.VolumeTypeTmpfs:
|
||||||
|
volSpec = tmpfsVolumeSpecFromCompose(serviceVolume)
|
||||||
|
default:
|
||||||
|
return nil, nil, fmt.Errorf("unsupported volume type: '%s'", serviceVolume.Type)
|
||||||
|
}
|
||||||
|
|
||||||
|
if existing, ok := volumeSpecs[volSpec.Name]; ok {
|
||||||
|
if !existing.Equals(volSpec) {
|
||||||
|
return nil, nil, fmt.Errorf("volume '%s' is used multiple times with different options", volSpec.Name)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
volumeSpecs[volSpec.Name] = volSpec
|
||||||
|
}
|
||||||
|
|
||||||
|
volumeMounts = append(volumeMounts, api.VolumeMount{
|
||||||
|
VolumeName: volSpec.Name,
|
||||||
|
ContainerPath: serviceVolume.Target,
|
||||||
|
ReadOnly: serviceVolume.ReadOnly,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return slices.Collect(maps.Values(volumeSpecs)), volumeMounts, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func bindVolumeSpecFromCompose(serviceVolume types.ServiceVolumeConfig) api.VolumeSpec {
|
||||||
|
// compose-go parser deduplicates volumes by the target path so it's safe to use it as the unique name.
|
||||||
|
name := "bind-" + digest.SHA256.FromString(serviceVolume.Target).Encoded()
|
||||||
|
spec := api.VolumeSpec{
|
||||||
|
Name: name,
|
||||||
|
Type: api.VolumeTypeBind,
|
||||||
|
BindOptions: &api.BindOptions{
|
||||||
|
HostPath: serviceVolume.Source,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if serviceVolume.Bind != nil {
|
||||||
|
spec.BindOptions.CreateHostPath = serviceVolume.Bind.CreateHostPath
|
||||||
|
spec.BindOptions.Propagation = mount.Propagation(serviceVolume.Bind.Propagation)
|
||||||
|
spec.BindOptions.Recursive = serviceVolume.Bind.Recursive
|
||||||
|
}
|
||||||
|
|
||||||
|
return spec
|
||||||
|
}
|
||||||
|
|
||||||
|
func dockerVolumeSpecFromCompose(serviceVolume types.ServiceVolumeConfig, volume types.VolumeConfig) api.VolumeSpec {
|
||||||
|
spec := api.VolumeSpec{
|
||||||
|
Name: serviceVolume.Source,
|
||||||
|
Type: api.VolumeTypeVolume,
|
||||||
|
VolumeOptions: &api.VolumeOptions{
|
||||||
|
Name: strings.TrimPrefix(volume.Name, FakeProjectName+"_"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if serviceVolume.Volume != nil {
|
||||||
|
spec.VolumeOptions.NoCopy = serviceVolume.Volume.NoCopy
|
||||||
|
spec.VolumeOptions.SubPath = serviceVolume.Volume.Subpath
|
||||||
|
}
|
||||||
|
|
||||||
|
if !volume.External {
|
||||||
|
if volume.Driver != "" {
|
||||||
|
spec.VolumeOptions.Driver = &mount.Driver{
|
||||||
|
Name: volume.Driver,
|
||||||
|
Options: volume.DriverOpts,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
labels := mergeLabels(volume.Labels, volume.CustomLabels)
|
||||||
|
if len(labels) > 0 {
|
||||||
|
spec.VolumeOptions.Labels = labels
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return spec
|
||||||
|
}
|
||||||
|
|
||||||
|
func mergeLabels(labels ...types.Labels) types.Labels {
|
||||||
|
merged := types.Labels{}
|
||||||
|
for _, l := range labels {
|
||||||
|
for k, v := range l {
|
||||||
|
merged[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return merged
|
||||||
|
}
|
||||||
|
|
||||||
|
func tmpfsVolumeSpecFromCompose(serviceVolume types.ServiceVolumeConfig) api.VolumeSpec {
|
||||||
|
// compose-go parser deduplicates volumes by the target path so it's safe to use it as the unique name.
|
||||||
|
name := "tmpfs-" + digest.SHA256.FromString(serviceVolume.Target).Encoded()
|
||||||
|
spec := api.VolumeSpec{
|
||||||
|
Name: name,
|
||||||
|
Type: api.VolumeTypeTmpfs,
|
||||||
|
TmpfsOptions: &mount.TmpfsOptions{
|
||||||
|
SizeBytes: int64(serviceVolume.Tmpfs.Size),
|
||||||
|
Mode: os.FileMode(serviceVolume.Tmpfs.Mode),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return spec
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,238 @@
|
|||||||
|
package compose
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"path/filepath"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/compose-spec/compose-go/v2/cli"
|
||||||
|
"github.com/compose-spec/compose-go/v2/types"
|
||||||
|
"github.com/docker/docker/api/types/mount"
|
||||||
|
"github.com/docker/go-units"
|
||||||
|
"github.com/google/go-cmp/cmp"
|
||||||
|
"github.com/google/go-cmp/cmp/cmpopts"
|
||||||
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
// loadProjectFromFile loads a compose project from a YAML file
|
||||||
|
func loadProjectFromFile(t *testing.T, filename string) *types.Project {
|
||||||
|
t.Helper()
|
||||||
|
ctx := context.Background()
|
||||||
|
path := filepath.Join("testdata", filename)
|
||||||
|
|
||||||
|
options, err := cli.NewProjectOptions(
|
||||||
|
[]string{path},
|
||||||
|
cli.WithName(FakeProjectName),
|
||||||
|
cli.WithOsEnv,
|
||||||
|
cli.WithDotEnv,
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
project, err := options.LoadProject(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
return project
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestServiceSpecFromCompose(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
initTrue := true
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
filename string
|
||||||
|
want map[string]api.ServiceSpec
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "minimal",
|
||||||
|
filename: "compose-minimal.yaml",
|
||||||
|
want: map[string]api.ServiceSpec{
|
||||||
|
"test": {
|
||||||
|
Name: "test",
|
||||||
|
Mode: api.ServiceModeReplicated,
|
||||||
|
Container: api.ContainerSpec{
|
||||||
|
Image: "nginx:latest",
|
||||||
|
PullPolicy: api.PullPolicyMissing,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "deploy",
|
||||||
|
filename: "compose-deploy.yaml",
|
||||||
|
want: map[string]api.ServiceSpec{
|
||||||
|
"no-deploy": {
|
||||||
|
Name: "no-deploy",
|
||||||
|
Mode: api.ServiceModeReplicated,
|
||||||
|
Container: api.ContainerSpec{
|
||||||
|
Image: "nginx:latest",
|
||||||
|
PullPolicy: api.PullPolicyMissing,
|
||||||
|
Resources: api.ContainerResources{
|
||||||
|
CPU: 1.5 * api.Core,
|
||||||
|
Memory: 100 * units.MiB,
|
||||||
|
MemoryReservation: 50 * units.MiB,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Replicas: 3,
|
||||||
|
},
|
||||||
|
"deploy": {
|
||||||
|
Name: "deploy",
|
||||||
|
Mode: api.ServiceModeReplicated,
|
||||||
|
Container: api.ContainerSpec{
|
||||||
|
Image: "nginx:latest",
|
||||||
|
PullPolicy: api.PullPolicyMissing,
|
||||||
|
Resources: api.ContainerResources{
|
||||||
|
CPU: 1.5 * api.Core,
|
||||||
|
Memory: 100 * units.MiB,
|
||||||
|
MemoryReservation: 50 * units.MiB,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Replicas: 3,
|
||||||
|
},
|
||||||
|
"both": {
|
||||||
|
Name: "both",
|
||||||
|
Mode: api.ServiceModeReplicated,
|
||||||
|
Container: api.ContainerSpec{
|
||||||
|
Image: "nginx:latest",
|
||||||
|
PullPolicy: api.PullPolicyMissing,
|
||||||
|
Resources: api.ContainerResources{
|
||||||
|
CPU: 2 * api.Core,
|
||||||
|
Memory: 100 * units.MiB,
|
||||||
|
MemoryReservation: 50 * units.MiB,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Replicas: 3,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "full-spec",
|
||||||
|
filename: "compose-full-spec.yaml",
|
||||||
|
want: map[string]api.ServiceSpec{
|
||||||
|
"test": {
|
||||||
|
Name: "test",
|
||||||
|
Mode: api.ServiceModeReplicated,
|
||||||
|
Container: api.ContainerSpec{
|
||||||
|
Command: []string{"nginx", "updated", "command"},
|
||||||
|
Entrypoint: []string{"/updated-docker-entrypoint.sh"},
|
||||||
|
Env: map[string]string{
|
||||||
|
"BOOL": "true",
|
||||||
|
"EMPTY": "",
|
||||||
|
"VAR": "value",
|
||||||
|
},
|
||||||
|
Image: "nginx:latest",
|
||||||
|
Init: &initTrue,
|
||||||
|
LogDriver: &api.LogDriver{
|
||||||
|
Name: "json-file",
|
||||||
|
Options: map[string]string{
|
||||||
|
"max-size": "10m",
|
||||||
|
"max-file": "3",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Privileged: true,
|
||||||
|
PullPolicy: api.PullPolicyAlways,
|
||||||
|
Resources: api.ContainerResources{
|
||||||
|
CPU: 0.5 * api.Core,
|
||||||
|
Memory: 100 * units.MiB,
|
||||||
|
MemoryReservation: 50 * units.MiB,
|
||||||
|
},
|
||||||
|
User: "nginx:nginx",
|
||||||
|
VolumeMounts: []api.VolumeMount{
|
||||||
|
{
|
||||||
|
VolumeName: "bind-bb6aed1683cea1e0a1ae5cd227aacd0734f2f87f7a78fcf1baeff978ce300b90",
|
||||||
|
ContainerPath: "/host/etc/passwd",
|
||||||
|
ReadOnly: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
VolumeName: "data1",
|
||||||
|
ContainerPath: "/data1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
VolumeName: "data2-alias",
|
||||||
|
ContainerPath: "/data2/long/syntax",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
VolumeName: "data-external",
|
||||||
|
ContainerPath: "/external",
|
||||||
|
ReadOnly: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
VolumeName: "tmpfs-efa57ba8b6a1779674ac438de3af8729e2d55900b79eb929431cf9c5b0179542",
|
||||||
|
ContainerPath: "/tmpfs",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Replicas: 3,
|
||||||
|
Volumes: []api.VolumeSpec{
|
||||||
|
{
|
||||||
|
Name: "bind-bb6aed1683cea1e0a1ae5cd227aacd0734f2f87f7a78fcf1baeff978ce300b90",
|
||||||
|
Type: api.VolumeTypeBind,
|
||||||
|
BindOptions: &api.BindOptions{
|
||||||
|
HostPath: "/etc/passwd",
|
||||||
|
CreateHostPath: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "data-external",
|
||||||
|
Type: api.VolumeTypeVolume,
|
||||||
|
VolumeOptions: &api.VolumeOptions{
|
||||||
|
Name: "data-external",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "data1",
|
||||||
|
Type: api.VolumeTypeVolume,
|
||||||
|
VolumeOptions: &api.VolumeOptions{
|
||||||
|
Name: "data1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "data2-alias",
|
||||||
|
Type: api.VolumeTypeVolume,
|
||||||
|
VolumeOptions: &api.VolumeOptions{
|
||||||
|
Name: "data2",
|
||||||
|
Driver: &mount.Driver{
|
||||||
|
Name: "local",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "tmpfs-efa57ba8b6a1779674ac438de3af8729e2d55900b79eb929431cf9c5b0179542",
|
||||||
|
Type: api.VolumeTypeTmpfs,
|
||||||
|
TmpfsOptions: &mount.TmpfsOptions{
|
||||||
|
SizeBytes: 10 * units.MiB,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
project, err := LoadProject(ctx, []string{filepath.Join("testdata", tt.filename)})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
for name, expectedSpec := range tt.want {
|
||||||
|
spec, err := ServiceSpecFromCompose(project, name)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Due to the use of a map the order of volumes is non-deterministic.
|
||||||
|
slices.SortFunc(spec.Volumes, func(a, b api.VolumeSpec) int {
|
||||||
|
return strings.Compare(a.Name, b.Name)
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.True(t, cmp.Equal(spec, expectedSpec, cmpopts.EquateEmpty()),
|
||||||
|
cmp.Diff(spec, expectedSpec, cmpopts.EquateEmpty()))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
+33
@@ -0,0 +1,33 @@
|
|||||||
|
services:
|
||||||
|
no-deploy:
|
||||||
|
image: nginx:latest
|
||||||
|
cpus: 1.5
|
||||||
|
mem_limit: 100M
|
||||||
|
mem_reservation: 50M
|
||||||
|
scale: 3
|
||||||
|
|
||||||
|
deploy:
|
||||||
|
image: nginx:latest
|
||||||
|
deploy:
|
||||||
|
replicas: 3
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpus: 1.5
|
||||||
|
memory: 100M
|
||||||
|
reservations:
|
||||||
|
memory: 50M
|
||||||
|
|
||||||
|
both:
|
||||||
|
image: nginx:latest
|
||||||
|
cpus: 2
|
||||||
|
mem_limit: 102400K
|
||||||
|
mem_reservation: 52428800
|
||||||
|
scale: 3
|
||||||
|
deploy:
|
||||||
|
replicas: 3
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpus: 2.0
|
||||||
|
memory: 100M
|
||||||
|
reservations:
|
||||||
|
memory: 50M
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
services:
|
||||||
|
test:
|
||||||
|
command: ["nginx", "updated", "command"]
|
||||||
|
cpus: 0.5
|
||||||
|
entrypoint: ["/updated-docker-entrypoint.sh"]
|
||||||
|
environment:
|
||||||
|
BOOL: "true"
|
||||||
|
EMPTY: ""
|
||||||
|
VAR: value
|
||||||
|
image: nginx:latest
|
||||||
|
init: true
|
||||||
|
logging:
|
||||||
|
driver: json-file
|
||||||
|
options:
|
||||||
|
max-size: 10m
|
||||||
|
max-file: 3
|
||||||
|
mem_limit: 100M
|
||||||
|
mem_reservation: 50M
|
||||||
|
privileged: true
|
||||||
|
pull_policy: always
|
||||||
|
scale: 3
|
||||||
|
user: nginx:nginx
|
||||||
|
volumes:
|
||||||
|
- /etc/passwd:/host/etc/passwd:ro
|
||||||
|
- data1:/data1
|
||||||
|
- type: volume
|
||||||
|
source: data2-alias
|
||||||
|
target: /data2/long/syntax
|
||||||
|
- data-external:/external:ro
|
||||||
|
- type: tmpfs
|
||||||
|
target: /tmpfs
|
||||||
|
tmpfs:
|
||||||
|
size: 10485760
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
data1:
|
||||||
|
data2-alias:
|
||||||
|
name: data2
|
||||||
|
driver: local
|
||||||
|
data-external:
|
||||||
|
external: true
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
services:
|
||||||
|
test:
|
||||||
|
image: nginx:latest
|
||||||
@@ -61,8 +61,8 @@ func (cli *Client) CreateContainer(
|
|||||||
return resp, fmt.Errorf("unsupported pull policy: '%s'", spec.Container.PullPolicy)
|
return resp, fmt.Errorf("unsupported pull policy: '%s'", spec.Container.PullPolicy)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Not found error is expected if the image is missing.
|
// NotFound (No such image) error is expected if the image is missing.
|
||||||
if !dockerclient.IsErrNotFound(err) {
|
if !dockerclient.IsErrNotFound(err) || !strings.Contains(err.Error(), "No such image") {
|
||||||
return resp, err
|
return resp, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
package deploy
|
package deploy
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"reflect"
|
||||||
|
"sort"
|
||||||
|
|
||||||
|
"github.com/google/go-cmp/cmp"
|
||||||
|
"github.com/google/go-cmp/cmp/cmpopts"
|
||||||
"github.com/psviderski/uncloud/pkg/api"
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -14,12 +19,6 @@ func EvalContainerSpecChange(current api.ServiceSpec, new api.ServiceSpec) Conta
|
|||||||
current = current.SetDefaults()
|
current = current.SetDefaults()
|
||||||
new = new.SetDefaults()
|
new = new.SetDefaults()
|
||||||
|
|
||||||
// Pull policy doesn't affect the container configuration.
|
|
||||||
new.Container.PullPolicy = current.Container.PullPolicy
|
|
||||||
if !current.Container.Equals(new.Container) {
|
|
||||||
return ContainerNeedsRecreate
|
|
||||||
}
|
|
||||||
|
|
||||||
if current.Mode != new.Mode {
|
if current.Mode != new.Mode {
|
||||||
return ContainerNeedsRecreate
|
return ContainerNeedsRecreate
|
||||||
}
|
}
|
||||||
@@ -27,13 +26,56 @@ func EvalContainerSpecChange(current api.ServiceSpec, new api.ServiceSpec) Conta
|
|||||||
return ContainerNeedsRecreate
|
return ContainerNeedsRecreate
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: compare mutable properties such as memory or CPU limits when they are implemented.
|
// Pull policy doesn't affect the container configuration.
|
||||||
|
new.Container.PullPolicy = current.Container.PullPolicy
|
||||||
|
|
||||||
// TODO: change ports check to ContainerNeedsUpdate when ingress ports are stored only the machine DB instead
|
// Save mutable container resources that can be updated without recreation.
|
||||||
|
newResources := new.Container.Resources
|
||||||
|
// Temporarily set mutable container resources to current values to check if other properties changed.
|
||||||
|
new.Container.Resources = current.Container.Resources
|
||||||
|
|
||||||
|
// Check if immutable container properties changed.
|
||||||
|
if !current.Container.Equals(new.Container) {
|
||||||
|
return ContainerNeedsRecreate
|
||||||
|
}
|
||||||
|
|
||||||
|
if !cmp.Equal(current.Placement, new.Placement, cmpopts.EquateEmpty()) {
|
||||||
|
// TODO: this could be just an in-place spec update when available.
|
||||||
|
return ContainerNeedsRecreate
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: change ports check to ContainerNeedsUpdate when ingress ports are stored only in the machine DB instead
|
||||||
// of as labels ans synced to the cluster store. Host ports changes should be handled as ContainerNeedsRecreate.
|
// of as labels ans synced to the cluster store. Host ports changes should be handled as ContainerNeedsRecreate.
|
||||||
if !api.PortsEqual(current.Ports, new.Ports) {
|
if !api.PortsEqual(current.Ports, new.Ports) {
|
||||||
return ContainerNeedsRecreate
|
return ContainerNeedsRecreate
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Compare volumes.
|
||||||
|
if len(current.Volumes) != len(new.Volumes) {
|
||||||
|
return ContainerNeedsRecreate
|
||||||
|
}
|
||||||
|
sortVolumes(current.Volumes)
|
||||||
|
sortVolumes(new.Volumes)
|
||||||
|
for i := range current.Volumes {
|
||||||
|
if !current.Volumes[i].Equals(new.Volumes[i]) {
|
||||||
|
// TODO: require only spec update for cases (see TODOs in tests):
|
||||||
|
// * bind volumes with different CreateHostPath
|
||||||
|
// * changed reference name in spec but preserved original volume name
|
||||||
|
// TODO: should defined but not used (no corresponding mounts) volumes be simply ignored?
|
||||||
|
return ContainerNeedsRecreate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if any mutable properties changed.
|
||||||
|
if !reflect.DeepEqual(current.Container.Resources, newResources) {
|
||||||
|
return ContainerNeedsUpdate
|
||||||
|
}
|
||||||
|
|
||||||
return ContainerUpToDate
|
return ContainerUpToDate
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func sortVolumes(volumes []api.VolumeSpec) {
|
||||||
|
sort.Slice(volumes, func(i, j int) bool {
|
||||||
|
return volumes[i].Name < volumes[j].Name
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -4,7 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/psviderski/uncloud/internal/machine/api/pb"
|
|
||||||
"github.com/psviderski/uncloud/pkg/api"
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -14,6 +14,7 @@ type Client interface {
|
|||||||
api.ImageClient
|
api.ImageClient
|
||||||
api.MachineClient
|
api.MachineClient
|
||||||
api.ServiceClient
|
api.ServiceClient
|
||||||
|
api.VolumeClient
|
||||||
}
|
}
|
||||||
|
|
||||||
// Deployment manages the process of creating or updating a service to match a desired state.
|
// Deployment manages the process of creating or updating a service to match a desired state.
|
||||||
@@ -32,12 +33,6 @@ type Plan struct {
|
|||||||
SequenceOperation
|
SequenceOperation
|
||||||
}
|
}
|
||||||
|
|
||||||
// MachineFilter determines which machines participate in a deployment operation by returning true for
|
|
||||||
// machines that should be included.
|
|
||||||
type MachineFilter func(m *pb.MachineInfo) bool
|
|
||||||
|
|
||||||
var ErrNoMatchingMachines = errors.New("no machines match the filter")
|
|
||||||
|
|
||||||
// NewDeployment creates a new deployment for the given service specification.
|
// NewDeployment creates a new deployment for the given service specification.
|
||||||
// If strategy is nil, a default RollingStrategy will be used.
|
// If strategy is nil, a default RollingStrategy will be used.
|
||||||
func NewDeployment(cli Client, spec api.ServiceSpec, strategy Strategy) *Deployment {
|
func NewDeployment(cli Client, spec api.ServiceSpec, strategy Strategy) *Deployment {
|
||||||
@@ -68,15 +63,9 @@ func (d *Deployment) Plan(ctx context.Context) (Plan, error) {
|
|||||||
if err != nil && !errors.Is(err, api.ErrNotFound) {
|
if err != nil && !errors.Is(err, api.ErrNotFound) {
|
||||||
return Plan{}, fmt.Errorf("get cluster domain: %w", err)
|
return Plan{}, fmt.Errorf("get cluster domain: %w", err)
|
||||||
}
|
}
|
||||||
// TODO: enable image resolver when it's ready to pin digests and look up existing images.
|
|
||||||
//imageResolver := &ImageDigestResolver{
|
|
||||||
// Ctx: ctx,
|
|
||||||
// Client: d.cli,
|
|
||||||
//}
|
|
||||||
specResolver := &ServiceSpecResolver{
|
specResolver := &ServiceSpecResolver{
|
||||||
// If the domain is not found (not reserved), an empty domain is used for the resolver.
|
// If the domain is not found (not reserved), an empty domain is used for the resolver.
|
||||||
ClusterDomain: clusterDomain,
|
ClusterDomain: clusterDomain,
|
||||||
//ImageResolver: imageResolver,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
resolvedSpec, err := specResolver.Resolve(d.Spec)
|
resolvedSpec, err := specResolver.Resolve(d.Spec)
|
||||||
|
|||||||
@@ -3,9 +3,11 @@ package deploy
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/docker/docker/api/types/container"
|
|
||||||
"github.com/psviderski/uncloud/pkg/api"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/docker/docker/api/types/container"
|
||||||
|
"github.com/docker/docker/api/types/volume"
|
||||||
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Operation represents a single atomic operation in a deployment process.
|
// Operation represents a single atomic operation in a deployment process.
|
||||||
@@ -16,6 +18,7 @@ type Operation interface {
|
|||||||
// can be provided. But in reality, the operation is tightly coupled with the client that was used to create it.
|
// can be provided. But in reality, the operation is tightly coupled with the client that was used to create it.
|
||||||
Execute(ctx context.Context, cli Client) error
|
Execute(ctx context.Context, cli Client) error
|
||||||
// Format returns a human-readable representation of the operation.
|
// Format returns a human-readable representation of the operation.
|
||||||
|
// TODO: get rid of the resolver and assign the required names for formatting in the operation itself.
|
||||||
Format(resolver NameResolver) string
|
Format(resolver NameResolver) string
|
||||||
String() string
|
String() string
|
||||||
}
|
}
|
||||||
@@ -109,6 +112,46 @@ func (o *RemoveContainerOperation) String() string {
|
|||||||
o.ServiceID, o.ContainerID, o.MachineID)
|
o.ServiceID, o.ContainerID, o.MachineID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CreateVolumeOperation creates a volume on a specific machine.
|
||||||
|
type CreateVolumeOperation struct {
|
||||||
|
VolumeSpec api.VolumeSpec
|
||||||
|
MachineID string
|
||||||
|
// MachineName is used for formatting the operation output only.
|
||||||
|
MachineName string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *CreateVolumeOperation) Execute(ctx context.Context, cli Client) error {
|
||||||
|
if o.VolumeSpec.Type != api.VolumeTypeVolume {
|
||||||
|
return fmt.Errorf("invalid volume type: '%s', expected '%s'", o.VolumeSpec.Type, api.VolumeTypeVolume)
|
||||||
|
}
|
||||||
|
|
||||||
|
opts := volume.CreateOptions{
|
||||||
|
Name: o.VolumeSpec.DockerVolumeName(),
|
||||||
|
}
|
||||||
|
if o.VolumeSpec.VolumeOptions != nil {
|
||||||
|
if o.VolumeSpec.VolumeOptions.Driver != nil {
|
||||||
|
opts.Driver = o.VolumeSpec.VolumeOptions.Driver.Name
|
||||||
|
opts.DriverOpts = o.VolumeSpec.VolumeOptions.Driver.Options
|
||||||
|
}
|
||||||
|
opts.Labels = o.VolumeSpec.VolumeOptions.Labels
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := cli.CreateVolume(ctx, o.MachineID, opts); err != nil {
|
||||||
|
return fmt.Errorf("create volume: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *CreateVolumeOperation) Format(_ NameResolver) string {
|
||||||
|
return fmt.Sprintf("%s: Create volume [name=%s]", o.MachineName, o.VolumeSpec.DockerVolumeName())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *CreateVolumeOperation) String() string {
|
||||||
|
return fmt.Sprintf("CreateVolumeOperation[volume=%s, machine_id=%s]",
|
||||||
|
o.VolumeSpec.DockerVolumeName(), o.MachineID)
|
||||||
|
}
|
||||||
|
|
||||||
// SequenceOperation is a composite operation that executes a sequence of operations in order.
|
// SequenceOperation is a composite operation that executes a sequence of operations in order.
|
||||||
type SequenceOperation struct {
|
type SequenceOperation struct {
|
||||||
Operations []Operation
|
Operations []Operation
|
||||||
|
|||||||
@@ -4,14 +4,15 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/distribution/reference"
|
"github.com/distribution/reference"
|
||||||
"github.com/docker/docker/api/types"
|
"github.com/docker/docker/api/types"
|
||||||
"github.com/opencontainers/go-digest"
|
"github.com/opencontainers/go-digest"
|
||||||
"github.com/psviderski/uncloud/internal/secret"
|
"github.com/psviderski/uncloud/internal/secret"
|
||||||
"github.com/psviderski/uncloud/pkg/api"
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
"google.golang.org/grpc/codes"
|
"google.golang.org/grpc/codes"
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// ServiceSpecResolver transforms user-provided service specs into deployment-ready form.
|
// ServiceSpecResolver transforms user-provided service specs into deployment-ready form.
|
||||||
@@ -66,25 +67,9 @@ func (r *ServiceSpecResolver) resolveServiceName(spec *api.ServiceSpec) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate a random service name from the image when not provided.
|
var err error
|
||||||
img, err := reference.ParseDockerRef(spec.Container.Image)
|
spec.Name, err = GenerateServiceName(spec.Container.Image)
|
||||||
if err != nil {
|
return err
|
||||||
return fmt.Errorf("invalid image: %w", err)
|
|
||||||
}
|
|
||||||
// Get the image name without the repository and tag/digest parts.
|
|
||||||
imageName := reference.FamiliarName(img)
|
|
||||||
// Get the last part of the image name (path), e.g. "nginx" from "bitnami/nginx".
|
|
||||||
if i := strings.LastIndex(imageName, "/"); i != -1 {
|
|
||||||
imageName = imageName[i+1:]
|
|
||||||
}
|
|
||||||
// Append a random suffix to the image name to generate an optimistically unique service name.
|
|
||||||
suffix, err := secret.RandomAlphaNumeric(4)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("generate random suffix: %w", err)
|
|
||||||
}
|
|
||||||
spec.Name = fmt.Sprintf("%s-%s", imageName, suffix)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// expandIngressPorts processes HTTP(S) ingress ports in a service spec by:
|
// expandIngressPorts processes HTTP(S) ingress ports in a service spec by:
|
||||||
@@ -141,11 +126,32 @@ func (r *ServiceSpecResolver) resolveImageDigest(spec *api.ServiceSpec) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GenerateServiceName(image string) (string, error) {
|
||||||
|
img, err := reference.ParseDockerRef(image)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("invalid image '%s': %w", image, err)
|
||||||
|
}
|
||||||
|
// Get the image name without the repository and tag/digest parts.
|
||||||
|
imageName := reference.FamiliarName(img)
|
||||||
|
// Get the last part of the image name (path), e.g. "nginx" from "bitnami/nginx".
|
||||||
|
if i := strings.LastIndex(imageName, "/"); i != -1 {
|
||||||
|
imageName = imageName[i+1:]
|
||||||
|
}
|
||||||
|
// Append a random suffix to the image name to generate an optimistically unique service name.
|
||||||
|
suffix, err := secret.RandomAlphaNumeric(4)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("generate random suffix: %w", err)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s-%s", imageName, suffix), nil
|
||||||
|
}
|
||||||
|
|
||||||
type ImageResolverClient interface {
|
type ImageResolverClient interface {
|
||||||
api.ImageClient
|
api.ImageClient
|
||||||
api.MachineClient
|
api.MachineClient
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO(lhf): as of April 2025, ImageDigestResolver is not used in the codebase and considered more harmful
|
||||||
|
// than helpful. It's safe to remove it.
|
||||||
type ImageDigestResolver struct {
|
type ImageDigestResolver struct {
|
||||||
Ctx context.Context
|
Ctx context.Context
|
||||||
Client ImageResolverClient
|
Client ImageResolverClient
|
||||||
@@ -187,7 +193,7 @@ func (r *ImageDigestResolver) Resolve(image, policy string) (string, error) {
|
|||||||
// resolveAlways resolves the image to the image with the digest by querying the registry from all machines.
|
// resolveAlways resolves the image to the image with the digest by querying the registry from all machines.
|
||||||
func (r *ImageDigestResolver) resolveAlways(image string) (string, error) {
|
func (r *ImageDigestResolver) resolveAlways(image string) (string, error) {
|
||||||
// TODO: broadcast to a subset of machines in large clusters to avoid being rate-limited by the registry.
|
// TODO: broadcast to a subset of machines in large clusters to avoid being rate-limited by the registry.
|
||||||
ctx, err := api.ProxyMachinesContext(r.Ctx, r.Client, nil)
|
ctx, _, err := api.ProxyMachinesContext(r.Ctx, r.Client, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("create request context to broadcast to all machines: %w", err)
|
return "", fmt.Errorf("create request context to broadcast to all machines: %w", err)
|
||||||
}
|
}
|
||||||
@@ -215,7 +221,7 @@ func (r *ImageDigestResolver) resolveAlways(image string) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *ImageDigestResolver) resolveMissing(image string) (string, error) {
|
func (r *ImageDigestResolver) resolveMissing(image string) (string, error) {
|
||||||
ctx, err := api.ProxyMachinesContext(r.Ctx, r.Client, nil)
|
ctx, _, err := api.ProxyMachinesContext(r.Ctx, r.Client, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("create request context to broadcast to all machines: %w", err)
|
return "", fmt.Errorf("create request context to broadcast to all machines: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,140 @@
|
|||||||
|
package scheduler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/docker/docker/api/types/mount"
|
||||||
|
"github.com/docker/docker/api/types/volume"
|
||||||
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Constraint is the base interface for all scheduling constraints.
|
||||||
|
type Constraint interface {
|
||||||
|
// Evaluate determines if a machine satisfies the constraint.
|
||||||
|
Evaluate(machine *Machine) bool
|
||||||
|
|
||||||
|
// Description returns a human-readable description of the constraint.
|
||||||
|
Description() string
|
||||||
|
}
|
||||||
|
|
||||||
|
// constraintsFromSpec derives scheduling constraints from the service specification.
|
||||||
|
func constraintsFromSpec(spec api.ServiceSpec) []Constraint {
|
||||||
|
var constraints []Constraint
|
||||||
|
|
||||||
|
// TODO: add placement constraint based on the supported platforms of the image.
|
||||||
|
// TODO: add placement constraint to limit machines with the image if pull policy is never.
|
||||||
|
|
||||||
|
if len(spec.Placement.Machines) > 0 {
|
||||||
|
constraints = append(constraints, &PlacementConstraint{
|
||||||
|
Machines: spec.Placement.Machines,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a VolumesConstraint for named Docker volumes that are mounted in the container.
|
||||||
|
var volumes []api.VolumeSpec
|
||||||
|
for _, m := range spec.Container.VolumeMounts {
|
||||||
|
if v, ok := spec.Volume(m.VolumeName); ok && v.Type == api.VolumeTypeVolume {
|
||||||
|
volumes = append(volumes, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(volumes) > 0 {
|
||||||
|
constraints = append(constraints, &VolumesConstraint{
|
||||||
|
Volumes: volumes,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return constraints
|
||||||
|
}
|
||||||
|
|
||||||
|
type PlacementConstraint struct {
|
||||||
|
// Machines is a list of machine names or IDs where service containers are allowed to be deployed.
|
||||||
|
// If empty, containers can be deployed to any available machine in the cluster.
|
||||||
|
Machines []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *PlacementConstraint) Evaluate(machine *Machine) bool {
|
||||||
|
for _, nameOrID := range c.Machines {
|
||||||
|
if machine.Info.Id == nameOrID || machine.Info.Name == nameOrID {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *PlacementConstraint) Description() string {
|
||||||
|
slices.Sort(c.Machines)
|
||||||
|
return "Placement constraint by machines: " + strings.Join(c.Machines, ", ")
|
||||||
|
}
|
||||||
|
|
||||||
|
// VolumesConstraint restricts container placement to machines that have the required named Docker volumes.
|
||||||
|
type VolumesConstraint struct {
|
||||||
|
// Volumes is a list of named Docker volumes of type api.VolumeTypeVolume that must exist on the machine.
|
||||||
|
Volumes []api.VolumeSpec
|
||||||
|
}
|
||||||
|
|
||||||
|
// Evaluate determines if a machine has all the required volumes.
|
||||||
|
// Returns true if all required volumes exist or scheduled on the machine or if there are no required volumes.
|
||||||
|
func (c *VolumesConstraint) Evaluate(machine *Machine) bool {
|
||||||
|
if len(c.Volumes) == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, v := range c.Volumes {
|
||||||
|
if v.Type != api.VolumeTypeVolume {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the required volume already exists on the machine.
|
||||||
|
if slices.ContainsFunc(machine.Volumes, func(vol volume.Volume) bool {
|
||||||
|
if v.DockerVolumeName() == vol.Name {
|
||||||
|
return v.MatchesDockerVolume(vol)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the required volume has been scheduled on the machine. The driver names and options must match.
|
||||||
|
if !slices.ContainsFunc(machine.ScheduledVolumes, func(scheduled api.VolumeSpec) bool {
|
||||||
|
if v.DockerVolumeName() != scheduled.DockerVolumeName() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// The volume spec with an empty driver can mount the volume that matches the name no matter the driver.
|
||||||
|
if v.VolumeOptions.Driver == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the driver is specified in the spec, the spec's driver and options must match the volume's driver
|
||||||
|
// and options to successfully mount the volume.
|
||||||
|
scheduled = scheduled.SetDefaults()
|
||||||
|
scheduledDriver := scheduled.VolumeOptions.Driver
|
||||||
|
if scheduledDriver == nil {
|
||||||
|
scheduledDriver = &mount.Driver{Name: api.VolumeDriverLocal}
|
||||||
|
}
|
||||||
|
return reflect.DeepEqual(v.VolumeOptions.Driver, scheduledDriver)
|
||||||
|
}) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *VolumesConstraint) Description() string {
|
||||||
|
volumeNames := make([]string, 0, len(c.Volumes))
|
||||||
|
for _, v := range c.Volumes {
|
||||||
|
if v.Type == api.VolumeTypeVolume {
|
||||||
|
volumeNames = append(volumeNames, v.DockerVolumeName())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
slices.Sort(volumeNames)
|
||||||
|
|
||||||
|
if len(volumeNames) == 0 {
|
||||||
|
return "No volumes constraint"
|
||||||
|
}
|
||||||
|
|
||||||
|
return "Volumes: " + strings.Join(volumeNames, ", ")
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
package scheduler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/psviderski/uncloud/internal/machine/api/pb"
|
||||||
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ServiceScheduler struct {
|
||||||
|
state *ClusterState
|
||||||
|
spec api.ServiceSpec
|
||||||
|
constraints []Constraint
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewServiceScheduler creates a new ServiceScheduler with the given cluster state and service specification.
|
||||||
|
func NewServiceScheduler(state *ClusterState, spec api.ServiceSpec) *ServiceScheduler {
|
||||||
|
constraints := constraintsFromSpec(spec)
|
||||||
|
|
||||||
|
return &ServiceScheduler{
|
||||||
|
state: state,
|
||||||
|
spec: spec,
|
||||||
|
constraints: constraints,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// EligibleMachines returns a list of machines that satisfy all constraints for the next scheduled container.
|
||||||
|
func (s *ServiceScheduler) EligibleMachines() ([]*Machine, error) {
|
||||||
|
var available []*Machine
|
||||||
|
for _, machine := range s.state.Machines {
|
||||||
|
if s.evaluateConstraints(machine) {
|
||||||
|
available = append(available, machine)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(available) == 0 {
|
||||||
|
return nil, errors.New("no machines available that satisfy all constraints")
|
||||||
|
}
|
||||||
|
return available, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ServiceScheduler) evaluateConstraints(machine *Machine) bool {
|
||||||
|
for _, c := range s.constraints {
|
||||||
|
if !c.Evaluate(machine) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ServiceScheduler) ScheduleContainer() ([]*pb.MachineInfo, error) {
|
||||||
|
// TODO: organise machines in a heap and supply a sort function from the strategy. Each scheduled container
|
||||||
|
// should update the machine and reorder it in the heap.
|
||||||
|
return nil, errors.New("not implemented")
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
package scheduler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/docker/docker/api/types/volume"
|
||||||
|
"github.com/psviderski/uncloud/internal/machine/api/pb"
|
||||||
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ClusterState represents the current and planned state of machines and their resources in the cluster.
|
||||||
|
type ClusterState struct {
|
||||||
|
Machines []*Machine
|
||||||
|
}
|
||||||
|
|
||||||
|
type Machine struct {
|
||||||
|
Info *pb.MachineInfo
|
||||||
|
Volumes []volume.Volume
|
||||||
|
ScheduledVolumes []api.VolumeSpec
|
||||||
|
}
|
||||||
|
|
||||||
|
type Client interface {
|
||||||
|
api.MachineClient
|
||||||
|
api.VolumeClient
|
||||||
|
}
|
||||||
|
|
||||||
|
// InspectClusterState creates a new cluster state by inspecting the machines using the cluster client.
|
||||||
|
func InspectClusterState(ctx context.Context, cli Client) (*ClusterState, error) {
|
||||||
|
// TODO: refactor to get all the details in one broadcast call to machine API,
|
||||||
|
// e.g. InspectMachine with include options.
|
||||||
|
machineMembers, err := cli.ListMachines(ctx, &api.MachineFilter{Available: true})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("list machines: %w", err)
|
||||||
|
}
|
||||||
|
volumes, err := cli.ListVolumes(ctx, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("list volumes: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var machines []*Machine
|
||||||
|
for _, m := range machineMembers {
|
||||||
|
machine := &Machine{
|
||||||
|
Info: m.Machine,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, v := range volumes {
|
||||||
|
if v.MachineID == m.Machine.Id {
|
||||||
|
machine.Volumes = append(machine.Volumes, v.Volume)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
machines = append(machines, machine)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &ClusterState{
|
||||||
|
Machines: machines,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Machine returns the machine with the given name or ID from the cluster state.
|
||||||
|
func (s *ClusterState) Machine(nameOrID string) (*Machine, bool) {
|
||||||
|
for _, m := range s.Machines {
|
||||||
|
if m.Info.Id == nameOrID || m.Info.Name == nameOrID {
|
||||||
|
return m, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
@@ -0,0 +1,296 @@
|
|||||||
|
package scheduler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
mapset "github.com/deckarep/golang-set/v2"
|
||||||
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
// VolumeScheduler determines what missing volumes should be created and where for a multi-service deployment.
|
||||||
|
// It satisfies the following constraints:
|
||||||
|
// - Services that share a volume must be placed on the same machine where the volume is located.
|
||||||
|
// If the volume is located on multiple machines, services can be placed on any of them.
|
||||||
|
// - Services must respect their individual placement constraints.
|
||||||
|
// - If a volume already exists on a machine, it must be used instead of creating a new one.
|
||||||
|
// - A missing volume must only be created on one machine.
|
||||||
|
type VolumeScheduler struct {
|
||||||
|
// state is the current state of machines and their resources in the cluster.
|
||||||
|
state *ClusterState
|
||||||
|
// serviceSpecs is a list of service specifications included in the deployment.
|
||||||
|
serviceSpecs []api.ServiceSpec
|
||||||
|
// volumeSpecs is a map of volume names to their specifications from the service specs in a canonical form.
|
||||||
|
volumeSpecs map[string]api.VolumeSpec
|
||||||
|
// volumeServices is a map of volume names to the list of service names that use the volume.
|
||||||
|
// TODO: not all service spec may contain the service name, so use a slice of int indexes instead of names.
|
||||||
|
volumeServices map[string][]string
|
||||||
|
// existingVolumeMachines is a map of volume names to the set of machine IDs where those volumes are located.
|
||||||
|
// Contains only volumes that are used by at least one service in serviceSpecs.
|
||||||
|
existingVolumeMachines map[string]mapset.Set[string]
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewVolumeScheduler creates a new VolumeScheduler with the given cluster state and service specifications.
|
||||||
|
func NewVolumeScheduler(state *ClusterState, specs []api.ServiceSpec) (*VolumeScheduler, error) {
|
||||||
|
var specsWithVolumes []api.ServiceSpec
|
||||||
|
// Docker volume name -> VolumeSpec.
|
||||||
|
volumeSpecs := make(map[string]api.VolumeSpec)
|
||||||
|
// Volume name -> list of service names that use the volume.
|
||||||
|
volumeServices := make(map[string][]string)
|
||||||
|
// Volume name -> set of machine IDs where the volume is located.
|
||||||
|
existingVolumeMachines := make(map[string]mapset.Set[string])
|
||||||
|
|
||||||
|
// Validate all service names are unique to avoid scheduling conflicts.
|
||||||
|
serviceNames := make(map[string]struct{}, len(specs))
|
||||||
|
for _, spec := range specs {
|
||||||
|
if err := spec.Validate(); err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid service spec: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, exists := serviceNames[spec.Name]; exists {
|
||||||
|
return nil, fmt.Errorf("duplicate service name: '%s'", spec.Name)
|
||||||
|
}
|
||||||
|
serviceNames[spec.Name] = struct{}{}
|
||||||
|
|
||||||
|
mountedVolumes := spec.MountedDockerVolumes()
|
||||||
|
if len(mountedVolumes) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
specsWithVolumes = append(specsWithVolumes, spec)
|
||||||
|
|
||||||
|
for _, v := range mountedVolumes {
|
||||||
|
v = v.SetDefaults()
|
||||||
|
// Reset any aliases in a service spec to the actual Docker volume name.
|
||||||
|
v.Name = v.DockerVolumeName()
|
||||||
|
|
||||||
|
if seenVolume, ok := volumeSpecs[v.Name]; ok {
|
||||||
|
if !seenVolume.Equals(v) {
|
||||||
|
return nil, fmt.Errorf("volume '%s' is defined multiple times with different options", v.Name)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
volumeSpecs[v.Name] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
volumeServices[v.Name] = append(volumeServices[v.Name], spec.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate the configurations of existing volumes on machines don't conflict with the volume specs, for example,
|
||||||
|
// a volume and a spec with the same name don't have different drivers.
|
||||||
|
for _, machine := range state.Machines {
|
||||||
|
for _, vol := range machine.Volumes {
|
||||||
|
if spec, ok := volumeSpecs[vol.Name]; ok {
|
||||||
|
if !spec.MatchesDockerVolume(vol) {
|
||||||
|
return nil, fmt.Errorf("volume '%s' specification does not match the existing volume "+
|
||||||
|
"on machine '%s'", vol.Name, machine.Info.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, setInitialised := existingVolumeMachines[vol.Name]; !setInitialised {
|
||||||
|
existingVolumeMachines[vol.Name] = mapset.NewSet[string]()
|
||||||
|
}
|
||||||
|
existingVolumeMachines[vol.Name].Add(machine.Info.Id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &VolumeScheduler{
|
||||||
|
state: state,
|
||||||
|
serviceSpecs: specsWithVolumes,
|
||||||
|
volumeSpecs: volumeSpecs,
|
||||||
|
volumeServices: volumeServices,
|
||||||
|
existingVolumeMachines: existingVolumeMachines,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Schedule determines what missing volumes should be created and where for services in the multi-service deployment.
|
||||||
|
// It returns a map of machine IDs to a list of api.VolumeSpec that should be created on that machine,
|
||||||
|
// or an error if services can't be scheduled due to scheduling constraints.
|
||||||
|
func (s *VolumeScheduler) Schedule() (map[string][]api.VolumeSpec, error) {
|
||||||
|
if len(s.serviceSpecs) == 0 {
|
||||||
|
// No services with volume mounts, nothing to schedule.
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Service name -> set of machine IDs where the service can be scheduled.
|
||||||
|
serviceEligibleMachines := make(map[string]mapset.Set[string])
|
||||||
|
// Volume name -> list of service names that use the volume.
|
||||||
|
// Get eligible machines for each service without considering its volume mounts.
|
||||||
|
for _, spec := range s.serviceSpecs {
|
||||||
|
machineIDs, err := s.serviceEligibleMachinesWithoutVolumes(spec)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
serviceEligibleMachines[spec.Name] = machineIDs
|
||||||
|
}
|
||||||
|
|
||||||
|
// For each volume that exists on any machine(s) (which shouldn't be created), intersect each service's
|
||||||
|
// eligible machines that use the volume with the machines the volume is located on.
|
||||||
|
//
|
||||||
|
// Service name -> list of processed volume names (quoted) to format the error message.
|
||||||
|
quotedServiceVolumes := make(map[string][]string)
|
||||||
|
for volumeName, volumeMachines := range s.existingVolumeMachines {
|
||||||
|
for _, serviceName := range s.volumeServices[volumeName] {
|
||||||
|
quotedServiceVolumes[serviceName] = append(quotedServiceVolumes[serviceName],
|
||||||
|
fmt.Sprintf("'%s'", volumeName))
|
||||||
|
newEligibleMachines := serviceEligibleMachines[serviceName].Intersect(volumeMachines)
|
||||||
|
if newEligibleMachines.Cardinality() == 0 {
|
||||||
|
volumes := strings.Join(quotedServiceVolumes[serviceName], ", ")
|
||||||
|
return nil, fmt.Errorf("unable to find a machine that satisfies service '%s' "+
|
||||||
|
"placement constraints and has all required volumes: %s", serviceName, volumes)
|
||||||
|
}
|
||||||
|
serviceEligibleMachines[serviceName] = newEligibleMachines
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip constraints propagation for volumes that already exist on machines as the propagation only works
|
||||||
|
// for missing volumes.
|
||||||
|
placedVolumes := make(map[string]struct{})
|
||||||
|
for volumeName := range s.existingVolumeMachines {
|
||||||
|
placedVolumes[volumeName] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.propagateConstraintsUntilConvergence(serviceEligibleMachines, placedVolumes); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Schedule each missing volume on one of its eligible machines.
|
||||||
|
scheduledVolumes := make(map[string][]api.VolumeSpec)
|
||||||
|
for missingVolumeName, missingVolumeSpec := range s.volumeSpecs {
|
||||||
|
// Skip volumes that already exist on machines.
|
||||||
|
if _, ok := s.existingVolumeMachines[missingVolumeName]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
serviceNames := s.volumeServices[missingVolumeName]
|
||||||
|
if len(serviceNames) == 0 {
|
||||||
|
return nil, fmt.Errorf("bug detected: no services using volume '%s'", missingVolumeName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the current eligible machines (any service using the volume will have the same set after convergence).
|
||||||
|
eligibleMachines := serviceEligibleMachines[serviceNames[0]]
|
||||||
|
if eligibleMachines.Cardinality() == 0 {
|
||||||
|
return nil, fmt.Errorf("bug detected: no eligible machines for volume '%s'", missingVolumeName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Choose the first machine in the sorted eligible machines to schedule the volume on.
|
||||||
|
// Sort the eligible machines to ensure deterministic behavior.
|
||||||
|
sortedEligibleMachines := eligibleMachines.ToSlice()
|
||||||
|
slices.Sort(sortedEligibleMachines)
|
||||||
|
machineID := sortedEligibleMachines[0]
|
||||||
|
// Update constraints for all services that use this volume to be placed on the selected machine.
|
||||||
|
for _, serviceName := range serviceNames {
|
||||||
|
serviceEligibleMachines[serviceName] = mapset.NewSet(machineID)
|
||||||
|
}
|
||||||
|
placedVolumes[missingVolumeName] = struct{}{}
|
||||||
|
scheduledVolumes[machineID] = append(scheduledVolumes[machineID], missingVolumeSpec)
|
||||||
|
|
||||||
|
// Propagate the updated constraints.
|
||||||
|
if err := s.propagateConstraintsUntilConvergence(serviceEligibleMachines, placedVolumes); err != nil {
|
||||||
|
return nil, fmt.Errorf("unexpected error while propagating constraints after "+
|
||||||
|
"scheduling volume '%s' on machine '%s': %w", missingVolumeName, machineID, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the state of the machines with the scheduled volumes.
|
||||||
|
for machineID, volumes := range scheduledVolumes {
|
||||||
|
for _, m := range s.state.Machines {
|
||||||
|
if m.Info.Id == machineID {
|
||||||
|
m.ScheduledVolumes = append(m.ScheduledVolumes, volumes...)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return scheduledVolumes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// serviceEligibleMachinesWithoutVolumes returns a set of machine IDs where the service can be scheduled
|
||||||
|
// without considering its volume mounts.
|
||||||
|
func (s *VolumeScheduler) serviceEligibleMachinesWithoutVolumes(spec api.ServiceSpec) (mapset.Set[string], error) {
|
||||||
|
specWithoutVolumes := spec.Clone()
|
||||||
|
specWithoutVolumes.Container.VolumeMounts = nil
|
||||||
|
|
||||||
|
scheduler := NewServiceScheduler(s.state, specWithoutVolumes)
|
||||||
|
machines, err := scheduler.EligibleMachines()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("schedule service '%s': %w", spec.Name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
machineIDs := mapset.NewSetWithSize[string](len(machines))
|
||||||
|
for _, m := range machines {
|
||||||
|
machineIDs.Add(m.Info.Id)
|
||||||
|
}
|
||||||
|
|
||||||
|
return machineIDs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// propagateConstraintsUntilConvergence iteratively narrows down eligible machines for services by propagating
|
||||||
|
// constraints through shared volumes until convergence. It only processes volumes that need to be created (not
|
||||||
|
// existing volumes) and ensures services sharing a volume converge to the same set of eligible machines.
|
||||||
|
// If skipVolumes is provided, those volumes are excluded from constraint propagation.
|
||||||
|
// Returns an error if any services have no eligible machines after constraint propagation.
|
||||||
|
func (s *VolumeScheduler) propagateConstraintsUntilConvergence(
|
||||||
|
serviceEligibleMachines map[string]mapset.Set[string],
|
||||||
|
skipVolumes map[string]struct{},
|
||||||
|
) error {
|
||||||
|
changed := true
|
||||||
|
// Loop until no more changes occur.
|
||||||
|
for changed {
|
||||||
|
changed = false
|
||||||
|
|
||||||
|
// For each volume, find the intersection of eligible machines for all services using that volume and update
|
||||||
|
// their eligible machines with the intersection. This will narrow down the machines where the volume can
|
||||||
|
// be created.
|
||||||
|
for volumeName, serviceNames := range s.volumeServices {
|
||||||
|
if skipVolumes != nil {
|
||||||
|
if _, ok := skipVolumes[volumeName]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Skip if there are no services using this volume (shouldn't happen).
|
||||||
|
if len(serviceNames) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the intersection of eligible machines for all services using this volume.
|
||||||
|
var eligibleMachinesForVolume mapset.Set[string]
|
||||||
|
first := true
|
||||||
|
for _, serviceName := range serviceNames {
|
||||||
|
if first {
|
||||||
|
// First service: initialise the intersection.
|
||||||
|
eligibleMachinesForVolume = serviceEligibleMachines[serviceName].Clone()
|
||||||
|
first = false
|
||||||
|
} else {
|
||||||
|
eligibleMachinesForVolume = serviceEligibleMachines[serviceName].Intersect(
|
||||||
|
eligibleMachinesForVolume)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no machines are eligible for this volume, we have a constraint violation.
|
||||||
|
//goland:noinspection GoDfaNilDereference
|
||||||
|
if eligibleMachinesForVolume.Cardinality() == 0 {
|
||||||
|
var quotedServiceNames []string // Used to format the error message.
|
||||||
|
for _, svcName := range serviceNames {
|
||||||
|
quotedServiceNames = append(quotedServiceNames, fmt.Sprintf("'%s'", svcName))
|
||||||
|
}
|
||||||
|
return fmt.Errorf("unable to find a machine that satisfies placement constraints "+
|
||||||
|
"for services %s that must be placed together to share volume '%s'",
|
||||||
|
strings.Join(quotedServiceNames, ", "), volumeName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update eligible machines for all services using this volume.
|
||||||
|
newCount := eligibleMachinesForVolume.Cardinality()
|
||||||
|
for _, serviceName := range serviceNames {
|
||||||
|
oldCount := serviceEligibleMachines[serviceName].Cardinality()
|
||||||
|
serviceEligibleMachines[serviceName] = eligibleMachinesForVolume
|
||||||
|
|
||||||
|
if oldCount != newCount {
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,837 @@
|
|||||||
|
package scheduler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/docker/docker/api/types/mount"
|
||||||
|
"github.com/docker/docker/api/types/volume"
|
||||||
|
"github.com/psviderski/uncloud/internal/machine/api/pb"
|
||||||
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestVolumeScheduler_Schedule(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
machines []*Machine
|
||||||
|
serviceSpecs []api.ServiceSpec
|
||||||
|
want map[string][]api.VolumeSpec
|
||||||
|
wantErr string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "single service with missing volume",
|
||||||
|
machines: []*Machine{
|
||||||
|
{
|
||||||
|
Info: &pb.MachineInfo{
|
||||||
|
Id: "machine1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
serviceSpecs: []api.ServiceSpec{
|
||||||
|
{
|
||||||
|
Name: "service1",
|
||||||
|
Container: api.ContainerSpec{
|
||||||
|
Image: "portainer/pause:latest",
|
||||||
|
VolumeMounts: []api.VolumeMount{
|
||||||
|
{
|
||||||
|
VolumeName: "vol1",
|
||||||
|
ContainerPath: "/data",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Volumes: []api.VolumeSpec{
|
||||||
|
{
|
||||||
|
Name: "vol1",
|
||||||
|
Type: api.VolumeTypeVolume,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: map[string][]api.VolumeSpec{
|
||||||
|
"machine1": {
|
||||||
|
{
|
||||||
|
Name: "vol1",
|
||||||
|
Type: api.VolumeTypeVolume,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "multiple services sharing a missing volume",
|
||||||
|
machines: []*Machine{
|
||||||
|
{
|
||||||
|
Info: &pb.MachineInfo{
|
||||||
|
Id: "machine1",
|
||||||
|
},
|
||||||
|
Volumes: []volume.Volume{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Info: &pb.MachineInfo{
|
||||||
|
Id: "machine2",
|
||||||
|
},
|
||||||
|
Volumes: []volume.Volume{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
serviceSpecs: []api.ServiceSpec{
|
||||||
|
{
|
||||||
|
Name: "service1",
|
||||||
|
Container: api.ContainerSpec{
|
||||||
|
Image: "portainer/pause:latest",
|
||||||
|
VolumeMounts: []api.VolumeMount{
|
||||||
|
{
|
||||||
|
VolumeName: "vol1",
|
||||||
|
ContainerPath: "/data",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Volumes: []api.VolumeSpec{
|
||||||
|
{
|
||||||
|
Name: "vol1",
|
||||||
|
Type: api.VolumeTypeVolume,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "service2",
|
||||||
|
Container: api.ContainerSpec{
|
||||||
|
Image: "portainer/pause:latest",
|
||||||
|
VolumeMounts: []api.VolumeMount{
|
||||||
|
{
|
||||||
|
VolumeName: "vol1",
|
||||||
|
ContainerPath: "/data",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Volumes: []api.VolumeSpec{
|
||||||
|
{
|
||||||
|
Name: "vol1",
|
||||||
|
Type: api.VolumeTypeVolume,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: map[string][]api.VolumeSpec{
|
||||||
|
"machine1": {
|
||||||
|
{
|
||||||
|
Name: "vol1",
|
||||||
|
Type: api.VolumeTypeVolume,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "service with existing volume",
|
||||||
|
machines: []*Machine{
|
||||||
|
{
|
||||||
|
Info: &pb.MachineInfo{
|
||||||
|
Id: "machine1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Info: &pb.MachineInfo{
|
||||||
|
Id: "machine2",
|
||||||
|
},
|
||||||
|
Volumes: []volume.Volume{
|
||||||
|
{
|
||||||
|
Name: "vol1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
serviceSpecs: []api.ServiceSpec{
|
||||||
|
{
|
||||||
|
Name: "service1",
|
||||||
|
Container: api.ContainerSpec{
|
||||||
|
Image: "portainer/pause:latest",
|
||||||
|
VolumeMounts: []api.VolumeMount{
|
||||||
|
{
|
||||||
|
VolumeName: "vol1",
|
||||||
|
ContainerPath: "/data",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Volumes: []api.VolumeSpec{
|
||||||
|
{
|
||||||
|
Name: "vol1",
|
||||||
|
Type: api.VolumeTypeVolume,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: map[string][]api.VolumeSpec{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "service with placement constraint and missing volume",
|
||||||
|
machines: []*Machine{
|
||||||
|
{
|
||||||
|
Info: &pb.MachineInfo{
|
||||||
|
Id: "machine1",
|
||||||
|
},
|
||||||
|
Volumes: []volume.Volume{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Info: &pb.MachineInfo{
|
||||||
|
Id: "machine2",
|
||||||
|
},
|
||||||
|
Volumes: []volume.Volume{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
serviceSpecs: []api.ServiceSpec{
|
||||||
|
{
|
||||||
|
Name: "service1",
|
||||||
|
Placement: api.Placement{
|
||||||
|
Machines: []string{"machine2"},
|
||||||
|
},
|
||||||
|
Container: api.ContainerSpec{
|
||||||
|
Image: "portainer/pause:latest",
|
||||||
|
VolumeMounts: []api.VolumeMount{
|
||||||
|
{
|
||||||
|
VolumeName: "vol1",
|
||||||
|
ContainerPath: "/data",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Volumes: []api.VolumeSpec{
|
||||||
|
{
|
||||||
|
Name: "vol1",
|
||||||
|
Type: api.VolumeTypeVolume,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: map[string][]api.VolumeSpec{
|
||||||
|
"machine2": {
|
||||||
|
{
|
||||||
|
Name: "vol1",
|
||||||
|
Type: api.VolumeTypeVolume,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "services with conflicting placement constraints",
|
||||||
|
machines: []*Machine{
|
||||||
|
{
|
||||||
|
Info: &pb.MachineInfo{
|
||||||
|
Id: "machine1",
|
||||||
|
},
|
||||||
|
Volumes: []volume.Volume{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Info: &pb.MachineInfo{
|
||||||
|
Id: "machine2",
|
||||||
|
},
|
||||||
|
Volumes: []volume.Volume{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
serviceSpecs: []api.ServiceSpec{
|
||||||
|
{
|
||||||
|
Name: "service1",
|
||||||
|
Placement: api.Placement{
|
||||||
|
Machines: []string{"machine1"},
|
||||||
|
},
|
||||||
|
Container: api.ContainerSpec{
|
||||||
|
Image: "portainer/pause:latest",
|
||||||
|
VolumeMounts: []api.VolumeMount{
|
||||||
|
{
|
||||||
|
VolumeName: "vol1",
|
||||||
|
ContainerPath: "/data",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Volumes: []api.VolumeSpec{
|
||||||
|
{
|
||||||
|
Name: "vol1",
|
||||||
|
Type: api.VolumeTypeVolume,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "service2",
|
||||||
|
Placement: api.Placement{
|
||||||
|
Machines: []string{"machine2"},
|
||||||
|
},
|
||||||
|
Container: api.ContainerSpec{
|
||||||
|
Image: "portainer/pause:latest",
|
||||||
|
VolumeMounts: []api.VolumeMount{
|
||||||
|
{
|
||||||
|
VolumeName: "vol1",
|
||||||
|
ContainerPath: "/data",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Volumes: []api.VolumeSpec{
|
||||||
|
{
|
||||||
|
Name: "vol1",
|
||||||
|
Type: api.VolumeTypeVolume,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: "unable to find a machine that satisfies placement constraints for services " +
|
||||||
|
"'service1', 'service2' that must be placed together to share volume 'vol1'",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "service with existing volume on wrong machine",
|
||||||
|
machines: []*Machine{
|
||||||
|
{
|
||||||
|
Info: &pb.MachineInfo{
|
||||||
|
Id: "machine1",
|
||||||
|
},
|
||||||
|
Volumes: []volume.Volume{
|
||||||
|
{
|
||||||
|
Name: "vol1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Info: &pb.MachineInfo{
|
||||||
|
Id: "machine2",
|
||||||
|
},
|
||||||
|
Volumes: []volume.Volume{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
serviceSpecs: []api.ServiceSpec{
|
||||||
|
{
|
||||||
|
Name: "service1",
|
||||||
|
Placement: api.Placement{
|
||||||
|
Machines: []string{"machine2"},
|
||||||
|
},
|
||||||
|
Container: api.ContainerSpec{
|
||||||
|
Image: "portainer/pause:latest",
|
||||||
|
VolumeMounts: []api.VolumeMount{
|
||||||
|
{
|
||||||
|
VolumeName: "vol1",
|
||||||
|
ContainerPath: "/data",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Volumes: []api.VolumeSpec{
|
||||||
|
{
|
||||||
|
Name: "vol1",
|
||||||
|
Type: api.VolumeTypeVolume,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: "unable to find a machine that satisfies service 'service1' placement constraints " +
|
||||||
|
"and has all required volumes: 'vol1'",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "multiple services with multiple volumes, some shared",
|
||||||
|
machines: []*Machine{
|
||||||
|
{
|
||||||
|
Info: &pb.MachineInfo{
|
||||||
|
Id: "machine1",
|
||||||
|
},
|
||||||
|
Volumes: []volume.Volume{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Info: &pb.MachineInfo{
|
||||||
|
Id: "machine2",
|
||||||
|
},
|
||||||
|
Volumes: []volume.Volume{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
serviceSpecs: []api.ServiceSpec{
|
||||||
|
{
|
||||||
|
Name: "service1",
|
||||||
|
Container: api.ContainerSpec{
|
||||||
|
Image: "portainer/pause:latest",
|
||||||
|
VolumeMounts: []api.VolumeMount{
|
||||||
|
{
|
||||||
|
VolumeName: "vol1",
|
||||||
|
ContainerPath: "/data1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
VolumeName: "vol2",
|
||||||
|
ContainerPath: "/data2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Volumes: []api.VolumeSpec{
|
||||||
|
{
|
||||||
|
Name: "vol1",
|
||||||
|
Type: api.VolumeTypeVolume,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "vol2",
|
||||||
|
Type: api.VolumeTypeVolume,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "service2",
|
||||||
|
Container: api.ContainerSpec{
|
||||||
|
Image: "portainer/pause:latest",
|
||||||
|
VolumeMounts: []api.VolumeMount{
|
||||||
|
{
|
||||||
|
VolumeName: "vol2",
|
||||||
|
ContainerPath: "/data2",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
VolumeName: "vol3",
|
||||||
|
ContainerPath: "/data3",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Volumes: []api.VolumeSpec{
|
||||||
|
{
|
||||||
|
Name: "vol2",
|
||||||
|
Type: api.VolumeTypeVolume,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "vol3",
|
||||||
|
Type: api.VolumeTypeVolume,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "service3",
|
||||||
|
Container: api.ContainerSpec{
|
||||||
|
Image: "portainer/pause:latest",
|
||||||
|
VolumeMounts: []api.VolumeMount{
|
||||||
|
{
|
||||||
|
VolumeName: "vol3",
|
||||||
|
ContainerPath: "/data3",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
VolumeName: "vol4",
|
||||||
|
ContainerPath: "/data4",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Volumes: []api.VolumeSpec{
|
||||||
|
{
|
||||||
|
Name: "vol3",
|
||||||
|
Type: api.VolumeTypeVolume,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "vol4",
|
||||||
|
Type: api.VolumeTypeVolume,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: map[string][]api.VolumeSpec{
|
||||||
|
"machine1": {
|
||||||
|
{
|
||||||
|
Name: "vol1",
|
||||||
|
Type: api.VolumeTypeVolume,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "vol2",
|
||||||
|
Type: api.VolumeTypeVolume,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "vol3",
|
||||||
|
Type: api.VolumeTypeVolume,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "vol4",
|
||||||
|
Type: api.VolumeTypeVolume,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "multiple services with multiple volumes, some shared and existing",
|
||||||
|
machines: []*Machine{
|
||||||
|
{
|
||||||
|
Info: &pb.MachineInfo{
|
||||||
|
Id: "machine1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Info: &pb.MachineInfo{
|
||||||
|
Id: "machine2",
|
||||||
|
},
|
||||||
|
Volumes: []volume.Volume{
|
||||||
|
{
|
||||||
|
Name: "vol2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Info: &pb.MachineInfo{
|
||||||
|
Id: "machine3",
|
||||||
|
},
|
||||||
|
Volumes: []volume.Volume{
|
||||||
|
{
|
||||||
|
Name: "vol1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "vol2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
serviceSpecs: []api.ServiceSpec{
|
||||||
|
{
|
||||||
|
Name: "service1",
|
||||||
|
Container: api.ContainerSpec{
|
||||||
|
Image: "portainer/pause:latest",
|
||||||
|
VolumeMounts: []api.VolumeMount{
|
||||||
|
{
|
||||||
|
VolumeName: "vol3",
|
||||||
|
ContainerPath: "/data3",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
VolumeName: "vol4",
|
||||||
|
ContainerPath: "/data4",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Volumes: []api.VolumeSpec{
|
||||||
|
{
|
||||||
|
Name: "vol3",
|
||||||
|
Type: api.VolumeTypeVolume,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "vol4",
|
||||||
|
Type: api.VolumeTypeVolume,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "service2",
|
||||||
|
Container: api.ContainerSpec{
|
||||||
|
Image: "portainer/pause:latest",
|
||||||
|
VolumeMounts: []api.VolumeMount{
|
||||||
|
{
|
||||||
|
VolumeName: "vol1",
|
||||||
|
ContainerPath: "/data1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
VolumeName: "vol2",
|
||||||
|
ContainerPath: "/data2",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
VolumeName: "vol3",
|
||||||
|
ContainerPath: "/data3",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Volumes: []api.VolumeSpec{
|
||||||
|
{
|
||||||
|
Name: "vol1",
|
||||||
|
Type: api.VolumeTypeVolume,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "vol2",
|
||||||
|
Type: api.VolumeTypeVolume,
|
||||||
|
VolumeOptions: &api.VolumeOptions{
|
||||||
|
Driver: &mount.Driver{
|
||||||
|
Name: api.VolumeDriverLocal,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "vol3",
|
||||||
|
Type: api.VolumeTypeVolume,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "service3",
|
||||||
|
Container: api.ContainerSpec{
|
||||||
|
Image: "portainer/pause:latest",
|
||||||
|
VolumeMounts: []api.VolumeMount{
|
||||||
|
{
|
||||||
|
VolumeName: "vol2",
|
||||||
|
ContainerPath: "/data2",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
VolumeName: "vol4-alias",
|
||||||
|
ContainerPath: "/data4",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Volumes: []api.VolumeSpec{
|
||||||
|
{
|
||||||
|
Name: "vol2",
|
||||||
|
Type: api.VolumeTypeVolume,
|
||||||
|
VolumeOptions: &api.VolumeOptions{
|
||||||
|
Driver: &mount.Driver{
|
||||||
|
Name: api.VolumeDriverLocal,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "vol4-alias",
|
||||||
|
Type: api.VolumeTypeVolume,
|
||||||
|
VolumeOptions: &api.VolumeOptions{
|
||||||
|
Name: "vol4",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "service4",
|
||||||
|
Container: api.ContainerSpec{
|
||||||
|
Image: "portainer/pause:latest",
|
||||||
|
VolumeMounts: []api.VolumeMount{
|
||||||
|
{
|
||||||
|
VolumeName: "vol2-alias",
|
||||||
|
ContainerPath: "/data2",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
VolumeName: "vol5-with-driver",
|
||||||
|
ContainerPath: "/data5",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Volumes: []api.VolumeSpec{
|
||||||
|
{
|
||||||
|
Name: "vol2-alias",
|
||||||
|
Type: api.VolumeTypeVolume,
|
||||||
|
VolumeOptions: &api.VolumeOptions{
|
||||||
|
Name: "vol2",
|
||||||
|
Driver: &mount.Driver{
|
||||||
|
Name: api.VolumeDriverLocal,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "vol5-with-driver",
|
||||||
|
Type: api.VolumeTypeVolume,
|
||||||
|
VolumeOptions: &api.VolumeOptions{
|
||||||
|
Name: "vol5",
|
||||||
|
Driver: &mount.Driver{
|
||||||
|
Name: api.VolumeDriverLocal,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: map[string][]api.VolumeSpec{
|
||||||
|
"machine2": {
|
||||||
|
{
|
||||||
|
Name: "vol5",
|
||||||
|
Type: api.VolumeTypeVolume,
|
||||||
|
VolumeOptions: &api.VolumeOptions{
|
||||||
|
Driver: &mount.Driver{
|
||||||
|
Name: api.VolumeDriverLocal,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"machine3": {
|
||||||
|
{
|
||||||
|
Name: "vol3",
|
||||||
|
Type: api.VolumeTypeVolume,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "vol4",
|
||||||
|
Type: api.VolumeTypeVolume,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "multiple services with multiple volumes, some shared, with conflicting placement constraints",
|
||||||
|
machines: []*Machine{
|
||||||
|
{
|
||||||
|
Info: &pb.MachineInfo{
|
||||||
|
Id: "machine1",
|
||||||
|
},
|
||||||
|
Volumes: []volume.Volume{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Info: &pb.MachineInfo{
|
||||||
|
Id: "machine2",
|
||||||
|
},
|
||||||
|
Volumes: []volume.Volume{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
serviceSpecs: []api.ServiceSpec{
|
||||||
|
{
|
||||||
|
Name: "service1",
|
||||||
|
Placement: api.Placement{
|
||||||
|
Machines: []string{"machine1"},
|
||||||
|
},
|
||||||
|
Container: api.ContainerSpec{
|
||||||
|
Image: "portainer/pause:latest",
|
||||||
|
VolumeMounts: []api.VolumeMount{
|
||||||
|
{
|
||||||
|
VolumeName: "vol1",
|
||||||
|
ContainerPath: "/data1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
VolumeName: "vol2",
|
||||||
|
ContainerPath: "/data2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Volumes: []api.VolumeSpec{
|
||||||
|
{
|
||||||
|
Name: "vol1",
|
||||||
|
Type: api.VolumeTypeVolume,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "vol2",
|
||||||
|
Type: api.VolumeTypeVolume,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "service2",
|
||||||
|
Placement: api.Placement{
|
||||||
|
Machines: []string{"machine2"},
|
||||||
|
},
|
||||||
|
Container: api.ContainerSpec{
|
||||||
|
Image: "portainer/pause:latest",
|
||||||
|
VolumeMounts: []api.VolumeMount{
|
||||||
|
{
|
||||||
|
VolumeName: "vol2",
|
||||||
|
ContainerPath: "/data2",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
VolumeName: "vol3",
|
||||||
|
ContainerPath: "/data3",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Volumes: []api.VolumeSpec{
|
||||||
|
{
|
||||||
|
Name: "vol2",
|
||||||
|
Type: api.VolumeTypeVolume,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "vol3",
|
||||||
|
Type: api.VolumeTypeVolume,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: "unable to find a machine that satisfies placement constraints for services " +
|
||||||
|
"'service1', 'service2' that must be placed together to share volume 'vol2'",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "multiple services with multiple volumes, no shared",
|
||||||
|
machines: []*Machine{
|
||||||
|
{
|
||||||
|
Info: &pb.MachineInfo{
|
||||||
|
Id: "machine1",
|
||||||
|
},
|
||||||
|
Volumes: []volume.Volume{
|
||||||
|
{
|
||||||
|
Name: "vol1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Info: &pb.MachineInfo{
|
||||||
|
Id: "machine2",
|
||||||
|
},
|
||||||
|
Volumes: []volume.Volume{
|
||||||
|
{
|
||||||
|
Name: "vol3",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
serviceSpecs: []api.ServiceSpec{
|
||||||
|
{
|
||||||
|
Name: "service1",
|
||||||
|
Container: api.ContainerSpec{
|
||||||
|
Image: "portainer/pause:latest",
|
||||||
|
VolumeMounts: []api.VolumeMount{
|
||||||
|
{
|
||||||
|
VolumeName: "vol1",
|
||||||
|
ContainerPath: "/data1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
VolumeName: "vol2",
|
||||||
|
ContainerPath: "/data2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Volumes: []api.VolumeSpec{
|
||||||
|
{
|
||||||
|
Name: "vol1",
|
||||||
|
Type: api.VolumeTypeVolume,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "vol2",
|
||||||
|
Type: api.VolumeTypeVolume,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "service2",
|
||||||
|
Container: api.ContainerSpec{
|
||||||
|
Image: "portainer/pause:latest",
|
||||||
|
VolumeMounts: []api.VolumeMount{
|
||||||
|
{
|
||||||
|
VolumeName: "vol3",
|
||||||
|
ContainerPath: "/data3",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
VolumeName: "vol4",
|
||||||
|
ContainerPath: "/data4",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Volumes: []api.VolumeSpec{
|
||||||
|
{
|
||||||
|
Name: "vol3",
|
||||||
|
Type: api.VolumeTypeVolume,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "vol4",
|
||||||
|
Type: api.VolumeTypeVolume,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: map[string][]api.VolumeSpec{
|
||||||
|
"machine1": {
|
||||||
|
{
|
||||||
|
Name: "vol2",
|
||||||
|
Type: api.VolumeTypeVolume,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"machine2": {
|
||||||
|
{
|
||||||
|
Name: "vol4",
|
||||||
|
Type: api.VolumeTypeVolume,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
state := &ClusterState{
|
||||||
|
Machines: tt.machines,
|
||||||
|
}
|
||||||
|
scheduler, err := NewVolumeScheduler(state, tt.serviceSpecs)
|
||||||
|
require.NoError(t, err)
|
||||||
|
result, err := scheduler.Schedule()
|
||||||
|
|
||||||
|
if tt.wantErr != "" {
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), tt.wantErr)
|
||||||
|
} else {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Len(t, result, len(tt.want), "Number of machines with volumes to create should match")
|
||||||
|
for machineID, expectedVolumes := range tt.want {
|
||||||
|
// Transform the expected volumes to the canonical form with defaults set.
|
||||||
|
for i := range expectedVolumes {
|
||||||
|
expectedVolumes[i] = expectedVolumes[i].SetDefaults()
|
||||||
|
}
|
||||||
|
|
||||||
|
actualVolumes, ok := result[machineID]
|
||||||
|
assert.True(t, ok, "Machine %s should be in the result", machineID)
|
||||||
|
assert.ElementsMatch(t, expectedVolumes, actualVolumes,
|
||||||
|
"Volumes for machine %s should match", machineID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"github.com/psviderski/uncloud/internal/machine/api/pb"
|
"github.com/psviderski/uncloud/internal/machine/api/pb"
|
||||||
"github.com/psviderski/uncloud/internal/secret"
|
"github.com/psviderski/uncloud/internal/secret"
|
||||||
"github.com/psviderski/uncloud/pkg/api"
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
|
"github.com/psviderski/uncloud/pkg/client/deploy/scheduler"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Strategy defines how a service should be deployed or updated. Different implementations can provide various
|
// Strategy defines how a service should be deployed or updated. Different implementations can provide various
|
||||||
@@ -18,14 +19,13 @@ type Strategy interface {
|
|||||||
Type() string
|
Type() string
|
||||||
// Plan returns the operation to reconcile the service to the desired state.
|
// Plan returns the operation to reconcile the service to the desired state.
|
||||||
// If the service does not exist (new deployment), svc will be nil.
|
// If the service does not exist (new deployment), svc will be nil.
|
||||||
Plan(ctx context.Context, cli api.MachineClient, svc *api.Service, spec api.ServiceSpec) (Plan, error)
|
Plan(ctx context.Context, cli scheduler.Client, svc *api.Service, spec api.ServiceSpec) (Plan, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// RollingStrategy implements a rolling update deployment pattern where containers are updated one at a time
|
// RollingStrategy implements a rolling update deployment pattern where containers are updated one at a time
|
||||||
// to minimize service disruption.
|
// to minimize service disruption.
|
||||||
type RollingStrategy struct {
|
type RollingStrategy struct {
|
||||||
// MachineFilter optionally restricts which machines can be used for deployment.
|
State *scheduler.ClusterState
|
||||||
MachineFilter MachineFilter
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *RollingStrategy) Type() string {
|
func (s *RollingStrategy) Type() string {
|
||||||
@@ -33,14 +33,22 @@ func (s *RollingStrategy) Type() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *RollingStrategy) Plan(
|
func (s *RollingStrategy) Plan(
|
||||||
ctx context.Context, cli api.MachineClient, svc *api.Service, spec api.ServiceSpec,
|
ctx context.Context, cli scheduler.Client, svc *api.Service, spec api.ServiceSpec,
|
||||||
) (Plan, error) {
|
) (Plan, error) {
|
||||||
|
if s.State == nil {
|
||||||
|
state, err := scheduler.InspectClusterState(ctx, cli)
|
||||||
|
if err != nil {
|
||||||
|
return Plan{}, fmt.Errorf("inspect cluster state: %w", err)
|
||||||
|
}
|
||||||
|
s.State = state
|
||||||
|
}
|
||||||
|
|
||||||
// We can assume that the spec is valid at this point because it has been validated by the deployment.
|
// We can assume that the spec is valid at this point because it has been validated by the deployment.
|
||||||
switch spec.Mode {
|
switch spec.Mode {
|
||||||
case api.ServiceModeReplicated:
|
case api.ServiceModeReplicated:
|
||||||
return s.planReplicated(ctx, cli, svc, spec)
|
return s.planReplicated(svc, spec)
|
||||||
case api.ServiceModeGlobal:
|
case api.ServiceModeGlobal:
|
||||||
return s.planGlobal(ctx, cli, svc, spec)
|
return s.planGlobal(svc, spec)
|
||||||
default:
|
default:
|
||||||
return Plan{}, fmt.Errorf("unsupported service mode: '%s'", spec.Mode)
|
return Plan{}, fmt.Errorf("unsupported service mode: '%s'", spec.Mode)
|
||||||
}
|
}
|
||||||
@@ -50,43 +58,27 @@ func (s *RollingStrategy) Plan(
|
|||||||
// For replicated services, we want to maintain a specific number of containers (replicas) across the available machines
|
// For replicated services, we want to maintain a specific number of containers (replicas) across the available machines
|
||||||
// in the cluster.
|
// in the cluster.
|
||||||
// TODO: schedule containers only on machines that contain the image if pull policy is set to 'never'.
|
// TODO: schedule containers only on machines that contain the image if pull policy is set to 'never'.
|
||||||
func (s *RollingStrategy) planReplicated(
|
func (s *RollingStrategy) planReplicated(svc *api.Service, spec api.ServiceSpec) (Plan, error) {
|
||||||
ctx context.Context, cli api.MachineClient, svc *api.Service, spec api.ServiceSpec,
|
|
||||||
) (Plan, error) {
|
|
||||||
plan, err := newEmptyPlan(svc, spec)
|
plan, err := newEmptyPlan(svc, spec)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return plan, err
|
return plan, err
|
||||||
}
|
}
|
||||||
|
|
||||||
machines, err := cli.ListMachines(ctx)
|
sched := scheduler.NewServiceScheduler(s.State, spec)
|
||||||
|
// TODO: return a detailed report on required constraints and which ones are satisfied?
|
||||||
|
availableMachines, err := sched.EligibleMachines()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return plan, fmt.Errorf("list machines: %w", err)
|
return plan, err
|
||||||
}
|
|
||||||
// Filter machines that are not DOWN and match the machine filter if provided.
|
|
||||||
var availableMachines []*pb.MachineInfo
|
|
||||||
var unmatchedMachines []*pb.MachineInfo
|
|
||||||
var downMachines []*pb.MachineInfo
|
|
||||||
for _, m := range machines {
|
|
||||||
if m.State == pb.MachineMember_DOWN {
|
|
||||||
downMachines = append(downMachines, m.Machine)
|
|
||||||
} else {
|
|
||||||
if s.MachineFilter == nil || s.MachineFilter(m.Machine) {
|
|
||||||
availableMachines = append(availableMachines, m.Machine)
|
|
||||||
} else {
|
|
||||||
unmatchedMachines = append(unmatchedMachines, m.Machine)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(availableMachines) == 0 {
|
var matchedMachines []*pb.MachineInfo
|
||||||
if s.MachineFilter != nil {
|
for _, m := range availableMachines {
|
||||||
return plan, ErrNoMatchingMachines
|
matchedMachines = append(matchedMachines, m.Info)
|
||||||
}
|
|
||||||
return plan, fmt.Errorf("no available machines to deploy service")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Randomise the order of machines to avoid always deploying to the same machines first.
|
// Randomise the order of machines to avoid always deploying to the same machines first.
|
||||||
rand.Shuffle(len(availableMachines), func(i, j int) {
|
rand.Shuffle(len(matchedMachines), func(i, j int) {
|
||||||
availableMachines[i], availableMachines[j] = availableMachines[j], availableMachines[i]
|
matchedMachines[i], matchedMachines[j] = matchedMachines[j], matchedMachines[i]
|
||||||
})
|
})
|
||||||
|
|
||||||
// Organise existing containers by machine.
|
// Organise existing containers by machine.
|
||||||
@@ -125,7 +117,7 @@ func (s *RollingStrategy) planReplicated(
|
|||||||
|
|
||||||
// Sort machines such that machines with the most up-to-date containers are first, followed by machines with
|
// Sort machines such that machines with the most up-to-date containers are first, followed by machines with
|
||||||
// existing containers, and finally machines without containers.
|
// existing containers, and finally machines without containers.
|
||||||
slices.SortFunc(availableMachines, func(m1, m2 *pb.MachineInfo) int {
|
slices.SortFunc(matchedMachines, func(m1, m2 *pb.MachineInfo) int {
|
||||||
if upToDateContainersOnMachine[m1.Id] > 0 && upToDateContainersOnMachine[m2.Id] > 0 {
|
if upToDateContainersOnMachine[m1.Id] > 0 && upToDateContainersOnMachine[m2.Id] > 0 {
|
||||||
return upToDateContainersOnMachine[m2.Id] - upToDateContainersOnMachine[m1.Id]
|
return upToDateContainersOnMachine[m2.Id] - upToDateContainersOnMachine[m1.Id]
|
||||||
}
|
}
|
||||||
@@ -142,7 +134,7 @@ func (s *RollingStrategy) planReplicated(
|
|||||||
// Spread the containers across the available machines evenly using a simple round-robin approach, starting with
|
// Spread the containers across the available machines evenly using a simple round-robin approach, starting with
|
||||||
// machines that already have containers and prioritising machines with containers that match the desired spec.
|
// machines that already have containers and prioritising machines with containers that match the desired spec.
|
||||||
for i := 0; i < int(spec.Replicas); i++ {
|
for i := 0; i < int(spec.Replicas); i++ {
|
||||||
m := availableMachines[i%len(availableMachines)]
|
m := matchedMachines[i%len(matchedMachines)]
|
||||||
containers := containersOnMachine[m.Id]
|
containers := containersOnMachine[m.Id]
|
||||||
|
|
||||||
if len(containers) == 0 {
|
if len(containers) == 0 {
|
||||||
@@ -209,9 +201,7 @@ func (s *RollingStrategy) planReplicated(
|
|||||||
// possible. If the new container would have port conflicts with the existing one, the old container is removed first.
|
// possible. If the new container would have port conflicts with the existing one, the old container is removed first.
|
||||||
// It handles multiple containers per machine (though this should not occur in normal operation) and skips machines
|
// It handles multiple containers per machine (though this should not occur in normal operation) and skips machines
|
||||||
// that are down.
|
// that are down.
|
||||||
func (s *RollingStrategy) planGlobal(
|
func (s *RollingStrategy) planGlobal(svc *api.Service, spec api.ServiceSpec) (Plan, error) {
|
||||||
ctx context.Context, cli api.MachineClient, svc *api.Service, spec api.ServiceSpec,
|
|
||||||
) (Plan, error) {
|
|
||||||
plan, err := newEmptyPlan(svc, spec)
|
plan, err := newEmptyPlan(svc, spec)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return plan, err
|
return plan, err
|
||||||
@@ -227,39 +217,32 @@ func (s *RollingStrategy) planGlobal(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
machines, err := cli.ListMachines(ctx)
|
sched := scheduler.NewServiceScheduler(s.State, spec)
|
||||||
|
availableMachines, err := sched.EligibleMachines()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return plan, fmt.Errorf("list machines: %w", err)
|
return plan, err
|
||||||
}
|
|
||||||
// Filter machines if a machine filter is provided.
|
|
||||||
// TODO: not sure this is the right behaviour to ignore other machines that might run service containers.
|
|
||||||
// Maybe there should be another filter to specify which machines to deploy to but keep the rest running.
|
|
||||||
// Could be useful to test a new version on a subset of machines before rolling out to all.
|
|
||||||
if s.MachineFilter != nil {
|
|
||||||
machines = slices.DeleteFunc(machines, func(m *pb.MachineMember) bool {
|
|
||||||
return !s.MachineFilter(m.Machine)
|
|
||||||
})
|
|
||||||
if len(machines) == 0 {
|
|
||||||
return plan, ErrNoMatchingMachines
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: figure out how to return a warning if there are machines down. Embed the machinesDown in the plan?
|
for _, m := range availableMachines {
|
||||||
var machinesDown []*pb.MachineInfo
|
containers := containersOnMachine[m.Info.Id]
|
||||||
for _, m := range machines {
|
ops, err := reconcileGlobalContainer(containers, spec, plan.ServiceID, m.Info.Id)
|
||||||
// Skip machines that are down but collect them to report a warning later.
|
|
||||||
if m.State == pb.MachineMember_DOWN {
|
|
||||||
machinesDown = append(machinesDown, m.Machine)
|
|
||||||
fmt.Printf("WARNING: failed to run a service container on machine '%s' which is Down.\n", m.Machine.Id)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
containers := containersOnMachine[m.Machine.Id]
|
|
||||||
ops, err := reconcileGlobalContainer(containers, spec, plan.ServiceID, m.Machine.Id)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return plan, err
|
return plan, err
|
||||||
}
|
}
|
||||||
plan.Operations = append(plan.Operations, ops...)
|
plan.Operations = append(plan.Operations, ops...)
|
||||||
|
|
||||||
|
delete(containersOnMachine, m.Info.Id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove any remaining containers on machines that don't match the new placement constraints.
|
||||||
|
for _, containers := range containersOnMachine {
|
||||||
|
for _, c := range containers {
|
||||||
|
plan.Operations = append(plan.Operations, &RemoveContainerOperation{
|
||||||
|
ServiceID: plan.ServiceID,
|
||||||
|
ContainerID: c.Container.ID,
|
||||||
|
MachineID: c.MachineID,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return plan, nil
|
return plan, nil
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package client
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
dockerclient "github.com/docker/docker/client"
|
dockerclient "github.com/docker/docker/client"
|
||||||
"github.com/psviderski/uncloud/pkg/api"
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"slices"
|
||||||
|
|
||||||
|
"github.com/psviderski/uncloud/internal/machine/api/pb"
|
||||||
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
|
"google.golang.org/protobuf/types/known/emptypb"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (cli *Client) InspectMachine(ctx context.Context, nameOrID string) (*pb.MachineMember, error) {
|
||||||
|
machines, err := cli.ListMachines(ctx, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, m := range machines {
|
||||||
|
if m.Machine.Id == nameOrID || m.Machine.Name == nameOrID {
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, api.ErrNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListMachines returns a list of all machines registered in the cluster that match the filter.
|
||||||
|
func (cli *Client) ListMachines(ctx context.Context, filter *api.MachineFilter) (api.MachineMembersList, error) {
|
||||||
|
resp, err := cli.ClusterClient.ListMachines(ctx, &emptypb.Empty{})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
machines := resp.Machines
|
||||||
|
|
||||||
|
if filter != nil {
|
||||||
|
var matchedMachines api.MachineMembersList
|
||||||
|
for _, m := range machines {
|
||||||
|
if MachineMatchesFilter(m, filter) {
|
||||||
|
matchedMachines = append(matchedMachines, m)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
machines = matchedMachines
|
||||||
|
}
|
||||||
|
|
||||||
|
return machines, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func MachineMatchesFilter(machine *pb.MachineMember, filter *api.MachineFilter) bool {
|
||||||
|
if filter == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if filter.Available && machine.State == pb.MachineMember_DOWN {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(filter.NamesOrIDs) > 0 {
|
||||||
|
if !slices.ContainsFunc(filter.NamesOrIDs, func(nameOrID string) bool {
|
||||||
|
return machine.Machine.Id == nameOrID || machine.Machine.Name == nameOrID
|
||||||
|
}) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/lipgloss"
|
||||||
|
)
|
||||||
|
|
||||||
|
func PrintWarning(msg string) {
|
||||||
|
style := lipgloss.NewStyle().Foreground(lipgloss.Color("11")) // Bright yellow.
|
||||||
|
styledMsg := style.Render(fmt.Sprintf("WARNING: %s", msg))
|
||||||
|
fmt.Fprintln(os.Stderr, styledMsg)
|
||||||
|
}
|
||||||
@@ -37,7 +37,7 @@ func (r *MapNameResolver) ContainerName(containerID string) string {
|
|||||||
// ServiceOperationNameResolver returns a machine and container name resolver for a service that can be used to format
|
// ServiceOperationNameResolver returns a machine and container name resolver for a service that can be used to format
|
||||||
// deployment operations.
|
// deployment operations.
|
||||||
func (cli *Client) ServiceOperationNameResolver(ctx context.Context, svc api.Service) (*MapNameResolver, error) {
|
func (cli *Client) ServiceOperationNameResolver(ctx context.Context, svc api.Service) (*MapNameResolver, error) {
|
||||||
machines, err := cli.ListMachines(ctx)
|
machines, err := cli.ListMachines(ctx, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("list machines: %w", err)
|
return nil, fmt.Errorf("list machines: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
+48
-27
@@ -8,25 +8,18 @@ import (
|
|||||||
"slices"
|
"slices"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/docker/compose/v2/pkg/progress"
|
|
||||||
"github.com/docker/docker/api/types/container"
|
"github.com/docker/docker/api/types/container"
|
||||||
|
"github.com/docker/docker/api/types/volume"
|
||||||
"github.com/psviderski/uncloud/internal/machine/api/pb"
|
"github.com/psviderski/uncloud/internal/machine/api/pb"
|
||||||
"github.com/psviderski/uncloud/pkg/api"
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
"github.com/psviderski/uncloud/pkg/client/deploy"
|
"github.com/psviderski/uncloud/pkg/client/deploy/scheduler"
|
||||||
"google.golang.org/grpc/codes"
|
"google.golang.org/grpc/codes"
|
||||||
"google.golang.org/grpc/metadata"
|
"google.golang.org/grpc/metadata"
|
||||||
"google.golang.org/grpc/status"
|
"google.golang.org/grpc/status"
|
||||||
)
|
)
|
||||||
|
|
||||||
type RunServiceResponse struct {
|
func (cli *Client) RunService(ctx context.Context, spec api.ServiceSpec) (api.RunServiceResponse, error) {
|
||||||
ID string
|
var resp api.RunServiceResponse
|
||||||
Name string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cli *Client) RunService(
|
|
||||||
ctx context.Context, spec api.ServiceSpec, filter deploy.MachineFilter,
|
|
||||||
) (RunServiceResponse, error) {
|
|
||||||
var resp RunServiceResponse
|
|
||||||
|
|
||||||
if err := spec.Validate(); err != nil {
|
if err := spec.Validate(); err != nil {
|
||||||
return resp, fmt.Errorf("invalid service spec: %w", err)
|
return resp, fmt.Errorf("invalid service spec: %w", err)
|
||||||
@@ -43,23 +36,51 @@ func (cli *Client) RunService(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
deployment := cli.NewDeployment(spec, &deploy.RollingStrategy{MachineFilter: filter})
|
// Create missing named Docker volumes for the service.
|
||||||
plan, err := deployment.Plan(ctx)
|
if len(spec.MountedDockerVolumes()) > 0 {
|
||||||
if err != nil {
|
state, err := scheduler.InspectClusterState(ctx, cli)
|
||||||
return resp, fmt.Errorf("plan deployment: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = progress.RunWithTitle(ctx, func(ctx context.Context) error {
|
|
||||||
_, err = deployment.Run(ctx)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return resp, fmt.Errorf("inspect cluster state: %w", err)
|
||||||
|
}
|
||||||
|
volumeScheduler, err := scheduler.NewVolumeScheduler(state, []api.ServiceSpec{spec})
|
||||||
|
if err != nil {
|
||||||
|
return resp, fmt.Errorf("init volume scheduler: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
resp.ID = plan.ServiceID
|
scheduledVolumes, err := volumeScheduler.Schedule()
|
||||||
resp.Name = plan.ServiceName
|
if err != nil {
|
||||||
|
return resp, fmt.Errorf("schedule volumes: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
// Create the missing volumes on the scheduled machines.
|
||||||
}, cli.progressOut(), fmt.Sprintf("Running service %s (%s mode)", plan.ServiceName, spec.Mode))
|
for machineID, volumes := range scheduledVolumes {
|
||||||
|
for _, v := range volumes {
|
||||||
|
opts := volume.CreateOptions{
|
||||||
|
Name: v.Name,
|
||||||
|
}
|
||||||
|
if v.VolumeOptions != nil {
|
||||||
|
if v.VolumeOptions.Driver != nil {
|
||||||
|
opts.Driver = v.VolumeOptions.Driver.Name
|
||||||
|
opts.DriverOpts = v.VolumeOptions.Driver.Options
|
||||||
|
}
|
||||||
|
opts.Labels = v.VolumeOptions.Labels
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err = cli.CreateVolume(ctx, machineID, opts); err != nil {
|
||||||
|
return resp, fmt.Errorf("create volume '%s': %w", v.Name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
deployment := cli.NewDeployment(spec, nil)
|
||||||
|
plan, err := deployment.Run(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.ID = plan.ServiceID
|
||||||
|
resp.Name = plan.ServiceName
|
||||||
|
|
||||||
return resp, err
|
return resp, err
|
||||||
}
|
}
|
||||||
@@ -69,7 +90,7 @@ func (cli *Client) RunService(
|
|||||||
func (cli *Client) InspectService(ctx context.Context, nameOrID string) (api.Service, error) {
|
func (cli *Client) InspectService(ctx context.Context, nameOrID string) (api.Service, error) {
|
||||||
var svc api.Service
|
var svc api.Service
|
||||||
|
|
||||||
machines, err := cli.ListMachines(ctx)
|
machines, err := cli.ListMachines(ctx, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return svc, fmt.Errorf("list machines: %w", err)
|
return svc, fmt.Errorf("list machines: %w", err)
|
||||||
}
|
}
|
||||||
@@ -204,7 +225,7 @@ func (cli *Client) RemoveService(ctx context.Context, id string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
machines, err := cli.ListMachines(ctx)
|
machines, err := cli.ListMachines(ctx, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("list machines: %w", err)
|
return fmt.Errorf("list machines: %w", err)
|
||||||
}
|
}
|
||||||
@@ -251,7 +272,7 @@ func (cli *Client) RemoveService(ctx context.Context, id string) error {
|
|||||||
|
|
||||||
// ListServices returns a list of all services and their containers.
|
// ListServices returns a list of all services and their containers.
|
||||||
func (cli *Client) ListServices(ctx context.Context) ([]api.Service, error) {
|
func (cli *Client) ListServices(ctx context.Context) ([]api.Service, error) {
|
||||||
machines, err := cli.ListMachines(ctx)
|
machines, err := cli.ListMachines(ctx, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("list machines: %w", err)
|
return nil, fmt.Errorf("list machines: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,134 @@
|
|||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/docker/compose/v2/pkg/progress"
|
||||||
|
"github.com/docker/docker/api/types/volume"
|
||||||
|
dockerclient "github.com/docker/docker/client"
|
||||||
|
"github.com/psviderski/uncloud/internal/machine/api/pb"
|
||||||
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CreateVolume creates a new volume on the specified machine.
|
||||||
|
func (cli *Client) CreateVolume(
|
||||||
|
ctx context.Context, machineNameOrID string, opts volume.CreateOptions,
|
||||||
|
) (api.MachineVolume, error) {
|
||||||
|
var resp api.MachineVolume
|
||||||
|
|
||||||
|
if opts.Name == "" {
|
||||||
|
return resp, fmt.Errorf("volume name is required (anonymous volumes are not supported)")
|
||||||
|
}
|
||||||
|
|
||||||
|
machine, err := cli.InspectMachine(ctx, machineNameOrID)
|
||||||
|
if err != nil {
|
||||||
|
return resp, fmt.Errorf("inspect machine '%s': %w", machineNameOrID, err)
|
||||||
|
}
|
||||||
|
// Proxy Docker gRPC requests to the selected machine.
|
||||||
|
ctx = proxyToMachine(ctx, machine.Machine)
|
||||||
|
|
||||||
|
pw := progress.ContextWriter(ctx)
|
||||||
|
eventID := fmt.Sprintf("Volume %s on %s", opts.Name, machine.Machine.Name)
|
||||||
|
pw.Event(progress.CreatingEvent(eventID))
|
||||||
|
|
||||||
|
vol, err := cli.Docker.CreateVolume(ctx, opts)
|
||||||
|
if err != nil {
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
resp = api.MachineVolume{
|
||||||
|
MachineID: machine.Machine.Id,
|
||||||
|
MachineName: machine.Machine.Name,
|
||||||
|
Volume: vol,
|
||||||
|
}
|
||||||
|
pw.Event(progress.CreatedEvent(eventID))
|
||||||
|
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListVolumes returns a list of all volumes on the cluster machines that match the filter.
|
||||||
|
func (cli *Client) ListVolumes(ctx context.Context, filter *api.VolumeFilter) ([]api.MachineVolume, error) {
|
||||||
|
// Broadcast the volume list request to the specified machines in the filter or all machines if filter is nil.
|
||||||
|
var proxyMachines []string
|
||||||
|
if filter != nil {
|
||||||
|
proxyMachines = filter.Machines
|
||||||
|
}
|
||||||
|
|
||||||
|
listCtx, machines, err := api.ProxyMachinesContext(ctx, cli, proxyMachines)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("create request context to broadcast to all machines: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
machineVolumes, err := cli.Docker.ListVolumes(listCtx, volume.ListOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var volumes []api.MachineVolume
|
||||||
|
// Process responses from all machines.
|
||||||
|
for _, mv := range machineVolumes {
|
||||||
|
if mv.Metadata != nil && mv.Metadata.Error != "" {
|
||||||
|
// TODO: return failed machines in the response.
|
||||||
|
PrintWarning(fmt.Sprintf("failed to list volumes on machine '%s': %s",
|
||||||
|
mv.Metadata.Machine, mv.Metadata.Error))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var m *pb.MachineMember
|
||||||
|
if mv.Metadata == nil {
|
||||||
|
// ListVolumes was proxied to only one machine.
|
||||||
|
m = machines[0]
|
||||||
|
} else {
|
||||||
|
m = machines.FindByManagementIP(mv.Metadata.Machine)
|
||||||
|
if m == nil {
|
||||||
|
return nil, fmt.Errorf("machine not found by management IP: %s", mv.Metadata.Machine)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, vol := range mv.Response.Volumes {
|
||||||
|
volumes = append(volumes, api.MachineVolume{
|
||||||
|
MachineID: m.Machine.Id,
|
||||||
|
MachineName: m.Machine.Name,
|
||||||
|
Volume: *vol,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter volumes based on the provided filter criteria.
|
||||||
|
if filter != nil {
|
||||||
|
var filteredVolumes []api.MachineVolume
|
||||||
|
for _, vol := range volumes {
|
||||||
|
if vol.MatchesFilter(filter) {
|
||||||
|
filteredVolumes = append(filteredVolumes, vol)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
volumes = filteredVolumes
|
||||||
|
}
|
||||||
|
|
||||||
|
return volumes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveVolume removes a volume from the specified machine.
|
||||||
|
func (cli *Client) RemoveVolume(ctx context.Context, machineNameOrID, volumeName string, force bool) error {
|
||||||
|
machine, err := cli.InspectMachine(ctx, machineNameOrID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("inspect machine '%s': %w", machineNameOrID, err)
|
||||||
|
}
|
||||||
|
// Proxy Docker gRPC requests to the selected machine.
|
||||||
|
ctx = proxyToMachine(ctx, machine.Machine)
|
||||||
|
|
||||||
|
pw := progress.ContextWriter(ctx)
|
||||||
|
eventID := fmt.Sprintf("Volume %s on %s", volumeName, machine.Machine.Name)
|
||||||
|
pw.Event(progress.RemovingEvent(eventID))
|
||||||
|
|
||||||
|
if err = cli.Docker.RemoveVolume(ctx, volumeName, force); err != nil {
|
||||||
|
if dockerclient.IsErrNotFound(err) {
|
||||||
|
return api.ErrNotFound
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
pw.Event(progress.RemovedEvent(eventID))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
+63
-5
@@ -1,23 +1,28 @@
|
|||||||
package e2e
|
package e2e
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
mapset "github.com/deckarep/golang-set/v2"
|
mapset "github.com/deckarep/golang-set/v2"
|
||||||
"github.com/docker/docker/api/types/container"
|
"github.com/docker/docker/api/types/container"
|
||||||
|
"github.com/docker/docker/api/types/mount"
|
||||||
"github.com/docker/go-connections/nat"
|
"github.com/docker/go-connections/nat"
|
||||||
machinedocker "github.com/psviderski/uncloud/internal/machine/docker"
|
machinedocker "github.com/psviderski/uncloud/internal/machine/docker"
|
||||||
"github.com/psviderski/uncloud/pkg/api"
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
"github.com/psviderski/uncloud/pkg/client/deploy"
|
"github.com/psviderski/uncloud/pkg/client/deploy"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func assertServiceMatchesSpec(t *testing.T, svc api.Service, spec api.ServiceSpec) {
|
func assertServiceMatchesSpec(t *testing.T, svc api.Service, spec api.ServiceSpec) {
|
||||||
|
spec = spec.SetDefaults()
|
||||||
assert.Equal(t, spec.Name, svc.Name)
|
assert.Equal(t, spec.Name, svc.Name)
|
||||||
|
|
||||||
if svc.Mode == api.ServiceModeReplicated {
|
if svc.Mode == api.ServiceModeReplicated {
|
||||||
assert.Contains(t, []string{"", api.ServiceModeReplicated}, spec.Mode)
|
assert.Equal(t, api.ServiceModeReplicated, spec.Mode)
|
||||||
assert.Len(t, svc.Containers, int(spec.Replicas), "Expected %d replicas", spec.Replicas)
|
assert.Len(t, svc.Containers, int(spec.Replicas), "Expected %d replicas", spec.Replicas)
|
||||||
} else {
|
} else {
|
||||||
assert.Equal(t, spec.Mode, svc.Mode)
|
assert.Equal(t, spec.Mode, svc.Mode)
|
||||||
@@ -29,11 +34,12 @@ func assertServiceMatchesSpec(t *testing.T, svc api.Service, spec api.ServiceSpe
|
|||||||
}
|
}
|
||||||
|
|
||||||
func assertContainerMatchesSpec(t *testing.T, ctr api.ServiceContainer, spec api.ServiceSpec) {
|
func assertContainerMatchesSpec(t *testing.T, ctr api.ServiceContainer, spec api.ServiceSpec) {
|
||||||
|
spec = spec.SetDefaults()
|
||||||
status := deploy.EvalContainerSpecChange(ctr.ServiceSpec, spec)
|
status := deploy.EvalContainerSpecChange(ctr.ServiceSpec, spec)
|
||||||
assert.Equal(t, deploy.ContainerUpToDate, status)
|
assert.Equal(t, deploy.ContainerUpToDate, status)
|
||||||
|
|
||||||
spec = spec.SetDefaults()
|
|
||||||
// Verify labels.
|
// Verify labels.
|
||||||
|
assert.True(t, api.ValidateServiceID(ctr.Config.Labels[api.LabelServiceID]))
|
||||||
assert.Equal(t, spec.Name, ctr.Config.Labels[api.LabelServiceName])
|
assert.Equal(t, spec.Name, ctr.Config.Labels[api.LabelServiceName])
|
||||||
assert.Equal(t, spec.Mode, ctr.Config.Labels[api.LabelServiceMode])
|
assert.Equal(t, spec.Mode, ctr.Config.Labels[api.LabelServiceMode])
|
||||||
assert.Contains(t, ctr.Config.Labels, api.LabelManaged)
|
assert.Contains(t, ctr.Config.Labels, api.LabelManaged)
|
||||||
@@ -47,9 +53,34 @@ func assertContainerMatchesSpec(t *testing.T, ctr api.ServiceContainer, spec api
|
|||||||
assert.EqualValues(t, spec.Container.Entrypoint, ctr.Config.Entrypoint)
|
assert.EqualValues(t, spec.Container.Entrypoint, ctr.Config.Entrypoint)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
expectedEnvs := spec.Container.Env.ToSlice()
|
||||||
|
for _, env := range expectedEnvs {
|
||||||
|
assert.Contains(t, ctr.Config.Env, env)
|
||||||
|
}
|
||||||
|
|
||||||
assert.Equal(t, spec.Container.Image, ctr.Config.Image)
|
assert.Equal(t, spec.Container.Image, ctr.Config.Image)
|
||||||
assert.Equal(t, spec.Container.Init, ctr.HostConfig.Init)
|
assert.Equal(t, spec.Container.Init, ctr.HostConfig.Init)
|
||||||
|
assert.True(t, strings.HasPrefix(ctr.Name, spec.Name+"-"))
|
||||||
|
|
||||||
|
// If LogDriver is not set, any log driver set as default in the Docker daemon config could be used.
|
||||||
|
if spec.Container.LogDriver != nil {
|
||||||
|
assert.Equal(t, spec.Container.LogDriver.Name, ctr.HostConfig.LogConfig.Type)
|
||||||
|
assert.Equal(t, spec.Container.LogDriver.Options, ctr.HostConfig.LogConfig.Config)
|
||||||
|
}
|
||||||
|
assert.Equal(t, spec.Container.Privileged, ctr.HostConfig.Privileged)
|
||||||
|
// Compute resources.
|
||||||
|
assert.Equal(t, spec.Container.Resources.CPU, ctr.HostConfig.Resources.NanoCPUs)
|
||||||
|
assert.Equal(t, spec.Container.Resources.Memory, ctr.HostConfig.Resources.Memory)
|
||||||
|
assert.Equal(t, spec.Container.Resources.MemoryReservation, ctr.HostConfig.Resources.MemoryReservation)
|
||||||
|
|
||||||
|
// If User is not set, the default user in the image is used so we can't easily assert it.
|
||||||
|
if spec.Container.User != "" {
|
||||||
|
assert.Equal(t, spec.Container.User, ctr.Config.User)
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Empty(t, ctr.HostConfig.Binds, "Expected empty binds as all volumes should be mapped to mounts")
|
||||||
assert.ElementsMatch(t, spec.Container.Volumes, ctr.HostConfig.Binds)
|
assert.ElementsMatch(t, spec.Container.Volumes, ctr.HostConfig.Binds)
|
||||||
|
assertContainerMountsMatchSpec(t, ctr.HostConfig.Mounts, spec)
|
||||||
|
|
||||||
// Compare host ports.
|
// Compare host ports.
|
||||||
portBindings := make(nat.PortMap)
|
portBindings := make(nat.PortMap)
|
||||||
@@ -79,8 +110,35 @@ func assertContainerMatchesSpec(t *testing.T, ctr api.ServiceContainer, spec api
|
|||||||
assert.Contains(t, ctr.NetworkSettings.Networks, machinedocker.NetworkName)
|
assert.Contains(t, ctr.NetworkSettings.Networks, machinedocker.NetworkName)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func assertContainerMountsMatchSpec(t *testing.T, mounts []mount.Mount, spec api.ServiceSpec) {
|
||||||
|
expectedMounts, err := machinedocker.ToDockerMounts(spec.Volumes, spec.Container.VolumeMounts)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
sortMounts(mounts)
|
||||||
|
sortMounts(expectedMounts)
|
||||||
|
|
||||||
|
assert.Len(t, mounts, len(expectedMounts), "Expected %d mounts", len(expectedMounts))
|
||||||
|
for i, m := range mounts {
|
||||||
|
// The mount generated from the spec may not define the driver which means to use the default driver
|
||||||
|
// if creating a new volume or mounting an existing one no matter what its driver is.
|
||||||
|
// So skip driver comparison if the driver is not set in the spec.
|
||||||
|
if expectedMounts[i].VolumeOptions != nil && expectedMounts[i].VolumeOptions.DriverConfig == nil &&
|
||||||
|
m.VolumeOptions != nil {
|
||||||
|
expectedMounts[i].VolumeOptions.DriverConfig = m.VolumeOptions.DriverConfig
|
||||||
|
}
|
||||||
|
assert.Equal(t, expectedMounts[i], m,
|
||||||
|
"Expected mount type=%s,src=%s,dst=%s to match spec", m.Type, m.Source, m.Target)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func sortMounts(mounts []mount.Mount) {
|
||||||
|
slices.SortFunc(mounts, func(a, b mount.Mount) int {
|
||||||
|
return strings.Compare(a.Target, b.Target)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// serviceContainersByMachine returns a map of machine ID to service containers on that machine.
|
// serviceContainersByMachine returns a map of machine ID to service containers on that machine.
|
||||||
func serviceContainersByMachine(t *testing.T, svc api.Service) map[string][]api.ServiceContainer {
|
func serviceContainersByMachine(svc api.Service) map[string][]api.ServiceContainer {
|
||||||
containers := make(map[string][]api.ServiceContainer)
|
containers := make(map[string][]api.ServiceContainer)
|
||||||
for _, c := range svc.Containers {
|
for _, c := range svc.Containers {
|
||||||
containers[c.MachineID] = append(containers[c.MachineID], c.Container)
|
containers[c.MachineID] = append(containers[c.MachineID], c.Container)
|
||||||
@@ -88,7 +146,7 @@ func serviceContainersByMachine(t *testing.T, svc api.Service) map[string][]api.
|
|||||||
return containers
|
return containers
|
||||||
}
|
}
|
||||||
|
|
||||||
func serviceMachines(t *testing.T, svc api.Service) mapset.Set[string] {
|
func serviceMachines(svc api.Service) mapset.Set[string] {
|
||||||
machines := mapset.NewSet[string]()
|
machines := mapset.NewSet[string]()
|
||||||
for _, c := range svc.Containers {
|
for _, c := range svc.Containers {
|
||||||
machines.Add(c.MachineID)
|
machines.Add(c.MachineID)
|
||||||
@@ -97,7 +155,7 @@ func serviceMachines(t *testing.T, svc api.Service) mapset.Set[string] {
|
|||||||
return machines
|
return machines
|
||||||
}
|
}
|
||||||
|
|
||||||
func serviceContainerIDs(t *testing.T, svc api.Service) mapset.Set[string] {
|
func serviceContainerIDs(svc api.Service) mapset.Set[string] {
|
||||||
ids := mapset.NewSet[string]()
|
ids := mapset.NewSet[string]()
|
||||||
for _, c := range svc.Containers {
|
for _, c := range svc.Containers {
|
||||||
ids.Add(c.Container.ID)
|
ids.Add(c.Container.ID)
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package e2e
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func removeServices(t *testing.T, cli api.ServiceClient, names ...string) {
|
||||||
|
ctx := context.Background()
|
||||||
|
for _, name := range names {
|
||||||
|
err := cli.RemoveService(ctx, name)
|
||||||
|
if !errors.Is(err, api.ErrNotFound) {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeVolumes(t *testing.T, cli api.VolumeClient, names ...string) {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
volumes, err := cli.ListVolumes(ctx, &api.VolumeFilter{Names: names})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
for _, vol := range volumes {
|
||||||
|
err = cli.RemoveVolume(ctx, vol.MachineID, vol.Volume.Name, false)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,11 @@ package e2e
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
dockerclient "github.com/docker/docker/client"
|
dockerclient "github.com/docker/docker/client"
|
||||||
"github.com/psviderski/uncloud/internal/machine/api/pb"
|
"github.com/psviderski/uncloud/internal/machine/api/pb"
|
||||||
"github.com/psviderski/uncloud/internal/ucind"
|
"github.com/psviderski/uncloud/internal/ucind"
|
||||||
@@ -11,9 +16,6 @@ import (
|
|||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"google.golang.org/grpc/codes"
|
"google.golang.org/grpc/codes"
|
||||||
"google.golang.org/grpc/status"
|
"google.golang.org/grpc/status"
|
||||||
"os"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func createTestCluster(
|
func createTestCluster(
|
||||||
@@ -44,6 +46,9 @@ func createTestCluster(
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, name, c.Name)
|
assert.Equal(t, name, c.Name)
|
||||||
assert.Len(t, c.Machines, opts.Machines)
|
assert.Len(t, c.Machines, opts.Machines)
|
||||||
|
for _, m := range c.Machines {
|
||||||
|
assert.NotEmpty(t, m.ID)
|
||||||
|
}
|
||||||
|
|
||||||
t.Cleanup(func() {
|
t.Cleanup(func() {
|
||||||
require.NoError(t, p.RemoveCluster(ctx, name))
|
require.NoError(t, p.RemoveCluster(ctx, name))
|
||||||
@@ -68,7 +73,7 @@ func TestClusterLifecycle(t *testing.T) {
|
|||||||
// Create a client for each machine and wait for it to be ready.
|
// Create a client for each machine and wait for it to be ready.
|
||||||
clients := make([]*client.Client, len(c.Machines))
|
clients := make([]*client.Client, len(c.Machines))
|
||||||
for i, m := range c.Machines {
|
for i, m := range c.Machines {
|
||||||
require.NoError(t, p.WaitMachineReady(ctx, m, 5*time.Second))
|
require.NoError(t, ucind.WaitMachineReady(ctx, m, 5*time.Second))
|
||||||
clients[i], err = m.Connect(ctx)
|
clients[i], err = m.Connect(ctx)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
//goland:noinspection GoDeferInLoop
|
//goland:noinspection GoDeferInLoop
|
||||||
@@ -79,7 +84,7 @@ func TestClusterLifecycle(t *testing.T) {
|
|||||||
for i, cli := range clients {
|
for i, cli := range clients {
|
||||||
// Wait for the machine to reconcile the cluster store.
|
// Wait for the machine to reconcile the cluster store.
|
||||||
require.Eventually(t, func() bool {
|
require.Eventually(t, func() bool {
|
||||||
machines, err := cli.ListMachines(ctx)
|
machines, err := cli.ListMachines(ctx, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// FailedPrecondition "cluster is not initialised" is expected until the store is reconciled.
|
// FailedPrecondition "cluster is not initialised" is expected until the store is reconciled.
|
||||||
if s, ok := status.FromError(err); ok {
|
if s, ok := status.FromError(err); ok {
|
||||||
@@ -105,6 +110,18 @@ func TestClusterLifecycle(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("inspect", func(t *testing.T) {
|
||||||
|
cluster, err := p.InspectCluster(ctx, name)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, name, cluster.Name)
|
||||||
|
assert.Len(t, cluster.Machines, 3)
|
||||||
|
for _, m := range cluster.Machines {
|
||||||
|
assert.NotEmpty(t, m.ID)
|
||||||
|
assert.True(t, strings.HasPrefix(m.Name, "machine-"))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
t.Run("remove", func(t *testing.T) {
|
t.Run("remove", func(t *testing.T) {
|
||||||
err := p.RemoveCluster(ctx, name)
|
err := p.RemoveCluster(ctx, name)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|||||||
+194
-10
@@ -2,13 +2,15 @@ package e2e
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/docker/docker/api/types/mount"
|
||||||
|
"github.com/docker/docker/api/types/volume"
|
||||||
"github.com/psviderski/uncloud/internal/ucind"
|
"github.com/psviderski/uncloud/internal/ucind"
|
||||||
"github.com/psviderski/uncloud/pkg/api"
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
"github.com/psviderski/uncloud/pkg/client/compose"
|
"github.com/psviderski/uncloud/pkg/client/compose"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"testing"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestComposeDeployment(t *testing.T) {
|
func TestComposeDeployment(t *testing.T) {
|
||||||
@@ -21,18 +23,15 @@ func TestComposeDeployment(t *testing.T) {
|
|||||||
cli, err := c.Machines[0].Connect(ctx)
|
cli, err := c.Machines[0].Connect(ctx)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
t.Run("basic", func(t *testing.T) {
|
t.Run("basic with published ports", func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
name := "basic"
|
name := "test-compose-basic"
|
||||||
t.Cleanup(func() {
|
t.Cleanup(func() {
|
||||||
err := cli.RemoveService(ctx, name)
|
removeServices(t, cli, name)
|
||||||
if !errors.Is(err, api.ErrNotFound) {
|
|
||||||
require.NoError(t, err)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
project, err := compose.LoadProject(ctx, []string{"fixtures/basic-compose.yaml"})
|
project, err := compose.LoadProject(ctx, []string{"fixtures/compose-basic.yaml"})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
deploy, err := compose.NewDeployment(ctx, cli, project)
|
deploy, err := compose.NewDeployment(ctx, cli, project)
|
||||||
@@ -52,7 +51,11 @@ func TestComposeDeployment(t *testing.T) {
|
|||||||
Name: name,
|
Name: name,
|
||||||
Mode: api.ServiceModeReplicated,
|
Mode: api.ServiceModeReplicated,
|
||||||
Container: api.ContainerSpec{
|
Container: api.ContainerSpec{
|
||||||
// TODO: resolve image digest and substitute the image with the image@digest.
|
Env: map[string]string{
|
||||||
|
"VAR": "value",
|
||||||
|
"BOOL": "true",
|
||||||
|
"EMPTY": "",
|
||||||
|
},
|
||||||
Image: "portainer/pause:3.9",
|
Image: "portainer/pause:3.9",
|
||||||
},
|
},
|
||||||
Ports: []api.PortSpec{
|
Ports: []api.PortSpec{
|
||||||
@@ -67,4 +70,185 @@ func TestComposeDeployment(t *testing.T) {
|
|||||||
}
|
}
|
||||||
assertServiceMatchesSpec(t, svc, expectedSpec)
|
assertServiceMatchesSpec(t, svc, expectedSpec)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("multiple services with volumes", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
t.Cleanup(func() {
|
||||||
|
removeServices(
|
||||||
|
t,
|
||||||
|
cli,
|
||||||
|
"test-compose-volumes-service1",
|
||||||
|
"test-compose-volumes-service2",
|
||||||
|
"test-compose-volumes-service3",
|
||||||
|
)
|
||||||
|
removeVolumes(
|
||||||
|
t,
|
||||||
|
cli,
|
||||||
|
"test-compose-volumes-data1",
|
||||||
|
"test-compose-volumes-data2",
|
||||||
|
"test-compose-volumes-external",
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
project, err := compose.LoadProject(ctx, []string{"fixtures/compose-volumes.yaml"})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
deploy, err := compose.NewDeployment(ctx, cli, project)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
plan, err := deploy.Plan(ctx)
|
||||||
|
require.ErrorContains(t, err, "external volumes not found: 'test-compose-volumes-external'")
|
||||||
|
|
||||||
|
externalVolumeOpts := volume.CreateOptions{Name: "test-compose-volumes-external"}
|
||||||
|
externalVolume, err := cli.CreateVolume(ctx, c.Machines[2].Name, externalVolumeOpts)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Recreate the deployment as it caches the cluster state.
|
||||||
|
deploy, err = compose.NewDeployment(ctx, cli, project)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
plan, err = deploy.Plan(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Len(t, plan.Operations, 5, "Expected 2 volumes creation and 3 services to deploy")
|
||||||
|
|
||||||
|
err = deploy.Run(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Verify data1 and data2 volumes have been created.
|
||||||
|
volumes, err := cli.ListVolumes(ctx, &api.VolumeFilter{
|
||||||
|
Names: []string{"test-compose-volumes-data1", "test-compose-volumes-data2"},
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Len(t, volumes, 2, "Expected 2 volumes to be created")
|
||||||
|
|
||||||
|
data1Volume, data2Volume := volumes[0], volumes[1]
|
||||||
|
if volumes[0].Volume.Name == "test-compose-volumes-data2" {
|
||||||
|
data1Volume, data2Volume = volumes[1], volumes[0]
|
||||||
|
}
|
||||||
|
assert.Equal(t, c.Machines[2].ID, data2Volume.MachineID,
|
||||||
|
"data2 volume must be on machine #2 as service3 shares both data2 and external volumes")
|
||||||
|
|
||||||
|
service1, err := cli.InspectService(ctx, "test-compose-volumes-service1")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
expectedSpec1 := api.ServiceSpec{
|
||||||
|
Name: "test-compose-volumes-service1",
|
||||||
|
Container: api.ContainerSpec{
|
||||||
|
Image: "portainer/pause:3.9",
|
||||||
|
VolumeMounts: []api.VolumeMount{
|
||||||
|
{
|
||||||
|
VolumeName: "test-compose-volumes-data1",
|
||||||
|
ContainerPath: "/data1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
VolumeName: "bind-bb6aed1683cea1e0a1ae5cd227aacd0734f2f87f7a78fcf1baeff978ce300b90",
|
||||||
|
ContainerPath: "/host/etc/passwd",
|
||||||
|
ReadOnly: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Volumes: []api.VolumeSpec{
|
||||||
|
{
|
||||||
|
Name: "test-compose-volumes-data1",
|
||||||
|
Type: api.VolumeTypeVolume,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "bind-bb6aed1683cea1e0a1ae5cd227aacd0734f2f87f7a78fcf1baeff978ce300b90",
|
||||||
|
Type: api.VolumeTypeBind,
|
||||||
|
BindOptions: &api.BindOptions{
|
||||||
|
HostPath: "/etc/passwd",
|
||||||
|
CreateHostPath: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Replicas: 3,
|
||||||
|
}
|
||||||
|
assertServiceMatchesSpec(t, service1, expectedSpec1)
|
||||||
|
service1Machines := serviceMachines(service1)
|
||||||
|
assert.Equal(t, service1Machines.ToSlice(), []string{data1Volume.MachineID},
|
||||||
|
"service1 should be on the same machine as data1 volume")
|
||||||
|
|
||||||
|
service2, err := cli.InspectService(ctx, "test-compose-volumes-service2")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
expectedSpec2 := api.ServiceSpec{
|
||||||
|
Name: "test-compose-volumes-service2",
|
||||||
|
Container: api.ContainerSpec{
|
||||||
|
Image: "portainer/pause:3.9",
|
||||||
|
VolumeMounts: []api.VolumeMount{
|
||||||
|
{
|
||||||
|
VolumeName: "test-compose-volumes-data2-alias",
|
||||||
|
ContainerPath: "/data2/long/syntax",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Volumes: []api.VolumeSpec{
|
||||||
|
{
|
||||||
|
Name: "test-compose-volumes-data2-alias",
|
||||||
|
Type: api.VolumeTypeVolume,
|
||||||
|
VolumeOptions: &api.VolumeOptions{
|
||||||
|
Name: "test-compose-volumes-data2",
|
||||||
|
Driver: &mount.Driver{
|
||||||
|
Name: "local",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Replicas: 2,
|
||||||
|
}
|
||||||
|
assertServiceMatchesSpec(t, service2, expectedSpec2)
|
||||||
|
service2Machines := serviceMachines(service2)
|
||||||
|
assert.Equal(t, service2Machines.ToSlice(), []string{data2Volume.MachineID},
|
||||||
|
"service2 replicas should be on the same machine as data2 volume")
|
||||||
|
|
||||||
|
service3, err := cli.InspectService(ctx, "test-compose-volumes-service3")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
expectedSpec3 := api.ServiceSpec{
|
||||||
|
Name: "test-compose-volumes-service3",
|
||||||
|
Container: api.ContainerSpec{
|
||||||
|
Image: "portainer/pause:3.9",
|
||||||
|
VolumeMounts: []api.VolumeMount{
|
||||||
|
{
|
||||||
|
VolumeName: "test-compose-volumes-data2-alias",
|
||||||
|
ContainerPath: "/data2",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
VolumeName: "test-compose-volumes-external",
|
||||||
|
ContainerPath: "/external",
|
||||||
|
ReadOnly: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Volumes: []api.VolumeSpec{
|
||||||
|
{
|
||||||
|
Name: "test-compose-volumes-data2-alias",
|
||||||
|
Type: api.VolumeTypeVolume,
|
||||||
|
VolumeOptions: &api.VolumeOptions{
|
||||||
|
Name: "test-compose-volumes-data2",
|
||||||
|
Driver: &mount.Driver{
|
||||||
|
Name: "local",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "test-compose-volumes-external",
|
||||||
|
Type: api.VolumeTypeVolume,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Replicas: 1,
|
||||||
|
}
|
||||||
|
assertServiceMatchesSpec(t, service3, expectedSpec3)
|
||||||
|
assert.Equal(t, externalVolume.MachineID, service3.Containers[0].MachineID,
|
||||||
|
"service3 should be on the same machine as external volume")
|
||||||
|
|
||||||
|
// Verify deployment is up-to-date.
|
||||||
|
deploy, err = compose.NewDeployment(ctx, cli, project)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
plan, err = deploy.Plan(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Len(t, plan.Operations, 0, "Expected no new operations after deployment")
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user