mirror of
https://github.com/psviderski/uncloud.git
synced 2026-08-26 19:13:34 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bbdeedb44e | ||
|
|
3e64af910a | ||
|
|
fcdbf785bb | ||
|
|
7ebcba7c36 | ||
|
|
90d244fea8 | ||
|
|
497b9483f6 | ||
|
|
e33fd40033 | ||
|
|
2b93a1c089 | ||
|
|
15d4ef5518 | ||
|
|
2f5f515696 | ||
|
|
30bb33f1a0 | ||
|
|
7bff706aa0 | ||
|
|
b0cd752619 | ||
|
|
52dd0c71a5 | ||
|
|
2d292ed053 | ||
|
|
9770151583 | ||
|
|
89db50e8df | ||
|
|
3ac5992e1d | ||
|
|
8c4f21ff81 | ||
|
|
4de068c46e | ||
|
|
b6dfc2175f | ||
|
|
48d2239f5d | ||
|
|
06d988583d | ||
|
|
abea3e229e | ||
|
|
1c3f210848 | ||
|
|
722615cd9b | ||
|
|
809d1eca77 | ||
|
|
6e4439bf49 | ||
|
|
882f2f5d03 | ||
|
|
dde23b5549 | ||
|
|
c3ecd378f8 | ||
|
|
6da8e98c62 | ||
|
|
4d591f9efc | ||
|
|
26c3699a9e | ||
|
|
328ace1fd1 | ||
|
|
91bf135a4f | ||
|
|
e2b3260926 | ||
|
|
179289b504 | ||
|
|
bd8a843972 | ||
|
|
7e366ff980 | ||
|
|
d0c23fa840 | ||
|
|
4bc47af05b | ||
|
|
65c3e0a68d | ||
|
|
5c4f49b685 | ||
|
|
ba6290d616 | ||
|
|
6af7a9861d | ||
|
|
3b4bcb7932 | ||
|
|
0153e9f9e2 | ||
|
|
9a0b1a4ce3 |
@@ -40,7 +40,7 @@ jobs:
|
||||
run: |
|
||||
make ucind-image
|
||||
make test
|
||||
timeout-minutes: 10
|
||||
timeout-minutes: 15
|
||||
|
||||
check-protobuf:
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@ COPY go.mod go.sum ./
|
||||
RUN go mod download && go mod verify
|
||||
|
||||
COPY . .
|
||||
RUN GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -o uncloudd cmd/uncloudd/main.go
|
||||
RUN GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -o uncloudd ./cmd/uncloudd
|
||||
|
||||
|
||||
FROM alpine:${ALPINE_VERSION} AS corrosion-download
|
||||
|
||||
@@ -25,12 +25,14 @@ complexity of Kubernetes.
|
||||
|
||||
## ✨ Features
|
||||
|
||||
* **Deploy anywhere**: Combine cloud VMs, dedicated servers, and bare metal into a unified computing environment —
|
||||
* **Deploy anywhere**: Combine cloud VMs, dedicated servers, and bare metal into a unified computing environment,
|
||||
regardless of location or provider.
|
||||
* **Docker Compose**: Familiar [Docker Compose](https://compose-spec.io/) format for defining services and volumes. No
|
||||
need to learn a new bespoke DSL.
|
||||
* **Zero-downtime deployments**: Rolling updates without service interruption. Automatic rollback on failure is coming
|
||||
soon.
|
||||
* **[Unregistry](https://github.com/psviderski/unregistry) integration**: Build and push your Docker images directly to
|
||||
your machines without an external registry. It will transfer only the missing layers, making it fast and efficient.
|
||||
* **Service discovery**: Built-in DNS server resolves service names to container IPs.
|
||||
* **Persistent storage**: Run stateful services with Docker volumes managed across machines.
|
||||
* **Zero-config private network**: Automatic WireGuard mesh with peer discovery and NAT traversal. Containers get unique
|
||||
@@ -45,11 +47,6 @@ complexity of Kubernetes.
|
||||
* **Docker-like CLI**: Familiar commands for managing both infrastructure and applications.
|
||||
* **Remote management**: Control your entire infrastructure through SSH access to any single machine in the cluster.
|
||||
|
||||
### 🚀 Coming soon
|
||||
|
||||
* **[Unregistry](https://github.com/psviderski/unregistry) integration**: Push your Docker images directly to your
|
||||
machines without an external registry. It will transfer only the missing layers, making it fast and efficient.
|
||||
|
||||
## 🎬 Quick demo
|
||||
|
||||
The screenshot below demonstrates how I use Uncloud to deploy https://uncloud.run website to 2 remote machines from
|
||||
@@ -67,6 +64,12 @@ on-premises in just a couple minutes.
|
||||
<img src="https://embed-ssl.wistia.com/deliveries/3cf7014a48b93afc556444bed3e39a8c.jpg?image_crop_resized=900x526&image_play_button_rounded=true&image_play_button_size=2x&image_play_button_color=18181Be0" alt="Uncloud demo" width="450" height="263" />
|
||||
</a>
|
||||
|
||||
<br>
|
||||
<br>
|
||||
|
||||
> **📚 Want more examples?** Check out the [**uncloud-recipes**](https://github.com/psviderski/uncloud-recipes)
|
||||
> repository for community recipes and templates for deploying popular services on Uncloud.
|
||||
|
||||
## 💫 Why Uncloud?
|
||||
|
||||
Modern cloud platforms like Heroku and Render offer amazing developer experiences but at a premium price. Traditional
|
||||
|
||||
+68
-33
@@ -10,68 +10,103 @@ import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// NewBuildCommand creates a new command to build services from a Compose file.
|
||||
type buildOptions struct {
|
||||
cli.BuildServicesOptions
|
||||
files []string
|
||||
profiles []string
|
||||
}
|
||||
|
||||
// NewBuildCommand creates a new command to build images for services from a Compose file.
|
||||
func NewBuildCommand() *cobra.Command {
|
||||
opts := cli.BuildOptions{}
|
||||
opts := buildOptions{}
|
||||
cmd := &cobra.Command{
|
||||
Use: "build [FLAGS] [SERVICE...]",
|
||||
Short: "Build services from a Compose file.",
|
||||
Long: `Build images for services from a Compose file using local Docker.
|
||||
|
||||
By default, built images remain on the local Docker host. Use --push to upload them
|
||||
to cluster machines or --push-registry to upload them to external registries.`,
|
||||
Example: ` # Build all services that have a build section in compose.yaml.
|
||||
uc build
|
||||
|
||||
# Build specific services that have a build section.
|
||||
uc build web api
|
||||
|
||||
# Build services and push images to all cluster machines or service x-machines if specified.
|
||||
uc build --push
|
||||
|
||||
# Build services and push images to specific machines.
|
||||
uc build --push -m machine1,machine2
|
||||
|
||||
# Build services and push images to external registries (e.g., Docker Hub).
|
||||
uc build --push-registry
|
||||
|
||||
# Build services with build arguments, pull newer base images before building, and don't use cache.
|
||||
uc build --build-arg NODE_VERSION=24 --build-arg ENV=production --no-cache --pull`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
uncli := cmd.Context().Value("cli").(*cli.CLI)
|
||||
|
||||
if len(args) > 0 {
|
||||
opts.Services = args
|
||||
}
|
||||
|
||||
return runBuild(cmd.Context(), uncli, opts)
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().StringSliceVarP(&opts.Files, "file", "f", nil,
|
||||
"One or more Compose files to build (default compose.yaml)")
|
||||
cmd.Flags().StringSliceVarP(&opts.Profiles, "profile", "p", nil,
|
||||
cmd.Flags().StringArrayVar(&opts.BuildArgs, "build-arg", nil,
|
||||
"Set a build-time variable for services. Used in Dockerfiles that declare the variable with ARG.\n"+
|
||||
"Can be specified multiple times. Format: --build-arg VAR=VALUE")
|
||||
cmd.Flags().BoolVar(&opts.Check, "check", false,
|
||||
"Check the build configuration for services without building them.")
|
||||
cmd.Flags().BoolVar(&opts.Deps, "deps", false,
|
||||
"Also build services declared as dependencies of the selected services.")
|
||||
cmd.Flags().StringSliceVarP(&opts.files, "file", "f", nil,
|
||||
"One or more Compose files to build. (default compose.yaml)")
|
||||
cmd.Flags().StringSliceVarP(&opts.Machines, "machine", "m", nil,
|
||||
"Machine names or IDs to push the built images to (requires --push).\n"+
|
||||
"Can be specified multiple times or as a comma-separated list. (default is all machines or x-machines)")
|
||||
cmd.Flags().BoolVar(&opts.NoCache, "no-cache", false,
|
||||
"Do not use cache when building images.")
|
||||
cmd.Flags().StringSliceVarP(&opts.profiles, "profile", "p", nil,
|
||||
"One or more Compose profiles to enable.")
|
||||
cmd.Flags().BoolVarP(&opts.Push, "push", "P", false,
|
||||
"Push built images to the registry after building. (default false)")
|
||||
cmd.Flags().BoolVarP(&opts.NoCache, "no-cache", "n", false,
|
||||
"Do not use cache when building images. (default false)")
|
||||
cmd.Flags().BoolVar(&opts.Pull, "pull", false,
|
||||
"Always attempt to pull newer versions of base images before building.")
|
||||
cmd.Flags().BoolVar(&opts.PushCluster, "push", false,
|
||||
"Upload the built images to cluster machines after building.\n"+
|
||||
"Use --machine to specify which machines. (default is all machines)")
|
||||
cmd.Flags().BoolVar(&opts.PushRegistry, "push-registry", false,
|
||||
"Upload the built images to external registries (e.g., Docker Hub) after building.")
|
||||
cmd.Flags().StringVarP(&opts.Context, "context", "c", "",
|
||||
"Name of the cluster context. (default is the current context)")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
// TODO: deduplicate with a similar functino for deploy options
|
||||
// projectOpts returns the project options for the Compose file(s).
|
||||
func projectOptsFromBuildOpts(opts cli.BuildOptions) []composecli.ProjectOptionsFn {
|
||||
projectOpts := []composecli.ProjectOptionsFn{}
|
||||
|
||||
if len(opts.Profiles) > 0 {
|
||||
projectOpts = append(projectOpts, composecli.WithDefaultProfiles(opts.Profiles...))
|
||||
// runBuild parses the Compose file(s) and builds the images for selected services.
|
||||
func runBuild(ctx context.Context, uncli *cli.CLI, opts buildOptions) error {
|
||||
// Validate push flags.
|
||||
if opts.PushCluster && opts.PushRegistry {
|
||||
return fmt.Errorf("cannot specify both --push and --push-registry: choose one push target")
|
||||
}
|
||||
|
||||
return projectOpts
|
||||
}
|
||||
machines := cli.ExpandCommaSeparatedValues(opts.Machines)
|
||||
// Special handling for an explicit "all" keyword to push to all machines.
|
||||
if len(machines) == 1 && machines[0] == "all" {
|
||||
machines = nil
|
||||
}
|
||||
opts.Machines = machines
|
||||
|
||||
// runBuild parses the Compose file(s), builds the services, and pushes them if requested.
|
||||
func runBuild(ctx context.Context, uncli *cli.CLI, opts cli.BuildOptions) error {
|
||||
projectOpts := projectOptsFromBuildOpts(opts)
|
||||
project, err := compose.LoadProject(ctx, opts.Files, projectOpts...)
|
||||
project, err := compose.LoadProject(ctx, opts.files, composecli.WithDefaultProfiles(opts.profiles...))
|
||||
if err != nil {
|
||||
return fmt.Errorf("load compose file(s): %w", err)
|
||||
}
|
||||
|
||||
if len(opts.Services) > 0 {
|
||||
project, err = project.WithSelectedServices(opts.Services)
|
||||
servicesToBuild, err := cli.ServicesThatNeedBuild(project, opts.Services, opts.Deps)
|
||||
if err != nil {
|
||||
return fmt.Errorf("select services: %w", err)
|
||||
return fmt.Errorf("determine services to build: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
servicesToBuild := cli.GetServicesThatNeedBuild(project)
|
||||
|
||||
if len(servicesToBuild) == 0 {
|
||||
fmt.Println("No services to build.")
|
||||
return nil
|
||||
}
|
||||
|
||||
return cli.BuildServices(ctx, servicesToBuild, opts)
|
||||
return uncli.BuildServices(ctx, project, opts.BuildServicesOptions)
|
||||
}
|
||||
|
||||
@@ -44,8 +44,8 @@ func NewDeployCommand() *cobra.Command {
|
||||
cmd.Flags().StringVar(&opts.image, "image", "",
|
||||
"Caddy Docker image to deploy. (default caddy:LATEST_VERSION)")
|
||||
cmd.Flags().StringSliceVarP(&opts.machines, "machine", "m", nil,
|
||||
"Machine names to deploy to. Can be specified multiple times or as a comma-separated "+
|
||||
"list of machine names. (default is all machines)")
|
||||
"Machine names or IDs to deploy to. Can be specified multiple times or as a comma-separated "+
|
||||
"list. (default is all machines)")
|
||||
cmd.Flags().StringVarP(
|
||||
&opts.context, "context", "c", "",
|
||||
"Name of the cluster context to deploy to. (default is the current context)",
|
||||
|
||||
+60
-26
@@ -6,6 +6,7 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
composecli "github.com/compose-spec/compose-go/v2/cli"
|
||||
"github.com/docker/compose/v2/pkg/progress"
|
||||
"github.com/psviderski/uncloud/internal/cli"
|
||||
@@ -17,6 +18,8 @@ import (
|
||||
)
|
||||
|
||||
type deployOptions struct {
|
||||
cli.BuildServicesOptions
|
||||
|
||||
files []string
|
||||
profiles []string
|
||||
services []string
|
||||
@@ -37,21 +40,25 @@ func NewDeployCommand() *cobra.Command {
|
||||
cli.BindEnvToFlag(cmd, "yes", "UNCLOUD_AUTO_CONFIRM")
|
||||
|
||||
uncli := cmd.Context().Value("cli").(*cli.CLI)
|
||||
|
||||
if len(args) > 0 {
|
||||
opts.services = args
|
||||
}
|
||||
|
||||
return runDeploy(cmd.Context(), uncli, opts)
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().StringArrayVar(&opts.BuildServicesOptions.BuildArgs, "build-arg", nil,
|
||||
"Set a build-time variable for services. Used in Dockerfiles that declare the variable with ARG.\n"+
|
||||
"Can be specified multiple times. Format: --build-arg VAR=VALUE")
|
||||
cmd.Flags().BoolVar(&opts.BuildServicesOptions.Pull, "build-pull", false,
|
||||
"Always attempt to pull newer versions of base images before building service images.")
|
||||
cmd.Flags().StringVarP(&opts.context, "context", "c", "",
|
||||
"Name of the cluster context to deploy to (default is the current context)")
|
||||
cmd.Flags().StringSliceVarP(&opts.files, "file", "f", nil,
|
||||
"One or more Compose files to deploy services from. (default compose.yaml)")
|
||||
cmd.Flags().BoolVarP(&opts.noBuild, "no-build", "n", false,
|
||||
"Do not build images before deploying services. (default false)")
|
||||
cmd.Flags().BoolVar(&opts.noBuild, "no-build", false,
|
||||
"Do not build new images before deploying services.")
|
||||
cmd.Flags().BoolVar(&opts.BuildServicesOptions.NoCache, "no-cache", false,
|
||||
"Do not use cache when building images.")
|
||||
cmd.Flags().StringSliceVarP(&opts.profiles, "profile", "p", nil,
|
||||
"One or more Compose profiles to enable.")
|
||||
cmd.Flags().BoolVar(&opts.recreate, "recreate", false,
|
||||
@@ -66,22 +73,9 @@ func NewDeployCommand() *cobra.Command {
|
||||
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.
|
||||
func runDeploy(ctx context.Context, uncli *cli.CLI, opts deployOptions) error {
|
||||
projectOpts := projectOpts(opts)
|
||||
|
||||
project, err := compose.LoadProject(ctx, opts.files, projectOpts...)
|
||||
project, err := compose.LoadProject(ctx, opts.files, composecli.WithDefaultProfiles(opts.profiles...))
|
||||
if err != nil {
|
||||
return fmt.Errorf("load compose file(s): %w", err)
|
||||
}
|
||||
@@ -94,21 +88,24 @@ func runDeploy(ctx context.Context, uncli *cli.CLI, opts deployOptions) error {
|
||||
}
|
||||
}
|
||||
|
||||
servicesToBuild := cli.GetServicesThatNeedBuild(project)
|
||||
servicesToBuild, err := cli.ServicesThatNeedBuild(project, opts.services, false)
|
||||
if err != nil {
|
||||
return fmt.Errorf("determine services to build: %w", err)
|
||||
}
|
||||
|
||||
if len(servicesToBuild) > 0 {
|
||||
if opts.noBuild {
|
||||
fmt.Println("Not building services as requested.")
|
||||
} else {
|
||||
buildOpts := cli.BuildOptions{
|
||||
Push: true,
|
||||
NoCache: false,
|
||||
}
|
||||
// Build service images without pushing them to cluster yet to not connect to the cluster twice.
|
||||
opts.BuildServicesOptions.Deps = true // build dependencies as deploy includes them by default
|
||||
opts.BuildServicesOptions.Services = opts.services
|
||||
|
||||
if err := cli.BuildServices(ctx, servicesToBuild, buildOpts); err != nil {
|
||||
if err = uncli.BuildServices(ctx, project, opts.BuildServicesOptions); err != nil {
|
||||
return fmt.Errorf("build services: %w", err)
|
||||
}
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
clusterClient, err := uncli.ConnectCluster(ctx, opts.context)
|
||||
@@ -117,6 +114,43 @@ func runDeploy(ctx context.Context, uncli *cli.CLI, opts deployOptions) error {
|
||||
}
|
||||
defer clusterClient.Close()
|
||||
|
||||
if len(servicesToBuild) > 0 && !opts.noBuild {
|
||||
// Push built service images to cluster machines one at a time.
|
||||
var errs []error
|
||||
for _, s := range servicesToBuild {
|
||||
if s.Image == "" {
|
||||
// Skip services without an image name (shouldn't happen for services with build config).
|
||||
continue
|
||||
}
|
||||
|
||||
// Push to the specified x-machines or to *all* cluster machines if not specified.
|
||||
var pushOpts client.PushImageOptions
|
||||
if machines, ok := s.Extensions[compose.MachinesExtensionKey].(compose.MachinesSource); ok {
|
||||
pushOpts.Machines = machines
|
||||
}
|
||||
if len(pushOpts.Machines) == 0 {
|
||||
pushOpts.AllMachines = true
|
||||
}
|
||||
|
||||
boldStyle := lipgloss.NewStyle().Bold(true)
|
||||
err = progress.RunWithTitle(ctx, func(ctx context.Context) error {
|
||||
if err = clusterClient.PushImage(ctx, s.Image, pushOpts); err != nil {
|
||||
return fmt.Errorf("push image '%s' for service '%s': %w", s.Image, s.Name, err)
|
||||
}
|
||||
return nil
|
||||
}, uncli.ProgressOut(), fmt.Sprintf("Pushing image %s to cluster", boldStyle.Render(s.Image)))
|
||||
// Collect errors to try pushing all images.
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
|
||||
if err = errors.Join(errs...); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
var strategy deploy.Strategy
|
||||
if opts.recreate {
|
||||
strategy = &deploy.RollingStrategy{ForceRecreate: true}
|
||||
@@ -136,7 +170,7 @@ func runDeploy(ctx context.Context, uncli *cli.CLI, opts deployOptions) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
fmt.Println("Deployment plan:")
|
||||
fmt.Println(lipgloss.NewStyle().Bold(true).Render("Deployment plan"))
|
||||
if err = printPlan(ctx, clusterClient, plan); err != nil {
|
||||
return fmt.Errorf("print deployment plan: %w", err)
|
||||
}
|
||||
|
||||
+10
-11
@@ -14,8 +14,8 @@ import (
|
||||
type pushOptions struct {
|
||||
image string
|
||||
machines []string
|
||||
context string
|
||||
platform string
|
||||
context string
|
||||
}
|
||||
|
||||
func NewPushCommand() *cobra.Command {
|
||||
@@ -25,8 +25,8 @@ func NewPushCommand() *cobra.Command {
|
||||
Use: "push IMAGE",
|
||||
Short: "Upload a local Docker image to the cluster.",
|
||||
Long: `Upload a local Docker image to the cluster transferring only the missing layers.
|
||||
The image is uploaded to the machine which CLI is connected to (default) or the specified machine(s).`,
|
||||
Example: ` # Push image to the machine the CLI is connected to.
|
||||
The image is uploaded to all cluster machines (default) or the specified machine(s).`,
|
||||
Example: ` # Push image to all machines in the cluster.
|
||||
uc image push myapp:latest
|
||||
|
||||
# Push image to specific machine.
|
||||
@@ -35,9 +35,6 @@ The image is uploaded to the machine which CLI is connected to (default) or the
|
||||
# Push image to multiple machines.
|
||||
uc image push myapp:latest -m machine1,machine2,machine3
|
||||
|
||||
# Push image to all machines in the cluster.
|
||||
uc image push myapp:latest -m all
|
||||
|
||||
# Push a specific platform of a multi-platform image.
|
||||
uc image push myapp:latest --platform linux/amd64`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
@@ -50,9 +47,8 @@ The image is uploaded to the machine which CLI is connected to (default) or the
|
||||
}
|
||||
|
||||
cmd.Flags().StringSliceVarP(&opts.machines, "machine", "m", nil,
|
||||
"Machine names to push the image to. Can be specified multiple times or as a comma-separated "+
|
||||
"list of machine names.\n"+
|
||||
"Use 'all' to push to all machines. (default is connected machine)")
|
||||
"Machine names or IDs to push the image to. Can be specified multiple times or as a comma-separated list. "+
|
||||
"(default is all machines)")
|
||||
cmd.Flags().StringVar(
|
||||
&opts.platform, "platform", "",
|
||||
"Push a specific platform of a multi-platform image (e.g., linux/amd64, linux/arm64).\n"+
|
||||
@@ -76,11 +72,14 @@ func push(ctx context.Context, uncli *cli.CLI, opts pushOptions) error {
|
||||
machines := cli.ExpandCommaSeparatedValues(opts.machines)
|
||||
pushOpts := client.PushImageOptions{}
|
||||
|
||||
// Special handling for "all" keyword to push to all machines.
|
||||
// Special handling for an explicit "all" keyword to push to all machines.
|
||||
if len(machines) == 1 && machines[0] == "all" {
|
||||
pushOpts.AllMachines = true
|
||||
} else {
|
||||
} else if len(machines) > 0 {
|
||||
pushOpts.Machines = machines
|
||||
} else {
|
||||
// Default is to push to all machines in the cluster.
|
||||
pushOpts.AllMachines = true
|
||||
}
|
||||
|
||||
if opts.platform != "" {
|
||||
|
||||
+10
-1
@@ -16,6 +16,7 @@ import (
|
||||
"github.com/psviderski/uncloud/internal/cli"
|
||||
"github.com/psviderski/uncloud/internal/cli/config"
|
||||
"github.com/psviderski/uncloud/internal/fs"
|
||||
"github.com/psviderski/uncloud/internal/log"
|
||||
"github.com/psviderski/uncloud/internal/version"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
@@ -26,6 +27,8 @@ type globalOptions struct {
|
||||
}
|
||||
|
||||
func main() {
|
||||
log.InitLoggerFromEnv()
|
||||
|
||||
opts := globalOptions{}
|
||||
cmd := &cobra.Command{
|
||||
Use: "uc",
|
||||
@@ -47,6 +50,11 @@ func main() {
|
||||
conn = &config.MachineConnection{
|
||||
TCP: &addrPort,
|
||||
}
|
||||
} else if strings.HasPrefix(opts.connect, "ssh+cli://") {
|
||||
dest := opts.connect[len("ssh+cli://"):]
|
||||
conn = &config.MachineConnection{
|
||||
SSHCLI: config.SSHDestination(dest),
|
||||
}
|
||||
} else {
|
||||
dest := opts.connect
|
||||
if strings.HasPrefix(dest, "ssh://") {
|
||||
@@ -70,7 +78,7 @@ func main() {
|
||||
|
||||
cmd.PersistentFlags().StringVar(&opts.connect, "connect", "",
|
||||
"Connect to a remote cluster machine without using the Uncloud configuration file. [$UNCLOUD_CONNECT]\n"+
|
||||
"Format: [ssh://]user@host[:port] or tcp://host:port")
|
||||
"Format: [ssh://]user@host[:port], ssh+cli://user@host[:port], or tcp://host:port")
|
||||
cmd.PersistentFlags().StringVar(&opts.configPath, "uncloud-config", "~/.config/uncloud/config.yaml",
|
||||
"Path to the Uncloud configuration file. [$UNCLOUD_CONFIG]")
|
||||
_ = cmd.MarkPersistentFlagFilename("uncloud-config", "yaml", "yml")
|
||||
@@ -87,6 +95,7 @@ func main() {
|
||||
image.NewRootCommand(),
|
||||
machine.NewRootCommand(),
|
||||
service.NewRootCommand(),
|
||||
service.NewExecCommand(),
|
||||
service.NewInspectCommand(),
|
||||
service.NewListCommand(),
|
||||
service.NewRmCommand(),
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/docker/cli/cli/streams"
|
||||
"github.com/psviderski/uncloud/internal/cli"
|
||||
"github.com/psviderski/uncloud/pkg/api"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type execCliOptions struct {
|
||||
detach bool
|
||||
interactive bool
|
||||
noTty bool
|
||||
context string
|
||||
containerId string
|
||||
}
|
||||
|
||||
var DEFAULT_COMMAND = []string{"sh", "-c", "command -v bash >/dev/null 2>&1 && exec bash || exec sh"}
|
||||
|
||||
func NewExecCommand() *cobra.Command {
|
||||
opts := execCliOptions{}
|
||||
|
||||
execCmd := &cobra.Command{
|
||||
Use: "exec [OPTIONS] SERVICE [COMMAND ARGS...]",
|
||||
Short: "Execute a command in a running service container",
|
||||
Long: `Execute a command (interactive shell by default) in a running container within a service.
|
||||
If the service has multiple replicas and no container ID is specified, the command will be executed in a random container.
|
||||
`,
|
||||
Example: `
|
||||
# Start an interactive shell ("bash" or "sh" will be tried by default)
|
||||
uc exec web-service
|
||||
|
||||
# Start an interactive shell with explicit command
|
||||
uc exec web-service /bin/zsh
|
||||
|
||||
# List files in the specific container of the service; --container accepts full ID or a (unique) prefix
|
||||
uc exec --container d792e web-service ls -la
|
||||
|
||||
# Pipe input to a command inside the service container
|
||||
cat backup.sql | uc exec -T db-service psql -U postgres mydb
|
||||
|
||||
# Run a task in the background (detached mode)
|
||||
uc exec -d web-service /scripts/cleanup.sh`,
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
uncli := cmd.Context().Value("cli").(*cli.CLI)
|
||||
serviceName := args[0]
|
||||
command := args[1:]
|
||||
if len(command) == 0 {
|
||||
command = DEFAULT_COMMAND
|
||||
}
|
||||
return runExec(cmd.Context(), uncli, serviceName, command, opts)
|
||||
},
|
||||
}
|
||||
|
||||
execCmd.Flags().BoolVarP(&opts.detach, "detach", "d", false, "Detached mode: run command in the background")
|
||||
|
||||
execCmd.Flags().BoolVarP(&opts.noTty, "no-tty", "T", !cli.IsStdoutTerminal(),
|
||||
"Disable pseudo-TTY allocation. By default 'uc exec' allocates a TTY when connected to a terminal.")
|
||||
|
||||
// Keep "-i" and "-t" flags hidden for compatibility with docker exec
|
||||
execCmd.Flags().BoolVarP(&opts.interactive, "interactive", "i", true, "Keep STDIN open even if not attached")
|
||||
execCmd.Flags().MarkHidden("interactive")
|
||||
|
||||
execCmd.Flags().BoolP("tty", "t", false, "Allocate a pseudo-TTY")
|
||||
execCmd.Flags().MarkHidden("tty")
|
||||
|
||||
execCmd.Flags().StringVarP(&opts.context, "context", "c", "",
|
||||
"Name of the cluster context. (default is the current context)")
|
||||
|
||||
// Common flags
|
||||
execCmd.Flags().StringVar(&opts.containerId, "container", "",
|
||||
"ID of the container to exec into. Accepts full ID or a unique prefix "+
|
||||
"(default is the random container of the service)")
|
||||
|
||||
// This tells Cobra that all flags must come before positional arguments, so that
|
||||
// commands with their own flags can be handled correctly.
|
||||
execCmd.Flags().SetInterspersed(false)
|
||||
|
||||
return execCmd
|
||||
}
|
||||
|
||||
func runExec(ctx context.Context, uncli *cli.CLI, serviceName string, command []string, opts execCliOptions) error {
|
||||
if !opts.detach {
|
||||
// Check if we're trying to attach to a TTY from a non-TTY client, e.g.
|
||||
// when doing an 'cmd | uc exec ...'
|
||||
stdin := streams.NewIn(os.Stdin)
|
||||
// TODO: this logic/behavior mirrors docker-compose, but we can be smarter about it and detect TTY dynamically
|
||||
if err := stdin.CheckTty(opts.interactive, !opts.noTty); err != nil {
|
||||
return fmt.Errorf("check TTY: %w; use -T option to disable TTY allocation", err)
|
||||
}
|
||||
}
|
||||
|
||||
client, err := uncli.ConnectClusterWithOptions(ctx, opts.context, cli.ConnectOptions{
|
||||
ShowProgress: false,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("connect to cluster: %w", err)
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
execConfig := api.ExecOptions{
|
||||
Command: command,
|
||||
AttachStdin: opts.interactive,
|
||||
Tty: !opts.noTty,
|
||||
Detach: opts.detach,
|
||||
}
|
||||
|
||||
if !opts.detach {
|
||||
execConfig.AttachStdout = true
|
||||
execConfig.AttachStderr = true
|
||||
}
|
||||
|
||||
exitCode, err := client.ExecContainer(ctx, serviceName, opts.containerId, execConfig)
|
||||
if err != nil {
|
||||
return fmt.Errorf("exec container: %w", err)
|
||||
}
|
||||
|
||||
// For non-detached mode, exit with the same code as the executed command
|
||||
if !opts.detach {
|
||||
if exitCode != 0 {
|
||||
os.Exit(exitCode)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -58,7 +58,7 @@ func inspect(ctx context.Context, uncli *cli.CLI, opts inspectOptions) error {
|
||||
machinesNamesByID[m.Machine.Id] = m.Machine.Name
|
||||
}
|
||||
|
||||
fmt.Printf("ID: %s\n", svc.ID)
|
||||
fmt.Printf("Service ID: %s\n", svc.ID)
|
||||
fmt.Printf("Name: %s\n", svc.Name)
|
||||
fmt.Printf("Mode: %s\n", svc.Mode)
|
||||
fmt.Println()
|
||||
|
||||
@@ -67,6 +67,15 @@ func list(ctx context.Context, uncli *cli.CLI, contextName string) error {
|
||||
images := strings.Join(s.Images(), ", ")
|
||||
endpoints := strings.Join(s.Endpoints(), ", ")
|
||||
|
||||
// If no endpoints from ports, check if the service uses custom Caddy config.
|
||||
if endpoints == "" {
|
||||
for _, ctr := range s.Containers {
|
||||
if ctr.Container.ServiceSpec.CaddyConfig() != "" {
|
||||
endpoints = "(custom Caddy config)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if haveDuplicateNames {
|
||||
if _, err = fmt.Fprintf(tw, "%s\t", s.ID); err != nil {
|
||||
return fmt.Errorf("write row: %w", err)
|
||||
|
||||
@@ -16,6 +16,7 @@ func NewRootCommand() *cobra.Command {
|
||||
NewRmCommand(),
|
||||
NewRunCommand(),
|
||||
NewScaleCommand(),
|
||||
NewExecCommand(),
|
||||
)
|
||||
return cmd
|
||||
}
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
|
||||
"github.com/psviderski/uncloud/internal/machine"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func newDialStdioCommand() *cobra.Command {
|
||||
var socketPath string
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "dial-stdio",
|
||||
Short: "Proxy stdin/stdout to the Uncloud API socket",
|
||||
Hidden: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runDialStdio(cmd.Context(), socketPath, os.Stdin, os.Stdout)
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().StringVar(&socketPath, "socket", machine.DefaultUncloudSockPath,
|
||||
"Path to the Uncloud API socket")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
// halfReadCloser is the read side of a half-duplex connection.
|
||||
type halfReadCloser interface {
|
||||
io.Reader
|
||||
CloseRead() error
|
||||
}
|
||||
|
||||
// halfWriteCloser is the write side of a half-duplex connection.
|
||||
type halfWriteCloser interface {
|
||||
io.Writer
|
||||
CloseWrite() error
|
||||
}
|
||||
|
||||
// halfReadCloserWrapper wraps an io.ReadCloser to implement halfReadCloser.
|
||||
type halfReadCloserWrapper struct {
|
||||
io.ReadCloser
|
||||
}
|
||||
|
||||
func (x *halfReadCloserWrapper) CloseRead() error {
|
||||
return x.Close()
|
||||
}
|
||||
|
||||
// halfWriteCloserWrapper wraps an io.WriteCloser to implement halfWriteCloser.
|
||||
type halfWriteCloserWrapper struct {
|
||||
io.WriteCloser
|
||||
}
|
||||
|
||||
func (x *halfWriteCloserWrapper) CloseWrite() error {
|
||||
return x.Close()
|
||||
}
|
||||
|
||||
func runDialStdio(ctx context.Context, socketPath string, stdin io.Reader, stdout io.Writer) error {
|
||||
// Connect to the unix socket.
|
||||
var dialer net.Dialer
|
||||
conn, err := dialer.DialContext(ctx, "unix", socketPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("connect to socket %q: %w", socketPath, err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// Wrap stdin/stdout to support half-closing.
|
||||
var stdinCloser halfReadCloser
|
||||
if c, ok := stdin.(halfReadCloser); ok {
|
||||
stdinCloser = c
|
||||
} else if c, ok := stdin.(io.ReadCloser); ok {
|
||||
stdinCloser = &halfReadCloserWrapper{c}
|
||||
}
|
||||
|
||||
var stdoutCloser halfWriteCloser
|
||||
if c, ok := stdout.(halfWriteCloser); ok {
|
||||
stdoutCloser = c
|
||||
} else if c, ok := stdout.(io.WriteCloser); ok {
|
||||
stdoutCloser = &halfWriteCloserWrapper{c}
|
||||
}
|
||||
|
||||
// Copy data bidirectionally between stdin/stdout and the socket.
|
||||
stdin2socket := make(chan error, 1)
|
||||
socket2stdout := make(chan error, 1)
|
||||
|
||||
// Copy from stdin to socket.
|
||||
go func() {
|
||||
_, err := io.Copy(conn, stdin)
|
||||
stdin2socket <- err
|
||||
// Close write side of connection after stdin is done.
|
||||
if unixConn, ok := conn.(*net.UnixConn); ok {
|
||||
unixConn.CloseWrite()
|
||||
}
|
||||
if stdinCloser != nil {
|
||||
stdinCloser.CloseRead()
|
||||
}
|
||||
}()
|
||||
|
||||
// Copy from socket to stdout.
|
||||
go func() {
|
||||
_, err := io.Copy(stdout, conn)
|
||||
socket2stdout <- err
|
||||
// Close read side of connection after socket is done sending.
|
||||
if unixConn, ok := conn.(*net.UnixConn); ok {
|
||||
unixConn.CloseRead()
|
||||
}
|
||||
if stdoutCloser != nil {
|
||||
stdoutCloser.CloseWrite()
|
||||
}
|
||||
}()
|
||||
|
||||
select {
|
||||
case err = <-stdin2socket:
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// wait for stdout
|
||||
err = <-socket2stdout
|
||||
case err = <-socket2stdout:
|
||||
// return immediately, matching Docker's approach
|
||||
// (stdin is never closed when TTY)
|
||||
}
|
||||
return err
|
||||
}
|
||||
@@ -42,6 +42,9 @@ func main() {
|
||||
"Directory for storing persistent machine state")
|
||||
_ = cmd.MarkFlagDirname("data-dir")
|
||||
|
||||
// Add dial-stdio subcommand.
|
||||
cmd.AddCommand(newDialStdioCommand())
|
||||
|
||||
// ctx is canceled when the daemon command is interrupted.
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
|
||||
@@ -71,19 +71,36 @@ require (
|
||||
cloud.google.com/go/kms v1.19.0 // indirect
|
||||
cloud.google.com/go/longrunning v0.6.0 // indirect
|
||||
filippo.io/edwards25519 v1.1.0 // indirect
|
||||
github.com/AlecAivazis/survey/v2 v2.3.7 // indirect
|
||||
github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 // indirect
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
|
||||
github.com/DefangLabs/secret-detector v0.0.0-20250403165618-22662109213e // indirect
|
||||
github.com/Masterminds/goutils v1.1.1 // indirect
|
||||
github.com/Masterminds/semver/v3 v3.4.0 // indirect
|
||||
github.com/Masterminds/sprig/v3 v3.2.3 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/Microsoft/hcsshim v0.13.0 // indirect
|
||||
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect
|
||||
github.com/antlr4-go/antlr/v4 v4.13.0 // indirect
|
||||
github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
|
||||
github.com/armon/circbuf v0.0.0-20190214190532-5111143e8da2 // indirect
|
||||
github.com/armon/go-metrics v0.4.1 // indirect
|
||||
github.com/armon/go-radix v1.0.0 // indirect
|
||||
github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b // indirect
|
||||
github.com/atotto/clipboard v0.1.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2 v1.30.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/config v1.27.27 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.27 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.15 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.15 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.22.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.30.3 // indirect
|
||||
github.com/aws/smithy-go v1.20.3 // indirect
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/bgentry/speakeasy v0.2.0 // indirect
|
||||
@@ -112,7 +129,7 @@ require (
|
||||
github.com/containerd/ttrpc v1.2.7 // indirect
|
||||
github.com/containerd/typeurl/v2 v2.2.3 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect
|
||||
github.com/dgraph-io/badger v1.6.2 // indirect
|
||||
github.com/dgraph-io/badger/v2 v2.2007.4 // indirect
|
||||
@@ -122,28 +139,37 @@ require (
|
||||
github.com/distribution/distribution/v3 v3.0.0 // indirect
|
||||
github.com/dlclark/regexp2 v1.11.5 // indirect
|
||||
github.com/docker/buildx v0.29.1 // indirect
|
||||
github.com/docker/cli-docs-tool v0.10.0 // indirect
|
||||
github.com/docker/distribution v2.8.3+incompatible // indirect
|
||||
github.com/docker/docker-credential-helpers v0.9.3 // indirect
|
||||
github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c // indirect
|
||||
github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c // indirect
|
||||
github.com/docker/go-metrics v0.0.1 // indirect
|
||||
github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/eiannone/keyboard v0.0.0-20220611211555-0d226195f203 // indirect
|
||||
github.com/emicklei/go-restful/v3 v3.11.0 // indirect
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
||||
github.com/fatih/color v1.17.0 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/fsnotify/fsevents v0.2.0 // indirect
|
||||
github.com/fvbommel/sortorder v1.1.0 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
|
||||
github.com/go-jose/go-jose/v3 v3.0.3 // indirect
|
||||
github.com/go-kit/kit v0.13.0 // indirect
|
||||
github.com/go-kit/log v0.2.1 // indirect
|
||||
github.com/go-logfmt/logfmt v0.6.0 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.21.0 // indirect
|
||||
github.com/go-openapi/jsonreference v0.20.2 // indirect
|
||||
github.com/go-openapi/swag v0.23.0 // indirect
|
||||
github.com/go-sql-driver/mysql v1.8.1 // indirect
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
||||
github.com/godbus/dbus/v5 v5.1.0 // indirect
|
||||
github.com/gofrs/flock v0.12.1 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2 // indirect
|
||||
github.com/golang/glog v1.2.5 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
@@ -151,13 +177,17 @@ require (
|
||||
github.com/google/btree v1.1.2 // indirect
|
||||
github.com/google/cel-go v0.20.1 // indirect
|
||||
github.com/google/flatbuffers v24.3.25+incompatible // indirect
|
||||
github.com/google/gnostic-models v0.6.8 // indirect
|
||||
github.com/google/gofuzz v1.2.0 // indirect
|
||||
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/gorilla/handlers v1.5.2 // indirect
|
||||
github.com/gorilla/mux v1.8.1 // indirect
|
||||
github.com/gorilla/websocket v1.5.3 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||
github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
|
||||
github.com/hashicorp/go-immutable-radix/v2 v2.1.0 // indirect
|
||||
github.com/hashicorp/go-msgpack v0.5.5 // indirect
|
||||
@@ -175,6 +205,7 @@ require (
|
||||
github.com/imdario/mergo v0.3.16 // indirect
|
||||
github.com/in-toto/in-toto-golang v0.9.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/inhies/go-bytesize v0.0.0-20220417184213-4913239db9cf // indirect
|
||||
github.com/ipfs/bbloom v0.0.4 // indirect
|
||||
github.com/ipfs/go-block-format v0.2.0 // indirect
|
||||
github.com/ipfs/go-ipfs-util v0.0.3 // indirect
|
||||
@@ -192,7 +223,11 @@ require (
|
||||
github.com/jackc/pgtype v1.14.0 // indirect
|
||||
github.com/jackc/pgx/v4 v4.18.3 // indirect
|
||||
github.com/jbenet/goprocess v0.1.4 // indirect
|
||||
github.com/jonboulle/clockwork v0.5.0 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/josharian/native v1.1.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
|
||||
github.com/klauspost/compress v1.18.0 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.9 // indirect
|
||||
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect
|
||||
@@ -203,6 +238,7 @@ require (
|
||||
github.com/libp2p/go-libp2p-pubsub v0.11.0 // indirect
|
||||
github.com/libp2p/go-msgio v0.3.0 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/manifoldco/promptui v0.9.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
@@ -214,6 +250,7 @@ require (
|
||||
github.com/mdlayher/socket v0.5.1 // indirect
|
||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
|
||||
github.com/mholt/acmez/v2 v2.0.3 // indirect
|
||||
github.com/miekg/pkcs11 v1.1.1 // indirect
|
||||
github.com/minio/sha256-simd v1.0.1 // indirect
|
||||
github.com/mitchellh/cli v1.1.5 // indirect
|
||||
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||
@@ -226,12 +263,17 @@ require (
|
||||
github.com/moby/go-archive v0.1.0 // indirect
|
||||
github.com/moby/locker v1.0.1 // indirect
|
||||
github.com/moby/patternmatcher v0.6.0 // indirect
|
||||
github.com/moby/spdystream v0.5.0 // indirect
|
||||
github.com/moby/sys/atomicwriter v0.1.0 // indirect
|
||||
github.com/moby/sys/capability v0.4.0 // indirect
|
||||
github.com/moby/sys/mountinfo v0.7.2 // indirect
|
||||
github.com/moby/sys/sequential v0.6.0 // indirect
|
||||
github.com/moby/sys/signal v0.7.1 // indirect
|
||||
github.com/moby/sys/symlink v0.3.0 // indirect
|
||||
github.com/moby/sys/user v0.4.0 // indirect
|
||||
github.com/moby/sys/userns v0.1.0 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/morikuni/aec v1.0.0 // indirect
|
||||
github.com/mr-tron/base58 v1.2.0 // indirect
|
||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
|
||||
@@ -246,6 +288,7 @@ require (
|
||||
github.com/multiformats/go-multistream v0.5.0 // indirect
|
||||
github.com/multiformats/go-varint v0.0.7 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect
|
||||
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||
github.com/onsi/ginkgo/v2 v2.22.0 // indirect
|
||||
github.com/opencontainers/runtime-spec v1.2.1 // indirect
|
||||
@@ -273,11 +316,13 @@ require (
|
||||
github.com/santhosh-tekuri/jsonschema/v6 v6.0.1 // indirect
|
||||
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 // indirect
|
||||
github.com/secure-systems-lab/go-securesystemslib v0.6.0 // indirect
|
||||
github.com/serialx/hashring v0.0.0-20200727003509-22c0c7ab6b1b // indirect
|
||||
github.com/shibumi/go-pathspec v1.3.0 // indirect
|
||||
github.com/shopspring/decimal v1.4.0 // indirect
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
|
||||
github.com/siderolabs/gen v0.4.8 // indirect
|
||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 // indirect
|
||||
github.com/slackhq/nebula v1.6.1 // indirect
|
||||
github.com/smallstep/certificates v0.26.1 // indirect
|
||||
github.com/smallstep/nosql v0.6.1 // indirect
|
||||
@@ -290,6 +335,8 @@ require (
|
||||
github.com/stoewer/go-strcase v1.2.0 // indirect
|
||||
github.com/stretchr/objx v0.5.2 // indirect
|
||||
github.com/tailscale/tscert v0.0.0-20240517230440-bbccfbf48933 // indirect
|
||||
github.com/theupdateframework/notary v0.7.0 // indirect
|
||||
github.com/tilt-dev/fsnotify v1.4.8-0.20220602155310-fff9c274a375 // indirect
|
||||
github.com/tonistiigi/dchapes-mode v0.0.0-20250318174251-73d941a28323 // indirect
|
||||
github.com/tonistiigi/fsutil v0.0.0-20250605211040-586307ad452f // indirect
|
||||
github.com/tonistiigi/go-csvvalue v0.0.0-20240814133006-030d3b2625d0 // indirect
|
||||
@@ -298,8 +345,10 @@ require (
|
||||
github.com/urfave/cli v1.22.17 // indirect
|
||||
github.com/vbatts/tar-split v0.12.1 // indirect
|
||||
github.com/vishvananda/netns v0.0.5 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
github.com/xhit/go-str2duration/v2 v2.1.0 // indirect
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||
github.com/zclconf/go-cty v1.17.0 // indirect
|
||||
github.com/zeebo/blake3 v0.2.4 // indirect
|
||||
go.etcd.io/bbolt v1.4.3 // indirect
|
||||
go.opencensus.io v0.24.0 // indirect
|
||||
@@ -339,17 +388,31 @@ require (
|
||||
golang.org/x/crypto/x509roots/fallback v0.0.0-20240507223354-67b13616a595 // indirect
|
||||
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect
|
||||
golang.org/x/mod v0.27.0 // indirect
|
||||
golang.org/x/oauth2 v0.30.0 // indirect
|
||||
golang.org/x/text v0.28.0 // indirect
|
||||
golang.org/x/time v0.11.0 // indirect
|
||||
golang.org/x/tools v0.36.0 // indirect
|
||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a // indirect
|
||||
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259 // indirect
|
||||
howett.net/plist v1.0.0 // indirect
|
||||
k8s.io/api v0.32.3 // indirect
|
||||
k8s.io/apimachinery v0.32.3 // indirect
|
||||
k8s.io/client-go v0.32.3 // indirect
|
||||
k8s.io/klog/v2 v2.130.1 // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f // indirect
|
||||
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect
|
||||
lukechampine.com/blake3 v1.3.0 // indirect
|
||||
modernc.org/libc v1.61.13 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.8.2 // indirect
|
||||
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.4.2 // indirect
|
||||
sigs.k8s.io/yaml v1.4.0 // indirect
|
||||
tags.cncf.io/container-device-interface v1.0.1 // indirect
|
||||
)
|
||||
|
||||
@@ -18,6 +18,8 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk=
|
||||
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
|
||||
github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ=
|
||||
github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo=
|
||||
github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 h1:cTp8I5+VIoKjsnZuH8vjyaysT/ses3EvZeaV/1UkF2M=
|
||||
github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
|
||||
@@ -26,6 +28,8 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03
|
||||
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
|
||||
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||
github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
|
||||
github.com/DefangLabs/secret-detector v0.0.0-20250403165618-22662109213e h1:rd4bOvKmDIx0WeTv9Qz+hghsgyjikFiPrseXHlKepO0=
|
||||
github.com/DefangLabs/secret-detector v0.0.0-20250403165618-22662109213e/go.mod h1:blbwPQh4DTlCZEfk1BLU4oMIhLda2U+A840Uag9DsZw=
|
||||
github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
|
||||
github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
|
||||
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
|
||||
@@ -46,9 +50,15 @@ github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERo
|
||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||
github.com/Microsoft/hcsshim v0.13.0 h1:/BcXOiS6Qi7N9XqUcv27vkIuVOkBEcWstd2pMlWSeaA=
|
||||
github.com/Microsoft/hcsshim v0.13.0/go.mod h1:9KWJ/8DgU+QzYGupX4tzMhRQE8h6w90lH6HAaclpEok=
|
||||
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s=
|
||||
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w=
|
||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||
github.com/OneOfOne/xxhash v1.2.8 h1:31czK/TI9sNkxIKfaUfGlU47BAxQ0ztGgd9vPyqimf8=
|
||||
github.com/OneOfOne/xxhash v1.2.8/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q=
|
||||
github.com/Shopify/logrus-bugsnag v0.0.0-20170309145241-6dbc35f2c30d h1:hi6J4K6DKrR4/ljxn6SF6nURyu785wKMuQcjt7H3VCQ=
|
||||
github.com/Shopify/logrus-bugsnag v0.0.0-20170309145241-6dbc35f2c30d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ=
|
||||
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8=
|
||||
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo=
|
||||
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
|
||||
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
|
||||
github.com/alecthomas/chroma/v2 v2.20.0 h1:sfIHpxPyR07/Oylvmcai3X/exDlE8+FA820NTz+9sGw=
|
||||
@@ -63,6 +73,8 @@ github.com/anchore/go-struct-converter v0.0.0-20221118182256-c68fdcfa2092 h1:aM1
|
||||
github.com/anchore/go-struct-converter v0.0.0-20221118182256-c68fdcfa2092/go.mod h1:rYqSE9HbjzpHTI74vwPvae4ZVYZd1lue2ta6xHPdblA=
|
||||
github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI=
|
||||
github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g=
|
||||
github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY=
|
||||
github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4=
|
||||
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
|
||||
github.com/armon/circbuf v0.0.0-20190214190532-5111143e8da2 h1:7Ip0wMmLHLRJdrloDxZfhMm0xrLXZS8+COSu2bXmEQs=
|
||||
github.com/armon/circbuf v0.0.0-20190214190532-5111143e8da2/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
|
||||
@@ -73,6 +85,8 @@ github.com/armon/go-metrics v0.4.1/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+
|
||||
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
|
||||
github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI=
|
||||
github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
|
||||
github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b h1:uUXgbcPDK3KpW29o4iy7GtuappbWT0l5NaMo9H9pJDw=
|
||||
github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A=
|
||||
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
|
||||
@@ -112,6 +126,7 @@ github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/
|
||||
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
||||
github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o=
|
||||
github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
||||
github.com/beorn7/perks v0.0.0-20150223135152-b965b613227f/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
@@ -119,6 +134,9 @@ github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6r
|
||||
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
|
||||
github.com/bgentry/speakeasy v0.2.0 h1:tgObeVOf8WAvtuAX6DhJ4xks4CFNwPDZiqzGqIHE51E=
|
||||
github.com/bgentry/speakeasy v0.2.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
|
||||
github.com/bitly/go-hostpool v0.1.0/go.mod h1:4gOCgp6+NZnVqlKyZ/iBZFTAJKembaVENUpMkpg42fw=
|
||||
github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA=
|
||||
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
|
||||
github.com/bsm/ginkgo/v2 v2.7.0/go.mod h1:AiKlXPm7ItEHNc/2+OkrNG4E0ITzojb9/xWzvQ9XZ9w=
|
||||
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
||||
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
|
||||
@@ -127,6 +145,12 @@ github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
||||
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
|
||||
github.com/buger/goterm v1.0.4 h1:Z9YvGmOih81P0FbVtEYTFF6YsSgxSUKEhf/f9bTMXbY=
|
||||
github.com/buger/goterm v1.0.4/go.mod h1:HiFWV3xnkolgrBV3mY8m0X0Pumt4zg4QhbdOzQtB8tE=
|
||||
github.com/bugsnag/bugsnag-go v1.0.5-0.20150529004307-13fd6b8acda0 h1:s7+5BfS4WFJoVF9pnB8kBk03S7pZXRdKamnV0FOl5Sc=
|
||||
github.com/bugsnag/bugsnag-go v1.0.5-0.20150529004307-13fd6b8acda0/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8=
|
||||
github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b h1:otBG+dV+YK+Soembjv71DPz3uX/V/6MMlSyD9JBQ6kQ=
|
||||
github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b/go.mod h1:obH5gd0BsqsP2LwDJ9aOkm/6J86V6lyAXCoQWGw3K50=
|
||||
github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0 h1:nvj0OLI3YqYXer/kZD8Ri1aaunCxIEsOst1BVJswV0o=
|
||||
github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE=
|
||||
github.com/caddyserver/caddy/v2 v2.8.4 h1:q3pe0wpBj1OcHFZ3n/1nl4V4bxBrYoSoab7rL9BMYNk=
|
||||
github.com/caddyserver/caddy/v2 v2.8.4/go.mod h1:vmDAHp3d05JIvuhc24LmnxVlsZmWnUwbP5WMjzcMPWw=
|
||||
github.com/caddyserver/certmagic v0.21.4 h1:e7VobB8rffHv8ZZpSiZtEwnLDHUwLVYLWzWSa1FfKI0=
|
||||
@@ -176,6 +200,8 @@ github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38
|
||||
github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag=
|
||||
github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cloudflare/cfssl v0.0.0-20180223231731-4e2dcbde5004 h1:lkAMpLVBDaj17e85keuznYcH5rqI438v41pKcBl4ZxQ=
|
||||
github.com/cloudflare/cfssl v0.0.0-20180223231731-4e2dcbde5004/go.mod h1:yMWuSON2oQp+43nFtAV/uvKQIFpSPerB57DCt9t8sSA=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I=
|
||||
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
|
||||
@@ -230,13 +256,16 @@ github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6N
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
|
||||
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
|
||||
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
|
||||
github.com/cskr/pubsub v1.0.2 h1:vlOzMhl6PFn60gRlTQQsIfVwaPB/B/8MziK8FhEPt/0=
|
||||
github.com/cskr/pubsub v1.0.2/go.mod h1:/8MzYXk/NJAz782G8RPkFzXTZVu63VotefPnR9TIRis=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c h1:pFUpOrbxDR6AkioZ1ySsx5yxlDQZ8stG2b88gTPxgJU=
|
||||
github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c/go.mod h1:6UhI8N9EjYm1c2odKpFpAYeR8dsBeM7PtzQhRgxRr9U=
|
||||
github.com/deckarep/golang-set/v2 v2.8.0 h1:swm0rlPCmdWn9mESxKOjWk8hXSqoxOp+ZlfuyaAdFlQ=
|
||||
@@ -245,6 +274,7 @@ github.com/decred/dcrd/crypto/blake256 v1.0.1 h1:7PltbUIQB7u/FfZ39+DGa/ShuMyJ5il
|
||||
github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnNEcHYvcCuK6dPZSg=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0=
|
||||
github.com/denisenkom/go-mssqldb v0.0.0-20191128021309-1d7a30a10f73/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
|
||||
github.com/dgraph-io/badger v1.6.2 h1:mNw0qs90GVgGGWylh0umH5iag1j6n/PeJtNvL6KY/x8=
|
||||
github.com/dgraph-io/badger v1.6.2/go.mod h1:JW2yswe3V058sS0kZ2h/AXeDSqFjxnZcRrVH//y2UQE=
|
||||
github.com/dgraph-io/badger/v2 v2.2007.4 h1:TRWBQg8UrlUhaFdco01nO2uXwzKS7zd+HVdwV/GHc4o=
|
||||
@@ -270,8 +300,11 @@ github.com/docker/buildx v0.29.1 h1:58hxM5Z4mnNje3G5NKfULT9xCr8ooM8XFtlfUK9bKaA=
|
||||
github.com/docker/buildx v0.29.1/go.mod h1:J4EFv6oxlPiV1MjO0VyJx2u5tLM7ImDEl9zyB8d4wPI=
|
||||
github.com/docker/cli v28.5.0+incompatible h1:crVqLrtKsrhC9c00ythRx435H8LiQnUKRtJLRR+Auxk=
|
||||
github.com/docker/cli v28.5.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
github.com/docker/cli-docs-tool v0.10.0 h1:bOD6mKynPQgojQi3s2jgcUWGp/Ebqy1SeCr9VfKQLLU=
|
||||
github.com/docker/cli-docs-tool v0.10.0/go.mod h1:5EM5zPnT2E7yCLERZmrDA234Vwn09fzRHP4aX1qwp1U=
|
||||
github.com/docker/compose/v2 v2.40.0 h1:y6wuWuxvoIHgDdo9J12OGnXAQmLviRInwFKSEqKW5Cs=
|
||||
github.com/docker/compose/v2 v2.40.0/go.mod h1:AZk82SDjtT2YI+7fXLjm8U0jxZU3Xrr0XOfL7OIsP4M=
|
||||
github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
|
||||
github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk=
|
||||
github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
|
||||
github.com/docker/docker v28.5.0+incompatible h1:ZdSQoRUE9XxhFI/B8YLvhnEFMmYN9Pp8Egd2qcaFk1E=
|
||||
@@ -280,10 +313,12 @@ github.com/docker/docker-credential-helpers v0.9.3 h1:gAm/VtF9wgqJMoxzT3Gj5p4AqI
|
||||
github.com/docker/docker-credential-helpers v0.9.3/go.mod h1:x+4Gbw9aGmChi3qTLZj8Dfn0TD20M/fuWy0E5+WDeCo=
|
||||
github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c h1:lzqkGL9b3znc+ZUgi7FlLnqjQhcXxkNM/quxIjBVMD0=
|
||||
github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c/go.mod h1:CADgU4DSXK5QUlFslkQu2yW2TKzFZcXq/leZfM0UH5Q=
|
||||
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
|
||||
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
|
||||
github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
|
||||
github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c h1:+pKlWGMw7gf6bQ+oDZB4KHQFypsfjYlq/C4rfL7D3g8=
|
||||
github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA=
|
||||
github.com/docker/go-metrics v0.0.0-20180209012529-399ea8c73916/go.mod h1:/u0gXw0Gay3ceNrsHubL3BtdOL2fHf93USgMTe0W5dI=
|
||||
github.com/docker/go-metrics v0.0.1 h1:AgB/0SvBxihN0X8OR4SjsblXkbMvalQ8cjmtKQ2rQV8=
|
||||
github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw=
|
||||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||
@@ -293,14 +328,20 @@ github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7/go.mod h1:cyGadeNE
|
||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/dvsekhvalnov/jose2go v0.0.0-20170216131308-f21a8cedbbae/go.mod h1:7BvyPhdbLxMXIYTFPLsyJRFMsKmOZnQmzh6Gb+uquuM=
|
||||
github.com/eiannone/keyboard v0.0.0-20220611211555-0d226195f203 h1:XBBHcIb256gUJtLmY22n99HaZTz+r2Z51xUPi01m3wg=
|
||||
github.com/eiannone/keyboard v0.0.0-20220611211555-0d226195f203/go.mod h1:E1jcSv8FaEny+OP/5k9UxZVw9YFWGj7eI4KR/iOBqCg=
|
||||
github.com/elastic/gosigar v0.14.3 h1:xwkKwPia+hSfg9GqrCUKYdId102m9qTJIIr7egmK/uo=
|
||||
github.com/elastic/gosigar v0.14.3/go.mod h1:iXRIGg2tLnu7LBdpqzyQfGDEidKCfWcCMS0WKyPWoMs=
|
||||
github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g=
|
||||
github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
|
||||
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0=
|
||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
|
||||
github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4=
|
||||
@@ -313,9 +354,15 @@ github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJn
|
||||
github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fsnotify/fsevents v0.2.0 h1:BRlvlqjvNTfogHfeBOFvSC9N0Ddy+wzQCQukyoD7o/c=
|
||||
github.com/fsnotify/fsevents v0.2.0/go.mod h1:B3eEk39i4hz8y1zaWS/wPrAP4O6wkIl7HQwKBr1qH/w=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/fvbommel/sortorder v1.1.0 h1:fUmoe+HLsBTctBDoaBwpQo5N+nrCp8g/BjKb/6ZQmYw=
|
||||
github.com/fvbommel/sortorder v1.1.0/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0=
|
||||
github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
|
||||
github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
|
||||
github.com/go-jose/go-jose/v3 v3.0.3 h1:fFKWeig/irsp7XD2zBxvnmA/XaRWp5V3CBsZXJF7G7k=
|
||||
github.com/go-jose/go-jose/v3 v3.0.3/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=
|
||||
github.com/go-kit/kit v0.4.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
@@ -337,6 +384,15 @@ github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=
|
||||
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
|
||||
github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
|
||||
github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE=
|
||||
github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k=
|
||||
github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
|
||||
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
|
||||
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
|
||||
github.com/go-sql-driver/mysql v1.3.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
||||
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
|
||||
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
||||
github.com/go-stack/stack v1.6.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
@@ -354,9 +410,13 @@ github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E=
|
||||
github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0=
|
||||
github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw=
|
||||
github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||
github.com/gogo/protobuf v1.0.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/glog v1.2.5 h1:DrW6hGnjIhtvhOIiAKT6Psh/Kd/ldepEa81DKeiRJ5I=
|
||||
github.com/golang/glog v1.2.5/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w=
|
||||
@@ -368,6 +428,7 @@ github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfb
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
@@ -385,11 +446,14 @@ github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU=
|
||||
github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
|
||||
github.com/google/cel-go v0.20.1 h1:nDx9r8S3L4pE61eDdt8igGj8rf5kjYR3ILxWIpWNi84=
|
||||
github.com/google/cel-go v0.20.1/go.mod h1:kWcIzTsPX0zmQ+H3TirHstLLf9ep5QTsZBN9u4dOYLg=
|
||||
github.com/google/certificate-transparency-go v1.0.10-0.20180222191210-5ab67e519c93/go.mod h1:QeJfpSbVSfYc7RgB3gJFj9cbuQMMchQxrWXz8Ruopmg=
|
||||
github.com/google/certificate-transparency-go v1.1.8-0.20240110162603-74a5dd331745 h1:heyoXNxkRT155x4jTAiSv5BVSVkueifPUm+Q8LUXMRo=
|
||||
github.com/google/certificate-transparency-go v1.1.8-0.20240110162603-74a5dd331745/go.mod h1:zN0wUQgV9LjwLZeFHnrAbQi8hzMVvEWePyk+MhPOk7k=
|
||||
github.com/google/flatbuffers v1.12.1/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
|
||||
github.com/google/flatbuffers v24.3.25+incompatible h1:CX395cjN9Kke9mmalRoL3d81AtFUxJM+yDthflgJGkI=
|
||||
github.com/google/flatbuffers v24.3.25+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
|
||||
github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I=
|
||||
github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
@@ -410,6 +474,8 @@ github.com/google/go-tpm-tools v0.4.4/go.mod h1:T8jXkp2s+eltnCDIsXR84/MTcVU9Ja7b
|
||||
github.com/google/go-tspi v0.3.0 h1:ADtq8RKfP+jrTyIWIZDIYcKOMecRqNJFOew2IT0Inus=
|
||||
github.com/google/go-tspi v0.3.0/go.mod h1:xfMGI3G0PhxCdNVcYr1C4C+EizojDg/TXuX5by8CiHI=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
|
||||
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8=
|
||||
github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo=
|
||||
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8=
|
||||
@@ -432,16 +498,21 @@ github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c h1:7lF+Vz0LqiRid
|
||||
github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE=
|
||||
github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w=
|
||||
github.com/gorilla/mux v1.7.0/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
||||
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 h1:e9Rjr40Z98/clHv5Yg79Is0NtosR5LXRvdr7o/6NwbA=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1/go.mod h1:tIxuGz/9mpox++sgp9fJjHO0+q1X9/UOWd798aAm22M=
|
||||
github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed h1:5upAirOpQc1Q53c0bnx2ufif5kANL7bfZWcc6VJWJd8=
|
||||
github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
|
||||
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
|
||||
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
|
||||
github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc=
|
||||
github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
|
||||
@@ -475,6 +546,7 @@ github.com/hashicorp/golang-lru/arc/v2 v2.0.7 h1:QxkVTxwColcduO+LP7eJO56r2hFiG8z
|
||||
github.com/hashicorp/golang-lru/arc/v2 v2.0.7/go.mod h1:Pe7gBlGdc8clY5LJ0LpJXMt5AmgmWNH1g+oFFVUHOEc=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI65Y=
|
||||
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
|
||||
@@ -488,6 +560,9 @@ github.com/hashicorp/serf v0.10.1 h1:Z1H2J60yRKvfDYAOZLd2MU0ND4AH/WDz7xYHDWQsIPY
|
||||
github.com/hashicorp/serf v0.10.1/go.mod h1:yL2t6BqATOLGc5HF7qbFkTfXoPIY0WZdWHfEvMqbG+4=
|
||||
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
|
||||
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
|
||||
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog=
|
||||
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
||||
github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
||||
github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
||||
@@ -503,6 +578,8 @@ github.com/in-toto/in-toto-golang v0.9.0/go.mod h1:xsBVrVsHNsB61++S6Dy2vWosKhuA3
|
||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/inhies/go-bytesize v0.0.0-20220417184213-4913239db9cf h1:FtEj8sfIcaaBfAKrE1Cwb61YDtYq9JxChK1c7AKce7s=
|
||||
github.com/inhies/go-bytesize v0.0.0-20220417184213-4913239db9cf/go.mod h1:yrqSXGoD/4EKfF26AOGzscPOgTTJcyAwM2rpixWT+t4=
|
||||
github.com/ipfs/bbloom v0.0.4 h1:Gi+8EGJ2y5qiD5FbsbpX/TMNcJw8gSqr7eyjHa4Fhvs=
|
||||
github.com/ipfs/bbloom v0.0.4/go.mod h1:cS9YprKXpoZ9lT0n/Mw/a6/aFV6DTjTLYHeA+gyqMG0=
|
||||
github.com/ipfs/boxo v0.21.0 h1:XpGXb+TQQ0IUdYaeAxGzWjSs6ow/Lce148A/2IbRDVE=
|
||||
@@ -600,16 +677,30 @@ github.com/jbenet/go-temp-err-catcher v0.1.0/go.mod h1:0kJRvmDZXNMIiJirNPEYfhpPw
|
||||
github.com/jbenet/goprocess v0.1.4 h1:DRGOFReOMqqDNXwW70QkacFW0YN9QnwLV0Vqk+3oU0o=
|
||||
github.com/jbenet/goprocess v0.1.4/go.mod h1:5yspPrukOVuOLORacaBi858NqyClJPQxYZlqdZVfqY4=
|
||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
github.com/jinzhu/gorm v0.0.0-20170222002820-5409931a1bb8 h1:CZkYfurY6KGhVtlalI4QwQ6T0Cu6iuY3e0x5RLu96WE=
|
||||
github.com/jinzhu/gorm v0.0.0-20170222002820-5409931a1bb8/go.mod h1:Vla75njaFJ8clLU1W44h34PjIkijhjHIYnZxMqCdxqo=
|
||||
github.com/jinzhu/inflection v0.0.0-20170102125226-1c35d901db3d h1:jRQLvyVGL+iVtDElaEIDdKwpPqUIZJfzkNLV34htpEc=
|
||||
github.com/jinzhu/inflection v0.0.0-20170102125226-1c35d901db3d/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
|
||||
github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
|
||||
github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I=
|
||||
github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=
|
||||
github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
|
||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
|
||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/juju/loggo v0.0.0-20190526231331-6e530bcce5d8/go.mod h1:vgyd7OREkbtVEN/8IXZe5Ooef3LQePvuBm9UWj6ZL8U=
|
||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
|
||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.12.3/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=
|
||||
@@ -624,6 +715,7 @@ github.com/koron/go-ssdp v0.0.4/go.mod h1:oDXq+E5IL5q0U8uSBcoAXzTzInwy5lEgC91HoK
|
||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
@@ -637,6 +729,7 @@ github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq
|
||||
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o=
|
||||
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk=
|
||||
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw=
|
||||
github.com/lib/pq v0.0.0-20150723085316-0dad96c0b94f/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
@@ -673,13 +766,19 @@ github.com/lmittmann/tint v1.0.5 h1:NQclAutOfYsqs2F1Lenue6OoWCajs5wJcP3DfWVpePw=
|
||||
github.com/lmittmann/tint v1.0.5/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/magiconair/properties v1.5.3/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/magiconair/properties v1.8.9 h1:nWcCbLq1N2v/cpNsy5WvQ37Fb+YElfq20WJ/a8RkpQM=
|
||||
github.com/magiconair/properties v1.8.9/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA=
|
||||
github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg=
|
||||
github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd h1:br0buuQ854V8u83wA0rVZ8ttrq5CpaPZdvrK0LP2lOk=
|
||||
github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd/go.mod h1:QuCEs1Nt24+FYQEqAAncTDPJIuGs+LxK1MCiFL25pMU=
|
||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
|
||||
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
@@ -700,6 +799,7 @@ github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6T
|
||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk=
|
||||
github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
|
||||
github.com/mattn/go-sqlite3 v1.6.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
||||
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
@@ -709,6 +809,7 @@ github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/
|
||||
github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw=
|
||||
github.com/mdlayher/socket v0.5.1 h1:VZaqt6RkGkt2OE9l3GcC6nZkqD3xKeQLyfleW/uBcos=
|
||||
github.com/mdlayher/socket v0.5.1/go.mod h1:TjPLHI1UgwEv5J1B5q0zTZq12A/6H7nKmtTanQE37IQ=
|
||||
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
|
||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=
|
||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
|
||||
github.com/mholt/acmez/v2 v2.0.3 h1:CgDBlEwg3QBp6s45tPQmFIBrkRIkBT4rW4orMM6p4sw=
|
||||
@@ -717,6 +818,7 @@ github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKju
|
||||
github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI=
|
||||
github.com/miekg/dns v1.1.65 h1:0+tIPHzUW0GCge7IiK3guGP57VAw7hoPDfApjkMD1Fc=
|
||||
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.1.1 h1:Ugu9pdy6vAYku5DEpVWVFPYnzV+bxB+iRdbuFSu7TvU=
|
||||
github.com/miekg/pkcs11 v1.1.1/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs=
|
||||
github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721 h1:RlZweED6sbSArvlE924+mUcZuXKLBHA35U7LN621Bws=
|
||||
@@ -741,6 +843,7 @@ github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc
|
||||
github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg=
|
||||
github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4=
|
||||
github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE=
|
||||
github.com/mitchellh/mapstructure v0.0.0-20150613213606-2caf8efc9366/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
@@ -758,14 +861,20 @@ github.com/moby/locker v1.0.1 h1:fOXqR41zeveg4fFODix+1Ch4mj/gT0NE1XJbp/epuBg=
|
||||
github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc=
|
||||
github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk=
|
||||
github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
|
||||
github.com/moby/spdystream v0.5.0 h1:7r0J1Si3QO/kjRitvSLVVFUjxMEb/YLj6S9FF62JBCU=
|
||||
github.com/moby/spdystream v0.5.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI=
|
||||
github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw=
|
||||
github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs=
|
||||
github.com/moby/sys/capability v0.4.0 h1:4D4mI6KlNtWMCM1Z/K0i7RV1FkX+DBDHKVJpCndZoHk=
|
||||
github.com/moby/sys/capability v0.4.0/go.mod h1:4g9IK291rVkms3LKCDOoYlnV8xKwoDTpIrNEE35Wq0I=
|
||||
github.com/moby/sys/mountinfo v0.7.2 h1:1shs6aH5s4o5H2zQLn796ADW1wMrIwHsyJ2v9KouLrg=
|
||||
github.com/moby/sys/mountinfo v0.7.2/go.mod h1:1YOa8w8Ih7uW0wALDUgT1dTTSBrZ+HiBLGws92L2RU4=
|
||||
github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
|
||||
github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
|
||||
github.com/moby/sys/signal v0.7.1 h1:PrQxdvxcGijdo6UXXo/lU/TvHUWyPhj7UOpSo8tuvk0=
|
||||
github.com/moby/sys/signal v0.7.1/go.mod h1:Se1VGehYokAkrSQwL4tDzHvETwUZlnY7S5XtQ50mQp8=
|
||||
github.com/moby/sys/symlink v0.3.0 h1:GZX89mEZ9u53f97npBy4Rc3vJKj7JBDj/PN2I22GrNU=
|
||||
github.com/moby/sys/symlink v0.3.0/go.mod h1:3eNdhduHmYPcgsJtZXW1W4XUJdZGBIkttZ8xKqPUJq0=
|
||||
github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs=
|
||||
github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs=
|
||||
github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g=
|
||||
@@ -773,9 +882,12 @@ github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcY
|
||||
github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
|
||||
github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
|
||||
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
||||
github.com/mr-tron/base58 v1.1.2/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc=
|
||||
@@ -812,20 +924,32 @@ github.com/multiformats/go-varint v0.0.7/go.mod h1:r8PUYw/fD/SjBCiKOoDlGF6QawOEL
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus=
|
||||
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
|
||||
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
|
||||
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.12.0/go.mod h1:oUhWkIvk5aDxtKvDDuw8gItl8pKl42LzjC9KZE0HfGg=
|
||||
github.com/onsi/ginkgo/v2 v2.22.0 h1:Yed107/8DjTr0lKCNt7Dn8yQ6ybuDRQoMGrNFKzMfHg=
|
||||
github.com/onsi/ginkgo/v2 v2.22.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo=
|
||||
github.com/onsi/gomega v1.34.2 h1:pNCwDkzrsv7MS9kpaQvVb1aVLahQXyJ/Tv5oAZMI3i8=
|
||||
github.com/onsi/gomega v1.34.2/go.mod h1:v1xfxRgk0KIsG+QOdm7p8UosrOzPYRo60fd3B/1Dukc=
|
||||
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
||||
github.com/onsi/gomega v1.9.0/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA=
|
||||
github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4=
|
||||
github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog=
|
||||
github.com/opencontainers/go-digest v0.0.0-20170106003457-a6d0ee40d420/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
|
||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||
github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
|
||||
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
|
||||
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
|
||||
github.com/opencontainers/runtime-spec v1.2.1 h1:S4k4ryNgEpxW1dzyqffOmhI1BHYcjzU8lpJfSlR0xww=
|
||||
github.com/opencontainers/runtime-spec v1.2.1/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
|
||||
github.com/opencontainers/selinux v1.12.0 h1:6n5JV4Cf+4y0KNXW48TLj5DwfXpvWlxXplUkdTrmPb8=
|
||||
github.com/opencontainers/selinux v1.12.0/go.mod h1:BTPX+bjVbWGXw7ZZWUbdENt8w0htPSrlgOOysQaU62U=
|
||||
github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
|
||||
github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs=
|
||||
github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc=
|
||||
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||
github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY=
|
||||
github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||
@@ -834,6 +958,7 @@ github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhM
|
||||
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
|
||||
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
|
||||
github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI=
|
||||
github.com/peterbourgon/diskv/v3 v3.0.1 h1:x06SQA46+PKIUftmEujdwSEpIx8kR+M9eLYsUxeYveU=
|
||||
github.com/peterbourgon/diskv/v3 v3.0.1/go.mod h1:kJ5Ny7vLdARGU3WUuy6uzO6T0nb/2gWcT1JiBvRmb5o=
|
||||
github.com/pion/datachannel v1.5.6 h1:1IxKJntfSlYkpUj8LlYRSWpYiTTC02nUrOE8T3DqGeg=
|
||||
@@ -886,23 +1011,27 @@ github.com/posener/complete v1.2.3 h1:NP0eAhjcjImqslEwo/1hq7gpajME0fTLTezBKDqfXq
|
||||
github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s=
|
||||
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
|
||||
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
|
||||
github.com/prometheus/client_golang v0.9.0-pre1.0.20180209125602-c332b6f63c06/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
|
||||
github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g=
|
||||
github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU=
|
||||
github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
|
||||
github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
|
||||
github.com/prometheus/client_model v0.0.0-20171117100541-99fa1f4be8e5/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
|
||||
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
|
||||
github.com/prometheus/common v0.0.0-20180110214958-89604d197083/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
|
||||
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||
github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc=
|
||||
github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4=
|
||||
github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io=
|
||||
github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
|
||||
github.com/prometheus/procfs v0.0.0-20180125133057-cb4147076ac7/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||
github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ=
|
||||
@@ -953,6 +1082,8 @@ github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUt
|
||||
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
|
||||
github.com/secure-systems-lab/go-securesystemslib v0.6.0 h1:T65atpAVCJQK14UA57LMdZGpHi4QYSH/9FZyNGqMYIA=
|
||||
github.com/secure-systems-lab/go-securesystemslib v0.6.0/go.mod h1:8Mtpo9JKks/qhPG4HGZ2LGMvrPbzuxwfz/f/zLfEWkk=
|
||||
github.com/serialx/hashring v0.0.0-20200727003509-22c0c7ab6b1b h1:h+3JX2VoWTFuyQEo87pStk/a99dzIO1mM9KxIyLPGTU=
|
||||
github.com/serialx/hashring v0.0.0-20200727003509-22c0c7ab6b1b/go.mod h1:/yeG0My1xr/u+HZrFQ1tOQQQQrOawfyMUH13ai5brBc=
|
||||
github.com/shibumi/go-pathspec v1.3.0 h1:QUyMZhFo0Md5B8zV8x2tesohbb5kfbpTi9rBnKh5dkI=
|
||||
github.com/shibumi/go-pathspec v1.3.0/go.mod h1:Xutfslp817l2I1cZvgcfeMQJG5QnU2lh5tVaaMCl3jE=
|
||||
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
|
||||
@@ -969,12 +1100,15 @@ github.com/siderolabs/gen v0.4.8 h1:VNpbmDLhkXp7qcSEkKk1Ee7vU2afs3xvHrWLGR2UuiY=
|
||||
github.com/siderolabs/gen v0.4.8/go.mod h1:7ROKVHHB68R3Amrd4a1ZXz/oMrXWF3Mg3lSEgnkJY5c=
|
||||
github.com/siderolabs/grpc-proxy v0.5.1 h1:WTZYLMPTZPt43BzEJ02LT9kYA9qAfquWwCezc6NPPYE=
|
||||
github.com/siderolabs/grpc-proxy v0.5.1/go.mod h1:EQwE87LiWxhiIUPBeWmpjJb9DIWxWID8R6ARtdTC+8A=
|
||||
github.com/sirupsen/logrus v1.0.6/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 h1:JIAuq3EEf9cgbU6AtGPK4CTG3Zf6CKMNqf0MHTggAUA=
|
||||
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog=
|
||||
github.com/slackhq/nebula v1.6.1 h1:/OCTR3abj0Sbf2nGoLUrdDXImrCv0ZVFpVPP5qa0DsM=
|
||||
github.com/slackhq/nebula v1.6.1/go.mod h1:UmkqnXe4O53QwToSl/gG7sM4BroQwAB7dd4hUaT6MlI=
|
||||
github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262 h1:unQFBIznI+VYD1/1fApl1A+9VcBk+9dcqGfnePY87LY=
|
||||
@@ -1001,19 +1135,27 @@ github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0b
|
||||
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||
github.com/spdx/tools-golang v0.5.5 h1:61c0KLfAcNqAjlg6UNMdkwpMernhw3zVRwDZ2x9XOmk=
|
||||
github.com/spdx/tools-golang v0.5.5/go.mod h1:MVIsXx8ZZzaRWNQpUDhC4Dud34edUYJYecciXgrw5vE=
|
||||
github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI=
|
||||
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
||||
github.com/spf13/cast v0.0.0-20150508191742-4d07383ffe94/go.mod h1:r2rcYCSwa1IExKTDiTfzaxqT2FNHs8hODu4LnUfgKEg=
|
||||
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||
github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w=
|
||||
github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||
github.com/spf13/cobra v0.0.1/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
|
||||
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
|
||||
github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
|
||||
github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
|
||||
github.com/spf13/jwalterweatherman v0.0.0-20141219030609-3d60171a6431/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
||||
github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk=
|
||||
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
||||
github.com/spf13/pflag v1.0.0/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
|
||||
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v0.0.0-20150530192845-be5ff3e4840c/go.mod h1:A8kyI5cUJhb8N+3pkfONlcEcZbueH6nhAm0Fq7SrnBM=
|
||||
github.com/spf13/viper v1.3.2 h1:VUFqw5KcqRf7i70GOzW7N+Q7+gxVBkSSqiXB12+JQ4M=
|
||||
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
|
||||
github.com/stoewer/go-strcase v1.2.0 h1:Z2iHWqGXH00XYgqDmNgQbIBxf3wrNq0F3feEy0ainaU=
|
||||
github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8=
|
||||
@@ -1041,6 +1183,8 @@ github.com/tailscale/tscert v0.0.0-20240517230440-bbccfbf48933 h1:pV0H+XIvFoP7pl
|
||||
github.com/tailscale/tscert v0.0.0-20240517230440-bbccfbf48933/go.mod h1:kNGUQ3VESx3VZwRwA9MSCUegIl6+saPL8Noq82ozCaU=
|
||||
github.com/theupdateframework/notary v0.7.0 h1:QyagRZ7wlSpjT5N2qQAh/pN+DVqgekv4DzbAiAiEL3c=
|
||||
github.com/theupdateframework/notary v0.7.0/go.mod h1:c9DRxcmhHmVLDay4/2fUYdISnHqbFDGRSlXPO0AhYWw=
|
||||
github.com/tilt-dev/fsnotify v1.4.8-0.20220602155310-fff9c274a375 h1:QB54BJwA6x8QU9nHY3xJSZR2kX9bgpZekRKGkLTmEXA=
|
||||
github.com/tilt-dev/fsnotify v1.4.8-0.20220602155310-fff9c274a375/go.mod h1:xRroudyp5iVtxKqZCrA6n2TLFRBf8bmnjr1UD4x+z7g=
|
||||
github.com/tonistiigi/dchapes-mode v0.0.0-20250318174251-73d941a28323 h1:r0p7fK56l8WPequOaR3i9LBqfPtEdXIQbUTzT55iqT4=
|
||||
github.com/tonistiigi/dchapes-mode v0.0.0-20250318174251-73d941a28323/go.mod h1:3Iuxbr0P7D3zUzBMAZB+ois3h/et0shEz0qApgHYGpY=
|
||||
github.com/tonistiigi/fsutil v0.0.0-20250605211040-586307ad452f h1:MoxeMfHAe5Qj/ySSBfL8A7l1V+hxuluj8owsIEEZipI=
|
||||
@@ -1066,6 +1210,8 @@ github.com/warpfork/go-testmark v0.12.1 h1:rMgCpJfwy1sJ50x0M0NgyphxYYPMOODIJHhsX
|
||||
github.com/warpfork/go-testmark v0.12.1/go.mod h1:kHwy7wfvGSPh1rQJYKayD4AbtNaeyZdcGi9tNJTaa5Y=
|
||||
github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0 h1:GDDkbFiaK8jsSDJfjId/PEGEShv6ugrt4kYsC5UIDaQ=
|
||||
github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0/go.mod h1:x6AKhvSSexNrVSrViXSHUEbICjmGXhtgABaHIySUSGw=
|
||||
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||
github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc=
|
||||
github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||
@@ -1075,6 +1221,8 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/zclconf/go-cty v1.17.0 h1:seZvECve6XX4tmnvRzWtJNHdscMtYEx5R7bnnVyd/d0=
|
||||
github.com/zclconf/go-cty v1.17.0/go.mod h1:wqFzcImaLTI6A5HfsRwB0nj5n0MRZFwmey8YoFPPs3U=
|
||||
github.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY=
|
||||
github.com/zeebo/assert v1.1.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
|
||||
github.com/zeebo/blake3 v0.2.4 h1:KYQPkhpRtcqh0ssGYcKLG1JYvddkEA8QwCM/yBqhaZI=
|
||||
@@ -1181,15 +1329,18 @@ go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/W
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200414173820-0848c9571904/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20201117144127-c1f2f97bffc9/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||
golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
@@ -1224,6 +1375,7 @@ golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
|
||||
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
|
||||
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-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
@@ -1269,6 +1421,7 @@ golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
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-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@@ -1284,6 +1437,7 @@ golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -1298,6 +1452,7 @@ golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
@@ -1395,6 +1550,7 @@ google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a h1:
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a/go.mod h1:a77HrdMjoeKbnd2jmgcWdaS++ZLZAEq3orIOAEIKiVw=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a h1:v2PbRU4K3llS09c7zodFpNePeamkAwG3mPrAery9VeE=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
|
||||
google.golang.org/grpc v1.0.5/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
@@ -1414,16 +1570,31 @@ google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpAD
|
||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
|
||||
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
||||
gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
gopkg.in/cenkalti/backoff.v2 v2.2.1 h1:eJ9UAg01/HIHG987TwxvnzK2MgxXq97YY6rYDpY9aII=
|
||||
gopkg.in/cenkalti/backoff.v2 v2.2.1/go.mod h1:S0QdOvT2AlerfSBkp0O+dk+bbIMaNbEmVk876gPCthU=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4=
|
||||
gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo=
|
||||
gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s=
|
||||
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
|
||||
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
||||
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
|
||||
gopkg.in/rethinkdb/rethinkdb-go.v6 v6.2.1 h1:d4KQkxAaAiRY2h5Zqis161Pv91A37uZyJOx73duwUwM=
|
||||
gopkg.in/rethinkdb/rethinkdb-go.v6 v6.2.1/go.mod h1:WbjuEoo1oadwzQ4apSDU+JTvmllEHtsNHS6y7vFc7iw=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
@@ -1446,6 +1617,18 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh
|
||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||
howett.net/plist v1.0.0 h1:7CrbWYbPPO/PyNy38b2EB/+gYbjCe2DXBxgtOOZbSQM=
|
||||
howett.net/plist v1.0.0/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g=
|
||||
k8s.io/api v0.32.3 h1:Hw7KqxRusq+6QSplE3NYG4MBxZw1BZnq4aP4cJVINls=
|
||||
k8s.io/api v0.32.3/go.mod h1:2wEDTXADtm/HA7CCMD8D8bK4yuBUptzaRhYcYEEYA3k=
|
||||
k8s.io/apimachinery v0.32.3 h1:JmDuDarhDmA/Li7j3aPrwhpNBA94Nvk5zLeOge9HH1U=
|
||||
k8s.io/apimachinery v0.32.3/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE=
|
||||
k8s.io/client-go v0.32.3 h1:RKPVltzopkSgHS7aS98QdscAgtgah/+zmpAogooIqVU=
|
||||
k8s.io/client-go v0.32.3/go.mod h1:3v0+3k4IcT9bXTc4V2rt+d2ZPPG700Xy6Oi0Gdl2PaY=
|
||||
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
|
||||
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
|
||||
k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f h1:GA7//TjRY9yWGy1poLzYYJJ4JRdzg3+O6e8I+e+8T5Y=
|
||||
k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f/go.mod h1:R/HEjbvWI0qdfb8viZUeVZm0X6IZnxAydC7YU42CMw4=
|
||||
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro=
|
||||
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
|
||||
lukechampine.com/blake3 v1.3.0 h1:sJ3XhFINmHSrYCgl958hscfIa3bw8x4DqMP3u1YvoYE=
|
||||
lukechampine.com/blake3 v1.3.0/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k=
|
||||
modernc.org/cc/v4 v4.24.4 h1:TFkx1s6dCkQpd6dKurBNmpo+G8Zl4Sq/ztJ+2+DEsh0=
|
||||
@@ -1472,3 +1655,11 @@ modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8=
|
||||
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo=
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.4.2 h1:MdmvkGuXi/8io6ixD5wud3vOLwc1rj0aNqRlpuvjmwA=
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.4.2/go.mod h1:N8f93tFZh9U6vpxwRArLiikrE5/2tiu1w1AGfACIGE4=
|
||||
sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
|
||||
sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
|
||||
tags.cncf.io/container-device-interface v1.0.1 h1:KqQDr4vIlxwfYh0Ed/uJGVgX+CHAkahrgabg6Q8GYxc=
|
||||
tags.cncf.io/container-device-interface v1.0.1/go.mod h1:JojJIOeW3hNbcnOH2q0NrWNha/JuHoDZcmYxAZwb2i0=
|
||||
|
||||
+178
-154
@@ -2,176 +2,200 @@ package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
composetypes "github.com/compose-spec/compose-go/v2/types"
|
||||
"github.com/distribution/reference"
|
||||
"github.com/docker/cli/cli/config"
|
||||
"github.com/docker/docker/api/types/build"
|
||||
"github.com/docker/docker/api/types/image"
|
||||
dockerclient "github.com/docker/docker/client"
|
||||
"github.com/docker/docker/pkg/archive"
|
||||
"github.com/docker/docker/pkg/jsonmessage"
|
||||
"github.com/docker/docker/registry"
|
||||
"github.com/moby/term"
|
||||
mapset "github.com/deckarep/golang-set/v2"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/flags"
|
||||
composeapi "github.com/docker/compose/v2/pkg/api"
|
||||
composev2 "github.com/docker/compose/v2/pkg/compose"
|
||||
"github.com/docker/compose/v2/pkg/progress"
|
||||
"github.com/psviderski/uncloud/pkg/client"
|
||||
"github.com/psviderski/uncloud/pkg/client/compose"
|
||||
)
|
||||
|
||||
type BuildOptions struct {
|
||||
Files []string
|
||||
Profiles []string
|
||||
Services []string
|
||||
Push bool
|
||||
// BuildServicesOptions contains options for building services in a Compose project.
|
||||
type BuildServicesOptions struct {
|
||||
// BuildArgs sets build-time variables for services. Used in Dockerfiles that declare variables with ARG.
|
||||
BuildArgs []string
|
||||
// Check the build configuration for services without building them.
|
||||
Check bool
|
||||
// Deps enables to also build services declared as dependencies of the selected Services.
|
||||
Deps bool
|
||||
// NoCache disables the use of cache when building images.
|
||||
NoCache bool
|
||||
// Pull attempts to pull newer versions of the base images before building.
|
||||
Pull bool
|
||||
// Services specifies which services to build. If empty, all services with a build config are built.
|
||||
Services []string
|
||||
|
||||
// Push targets are mutually exclusive.
|
||||
// PushCluster uploads the built images to cluster machines after building.
|
||||
PushCluster bool
|
||||
// PushRegistry uploads the built images to external registries after building.
|
||||
PushRegistry bool
|
||||
|
||||
// Cluster-specific options (only used if PushCluster is true).
|
||||
// Context is the name of the cluster context.
|
||||
Context string
|
||||
// Machines is a list of machine names or IDs to push the image to. If empty, images are pushed to all machines.
|
||||
Machines []string
|
||||
}
|
||||
|
||||
// GetServicesThatNeedBuild returns a map of services that require building
|
||||
func GetServicesThatNeedBuild(project *composetypes.Project) map[string]composetypes.ServiceConfig {
|
||||
servicesToBuild := make(map[string]composetypes.ServiceConfig, len(project.Services))
|
||||
for serviceName, service := range project.Services {
|
||||
if service.Build == nil {
|
||||
// BuildServices builds images for services in the Compose project.
|
||||
func (cli *CLI) BuildServices(ctx context.Context, project *composetypes.Project, opts BuildServicesOptions) error {
|
||||
// Validate push targets.
|
||||
if opts.PushCluster && opts.PushRegistry {
|
||||
return fmt.Errorf("cannot specify both PushCluster and PushRegistry: choose one push target")
|
||||
}
|
||||
|
||||
// Build service images using Compose implementation.
|
||||
dockerCli, err := command.NewDockerCli()
|
||||
if err != nil {
|
||||
return fmt.Errorf("create docker client: %w", err)
|
||||
}
|
||||
// Initialise the Docker CLI with default options.
|
||||
if err = dockerCli.Initialize(flags.NewClientOptions()); err != nil {
|
||||
return fmt.Errorf("initialise docker client: %w", err)
|
||||
}
|
||||
|
||||
composeService := composev2.NewComposeService(dockerCli)
|
||||
buildOpts := composeapi.BuildOptions{
|
||||
Args: composetypes.NewMappingWithEquals(opts.BuildArgs),
|
||||
Check: opts.Check,
|
||||
Deps: opts.Deps,
|
||||
NoCache: opts.NoCache,
|
||||
Pull: opts.Pull,
|
||||
Push: opts.PushRegistry,
|
||||
Services: opts.Services,
|
||||
}
|
||||
|
||||
if err = composeService.Build(ctx, project, buildOpts); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !opts.PushCluster {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Push built service images to cluster machines.
|
||||
builtServices, err := ServicesThatNeedBuild(project, opts.Services, opts.Deps)
|
||||
if err != nil {
|
||||
return fmt.Errorf("determine built services: %w", err)
|
||||
}
|
||||
if len(builtServices) == 0 {
|
||||
// No services were built, nothing to push.
|
||||
return nil
|
||||
}
|
||||
|
||||
// Add a line break after the build output.
|
||||
fmt.Fprintln(cli.ProgressOut())
|
||||
|
||||
clusterClient, err := cli.ConnectCluster(ctx, opts.Context)
|
||||
if err != nil {
|
||||
return fmt.Errorf("connect to cluster: %w", err)
|
||||
}
|
||||
defer clusterClient.Close()
|
||||
|
||||
// Push one service image at a time.
|
||||
var errs []error
|
||||
for _, s := range builtServices {
|
||||
if s.Image == "" {
|
||||
// Skip services without an image name (shouldn't happen for services with build config).
|
||||
continue
|
||||
}
|
||||
servicesToBuild[serviceName] = service
|
||||
}
|
||||
return servicesToBuild
|
||||
}
|
||||
|
||||
// BuildServices builds the services defined in the provided map.
|
||||
func BuildServices(ctx context.Context, servicesToBuild map[string]composetypes.ServiceConfig, opts BuildOptions) error {
|
||||
fmt.Println("Building services...")
|
||||
// Push to the specified machines falling back to service x-machines.
|
||||
// If none specified, push to *all* cluster machines.
|
||||
var pushOpts client.PushImageOptions
|
||||
|
||||
// Init docker client (can be local or remote, depending on DOCKER_HOST environment variable)
|
||||
dockerCli, err := dockerclient.NewClientWithOpts(dockerclient.FromEnv, dockerclient.WithAPIVersionNegotiation())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer dockerCli.Close()
|
||||
|
||||
serviceImages := make(map[string]string, len(servicesToBuild))
|
||||
|
||||
// Build the services using the local docker client and compose libraries
|
||||
for _, service := range servicesToBuild {
|
||||
fmt.Printf("Building service: %s\n", service.Name)
|
||||
imageName, err := buildSingleService(ctx, dockerCli, service, opts)
|
||||
if err != nil {
|
||||
return fmt.Errorf("build service %s: %w", service.Name, err)
|
||||
}
|
||||
serviceImages[service.Name] = imageName
|
||||
}
|
||||
fmt.Printf("Service images are built.\n")
|
||||
|
||||
if opts.Push {
|
||||
err = pushServiceImages(ctx, dockerCli, serviceImages)
|
||||
if len(opts.Machines) > 0 {
|
||||
pushOpts.Machines = opts.Machines
|
||||
} else if machines, ok := s.Extensions[compose.MachinesExtensionKey].(compose.MachinesSource); ok {
|
||||
pushOpts.Machines = machines
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// buildSingleService builds a single service using the Docker client and Compose libraries.
|
||||
func buildSingleService(ctx context.Context, dockerCli *dockerclient.Client, service composetypes.ServiceConfig, opts BuildOptions) (string, error) {
|
||||
if service.Build == nil {
|
||||
return "", fmt.Errorf("service %s has no build configuration", service.Name)
|
||||
}
|
||||
if service.Image == "" {
|
||||
return "", fmt.Errorf("service %s has no image specified; building services without image is not supported yet",
|
||||
service.Name)
|
||||
if len(pushOpts.Machines) == 0 {
|
||||
pushOpts.AllMachines = true
|
||||
}
|
||||
|
||||
buildContextPath := service.Build.Context
|
||||
imageName := service.Image
|
||||
|
||||
// Create a tar archive of the build context
|
||||
buildContext, err := archive.TarWithOptions(buildContextPath, &archive.TarOptions{})
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create build context for service %s: %w", service.Name, err)
|
||||
}
|
||||
|
||||
buildOptions := build.ImageBuildOptions{
|
||||
// TODO: Support Dockerfiles outside the build context
|
||||
// See https://github.com/docker/compose/blob/cf89fd1aa1328d5af77658ccc5a1e1b29981ae80/pkg/compose/build_classic.go#L92
|
||||
Dockerfile: service.Build.Dockerfile,
|
||||
Tags: []string{imageName},
|
||||
Remove: true, // Remove intermediate containers
|
||||
NoCache: opts.NoCache,
|
||||
}
|
||||
|
||||
buildResponse, err := dockerCli.ImageBuild(ctx, buildContext, buildOptions)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to build image for service %s: %w", service.Name, err)
|
||||
}
|
||||
defer buildResponse.Body.Close()
|
||||
|
||||
// Display the build response
|
||||
fd, isTerminal := term.GetFdInfo(os.Stdout)
|
||||
if err := jsonmessage.DisplayJSONMessagesStream(buildResponse.Body, os.Stdout, fd, isTerminal, nil); err != nil {
|
||||
return "", fmt.Errorf("failed to display build response for service %s: %w", service.Name, err)
|
||||
}
|
||||
|
||||
return imageName, nil
|
||||
}
|
||||
|
||||
// pushSingleServiceImage pushes a single service image.
|
||||
func pushSingleServiceImage(ctx context.Context, dockerCli *dockerclient.Client, serviceName string, imageName string) error {
|
||||
ref, err := reference.ParseNormalizedNamed(imageName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
repoInfo, err := registry.ParseRepositoryInfo(ref)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
registryKey := repoInfo.Index.Name
|
||||
if repoInfo.Index.Official {
|
||||
registryKey = registry.IndexServer
|
||||
}
|
||||
|
||||
// Load the Docker config file with auth details, if available
|
||||
configFile := config.LoadDefaultConfigFile(os.Stderr)
|
||||
|
||||
authConfig, err := configFile.GetAuthConfig(registryKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
authJSON, err := json.Marshal(authConfig)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal auth config for registry %s: %w", registryKey, err)
|
||||
}
|
||||
|
||||
authStr := base64.URLEncoding.EncodeToString(authJSON)
|
||||
|
||||
pushOptions := image.PushOptions{
|
||||
RegistryAuth: authStr,
|
||||
}
|
||||
pushResponse, err := dockerCli.ImagePush(ctx, imageName, pushOptions)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to push image %s: %w", imageName, err)
|
||||
}
|
||||
defer pushResponse.Close()
|
||||
|
||||
fmt.Printf("Pushing image %s for service %s...\n", imageName, serviceName)
|
||||
|
||||
fd, isTerminal := term.GetFdInfo(os.Stdout)
|
||||
if err := jsonmessage.DisplayJSONMessagesStream(pushResponse, os.Stdout, fd, isTerminal, nil); err != nil {
|
||||
return fmt.Errorf("failed to display push response for image %s: %w", imageName, err)
|
||||
}
|
||||
|
||||
fmt.Printf("Image %s pushed successfully.\n", imageName)
|
||||
return nil
|
||||
}
|
||||
|
||||
// pushServiceImages pushes all built service images to the registry.
|
||||
func pushServiceImages(ctx context.Context, dockerCli *dockerclient.Client, serviceImages map[string]string) error {
|
||||
fmt.Printf("Pushing images...\n")
|
||||
for serviceName, imageName := range serviceImages {
|
||||
if err := pushSingleServiceImage(ctx, dockerCli, serviceName, imageName); err != nil {
|
||||
return fmt.Errorf("push image for service %s: %w", serviceName, err)
|
||||
}
|
||||
boldStyle := lipgloss.NewStyle().Bold(true)
|
||||
err = progress.RunWithTitle(ctx, func(ctx context.Context) error {
|
||||
if err = clusterClient.PushImage(ctx, s.Image, pushOpts); err != nil {
|
||||
return fmt.Errorf("push image '%s' for service '%s': %w", s.Image, s.Name, err)
|
||||
}
|
||||
return nil
|
||||
}, cli.ProgressOut(), fmt.Sprintf("Pushing image %s to cluster", boldStyle.Render(s.Image)))
|
||||
// Collect errors to try pushing all images.
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
|
||||
// ServicesThatNeedBuild returns a list of services that require building.
|
||||
// deps indicates whether to include services that are dependencies of the selected services.
|
||||
// Implementation is based on the logic from docker/compose/v2/pkg/compose/build.go.
|
||||
func ServicesThatNeedBuild(
|
||||
project *composetypes.Project, selectedServices []string, deps bool,
|
||||
) ([]composetypes.ServiceConfig, error) {
|
||||
servicesToBuild := make([]composetypes.ServiceConfig, 0, len(project.Services))
|
||||
|
||||
var policy composetypes.DependencyOption = composetypes.IgnoreDependencies
|
||||
if deps {
|
||||
policy = composetypes.IncludeDependencies
|
||||
}
|
||||
|
||||
// Also include services used as build.additional_contexts with service: prefix.
|
||||
selectedServices = includeAdditionalContextsServices(project, selectedServices)
|
||||
// Some build dependencies we just introduced may not be enabled, enable them.
|
||||
var err error
|
||||
if project, err = project.WithServicesEnabled(selectedServices...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if project, err = project.WithSelectedServices(selectedServices); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = project.ForEachService(selectedServices, func(serviceName string, service *composetypes.ServiceConfig) error {
|
||||
if service.Build != nil {
|
||||
servicesToBuild = append(servicesToBuild, *service)
|
||||
}
|
||||
return nil
|
||||
}, policy)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return servicesToBuild, nil
|
||||
}
|
||||
|
||||
// includeAdditionalContextsServices adds services referenced in build.additional_contexts to the list
|
||||
// of selected services.
|
||||
func includeAdditionalContextsServices(project *composetypes.Project, selectedServices []string) []string {
|
||||
servicesWithDependencies := mapset.NewSet(selectedServices...)
|
||||
for _, service := range selectedServices {
|
||||
s, ok := project.Services[service]
|
||||
if !ok {
|
||||
s = project.DisabledServices[service]
|
||||
}
|
||||
if s.Build != nil {
|
||||
for _, target := range s.Build.AdditionalContexts {
|
||||
if name, found := strings.CutPrefix(target, composetypes.ServicePrefix); found {
|
||||
servicesWithDependencies.Add(name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if servicesWithDependencies.Cardinality() > len(selectedServices) {
|
||||
return includeAdditionalContextsServices(project, servicesWithDependencies.ToSlice())
|
||||
}
|
||||
|
||||
return servicesWithDependencies.ToSlice()
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/netip"
|
||||
@@ -17,6 +18,7 @@ const (
|
||||
|
||||
type MachineConnection struct {
|
||||
SSH SSHDestination `yaml:"ssh,omitempty"`
|
||||
SSHCLI SSHDestination `yaml:"ssh_cli,omitempty"`
|
||||
SSHKeyFile string `yaml:"ssh_key_file,omitempty"`
|
||||
// TCP is the address and port of the machine's API server.
|
||||
// The pointer is used to omit the field when not set. Otherwise, yaml marshalling includes an empty object.
|
||||
@@ -27,13 +29,37 @@ type MachineConnection struct {
|
||||
|
||||
func (c MachineConnection) String() string {
|
||||
if c.SSH != "" {
|
||||
return string(c.SSH)
|
||||
return "ssh://" + string(c.SSH)
|
||||
} else if c.SSHCLI != "" {
|
||||
return "ssh+cli://" + string(c.SSHCLI)
|
||||
} else if c.TCP != nil && c.TCP.IsValid() {
|
||||
return fmt.Sprintf("tcp://%s", c.TCP)
|
||||
}
|
||||
return "unknown connection"
|
||||
}
|
||||
|
||||
func (c *MachineConnection) Validate() error {
|
||||
setCount := 0
|
||||
if c.SSH != "" {
|
||||
setCount++
|
||||
}
|
||||
if c.SSHCLI != "" {
|
||||
setCount++
|
||||
}
|
||||
if c.TCP != nil && c.TCP.IsValid() {
|
||||
setCount++
|
||||
}
|
||||
|
||||
if setCount == 0 {
|
||||
return errors.New("no connection method specified (ssh, ssh_cli, or tcp required)")
|
||||
}
|
||||
if setCount > 1 {
|
||||
return errors.New("only one connection method allowed per connection (ssh, ssh_cli, or tcp)")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SSHDestination represents an SSH destination string in the canonical form of "user@host:port".
|
||||
// The default user "root" and port 22 can be omitted.
|
||||
type SSHDestination string
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestMachineConnection_String(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
conn MachineConnection
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "ssh connection",
|
||||
conn: MachineConnection{
|
||||
SSH: "user@host.com",
|
||||
},
|
||||
want: "ssh://user@host.com",
|
||||
},
|
||||
{
|
||||
name: "ssh connection with port",
|
||||
conn: MachineConnection{
|
||||
SSH: "user@host.com:2222",
|
||||
},
|
||||
want: "ssh://user@host.com:2222",
|
||||
},
|
||||
{
|
||||
name: "ssh_cli connection",
|
||||
conn: MachineConnection{
|
||||
SSHCLI: "user@host.com",
|
||||
},
|
||||
want: "ssh+cli://user@host.com",
|
||||
},
|
||||
{
|
||||
name: "ssh_cli connection with port",
|
||||
conn: MachineConnection{
|
||||
SSHCLI: "user@host.com:2222",
|
||||
},
|
||||
want: "ssh+cli://user@host.com:2222",
|
||||
},
|
||||
{
|
||||
name: "tcp connection",
|
||||
conn: MachineConnection{
|
||||
TCP: func() *netip.AddrPort {
|
||||
addr := netip.MustParseAddrPort("10.0.0.1:8080")
|
||||
return &addr
|
||||
}(),
|
||||
},
|
||||
want: "tcp://10.0.0.1:8080",
|
||||
},
|
||||
{
|
||||
name: "no connection",
|
||||
conn: MachineConnection{},
|
||||
want: "unknown connection",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
got := tt.conn.String()
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMachineConnection_Validate(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
conn MachineConnection
|
||||
wantErr bool
|
||||
errMsg string
|
||||
}{
|
||||
{
|
||||
name: "ssh only - valid",
|
||||
conn: MachineConnection{
|
||||
SSH: "user@host",
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "ssh_cli only - valid",
|
||||
conn: MachineConnection{
|
||||
SSHCLI: "user@host",
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "tcp only - valid",
|
||||
conn: MachineConnection{
|
||||
TCP: func() *netip.AddrPort {
|
||||
addr := netip.MustParseAddrPort("10.0.0.1:8080")
|
||||
return &addr
|
||||
}(),
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "no connection method - error",
|
||||
conn: MachineConnection{},
|
||||
wantErr: true,
|
||||
errMsg: "no connection method specified",
|
||||
},
|
||||
{
|
||||
name: "ssh and ssh_cli - error",
|
||||
conn: MachineConnection{
|
||||
SSH: "user@host",
|
||||
SSHCLI: "user@host",
|
||||
},
|
||||
wantErr: true,
|
||||
errMsg: "only one connection method allowed",
|
||||
},
|
||||
{
|
||||
name: "ssh and tcp - error",
|
||||
conn: MachineConnection{
|
||||
SSH: "user@host",
|
||||
TCP: func() *netip.AddrPort {
|
||||
addr := netip.MustParseAddrPort("10.0.0.1:8080")
|
||||
return &addr
|
||||
}(),
|
||||
},
|
||||
wantErr: true,
|
||||
errMsg: "only one connection method allowed",
|
||||
},
|
||||
{
|
||||
name: "ssh_cli and tcp - error",
|
||||
conn: MachineConnection{
|
||||
SSHCLI: "user@host",
|
||||
TCP: func() *netip.AddrPort {
|
||||
addr := netip.MustParseAddrPort("10.0.0.1:8080")
|
||||
return &addr
|
||||
}(),
|
||||
},
|
||||
wantErr: true,
|
||||
errMsg: "only one connection method allowed",
|
||||
},
|
||||
{
|
||||
name: "all three - error",
|
||||
conn: MachineConnection{
|
||||
SSH: "user@host",
|
||||
SSHCLI: "user@host2",
|
||||
TCP: func() *netip.AddrPort {
|
||||
addr := netip.MustParseAddrPort("10.0.0.1:8080")
|
||||
return &addr
|
||||
}(),
|
||||
},
|
||||
wantErr: true,
|
||||
errMsg: "only one connection method allowed",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
err := tt.conn.Validate()
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), tt.errMsg)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
+28
-7
@@ -56,10 +56,31 @@ func connectClusterWithProgress(ctx context.Context, conn config.MachineConnecti
|
||||
}
|
||||
|
||||
func connectCluster(ctx context.Context, conn config.MachineConnection) (*client.Client, error) {
|
||||
// Determine which SSH type is configured
|
||||
var sshDest config.SSHDestination
|
||||
var useSSHCLI bool
|
||||
|
||||
// Validate connection configuration early to provide clear error messages.
|
||||
if err := conn.Validate(); err != nil {
|
||||
return nil, fmt.Errorf("invalid connection configuration: %w", err)
|
||||
}
|
||||
|
||||
if conn.SSH != "" {
|
||||
user, host, port, err := conn.SSH.Parse()
|
||||
sshDest = conn.SSH
|
||||
useSSHCLI = false
|
||||
} else if conn.SSHCLI != "" {
|
||||
sshDest = conn.SSHCLI
|
||||
useSSHCLI = true
|
||||
} else if conn.TCP != nil && conn.TCP.IsValid() {
|
||||
return client.New(ctx, connector.NewTCPConnector(*conn.TCP))
|
||||
} else {
|
||||
return nil, errors.New("connection configuration is invalid")
|
||||
}
|
||||
|
||||
// Parse SSH destination and create config (shared for both types)
|
||||
user, host, port, err := sshDest.Parse()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parse SSH connection %q: %w", conn.SSH, err)
|
||||
return nil, fmt.Errorf("parse SSH connection %q: %w", sshDest, err)
|
||||
}
|
||||
|
||||
keyPath := fs.ExpandHomeDir(conn.SSHKeyFile)
|
||||
@@ -70,12 +91,12 @@ func connectCluster(ctx context.Context, conn config.MachineConnection) (*client
|
||||
Port: port,
|
||||
KeyPath: keyPath,
|
||||
}
|
||||
return client.New(ctx, connector.NewSSHConnector(sshConfig))
|
||||
} else if conn.TCP != nil && conn.TCP.IsValid() {
|
||||
return client.New(ctx, connector.NewTCPConnector(*conn.TCP))
|
||||
}
|
||||
|
||||
return nil, errors.New("connection configuration is invalid")
|
||||
// Create appropriate connector based on type
|
||||
if useSSHCLI {
|
||||
return client.New(ctx, connector.NewSSHCLIConnector(sshConfig))
|
||||
}
|
||||
return client.New(ctx, connector.NewSSHConnector(sshConfig))
|
||||
}
|
||||
|
||||
// connectModel is a TUI model for connecting to a cluster with a progress spinner.
|
||||
|
||||
@@ -16,6 +16,7 @@ type Client struct {
|
||||
}
|
||||
|
||||
// WaitDaemonReady waits for the Docker daemon to start and be ready to serve requests.
|
||||
// TODO(lhf): convert into a Client method.
|
||||
func WaitDaemonReady(ctx context.Context, cli *client.Client) error {
|
||||
// Retry to ping the Docker daemon until it's ready or the context is canceled.
|
||||
boff := backoff.WithContext(backoff.NewExponentialBackOff(
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
package gitutil
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// GitState contains information about the Git repository state.
|
||||
type GitState struct {
|
||||
// Date is the current commit datetime.
|
||||
Date time.Time
|
||||
// IsDirty indicates whether there are uncommitted changes.
|
||||
IsDirty bool
|
||||
// IsRepo indicates whether the directory is a Git repository.
|
||||
IsRepo bool
|
||||
// SHA is the full SHA-1 (40 characters) of the current commit.
|
||||
SHA string
|
||||
}
|
||||
|
||||
// InspectGitState inspects the Git repo state from the specified directory.
|
||||
// If the directory is not a Git repository, or if git utility is not available,
|
||||
// returns GitState with IsRepo=false and no error.
|
||||
func InspectGitState(dir string) (GitState, error) {
|
||||
// Check if git command is available.
|
||||
if _, err := exec.LookPath("git"); err != nil {
|
||||
return GitState{}, nil
|
||||
}
|
||||
|
||||
state := GitState{
|
||||
IsRepo: isGitRepo(dir),
|
||||
}
|
||||
if !state.IsRepo {
|
||||
return state, nil
|
||||
}
|
||||
|
||||
// Get the current commit SHA.
|
||||
sha, err := gitCommand(dir, "rev-parse", "HEAD")
|
||||
if err != nil {
|
||||
return state, fmt.Errorf("get current commit SHA: %w", err)
|
||||
}
|
||||
state.SHA = strings.TrimSpace(sha)
|
||||
|
||||
// Get the current commit timestamp.
|
||||
timestamp, err := gitCommand(dir, "log", "-1", "--format=%ct")
|
||||
if err != nil {
|
||||
return state, fmt.Errorf("get current commit timestamp: %w", err)
|
||||
}
|
||||
seconds, err := strconv.ParseInt(strings.TrimSpace(timestamp), 10, 64)
|
||||
if err != nil {
|
||||
return state, fmt.Errorf("parse current commit timestamp: %w", err)
|
||||
}
|
||||
state.Date = time.Unix(seconds, 0).UTC()
|
||||
|
||||
// Check for uncommitted changes.
|
||||
status, err := gitCommand(dir, "status", "--porcelain")
|
||||
if err != nil {
|
||||
return state, fmt.Errorf("check git status: %w", err)
|
||||
}
|
||||
state.IsDirty = strings.TrimSpace(status) != ""
|
||||
|
||||
return state, nil
|
||||
}
|
||||
|
||||
// isGitRepo checks if the directory is a Git repository.
|
||||
func isGitRepo(dir string) bool {
|
||||
_, err := gitCommand(dir, "rev-parse", "--git-dir")
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// gitCommand runs a git command in the specified directory.
|
||||
func gitCommand(dir string, args ...string) (string, error) {
|
||||
cmd := exec.Command("git", args...)
|
||||
cmd.Dir = dir
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
if exitErr, ok := err.(*exec.ExitError); ok {
|
||||
return "", fmt.Errorf("git command failed: %s", exitErr.Stderr)
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(output), nil
|
||||
}
|
||||
|
||||
// ShortSHA returns a truncated SHA with the specified length.
|
||||
// If length is negative, zero, or greater than the SHA length, it returns the full SHA.
|
||||
// Returns empty string if SHA is empty.
|
||||
func (s *GitState) ShortSHA(length int) string {
|
||||
if length <= 0 || length > len(s.SHA) {
|
||||
length = len(s.SHA)
|
||||
}
|
||||
return s.SHA[:length]
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
package gitutil
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestInspectGitState_GitNotAvailable(t *testing.T) {
|
||||
// Reset the PATH env var to simulate git not being available.
|
||||
t.Setenv("PATH", "")
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
state, err := InspectGitState(tmpDir)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.False(t, state.IsRepo)
|
||||
assert.Empty(t, state.SHA)
|
||||
assert.Empty(t, state.ShortSHA(7))
|
||||
assert.True(t, state.Date.IsZero())
|
||||
assert.False(t, state.IsDirty)
|
||||
}
|
||||
|
||||
func TestInspectGitState_NotARepo(t *testing.T) {
|
||||
// Create a temporary directory that's not a git repo.
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
state, err := InspectGitState(tmpDir)
|
||||
require.NoError(t, err)
|
||||
|
||||
// If tmpDir happens to be in a git repo (e.g., parent directory), skip this test.
|
||||
// This is common when running tests from within the project itself.
|
||||
if state.IsRepo {
|
||||
t.Skip("tmpDir is within a git repository, skipping test")
|
||||
}
|
||||
|
||||
assert.False(t, state.IsRepo)
|
||||
assert.Empty(t, state.SHA)
|
||||
assert.Empty(t, state.ShortSHA(7))
|
||||
assert.True(t, state.Date.IsZero()) // Should have zero time if not a repo.
|
||||
assert.False(t, state.IsDirty)
|
||||
}
|
||||
|
||||
func TestInspectGitState_CleanRepo(t *testing.T) {
|
||||
// Create a temporary git repo.
|
||||
tmpDir := t.TempDir()
|
||||
initGitRepo(t, tmpDir)
|
||||
|
||||
createAndCommitFile(t, tmpDir, "test.txt", "test content")
|
||||
|
||||
state, err := InspectGitState(tmpDir)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.True(t, state.IsRepo)
|
||||
assert.Len(t, state.SHA, 40)
|
||||
assert.Equal(t, state.ShortSHA(7), state.SHA[:7])
|
||||
assert.False(t, state.Date.IsZero())
|
||||
assert.False(t, state.IsDirty)
|
||||
}
|
||||
|
||||
func TestInspectGitState_DirtyRepo(t *testing.T) {
|
||||
// Create a temporary git repo.
|
||||
tmpDir := t.TempDir()
|
||||
initGitRepo(t, tmpDir)
|
||||
|
||||
createAndCommitFile(t, tmpDir, "test.txt", "test content")
|
||||
|
||||
// Modify the file to make the repo dirty.
|
||||
err := os.WriteFile(filepath.Join(tmpDir, "test.txt"), []byte("modified content"), 0o644)
|
||||
require.NoError(t, err)
|
||||
|
||||
state, err := InspectGitState(tmpDir)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.True(t, state.IsRepo)
|
||||
assert.True(t, state.IsDirty)
|
||||
}
|
||||
|
||||
func TestInspectGitState_UntrackedFiles(t *testing.T) {
|
||||
// Create a temporary git repo.
|
||||
tmpDir := t.TempDir()
|
||||
initGitRepo(t, tmpDir)
|
||||
|
||||
createAndCommitFile(t, tmpDir, "test.txt", "test content")
|
||||
|
||||
// Add an untracked file.
|
||||
err := os.WriteFile(filepath.Join(tmpDir, "untracked.txt"), []byte("untracked"), 0o644)
|
||||
require.NoError(t, err)
|
||||
|
||||
state, err := InspectGitState(tmpDir)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.True(t, state.IsRepo)
|
||||
assert.True(t, state.IsDirty) // Untracked files make the repo dirty.
|
||||
}
|
||||
|
||||
func TestGitState_ShortSHA(t *testing.T) {
|
||||
state := &GitState{
|
||||
SHA: "1234567890abcdef1234567890abcdef12345678",
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
length int
|
||||
expected string
|
||||
}{
|
||||
{"short 7", 7, "1234567"},
|
||||
{"short 10", 10, "1234567890"},
|
||||
{"full SHA", 40, "1234567890abcdef1234567890abcdef12345678"},
|
||||
{"longer than SHA", 50, "1234567890abcdef1234567890abcdef12345678"},
|
||||
{"negative", -42, "1234567890abcdef1234567890abcdef12345678"},
|
||||
{"zero", 0, "1234567890abcdef1234567890abcdef12345678"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := state.ShortSHA(tt.length)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGitState_ShortSHA_Empty(t *testing.T) {
|
||||
state := &GitState{
|
||||
SHA: "",
|
||||
}
|
||||
|
||||
result := state.ShortSHA(7)
|
||||
assert.Empty(t, result)
|
||||
}
|
||||
|
||||
// Helper functions.
|
||||
|
||||
func initGitRepo(t *testing.T, dir string) {
|
||||
t.Helper()
|
||||
|
||||
// Initialize git repo.
|
||||
runGitCommand(t, dir, "init")
|
||||
runGitCommand(t, dir, "config", "user.email", "test@example.com")
|
||||
runGitCommand(t, dir, "config", "user.name", "Test User")
|
||||
}
|
||||
|
||||
func createAndCommitFile(t *testing.T, dir, filename, content string) {
|
||||
t.Helper()
|
||||
|
||||
// Create file.
|
||||
path := filepath.Join(dir, filename)
|
||||
err := os.WriteFile(path, []byte(content), 0o644)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Commit file.
|
||||
runGitCommand(t, dir, "add", filename)
|
||||
runGitCommand(t, dir, "commit", "-m", "Add "+filename)
|
||||
}
|
||||
|
||||
func runGitCommand(t *testing.T, dir string, args ...string) {
|
||||
t.Helper()
|
||||
|
||||
cmd := exec.Command("git", args...)
|
||||
cmd.Dir = dir
|
||||
output, err := cmd.CombinedOutput()
|
||||
require.NoError(t, err, "git command failed: %s", output)
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package log
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"os"
|
||||
"slices"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func InitLoggerFromEnv() {
|
||||
debugValues := []string{"1", "true", "yes"}
|
||||
if slices.Contains(debugValues, strings.ToLower(os.Getenv("DEBUG"))) {
|
||||
logger := slog.New(NewSlogTextHandler(os.Stderr, &slog.HandlerOptions{
|
||||
Level: slog.LevelDebug,
|
||||
}))
|
||||
slog.SetDefault(logger)
|
||||
}
|
||||
slog.Debug("logger initialized")
|
||||
}
|
||||
@@ -1727,6 +1727,329 @@ func (x *MachineServiceContainers) GetContainers() []*ServiceContainer {
|
||||
return nil
|
||||
}
|
||||
|
||||
type ExecContainerRequest struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
// Types that are assignable to Payload:
|
||||
//
|
||||
// *ExecContainerRequest_Config
|
||||
// *ExecContainerRequest_Stdin
|
||||
// *ExecContainerRequest_Resize
|
||||
Payload isExecContainerRequest_Payload `protobuf_oneof:"payload"`
|
||||
}
|
||||
|
||||
func (x *ExecContainerRequest) Reset() {
|
||||
*x = ExecContainerRequest{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[32]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *ExecContainerRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*ExecContainerRequest) ProtoMessage() {}
|
||||
|
||||
func (x *ExecContainerRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[32]
|
||||
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 ExecContainerRequest.ProtoReflect.Descriptor instead.
|
||||
func (*ExecContainerRequest) Descriptor() ([]byte, []int) {
|
||||
return file_internal_machine_api_pb_docker_proto_rawDescGZIP(), []int{32}
|
||||
}
|
||||
|
||||
func (m *ExecContainerRequest) GetPayload() isExecContainerRequest_Payload {
|
||||
if m != nil {
|
||||
return m.Payload
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *ExecContainerRequest) GetConfig() *ExecConfig {
|
||||
if x, ok := x.GetPayload().(*ExecContainerRequest_Config); ok {
|
||||
return x.Config
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *ExecContainerRequest) GetStdin() []byte {
|
||||
if x, ok := x.GetPayload().(*ExecContainerRequest_Stdin); ok {
|
||||
return x.Stdin
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *ExecContainerRequest) GetResize() *ResizeEvent {
|
||||
if x, ok := x.GetPayload().(*ExecContainerRequest_Resize); ok {
|
||||
return x.Resize
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type isExecContainerRequest_Payload interface {
|
||||
isExecContainerRequest_Payload()
|
||||
}
|
||||
|
||||
type ExecContainerRequest_Config struct {
|
||||
// Initial configuration for the exec session. Must be sent as the first message.
|
||||
Config *ExecConfig `protobuf:"bytes,1,opt,name=config,proto3,oneof"`
|
||||
}
|
||||
|
||||
type ExecContainerRequest_Stdin struct {
|
||||
// Raw stdin data to be written to the exec process.
|
||||
Stdin []byte `protobuf:"bytes,2,opt,name=stdin,proto3,oneof"`
|
||||
}
|
||||
|
||||
type ExecContainerRequest_Resize struct {
|
||||
// TTY resize event (only used when TTY is enabled).
|
||||
Resize *ResizeEvent `protobuf:"bytes,3,opt,name=resize,proto3,oneof"`
|
||||
}
|
||||
|
||||
func (*ExecContainerRequest_Config) isExecContainerRequest_Payload() {}
|
||||
|
||||
func (*ExecContainerRequest_Stdin) isExecContainerRequest_Payload() {}
|
||||
|
||||
func (*ExecContainerRequest_Resize) isExecContainerRequest_Payload() {}
|
||||
|
||||
type ExecConfig struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
// Container ID to execute the command in.
|
||||
ContainerId string `protobuf:"bytes,1,opt,name=container_id,json=containerId,proto3" json:"container_id,omitempty"`
|
||||
// JSON serialised ExecOptions
|
||||
Options []byte `protobuf:"bytes,2,opt,name=options,proto3" json:"options,omitempty"`
|
||||
}
|
||||
|
||||
func (x *ExecConfig) Reset() {
|
||||
*x = ExecConfig{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[33]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *ExecConfig) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*ExecConfig) ProtoMessage() {}
|
||||
|
||||
func (x *ExecConfig) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[33]
|
||||
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 ExecConfig.ProtoReflect.Descriptor instead.
|
||||
func (*ExecConfig) Descriptor() ([]byte, []int) {
|
||||
return file_internal_machine_api_pb_docker_proto_rawDescGZIP(), []int{33}
|
||||
}
|
||||
|
||||
func (x *ExecConfig) GetContainerId() string {
|
||||
if x != nil {
|
||||
return x.ContainerId
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *ExecConfig) GetOptions() []byte {
|
||||
if x != nil {
|
||||
return x.Options
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type ResizeEvent struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Height uint32 `protobuf:"varint,1,opt,name=height,proto3" json:"height,omitempty"`
|
||||
Width uint32 `protobuf:"varint,2,opt,name=width,proto3" json:"width,omitempty"`
|
||||
}
|
||||
|
||||
func (x *ResizeEvent) Reset() {
|
||||
*x = ResizeEvent{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[34]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *ResizeEvent) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*ResizeEvent) ProtoMessage() {}
|
||||
|
||||
func (x *ResizeEvent) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[34]
|
||||
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 ResizeEvent.ProtoReflect.Descriptor instead.
|
||||
func (*ResizeEvent) Descriptor() ([]byte, []int) {
|
||||
return file_internal_machine_api_pb_docker_proto_rawDescGZIP(), []int{34}
|
||||
}
|
||||
|
||||
func (x *ResizeEvent) GetHeight() uint32 {
|
||||
if x != nil {
|
||||
return x.Height
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *ResizeEvent) GetWidth() uint32 {
|
||||
if x != nil {
|
||||
return x.Width
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
type ExecContainerResponse struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
// Types that are assignable to Payload:
|
||||
//
|
||||
// *ExecContainerResponse_ExecId
|
||||
// *ExecContainerResponse_Stdout
|
||||
// *ExecContainerResponse_Stderr
|
||||
// *ExecContainerResponse_ExitCode
|
||||
Payload isExecContainerResponse_Payload `protobuf_oneof:"payload"`
|
||||
}
|
||||
|
||||
func (x *ExecContainerResponse) Reset() {
|
||||
*x = ExecContainerResponse{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[35]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *ExecContainerResponse) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*ExecContainerResponse) ProtoMessage() {}
|
||||
|
||||
func (x *ExecContainerResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[35]
|
||||
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 ExecContainerResponse.ProtoReflect.Descriptor instead.
|
||||
func (*ExecContainerResponse) Descriptor() ([]byte, []int) {
|
||||
return file_internal_machine_api_pb_docker_proto_rawDescGZIP(), []int{35}
|
||||
}
|
||||
|
||||
func (m *ExecContainerResponse) GetPayload() isExecContainerResponse_Payload {
|
||||
if m != nil {
|
||||
return m.Payload
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *ExecContainerResponse) GetExecId() string {
|
||||
if x, ok := x.GetPayload().(*ExecContainerResponse_ExecId); ok {
|
||||
return x.ExecId
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *ExecContainerResponse) GetStdout() []byte {
|
||||
if x, ok := x.GetPayload().(*ExecContainerResponse_Stdout); ok {
|
||||
return x.Stdout
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *ExecContainerResponse) GetStderr() []byte {
|
||||
if x, ok := x.GetPayload().(*ExecContainerResponse_Stderr); ok {
|
||||
return x.Stderr
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *ExecContainerResponse) GetExitCode() int32 {
|
||||
if x, ok := x.GetPayload().(*ExecContainerResponse_ExitCode); ok {
|
||||
return x.ExitCode
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
type isExecContainerResponse_Payload interface {
|
||||
isExecContainerResponse_Payload()
|
||||
}
|
||||
|
||||
type ExecContainerResponse_ExecId struct {
|
||||
// Exec instance ID returned after creating the exec.
|
||||
ExecId string `protobuf:"bytes,1,opt,name=exec_id,json=execId,proto3,oneof"`
|
||||
}
|
||||
|
||||
type ExecContainerResponse_Stdout struct {
|
||||
// Raw stdout data from the exec process.
|
||||
Stdout []byte `protobuf:"bytes,2,opt,name=stdout,proto3,oneof"`
|
||||
}
|
||||
|
||||
type ExecContainerResponse_Stderr struct {
|
||||
// Raw stderr data from the exec process (only when TTY is disabled).
|
||||
Stderr []byte `protobuf:"bytes,3,opt,name=stderr,proto3,oneof"`
|
||||
}
|
||||
|
||||
type ExecContainerResponse_ExitCode struct {
|
||||
// Exit code of the exec process. Sent as the final message.
|
||||
ExitCode int32 `protobuf:"varint,4,opt,name=exit_code,json=exitCode,proto3,oneof"`
|
||||
}
|
||||
|
||||
func (*ExecContainerResponse_ExecId) isExecContainerResponse_Payload() {}
|
||||
|
||||
func (*ExecContainerResponse_Stdout) isExecContainerResponse_Payload() {}
|
||||
|
||||
func (*ExecContainerResponse_Stderr) isExecContainerResponse_Payload() {}
|
||||
|
||||
func (*ExecContainerResponse_ExitCode) isExecContainerResponse_Payload() {}
|
||||
|
||||
var File_internal_machine_api_pb_docker_proto protoreflect.FileDescriptor
|
||||
|
||||
var file_internal_machine_api_pb_docker_proto_rawDesc = []byte{
|
||||
@@ -1887,91 +2210,123 @@ var file_internal_machine_api_pb_docker_proto_rawDesc = []byte{
|
||||
0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x35, 0x0a, 0x0a, 0x63, 0x6f, 0x6e, 0x74, 0x61,
|
||||
0x69, 0x6e, 0x65, 0x72, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x61, 0x70,
|
||||
0x69, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e,
|
||||
0x65, 0x72, 0x52, 0x0a, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x32, 0xfb,
|
||||
0x09, 0x0a, 0x06, 0x44, 0x6f, 0x63, 0x6b, 0x65, 0x72, 0x12, 0x4c, 0x0a, 0x0f, 0x43, 0x72, 0x65,
|
||||
0x61, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x1b, 0x2e, 0x61,
|
||||
0x70, 0x69, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e,
|
||||
0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x61, 0x70, 0x69, 0x2e,
|
||||
0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52,
|
||||
0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4f, 0x0a, 0x10, 0x49, 0x6e, 0x73, 0x70, 0x65,
|
||||
0x63, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x1c, 0x2e, 0x61, 0x70,
|
||||
0x69, 0x2e, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e,
|
||||
0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x61, 0x70, 0x69, 0x2e,
|
||||
0x65, 0x72, 0x52, 0x0a, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x22, 0x90,
|
||||
0x01, 0x0a, 0x14, 0x45, 0x78, 0x65, 0x63, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72,
|
||||
0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x29, 0x0a, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69,
|
||||
0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x45, 0x78,
|
||||
0x65, 0x63, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x48, 0x00, 0x52, 0x06, 0x63, 0x6f, 0x6e, 0x66,
|
||||
0x69, 0x67, 0x12, 0x16, 0x0a, 0x05, 0x73, 0x74, 0x64, 0x69, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28,
|
||||
0x0c, 0x48, 0x00, 0x52, 0x05, 0x73, 0x74, 0x64, 0x69, 0x6e, 0x12, 0x2a, 0x0a, 0x06, 0x72, 0x65,
|
||||
0x73, 0x69, 0x7a, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x61, 0x70, 0x69,
|
||||
0x2e, 0x52, 0x65, 0x73, 0x69, 0x7a, 0x65, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x48, 0x00, 0x52, 0x06,
|
||||
0x72, 0x65, 0x73, 0x69, 0x7a, 0x65, 0x42, 0x09, 0x0a, 0x07, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61,
|
||||
0x64, 0x22, 0x49, 0x0a, 0x0a, 0x45, 0x78, 0x65, 0x63, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12,
|
||||
0x21, 0x0a, 0x0c, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18,
|
||||
0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72,
|
||||
0x49, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x02, 0x20,
|
||||
0x01, 0x28, 0x0c, 0x52, 0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x3b, 0x0a, 0x0b,
|
||||
0x52, 0x65, 0x73, 0x69, 0x7a, 0x65, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x68,
|
||||
0x65, 0x69, 0x67, 0x68, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x06, 0x68, 0x65, 0x69,
|
||||
0x67, 0x68, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x77, 0x69, 0x64, 0x74, 0x68, 0x18, 0x02, 0x20, 0x01,
|
||||
0x28, 0x0d, 0x52, 0x05, 0x77, 0x69, 0x64, 0x74, 0x68, 0x22, 0x90, 0x01, 0x0a, 0x15, 0x45, 0x78,
|
||||
0x65, 0x63, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f,
|
||||
0x6e, 0x73, 0x65, 0x12, 0x19, 0x0a, 0x07, 0x65, 0x78, 0x65, 0x63, 0x5f, 0x69, 0x64, 0x18, 0x01,
|
||||
0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x06, 0x65, 0x78, 0x65, 0x63, 0x49, 0x64, 0x12, 0x18,
|
||||
0x0a, 0x06, 0x73, 0x74, 0x64, 0x6f, 0x75, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x48, 0x00,
|
||||
0x52, 0x06, 0x73, 0x74, 0x64, 0x6f, 0x75, 0x74, 0x12, 0x18, 0x0a, 0x06, 0x73, 0x74, 0x64, 0x65,
|
||||
0x72, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x48, 0x00, 0x52, 0x06, 0x73, 0x74, 0x64, 0x65,
|
||||
0x72, 0x72, 0x12, 0x1d, 0x0a, 0x09, 0x65, 0x78, 0x69, 0x74, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18,
|
||||
0x04, 0x20, 0x01, 0x28, 0x05, 0x48, 0x00, 0x52, 0x08, 0x65, 0x78, 0x69, 0x74, 0x43, 0x6f, 0x64,
|
||||
0x65, 0x42, 0x09, 0x0a, 0x07, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x32, 0xc7, 0x0a, 0x0a,
|
||||
0x06, 0x44, 0x6f, 0x63, 0x6b, 0x65, 0x72, 0x12, 0x4c, 0x0a, 0x0f, 0x43, 0x72, 0x65, 0x61, 0x74,
|
||||
0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x1b, 0x2e, 0x61, 0x70, 0x69,
|
||||
0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72,
|
||||
0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x72,
|
||||
0x65, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x73,
|
||||
0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4f, 0x0a, 0x10, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74,
|
||||
0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x1c, 0x2e, 0x61, 0x70, 0x69, 0x2e,
|
||||
0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72,
|
||||
0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x44, 0x0a, 0x0e, 0x53, 0x74, 0x61, 0x72,
|
||||
0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x1a, 0x2e, 0x61, 0x70, 0x69,
|
||||
0x2e, 0x53, 0x74, 0x61, 0x72, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52,
|
||||
0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e,
|
||||
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x42,
|
||||
0x0a, 0x0d, 0x53, 0x74, 0x6f, 0x70, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12,
|
||||
0x19, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x53, 0x74, 0x6f, 0x70, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69,
|
||||
0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f,
|
||||
0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70,
|
||||
0x74, 0x79, 0x12, 0x49, 0x0a, 0x0e, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69,
|
||||
0x6e, 0x65, 0x72, 0x73, 0x12, 0x1a, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x43,
|
||||
0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
|
||||
0x1a, 0x1b, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61,
|
||||
0x69, 0x6e, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x46, 0x0a,
|
||||
0x0f, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72,
|
||||
0x12, 0x1b, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x43, 0x6f, 0x6e,
|
||||
0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e,
|
||||
0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e,
|
||||
0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x36, 0x0a, 0x09, 0x50, 0x75, 0x6c, 0x6c, 0x49, 0x6d, 0x61,
|
||||
0x67, 0x65, 0x12, 0x15, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x50, 0x75, 0x6c, 0x6c, 0x49, 0x6d, 0x61,
|
||||
0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x10, 0x2e, 0x61, 0x70, 0x69, 0x2e,
|
||||
0x4a, 0x53, 0x4f, 0x4e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x30, 0x01, 0x12, 0x43, 0x0a,
|
||||
0x0c, 0x49, 0x6e, 0x73, 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, 0x3d, 0x0a, 0x0a, 0x4c, 0x69, 0x73,
|
||||
0x74, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x73, 0x12, 0x16, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69,
|
||||
0x73, 0x74, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a,
|
||||
0x17, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x73,
|
||||
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,
|
||||
0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e,
|
||||
0x73, 0x70, 0x65, 0x63, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65,
|
||||
0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x44, 0x0a, 0x0e, 0x53, 0x74, 0x61, 0x72, 0x74, 0x43,
|
||||
0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x1a, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x53,
|
||||
0x74, 0x61, 0x72, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71,
|
||||
0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72,
|
||||
0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 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,
|
||||
0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x42, 0x0a, 0x0d,
|
||||
0x53, 0x74, 0x6f, 0x70, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x19, 0x2e,
|
||||
0x61, 0x70, 0x69, 0x2e, 0x53, 0x74, 0x6f, 0x70, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65,
|
||||
0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c,
|
||||
0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79,
|
||||
0x12, 0x49, 0x0a, 0x0e, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65,
|
||||
0x72, 0x73, 0x12, 0x1a, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, 0x6e,
|
||||
0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b,
|
||||
0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e,
|
||||
0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x46, 0x0a, 0x0f, 0x52,
|
||||
0x65, 0x6d, 0x6f, 0x76, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x1b,
|
||||
0x2e, 0x61, 0x70, 0x69, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61,
|
||||
0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f,
|
||||
0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d,
|
||||
0x70, 0x74, 0x79, 0x12, 0x36, 0x0a, 0x09, 0x50, 0x75, 0x6c, 0x6c, 0x49, 0x6d, 0x61, 0x67, 0x65,
|
||||
0x12, 0x15, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x50, 0x75, 0x6c, 0x6c, 0x49, 0x6d, 0x61, 0x67, 0x65,
|
||||
0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x10, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4a, 0x53,
|
||||
0x4f, 0x4e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x30, 0x01, 0x12, 0x43, 0x0a, 0x0c, 0x49,
|
||||
0x6e, 0x73, 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, 0x3d, 0x0a, 0x0a, 0x4c, 0x69, 0x73, 0x74, 0x49,
|
||||
0x6d, 0x61, 0x67, 0x65, 0x73, 0x12, 0x16, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74,
|
||||
0x49, 0x6d, 0x61, 0x67, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x17, 0x2e,
|
||||
0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x73, 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, 0x12, 0x4a, 0x0a, 0x0d, 0x45, 0x78,
|
||||
0x65, 0x63, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x19, 0x2e, 0x61, 0x70,
|
||||
0x69, 0x2e, 0x45, 0x78, 0x65, 0x63, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52,
|
||||
0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x45, 0x78, 0x65,
|
||||
0x63, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
|
||||
0x73, 0x65, 0x28, 0x01, 0x30, 0x01, 0x42, 0x37, 0x5a, 0x35, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62,
|
||||
0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x70, 0x73, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x6b, 0x69, 0x2f,
|
||||
0x75, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c,
|
||||
0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x62, 0x62,
|
||||
0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -1986,7 +2341,7 @@ func file_internal_machine_api_pb_docker_proto_rawDescGZIP() []byte {
|
||||
return file_internal_machine_api_pb_docker_proto_rawDescData
|
||||
}
|
||||
|
||||
var file_internal_machine_api_pb_docker_proto_msgTypes = make([]protoimpl.MessageInfo, 32)
|
||||
var file_internal_machine_api_pb_docker_proto_msgTypes = make([]protoimpl.MessageInfo, 36)
|
||||
var file_internal_machine_api_pb_docker_proto_goTypes = []any{
|
||||
(*CreateContainerRequest)(nil), // 0: api.CreateContainerRequest
|
||||
(*CreateContainerResponse)(nil), // 1: api.CreateContainerResponse
|
||||
@@ -2020,62 +2375,70 @@ var file_internal_machine_api_pb_docker_proto_goTypes = []any{
|
||||
(*ListServiceContainersRequest)(nil), // 29: api.ListServiceContainersRequest
|
||||
(*ListServiceContainersResponse)(nil), // 30: api.ListServiceContainersResponse
|
||||
(*MachineServiceContainers)(nil), // 31: api.MachineServiceContainers
|
||||
(*Metadata)(nil), // 32: api.Metadata
|
||||
(*emptypb.Empty)(nil), // 33: google.protobuf.Empty
|
||||
(*ExecContainerRequest)(nil), // 32: api.ExecContainerRequest
|
||||
(*ExecConfig)(nil), // 33: api.ExecConfig
|
||||
(*ResizeEvent)(nil), // 34: api.ResizeEvent
|
||||
(*ExecContainerResponse)(nil), // 35: api.ExecContainerResponse
|
||||
(*Metadata)(nil), // 36: api.Metadata
|
||||
(*emptypb.Empty)(nil), // 37: google.protobuf.Empty
|
||||
}
|
||||
var file_internal_machine_api_pb_docker_proto_depIdxs = []int32{
|
||||
8, // 0: api.ListContainersResponse.messages:type_name -> api.MachineContainers
|
||||
32, // 1: api.MachineContainers.metadata:type_name -> api.Metadata
|
||||
36, // 1: api.MachineContainers.metadata:type_name -> api.Metadata
|
||||
14, // 2: api.InspectImageResponse.messages:type_name -> api.Image
|
||||
32, // 3: api.Image.metadata:type_name -> api.Metadata
|
||||
36, // 3: api.Image.metadata:type_name -> api.Metadata
|
||||
17, // 4: api.InspectRemoteImageResponse.messages:type_name -> api.RemoteImage
|
||||
32, // 5: api.RemoteImage.metadata:type_name -> api.Metadata
|
||||
36, // 5: api.RemoteImage.metadata:type_name -> api.Metadata
|
||||
20, // 6: api.ListImagesResponse.messages:type_name -> api.MachineImages
|
||||
32, // 7: api.MachineImages.metadata:type_name -> api.Metadata
|
||||
36, // 7: api.MachineImages.metadata:type_name -> api.Metadata
|
||||
25, // 8: api.ListVolumesResponse.messages:type_name -> api.MachineVolumes
|
||||
32, // 9: api.MachineVolumes.metadata:type_name -> api.Metadata
|
||||
36, // 9: api.MachineVolumes.metadata:type_name -> api.Metadata
|
||||
31, // 10: api.ListServiceContainersResponse.messages:type_name -> api.MachineServiceContainers
|
||||
32, // 11: api.MachineServiceContainers.metadata:type_name -> api.Metadata
|
||||
36, // 11: api.MachineServiceContainers.metadata:type_name -> api.Metadata
|
||||
28, // 12: api.MachineServiceContainers.containers:type_name -> api.ServiceContainer
|
||||
0, // 13: api.Docker.CreateContainer:input_type -> api.CreateContainerRequest
|
||||
2, // 14: api.Docker.InspectContainer:input_type -> api.InspectContainerRequest
|
||||
4, // 15: api.Docker.StartContainer:input_type -> api.StartContainerRequest
|
||||
5, // 16: api.Docker.StopContainer:input_type -> api.StopContainerRequest
|
||||
6, // 17: api.Docker.ListContainers:input_type -> api.ListContainersRequest
|
||||
9, // 18: api.Docker.RemoveContainer:input_type -> api.RemoveContainerRequest
|
||||
10, // 19: api.Docker.PullImage:input_type -> api.PullImageRequest
|
||||
12, // 20: api.Docker.InspectImage:input_type -> api.InspectImageRequest
|
||||
15, // 21: api.Docker.InspectRemoteImage:input_type -> api.InspectRemoteImageRequest
|
||||
18, // 22: api.Docker.ListImages:input_type -> api.ListImagesRequest
|
||||
21, // 23: api.Docker.CreateVolume:input_type -> api.CreateVolumeRequest
|
||||
23, // 24: api.Docker.ListVolumes:input_type -> api.ListVolumesRequest
|
||||
26, // 25: api.Docker.RemoveVolume:input_type -> api.RemoveVolumeRequest
|
||||
27, // 26: api.Docker.CreateServiceContainer:input_type -> api.CreateServiceContainerRequest
|
||||
2, // 27: api.Docker.InspectServiceContainer:input_type -> api.InspectContainerRequest
|
||||
29, // 28: api.Docker.ListServiceContainers:input_type -> api.ListServiceContainersRequest
|
||||
9, // 29: api.Docker.RemoveServiceContainer:input_type -> api.RemoveContainerRequest
|
||||
1, // 30: api.Docker.CreateContainer:output_type -> api.CreateContainerResponse
|
||||
3, // 31: api.Docker.InspectContainer:output_type -> api.InspectContainerResponse
|
||||
33, // 32: api.Docker.StartContainer:output_type -> google.protobuf.Empty
|
||||
33, // 33: api.Docker.StopContainer:output_type -> google.protobuf.Empty
|
||||
7, // 34: api.Docker.ListContainers:output_type -> api.ListContainersResponse
|
||||
33, // 35: api.Docker.RemoveContainer:output_type -> google.protobuf.Empty
|
||||
11, // 36: api.Docker.PullImage:output_type -> api.JSONMessage
|
||||
13, // 37: api.Docker.InspectImage:output_type -> api.InspectImageResponse
|
||||
16, // 38: api.Docker.InspectRemoteImage:output_type -> api.InspectRemoteImageResponse
|
||||
19, // 39: api.Docker.ListImages:output_type -> api.ListImagesResponse
|
||||
22, // 40: api.Docker.CreateVolume:output_type -> api.CreateVolumeResponse
|
||||
24, // 41: api.Docker.ListVolumes:output_type -> api.ListVolumesResponse
|
||||
33, // 42: api.Docker.RemoveVolume:output_type -> google.protobuf.Empty
|
||||
1, // 43: api.Docker.CreateServiceContainer:output_type -> api.CreateContainerResponse
|
||||
28, // 44: api.Docker.InspectServiceContainer:output_type -> api.ServiceContainer
|
||||
30, // 45: api.Docker.ListServiceContainers:output_type -> api.ListServiceContainersResponse
|
||||
33, // 46: api.Docker.RemoveServiceContainer:output_type -> google.protobuf.Empty
|
||||
30, // [30:47] is the sub-list for method output_type
|
||||
13, // [13:30] is the sub-list for method input_type
|
||||
13, // [13:13] is the sub-list for extension type_name
|
||||
13, // [13:13] is the sub-list for extension extendee
|
||||
0, // [0:13] is the sub-list for field type_name
|
||||
33, // 13: api.ExecContainerRequest.config:type_name -> api.ExecConfig
|
||||
34, // 14: api.ExecContainerRequest.resize:type_name -> api.ResizeEvent
|
||||
0, // 15: api.Docker.CreateContainer:input_type -> api.CreateContainerRequest
|
||||
2, // 16: api.Docker.InspectContainer:input_type -> api.InspectContainerRequest
|
||||
4, // 17: api.Docker.StartContainer:input_type -> api.StartContainerRequest
|
||||
5, // 18: api.Docker.StopContainer:input_type -> api.StopContainerRequest
|
||||
6, // 19: api.Docker.ListContainers:input_type -> api.ListContainersRequest
|
||||
9, // 20: api.Docker.RemoveContainer:input_type -> api.RemoveContainerRequest
|
||||
10, // 21: api.Docker.PullImage:input_type -> api.PullImageRequest
|
||||
12, // 22: api.Docker.InspectImage:input_type -> api.InspectImageRequest
|
||||
15, // 23: api.Docker.InspectRemoteImage:input_type -> api.InspectRemoteImageRequest
|
||||
18, // 24: api.Docker.ListImages:input_type -> api.ListImagesRequest
|
||||
21, // 25: api.Docker.CreateVolume:input_type -> api.CreateVolumeRequest
|
||||
23, // 26: api.Docker.ListVolumes:input_type -> api.ListVolumesRequest
|
||||
26, // 27: api.Docker.RemoveVolume:input_type -> api.RemoveVolumeRequest
|
||||
27, // 28: api.Docker.CreateServiceContainer:input_type -> api.CreateServiceContainerRequest
|
||||
2, // 29: api.Docker.InspectServiceContainer:input_type -> api.InspectContainerRequest
|
||||
29, // 30: api.Docker.ListServiceContainers:input_type -> api.ListServiceContainersRequest
|
||||
9, // 31: api.Docker.RemoveServiceContainer:input_type -> api.RemoveContainerRequest
|
||||
32, // 32: api.Docker.ExecContainer:input_type -> api.ExecContainerRequest
|
||||
1, // 33: api.Docker.CreateContainer:output_type -> api.CreateContainerResponse
|
||||
3, // 34: api.Docker.InspectContainer:output_type -> api.InspectContainerResponse
|
||||
37, // 35: api.Docker.StartContainer:output_type -> google.protobuf.Empty
|
||||
37, // 36: api.Docker.StopContainer:output_type -> google.protobuf.Empty
|
||||
7, // 37: api.Docker.ListContainers:output_type -> api.ListContainersResponse
|
||||
37, // 38: api.Docker.RemoveContainer:output_type -> google.protobuf.Empty
|
||||
11, // 39: api.Docker.PullImage:output_type -> api.JSONMessage
|
||||
13, // 40: api.Docker.InspectImage:output_type -> api.InspectImageResponse
|
||||
16, // 41: api.Docker.InspectRemoteImage:output_type -> api.InspectRemoteImageResponse
|
||||
19, // 42: api.Docker.ListImages:output_type -> api.ListImagesResponse
|
||||
22, // 43: api.Docker.CreateVolume:output_type -> api.CreateVolumeResponse
|
||||
24, // 44: api.Docker.ListVolumes:output_type -> api.ListVolumesResponse
|
||||
37, // 45: api.Docker.RemoveVolume:output_type -> google.protobuf.Empty
|
||||
1, // 46: api.Docker.CreateServiceContainer:output_type -> api.CreateContainerResponse
|
||||
28, // 47: api.Docker.InspectServiceContainer:output_type -> api.ServiceContainer
|
||||
30, // 48: api.Docker.ListServiceContainers:output_type -> api.ListServiceContainersResponse
|
||||
37, // 49: api.Docker.RemoveServiceContainer:output_type -> google.protobuf.Empty
|
||||
35, // 50: api.Docker.ExecContainer:output_type -> api.ExecContainerResponse
|
||||
33, // [33:51] is the sub-list for method output_type
|
||||
15, // [15:33] is the sub-list for method input_type
|
||||
15, // [15:15] is the sub-list for extension type_name
|
||||
15, // [15:15] is the sub-list for extension extendee
|
||||
0, // [0:15] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_internal_machine_api_pb_docker_proto_init() }
|
||||
@@ -2469,6 +2832,65 @@ func file_internal_machine_api_pb_docker_proto_init() {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
file_internal_machine_api_pb_docker_proto_msgTypes[32].Exporter = func(v any, i int) any {
|
||||
switch v := v.(*ExecContainerRequest); 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[33].Exporter = func(v any, i int) any {
|
||||
switch v := v.(*ExecConfig); 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[34].Exporter = func(v any, i int) any {
|
||||
switch v := v.(*ResizeEvent); 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[35].Exporter = func(v any, i int) any {
|
||||
switch v := v.(*ExecContainerResponse); 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[32].OneofWrappers = []any{
|
||||
(*ExecContainerRequest_Config)(nil),
|
||||
(*ExecContainerRequest_Stdin)(nil),
|
||||
(*ExecContainerRequest_Resize)(nil),
|
||||
}
|
||||
file_internal_machine_api_pb_docker_proto_msgTypes[35].OneofWrappers = []any{
|
||||
(*ExecContainerResponse_ExecId)(nil),
|
||||
(*ExecContainerResponse_Stdout)(nil),
|
||||
(*ExecContainerResponse_Stderr)(nil),
|
||||
(*ExecContainerResponse_ExitCode)(nil),
|
||||
}
|
||||
type x struct{}
|
||||
out := protoimpl.TypeBuilder{
|
||||
@@ -2476,7 +2898,7 @@ func file_internal_machine_api_pb_docker_proto_init() {
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: file_internal_machine_api_pb_docker_proto_rawDesc,
|
||||
NumEnums: 0,
|
||||
NumMessages: 32,
|
||||
NumMessages: 36,
|
||||
NumExtensions: 0,
|
||||
NumServices: 1,
|
||||
},
|
||||
|
||||
@@ -30,6 +30,8 @@ service Docker {
|
||||
rpc InspectServiceContainer(InspectContainerRequest) returns (ServiceContainer);
|
||||
rpc ListServiceContainers(ListServiceContainersRequest) returns (ListServiceContainersResponse);
|
||||
rpc RemoveServiceContainer(RemoveContainerRequest) returns (google.protobuf.Empty);
|
||||
|
||||
rpc ExecContainer(stream ExecContainerRequest) returns (stream ExecContainerResponse);
|
||||
}
|
||||
|
||||
message CreateContainerRequest {
|
||||
@@ -214,3 +216,39 @@ message MachineServiceContainers {
|
||||
Metadata metadata = 1;
|
||||
repeated ServiceContainer containers = 2;
|
||||
}
|
||||
|
||||
message ExecContainerRequest {
|
||||
oneof payload {
|
||||
// Initial configuration for the exec session. Must be sent as the first message.
|
||||
ExecConfig config = 1;
|
||||
// Raw stdin data to be written to the exec process.
|
||||
bytes stdin = 2;
|
||||
// TTY resize event (only used when TTY is enabled).
|
||||
ResizeEvent resize = 3;
|
||||
}
|
||||
}
|
||||
|
||||
message ExecConfig {
|
||||
// Container ID to execute the command in.
|
||||
string container_id = 1;
|
||||
// JSON serialised ExecOptions
|
||||
bytes options = 2;
|
||||
}
|
||||
|
||||
message ResizeEvent {
|
||||
uint32 height = 1;
|
||||
uint32 width = 2;
|
||||
}
|
||||
|
||||
message ExecContainerResponse {
|
||||
oneof payload {
|
||||
// Exec instance ID returned after creating the exec.
|
||||
string exec_id = 1;
|
||||
// Raw stdout data from the exec process.
|
||||
bytes stdout = 2;
|
||||
// Raw stderr data from the exec process (only when TTY is disabled).
|
||||
bytes stderr = 3;
|
||||
// Exit code of the exec process. Sent as the final message.
|
||||
int32 exit_code = 4;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@ const (
|
||||
Docker_InspectServiceContainer_FullMethodName = "/api.Docker/InspectServiceContainer"
|
||||
Docker_ListServiceContainers_FullMethodName = "/api.Docker/ListServiceContainers"
|
||||
Docker_RemoveServiceContainer_FullMethodName = "/api.Docker/RemoveServiceContainer"
|
||||
Docker_ExecContainer_FullMethodName = "/api.Docker/ExecContainer"
|
||||
)
|
||||
|
||||
// DockerClient is the client API for Docker service.
|
||||
@@ -62,6 +63,7 @@ type DockerClient interface {
|
||||
InspectServiceContainer(ctx context.Context, in *InspectContainerRequest, opts ...grpc.CallOption) (*ServiceContainer, error)
|
||||
ListServiceContainers(ctx context.Context, in *ListServiceContainersRequest, opts ...grpc.CallOption) (*ListServiceContainersResponse, error)
|
||||
RemoveServiceContainer(ctx context.Context, in *RemoveContainerRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
|
||||
ExecContainer(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[ExecContainerRequest, ExecContainerResponse], error)
|
||||
}
|
||||
|
||||
type dockerClient struct {
|
||||
@@ -251,6 +253,19 @@ func (c *dockerClient) RemoveServiceContainer(ctx context.Context, in *RemoveCon
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *dockerClient) ExecContainer(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[ExecContainerRequest, ExecContainerResponse], error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
stream, err := c.cc.NewStream(ctx, &Docker_ServiceDesc.Streams[1], Docker_ExecContainer_FullMethodName, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
x := &grpc.GenericClientStream[ExecContainerRequest, ExecContainerResponse]{ClientStream: stream}
|
||||
return x, nil
|
||||
}
|
||||
|
||||
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
|
||||
type Docker_ExecContainerClient = grpc.BidiStreamingClient[ExecContainerRequest, ExecContainerResponse]
|
||||
|
||||
// DockerServer is the server API for Docker service.
|
||||
// All implementations must embed UnimplementedDockerServer
|
||||
// for forward compatibility.
|
||||
@@ -274,6 +289,7 @@ type DockerServer interface {
|
||||
InspectServiceContainer(context.Context, *InspectContainerRequest) (*ServiceContainer, error)
|
||||
ListServiceContainers(context.Context, *ListServiceContainersRequest) (*ListServiceContainersResponse, error)
|
||||
RemoveServiceContainer(context.Context, *RemoveContainerRequest) (*emptypb.Empty, error)
|
||||
ExecContainer(grpc.BidiStreamingServer[ExecContainerRequest, ExecContainerResponse]) error
|
||||
mustEmbedUnimplementedDockerServer()
|
||||
}
|
||||
|
||||
@@ -335,6 +351,9 @@ func (UnimplementedDockerServer) ListServiceContainers(context.Context, *ListSer
|
||||
func (UnimplementedDockerServer) RemoveServiceContainer(context.Context, *RemoveContainerRequest) (*emptypb.Empty, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method RemoveServiceContainer not implemented")
|
||||
}
|
||||
func (UnimplementedDockerServer) ExecContainer(grpc.BidiStreamingServer[ExecContainerRequest, ExecContainerResponse]) error {
|
||||
return status.Errorf(codes.Unimplemented, "method ExecContainer not implemented")
|
||||
}
|
||||
func (UnimplementedDockerServer) mustEmbedUnimplementedDockerServer() {}
|
||||
func (UnimplementedDockerServer) testEmbeddedByValue() {}
|
||||
|
||||
@@ -655,6 +674,13 @@ func _Docker_RemoveServiceContainer_Handler(srv interface{}, ctx context.Context
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _Docker_ExecContainer_Handler(srv interface{}, stream grpc.ServerStream) error {
|
||||
return srv.(DockerServer).ExecContainer(&grpc.GenericServerStream[ExecContainerRequest, ExecContainerResponse]{ServerStream: stream})
|
||||
}
|
||||
|
||||
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
|
||||
type Docker_ExecContainerServer = grpc.BidiStreamingServer[ExecContainerRequest, ExecContainerResponse]
|
||||
|
||||
// Docker_ServiceDesc is the grpc.ServiceDesc for Docker service.
|
||||
// It's only intended for direct use with grpc.RegisterService,
|
||||
// and not to be introspected or modified (even as a copy)
|
||||
@@ -733,6 +759,12 @@ var Docker_ServiceDesc = grpc.ServiceDesc{
|
||||
Handler: _Docker_PullImage_Handler,
|
||||
ServerStreams: true,
|
||||
},
|
||||
{
|
||||
StreamName: "ExecContainer",
|
||||
Handler: _Docker_ExecContainer_Handler,
|
||||
ServerStreams: true,
|
||||
ClientStreams: true,
|
||||
},
|
||||
},
|
||||
Metadata: "internal/machine/api/pb/docker.proto",
|
||||
}
|
||||
|
||||
@@ -358,6 +358,14 @@ func (cc *clusterController) handleMachineChanges(ctx context.Context) error {
|
||||
slog.Error("Failed to list machines.", "err", err)
|
||||
continue
|
||||
}
|
||||
// Skip reconfiguration if the machines list is empty. This can happen when joining the cluster.
|
||||
// Corrosion can notifies about table changes before the data is fully replicated.
|
||||
// Reconfiguring with an empty list would remove all peers and lock this machine out of the cluster.
|
||||
// See https://github.com/psviderski/uncloud/issues/155.
|
||||
if len(machines) == 0 {
|
||||
slog.Debug("Skipping peer reconfiguration: machines list in store is empty.")
|
||||
continue
|
||||
}
|
||||
if err = cc.configurePeers(machines); err != nil {
|
||||
slog.Error("Failed to configure peers.", "err", err)
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"math/rand/v2"
|
||||
"net"
|
||||
"net/netip"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -40,6 +41,7 @@ type Resolver interface {
|
||||
// to upstream DNS servers.
|
||||
type Server struct {
|
||||
listenAddr netip.Addr
|
||||
localSubnet netip.Prefix
|
||||
resolver Resolver
|
||||
upstreamServers []netip.AddrPort
|
||||
|
||||
@@ -53,7 +55,7 @@ type Server struct {
|
||||
// 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) {
|
||||
func NewServer(listenAddr netip.Addr, localSubnet netip.Prefix, resolver Resolver, upstreams []netip.AddrPort) (*Server, error) {
|
||||
if !listenAddr.IsValid() {
|
||||
return nil, fmt.Errorf("invalid listen address: %s", listenAddr)
|
||||
}
|
||||
@@ -87,6 +89,7 @@ func NewServer(listenAddr netip.Addr, resolver Resolver, upstreams []netip.AddrP
|
||||
|
||||
return &Server{
|
||||
listenAddr: listenAddr,
|
||||
localSubnet: localSubnet,
|
||||
resolver: resolver,
|
||||
upstreamServers: upstreams,
|
||||
forwardSemaphore: make(chan struct{}, maxConcurrentForwards),
|
||||
@@ -287,7 +290,7 @@ func (s *Server) forwardRequest(req *dns.Msg, proto string) (*dns.Msg, error) {
|
||||
// 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)
|
||||
serviceName, mode := extractModeFromDomain(trimInternalDomain(name))
|
||||
ips := s.resolver.Resolve(serviceName)
|
||||
if len(ips) == 0 {
|
||||
s.log.Debug("Failed to resolve service name.", "service", serviceName)
|
||||
@@ -296,10 +299,28 @@ func (s *Server) handleAQuery(name string) []dns.RR {
|
||||
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.
|
||||
// Shuffle the IPs to approximate round-robin.
|
||||
// We want to do this as a baseline for "nearest" mode, as well.
|
||||
rand.Shuffle(len(ips), func(i, j int) {
|
||||
ips[i], ips[j] = ips[j], ips[i]
|
||||
})
|
||||
|
||||
// Default (mode == "") currently behaves the same as round-robin,
|
||||
// and nothing additional to do for round-robin (mode == "rr").
|
||||
|
||||
if mode == "nearest" {
|
||||
// Sort IPs on local subnet to the top.
|
||||
slices.SortFunc(ips, func(a, b netip.Addr) int {
|
||||
aIsLocal := s.localSubnet.Contains(a)
|
||||
bIsLocal := s.localSubnet.Contains(b)
|
||||
if aIsLocal && !bIsLocal {
|
||||
return -1
|
||||
} else if bIsLocal && !aIsLocal {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Create A records for each IP.
|
||||
@@ -345,3 +366,13 @@ func trimInternalDomain(name string) string {
|
||||
|
||||
return strings.TrimSuffix(name, "."+InternalDomain)
|
||||
}
|
||||
|
||||
func extractModeFromDomain(name string) (string, string) {
|
||||
modes := []string{"nearest", "rr"}
|
||||
for _, mode := range modes {
|
||||
if cut, found := strings.CutPrefix(name, mode+"."); found {
|
||||
return cut, mode
|
||||
}
|
||||
}
|
||||
return name, ""
|
||||
}
|
||||
|
||||
@@ -0,0 +1,303 @@
|
||||
package docker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"os"
|
||||
"os/signal"
|
||||
|
||||
"github.com/moby/term"
|
||||
"github.com/psviderski/uncloud/internal/machine/api/pb"
|
||||
"github.com/psviderski/uncloud/pkg/api"
|
||||
"golang.org/x/sync/errgroup"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
// ExecConfig contains options for executing a command in a container.
|
||||
type ExecConfig struct {
|
||||
// Container ID to execute the command in.
|
||||
ContainerID string
|
||||
// Exec configuration.
|
||||
Options api.ExecOptions
|
||||
}
|
||||
|
||||
// sendResizeRequest sends a terminal resize request to the exec stream.
|
||||
func sendResizeRequest(stream pb.Docker_ExecContainerClient, size *term.Winsize) error {
|
||||
slog.Debug("sending resize request", "width", size.Width, "height", size.Height)
|
||||
return stream.Send(
|
||||
&pb.ExecContainerRequest{
|
||||
Payload: &pb.ExecContainerRequest_Resize{
|
||||
Resize: &pb.ResizeEvent{
|
||||
Height: uint32(size.Height),
|
||||
Width: uint32(size.Width),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// setupTerminal configures the terminal for interactive TTY sessions.
|
||||
// It checks if stdin is a terminal, sets it to raw mode, and sets up resize handling.
|
||||
// Returns a cleanup function to restore terminal state, or an error.
|
||||
func setupTerminal(ctx context.Context, stream pb.Docker_ExecContainerClient) (func(), error) {
|
||||
inFd, isTerminal := term.GetFdInfo(os.Stdin)
|
||||
if !isTerminal {
|
||||
return nil, fmt.Errorf("stdin is not a terminal")
|
||||
}
|
||||
|
||||
// Set terminal to raw mode
|
||||
oldState, err := term.SetRawTerminal(inFd)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("set raw terminal: %w", err)
|
||||
}
|
||||
|
||||
// Cleanup function
|
||||
restoreFunc := func() {
|
||||
_ = term.RestoreTerminal(inFd, oldState)
|
||||
}
|
||||
|
||||
// Set up resize handling
|
||||
if err := handleTerminalResize(ctx, inFd, stream); err != nil {
|
||||
restoreFunc()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return restoreFunc, nil
|
||||
}
|
||||
|
||||
// handleTerminalResize sends initial window size and handles window resize signals for TTY sessions.
|
||||
func handleTerminalResize(ctx context.Context, inFd uintptr, stream pb.Docker_ExecContainerClient) error {
|
||||
// Handle window resize signals
|
||||
sigCh := make(chan os.Signal, 1)
|
||||
signal.Notify(sigCh, unix.SIGWINCH)
|
||||
|
||||
// Send initial window size
|
||||
if size, err := term.GetWinsize(inFd); err == nil {
|
||||
_ = sendResizeRequest(stream, size)
|
||||
}
|
||||
|
||||
go func() {
|
||||
defer signal.Stop(sigCh)
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-sigCh:
|
||||
size, err := term.GetWinsize(inFd)
|
||||
if err != nil {
|
||||
slog.Debug("get window size", "error", err)
|
||||
continue
|
||||
}
|
||||
if err = sendResizeRequest(stream, size); err != nil {
|
||||
slog.Debug("send resize request", "error", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleClientInputStream reads from stdin and sends data to the remote server.
|
||||
// It also periodically checks for context cancellation to exit gracefully when e.g.
|
||||
// the output stream is closed.
|
||||
func handleClientInputStream(ctx context.Context, stream pb.Docker_ExecContainerClient, stdin io.Reader) error {
|
||||
slog.Debug("Input goroutine started")
|
||||
defer slog.Debug("Input goroutine exited")
|
||||
|
||||
defer stream.CloseSend()
|
||||
|
||||
// Channel to receive stdin data
|
||||
stdinCh := make(chan []byte)
|
||||
|
||||
stdinErrCh := make(chan error, 1)
|
||||
|
||||
// Read from stdin in a separate goroutine
|
||||
// Note: this goroutine may continue blocking on Read even after we exit from the function,
|
||||
// but that's OK - it will eventually unblock when data arrives or stdin closes.
|
||||
go func() {
|
||||
buf := make([]byte, 32*1024) // 32KB buffer
|
||||
for {
|
||||
n, err := stdin.Read(buf)
|
||||
if n > 0 {
|
||||
data := make([]byte, n)
|
||||
copy(data, buf[:n])
|
||||
select {
|
||||
case stdinCh <- data:
|
||||
case <-ctx.Done():
|
||||
slog.Debug("stdin reader exiting due to context done")
|
||||
return
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
slog.Debug("stdin reader: EOF received")
|
||||
} else {
|
||||
slog.Debug("stdin reader error", "error", err)
|
||||
}
|
||||
stdinErrCh <- err
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Send stdin data to the server or exit when context is cancelled
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
case data := <-stdinCh:
|
||||
if err := stream.Send(&pb.ExecContainerRequest{
|
||||
Payload: &pb.ExecContainerRequest_Stdin{Stdin: data},
|
||||
}); err != nil {
|
||||
return fmt.Errorf("send stdin: %w", err)
|
||||
}
|
||||
case err := <-stdinErrCh:
|
||||
if err != io.EOF {
|
||||
return fmt.Errorf("read stdin: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// handleClientOutputStream receives output from the exec stream and writes to stdout/stderr.
|
||||
// It also captures the exit code and signals completion via context cancellation.
|
||||
func handleClientOutputStream(ctx context.Context, stream pb.Docker_ExecContainerClient, stdout, stderr io.Writer, exitCode *int) error {
|
||||
slog.Debug("Output goroutine started")
|
||||
defer slog.Debug("Output goroutine exited")
|
||||
|
||||
for {
|
||||
resp, err := stream.Recv()
|
||||
if err == io.EOF {
|
||||
slog.Debug("output stream: EOF received")
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("receive from stream: %w", err)
|
||||
}
|
||||
|
||||
switch payload := resp.Payload.(type) {
|
||||
case *pb.ExecContainerResponse_ExecId:
|
||||
// This is sent first; we already processed it earlier, so just ignore duplicates.
|
||||
case *pb.ExecContainerResponse_Stdout:
|
||||
if _, err := stdout.Write(payload.Stdout); err != nil {
|
||||
return fmt.Errorf("write stdout: %w", err)
|
||||
}
|
||||
case *pb.ExecContainerResponse_Stderr:
|
||||
if _, err := stderr.Write(payload.Stderr); err != nil {
|
||||
return fmt.Errorf("write stderr: %w", err)
|
||||
}
|
||||
case *pb.ExecContainerResponse_ExitCode:
|
||||
slog.Debug("received exit code", "code", payload.ExitCode)
|
||||
*exitCode = int(payload.ExitCode)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ExecContainer executes a command in a running container with bidirectional streaming.
|
||||
// TODO: This can be merged with pkg/client as it's an unnecessary logic split.
|
||||
func (c *Client) ExecContainer(ctx context.Context, opts ExecConfig) (exitCode int, err error) {
|
||||
// TODO: We need to handle Ctrl-C and other signals here to forward them to the container process.
|
||||
// Right now, Ctrl-C will just terminate the client process, which is not ideal.
|
||||
// We should catch the signal, send it to the container process, and only exit
|
||||
// when the container process exits.
|
||||
|
||||
slog.Debug("starting ExecContainer", "containerID", opts.ContainerID, "options", opts.Options)
|
||||
|
||||
// Initialize exit code to non-zero in case we have to return early
|
||||
exitCode = 1
|
||||
|
||||
// Set up I/O streams - use custom streams if provided, otherwise default to os.Stdin/Stdout/Stderr
|
||||
stdin := io.Reader(os.Stdin)
|
||||
stdout := io.Writer(os.Stdout)
|
||||
stderr := io.Writer(os.Stderr)
|
||||
|
||||
if opts.Options.Stdin != nil {
|
||||
stdin = opts.Options.Stdin
|
||||
}
|
||||
if opts.Options.Stdout != nil {
|
||||
stdout = opts.Options.Stdout
|
||||
}
|
||||
if opts.Options.Stderr != nil {
|
||||
stderr = opts.Options.Stderr
|
||||
}
|
||||
|
||||
// Marshal the exec config
|
||||
configBytes, err := json.Marshal(opts.Options)
|
||||
if err != nil {
|
||||
return -1, fmt.Errorf("marshal exec config: %w", err)
|
||||
}
|
||||
|
||||
// Create the bidirectional stream
|
||||
stream, err := c.GRPCClient.ExecContainer(ctx)
|
||||
if err != nil {
|
||||
return -1, fmt.Errorf("create exec stream: %w", err)
|
||||
}
|
||||
|
||||
// Send the initial configuration
|
||||
if err := stream.Send(&pb.ExecContainerRequest{
|
||||
Payload: &pb.ExecContainerRequest_Config{
|
||||
Config: &pb.ExecConfig{
|
||||
ContainerId: opts.ContainerID,
|
||||
Options: configBytes,
|
||||
},
|
||||
},
|
||||
}); err != nil {
|
||||
return -1, fmt.Errorf("send exec config: %w", err)
|
||||
}
|
||||
|
||||
// Receive the exec ID
|
||||
resp, err := stream.Recv()
|
||||
if err != nil {
|
||||
return -1, fmt.Errorf("receive exec ID: %w", err)
|
||||
}
|
||||
execID := resp.GetExecId()
|
||||
if execID == "" {
|
||||
return -1, fmt.Errorf("expected exec ID in first response")
|
||||
}
|
||||
|
||||
errGroup, ctx := errgroup.WithContext(ctx)
|
||||
|
||||
// Create cancellable context for goroutine coordination
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
|
||||
// Handle terminal setup for interactive sessions
|
||||
if opts.Options.AttachStdin && opts.Options.Tty {
|
||||
restoreTerminal, err := setupTerminal(ctx, stream)
|
||||
if err != nil {
|
||||
return -1, fmt.Errorf("setup terminal: %w", err)
|
||||
}
|
||||
if restoreTerminal != nil {
|
||||
defer restoreTerminal()
|
||||
}
|
||||
}
|
||||
|
||||
// Handle stdin stream if needed
|
||||
if opts.Options.AttachStdin {
|
||||
errGroup.Go(func() error {
|
||||
return handleClientInputStream(ctx, stream, stdin)
|
||||
})
|
||||
} else {
|
||||
// Close send direction immediately if not attaching stdin
|
||||
stream.CloseSend()
|
||||
}
|
||||
|
||||
// Handle output streams (stdout/stderr)
|
||||
errGroup.Go(func() error {
|
||||
defer cancel()
|
||||
return handleClientOutputStream(ctx, stream, stdout, stderr, &exitCode)
|
||||
})
|
||||
|
||||
err = errGroup.Wait()
|
||||
|
||||
if err == nil && opts.Options.Detach {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
return exitCode, err
|
||||
}
|
||||
@@ -23,6 +23,7 @@ import (
|
||||
"github.com/distribution/reference"
|
||||
dockercommand "github.com/docker/cli/cli/command"
|
||||
dockerconfig "github.com/docker/cli/cli/config"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/api/types/image"
|
||||
@@ -30,6 +31,7 @@ import (
|
||||
"github.com/docker/docker/api/types/network"
|
||||
"github.com/docker/docker/api/types/volume"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/docker/docker/pkg/stdcopy"
|
||||
"github.com/docker/go-connections/nat"
|
||||
"github.com/google/go-containerregistry/pkg/authn"
|
||||
"github.com/google/go-containerregistry/pkg/name"
|
||||
@@ -611,6 +613,7 @@ func (s *Server) CreateServiceContainer(
|
||||
NanoCPUs: spec.Container.Resources.CPU,
|
||||
Memory: spec.Container.Resources.Memory,
|
||||
MemoryReservation: spec.Container.Resources.MemoryReservation,
|
||||
DeviceRequests: spec.Container.Resources.DeviceReservations,
|
||||
},
|
||||
// Restart service containers if they exit or a machine restarts unless they are explicitly stopped.
|
||||
// For one-off containers and batch jobs we plan to use a different service type/mode.
|
||||
@@ -1011,3 +1014,258 @@ func (s *Server) RemoveServiceContainer(ctx context.Context, req *pb.RemoveConta
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// receiveExecConfig receives and validates the initial exec configuration from the stream.
|
||||
func (s *Server) receiveExecConfig(stream pb.Docker_ExecContainerServer) (*pb.ExecConfig, api.ExecOptions, error) {
|
||||
req, err := stream.Recv()
|
||||
if err != nil {
|
||||
return nil, api.ExecOptions{}, status.Errorf(codes.InvalidArgument, "receive config: %v", err)
|
||||
}
|
||||
|
||||
execConfig := req.GetConfig()
|
||||
if execConfig == nil {
|
||||
return nil, api.ExecOptions{}, status.Error(codes.InvalidArgument, "first message must contain exec config")
|
||||
}
|
||||
|
||||
// Unmarshal the Uncloud's execOpts
|
||||
var execOpts api.ExecOptions
|
||||
if err := json.Unmarshal(execConfig.Options, &execOpts); err != nil {
|
||||
return nil, api.ExecOptions{}, status.Errorf(codes.InvalidArgument, "unmarshal exec config: %v", err)
|
||||
}
|
||||
|
||||
return execConfig, execOpts, nil
|
||||
}
|
||||
|
||||
// handleServerExecInput reads from the gRPC stream and writes to Docker stdin, handling resize requests.
|
||||
func (s *Server) handleServerExecInput(
|
||||
ctx context.Context,
|
||||
stream pb.Docker_ExecContainerServer,
|
||||
attachConn types.HijackedResponse,
|
||||
execID string,
|
||||
tty bool,
|
||||
) error {
|
||||
slog.Debug("Input goroutine started", "exec_id", execID, "tty", tty)
|
||||
defer slog.Debug("Input goroutine exited", "exec_id", execID)
|
||||
|
||||
defer attachConn.CloseWrite()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
slog.Debug("Input goroutine context canceled via context", "exec_id", execID)
|
||||
return nil
|
||||
default:
|
||||
}
|
||||
|
||||
req, err := stream.Recv()
|
||||
switch {
|
||||
case errors.Is(err, io.EOF):
|
||||
slog.Debug("Input goroutine received EOF", "exec_id", execID)
|
||||
return nil
|
||||
case status.Code(err) == codes.Canceled:
|
||||
// Can be the case when the output goroutine ends and the stream context is canceled.
|
||||
slog.Debug("Input goroutine context canceled", "exec_id", execID)
|
||||
return nil
|
||||
case err == nil:
|
||||
// continue processing
|
||||
default:
|
||||
return fmt.Errorf("receive from stream: %w", err)
|
||||
}
|
||||
|
||||
switch payload := req.Payload.(type) {
|
||||
case *pb.ExecContainerRequest_Stdin:
|
||||
if _, err := attachConn.Conn.Write(payload.Stdin); err != nil {
|
||||
return fmt.Errorf("write to stdin: %w", err)
|
||||
}
|
||||
case *pb.ExecContainerRequest_Resize:
|
||||
if tty {
|
||||
resizeOpts := container.ResizeOptions{
|
||||
Height: uint(payload.Resize.Height),
|
||||
Width: uint(payload.Resize.Width),
|
||||
}
|
||||
if err := s.client.ContainerExecResize(ctx, execID, resizeOpts); err != nil {
|
||||
slog.Warn("Failed to resize TTY", "err", err, "exec_id", execID)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// handleServerExecOutput reads from Docker stdout/stderr and writes to the gRPC stream.
|
||||
func (s *Server) handleServerExecOutput(
|
||||
stream pb.Docker_ExecContainerServer,
|
||||
attachResp types.HijackedResponse,
|
||||
execID string,
|
||||
tty bool,
|
||||
) error {
|
||||
slog.Debug("Output goroutine started", "exec_id", execID, "tty", tty)
|
||||
defer slog.Debug("Output goroutine exited", "exec_id", execID)
|
||||
|
||||
if tty {
|
||||
// In TTY mode, all output is stdout - copy directly to stream
|
||||
stdoutWriter := &grpcStreamWriter{stream: stream, isStderr: false}
|
||||
_, err := io.Copy(stdoutWriter, attachResp.Reader)
|
||||
if err != nil && err != io.EOF {
|
||||
return fmt.Errorf("copy tty output: %w", err)
|
||||
}
|
||||
return nil
|
||||
} else {
|
||||
// In non-TTY mode, Docker multiplexes stdout/stderr with headers
|
||||
// Use stdcopy to demultiplex
|
||||
slog.Debug("Starting StdCopy for non-TTY", "exec_id", execID)
|
||||
stdoutWriter := &grpcStreamWriter{stream: stream, isStderr: false}
|
||||
stderrWriter := &grpcStreamWriter{stream: stream, isStderr: true}
|
||||
|
||||
written, err := stdcopy.StdCopy(stdoutWriter, stderrWriter, attachResp.Reader)
|
||||
slog.Debug("StdCopy completed", "exec_id", execID, "bytes", written, "err", err)
|
||||
if err != nil && err != io.EOF {
|
||||
return fmt.Errorf("demultiplex docker output: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// grpcStreamWriter is a writer that sends data to a gRPC stream as stdout or stderr.
|
||||
type grpcStreamWriter struct {
|
||||
stream pb.Docker_ExecContainerServer
|
||||
isStderr bool
|
||||
}
|
||||
|
||||
func (w *grpcStreamWriter) Write(p []byte) (n int, err error) {
|
||||
data := make([]byte, len(p))
|
||||
copy(data, p)
|
||||
|
||||
var resp *pb.ExecContainerResponse
|
||||
if w.isStderr {
|
||||
resp = &pb.ExecContainerResponse{
|
||||
Payload: &pb.ExecContainerResponse_Stderr{Stderr: data},
|
||||
}
|
||||
} else {
|
||||
resp = &pb.ExecContainerResponse{
|
||||
Payload: &pb.ExecContainerResponse_Stdout{Stdout: data},
|
||||
}
|
||||
}
|
||||
|
||||
if err := w.stream.Send(resp); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
// ExecContainer executes a command in a running container with bidirectional streaming for stdin/stdout/stderr.
|
||||
func (s *Server) ExecContainer(stream pb.Docker_ExecContainerServer) error {
|
||||
slog.Debug("ExecContainer server-side called")
|
||||
defer slog.Debug("ExecContainer server-side ended")
|
||||
|
||||
ctx := stream.Context()
|
||||
|
||||
// Receive and validate configuration
|
||||
execConfig, execOpts, err := s.receiveExecConfig(stream)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Convert to Docker's ExecOptions
|
||||
dockerExecOpts := container.ExecOptions{
|
||||
Cmd: execOpts.Command,
|
||||
AttachStdin: execOpts.AttachStdin,
|
||||
AttachStdout: execOpts.AttachStdout,
|
||||
AttachStderr: execOpts.AttachStderr,
|
||||
Tty: execOpts.Tty,
|
||||
User: execOpts.User,
|
||||
Privileged: execOpts.Privileged,
|
||||
WorkingDir: execOpts.WorkingDir,
|
||||
Env: execOpts.Env,
|
||||
}
|
||||
|
||||
// Create the exec instance
|
||||
execResp, err := s.client.ContainerExecCreate(ctx, execConfig.ContainerId, dockerExecOpts)
|
||||
if err != nil {
|
||||
if errdefs.IsNotFound(err) {
|
||||
return status.Error(codes.NotFound, err.Error())
|
||||
}
|
||||
return status.Errorf(codes.Internal, "create exec: %v", err)
|
||||
}
|
||||
|
||||
// Send the exec ID back to the client
|
||||
if err := stream.Send(&pb.ExecContainerResponse{
|
||||
Payload: &pb.ExecContainerResponse_ExecId{ExecId: execResp.ID},
|
||||
}); err != nil {
|
||||
return status.Errorf(codes.Internal, "send exec ID: %v", err)
|
||||
}
|
||||
slog.Debug("Sent exec ID to the client", "exec_id", execResp.ID)
|
||||
|
||||
// For detached mode, start without attaching
|
||||
if execOpts.Detach {
|
||||
dockerStartOpts := container.ExecStartOptions{
|
||||
Tty: dockerExecOpts.Tty,
|
||||
Detach: true,
|
||||
}
|
||||
if err := s.client.ContainerExecStart(ctx, execResp.ID, dockerStartOpts); err != nil {
|
||||
return status.Errorf(codes.Internal, "start exec: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// For attached mode, attach to the exec instance
|
||||
attachOpts := container.ExecAttachOptions{
|
||||
Tty: dockerExecOpts.Tty,
|
||||
}
|
||||
attachConn, err := s.client.ContainerExecAttach(ctx, execResp.ID, attachOpts)
|
||||
if err != nil {
|
||||
return status.Errorf(codes.Internal, "attach to exec: %v", err)
|
||||
}
|
||||
defer attachConn.Close()
|
||||
|
||||
// Create a cancelable context for the input handler
|
||||
handlerCtx, cancelInput := context.WithCancel(ctx)
|
||||
defer cancelInput()
|
||||
|
||||
// Create a channel to wait for output completion
|
||||
outputDone := make(chan error, 1)
|
||||
|
||||
// Start stdin handler if stdin is attached
|
||||
if dockerExecOpts.AttachStdin {
|
||||
go func() {
|
||||
err := s.handleServerExecInput(handlerCtx, stream, attachConn, execResp.ID, dockerExecOpts.Tty)
|
||||
if err != nil {
|
||||
slog.Warn("Error in exec input handler", "err", err, "exec_id", execResp.ID)
|
||||
}
|
||||
}()
|
||||
} else {
|
||||
// If not attaching stdin, close the write side immediately
|
||||
attachConn.CloseWrite()
|
||||
}
|
||||
|
||||
// Start output handler
|
||||
// We only wait for this goroutine to complete - it signals when the exec process finishes
|
||||
go func() {
|
||||
outputDone <- s.handleServerExecOutput(stream, attachConn, execResp.ID, dockerExecOpts.Tty)
|
||||
}()
|
||||
|
||||
// Wait for the output goroutine to complete (it signals when done)
|
||||
// We only wait for the output handler goroutine, not for the stdin one.
|
||||
if err := <-outputDone; err != nil {
|
||||
slog.Warn("Error in exec output handler", "err", err, "exec_id", execResp.ID)
|
||||
}
|
||||
// Do a best-effort cancellation of the stdin handler.
|
||||
// We can't guarantee immediate exit because it may be blocked on stream.Recv(), but at
|
||||
// least we want to send a cancel signal explicitly.
|
||||
cancelInput()
|
||||
|
||||
inspectResp, err := s.client.ContainerExecInspect(ctx, execResp.ID)
|
||||
if err != nil {
|
||||
slog.Error("Failed to inspect exec after completion", "err", err, "exec_id", execResp.ID)
|
||||
return status.Errorf(codes.Internal, "inspect exec: %v", err)
|
||||
}
|
||||
|
||||
// Send the exit code
|
||||
slog.Debug("Sending exec exit code", "exec_id", execResp.ID, "exit_code", inspectResp.ExitCode)
|
||||
if err := stream.Send(&pb.ExecContainerResponse{
|
||||
Payload: &pb.ExecContainerResponse_ExitCode{ExitCode: int32(inspectResp.ExitCode)},
|
||||
}); err != nil {
|
||||
slog.Error("Failed to send exec exit code", "err", err, "exec_id", execResp.ID)
|
||||
return status.Errorf(codes.Internal, "send exit code: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -430,7 +430,12 @@ func (m *Machine) Run(ctx context.Context) error {
|
||||
}
|
||||
|
||||
dnsResolver := dns.NewClusterResolver(m.store)
|
||||
dnsServer, err := dns.NewServer(m.IP(), dnsResolver, m.config.DNSUpstreams)
|
||||
dnsServer, err := dns.NewServer(
|
||||
m.IP(),
|
||||
m.state.Network.Subnet,
|
||||
dnsResolver,
|
||||
m.config.DNSUpstreams,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create embedded DNS server: %w", err)
|
||||
}
|
||||
@@ -816,7 +821,14 @@ func (m *Machine) JoinCluster(_ context.Context, req *pb.JoinClusterRequest) (*e
|
||||
if err := m.state.Save(); err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "save machine state: %v", err)
|
||||
}
|
||||
slog.Info("Machine configured to join the cluster.", "id", m.state.ID, "name", m.state.Name)
|
||||
slog.Info(
|
||||
"Machine configured to join the cluster.",
|
||||
"id", m.state.ID,
|
||||
"name", m.state.Name,
|
||||
"subnet", m.state.Network.Subnet.String(),
|
||||
"management_ip", m.state.Network.ManagementIP.String(),
|
||||
"peers", len(m.state.Network.Peers),
|
||||
)
|
||||
// Signal that the machine is initialised as a member of a cluster.
|
||||
m.initialised <- struct{}{}
|
||||
|
||||
|
||||
@@ -79,7 +79,7 @@ func (n *WireGuardNetwork) Configure(config Config) error {
|
||||
if err := n.configureDevice(config); err != nil {
|
||||
return err
|
||||
}
|
||||
slog.Info("Configured WireGuard interface.", "name", n.link.Attrs().Name)
|
||||
slog.Info("Configured WireGuard interface.", "name", n.link.Attrs().Name, "peers", len(n.peers))
|
||||
|
||||
managementPrefix, err := addrToSingleIPPrefix(config.ManagementIP)
|
||||
if err != nil {
|
||||
|
||||
@@ -142,7 +142,7 @@ func (p *Provisioner) initCluster(ctx context.Context, machines []Machine) error
|
||||
|
||||
// Join the rest of the machines to the cluster.
|
||||
for _, m := range machines[1:] {
|
||||
if err = WaitMachineReady(ctx, m, 5*time.Second); err != nil {
|
||||
if err = WaitMachineReady(ctx, m, 30*time.Second); err != nil {
|
||||
return fmt.Errorf("wait for machine %q to be ready: %w", m.Name, err)
|
||||
}
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ type ContainerClient interface {
|
||||
RemoveContainer(ctx context.Context, serviceNameOrID, containerNameOrID string, opts container.RemoveOptions) error
|
||||
StartContainer(ctx context.Context, serviceNameOrID, containerNameOrID string) error
|
||||
StopContainer(ctx context.Context, serviceNameOrID, containerNameOrID string, opts container.StopOptions) error
|
||||
ExecContainer(ctx context.Context, serviceNameOrID, containerNameOrID string, config ExecOptions) (int, error)
|
||||
}
|
||||
|
||||
type DNSClient interface {
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/pkg/stringid"
|
||||
"github.com/docker/go-units"
|
||||
)
|
||||
|
||||
@@ -144,6 +145,11 @@ type ServiceContainer struct {
|
||||
ServiceSpec ServiceSpec
|
||||
}
|
||||
|
||||
// ShortID returns the truncated ID of the container (12 characters).
|
||||
func (c *ServiceContainer) ShortID() string {
|
||||
return stringid.TruncateID(c.ID)
|
||||
}
|
||||
|
||||
// ServiceID returns the ID of the service this container belongs to.
|
||||
func (c *ServiceContainer) ServiceID() string {
|
||||
return c.Config.Labels[LabelServiceID]
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
package api
|
||||
|
||||
import "io"
|
||||
|
||||
// ExecOptions contains configuration for executing a command in a container.
|
||||
type ExecOptions struct {
|
||||
// Command is the command to run in the container.
|
||||
Command []string
|
||||
// AttachStdin attaches the stdin stream to the exec session.
|
||||
AttachStdin bool
|
||||
// AttachStdout attaches the stdout stream to the exec session.
|
||||
AttachStdout bool
|
||||
// AttachStderr attaches the stderr stream to the exec session.
|
||||
AttachStderr bool
|
||||
// Tty allocates a pseudo-TTY for the exec session.
|
||||
Tty bool
|
||||
// Detach runs the command in the background without attaching to streams.
|
||||
Detach bool
|
||||
|
||||
//// Not yet implemented fields
|
||||
// User specifies the user to run the command as.
|
||||
User string
|
||||
// Privileged runs the command in privileged mode.
|
||||
Privileged bool
|
||||
// WorkingDir sets the working directory for the command.
|
||||
WorkingDir string
|
||||
// Env sets environment variables for the command.
|
||||
Env []string
|
||||
|
||||
// Client-side only fields (not serialized, not sent to server)
|
||||
// Stdin is the input stream. Defaults to os.Stdin if nil.
|
||||
Stdin io.Reader `json:"-"`
|
||||
// Stdout is the output stream. Defaults to os.Stdout if nil.
|
||||
Stdout io.Writer `json:"-"`
|
||||
// Stderr is the error stream. Defaults to os.Stderr if nil.
|
||||
Stderr io.Writer `json:"-"`
|
||||
}
|
||||
@@ -1,5 +1,9 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/docker/docker/api/types/container"
|
||||
)
|
||||
|
||||
const (
|
||||
MilliCore = 1_000_000
|
||||
Core = 1000 * MilliCore
|
||||
@@ -13,4 +17,6 @@ type ContainerResources struct {
|
||||
// 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
|
||||
// Device reservations/requests for access to things like GPUs
|
||||
DeviceReservations []container.DeviceRequest
|
||||
}
|
||||
|
||||
@@ -57,6 +57,7 @@ func New(ctx context.Context, connector Connector) (*Client, error) {
|
||||
c.ClusterClient = pb.NewClusterClient(c.conn)
|
||||
c.Caddy = pb.NewCaddyClient(c.conn)
|
||||
c.Docker = docker.NewClient(c.conn)
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
package compose
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"github.com/compose-spec/compose-go/v2/types"
|
||||
"github.com/distribution/reference"
|
||||
"github.com/psviderski/uncloud/internal/gitutil"
|
||||
)
|
||||
|
||||
const (
|
||||
// DefaultTagTemplate is the default tag template appended to images without a tag when building locally.
|
||||
// Format:
|
||||
// If the project workdir is a git repo: "2025-08-16-130734.84d33bb" or "2025-08-16-130734.84d33bb.dirty"
|
||||
// If not a git repo: "2025-08-16-130734" which is the current date and time.
|
||||
DefaultTagTemplate = `{{if .Git.IsRepo}}{{gitdate "2006-01-02-150405"}}.{{gitsha 7}}{{if .Git.IsDirty}}.dirty{{end}}{{else}}{{date "2006-01-02-150405"}}{{end}}`
|
||||
// DefaultImageTemplate is the default image template used when no image is specified.
|
||||
DefaultImageTemplate = "{{.Project}}/{{.Service}}:" + DefaultTagTemplate
|
||||
)
|
||||
|
||||
// ImageTemplateContext contains structured data for image template processing.
|
||||
type ImageTemplateContext struct {
|
||||
// Project is the project name.
|
||||
Project string
|
||||
// Service is the service name where the image is defined.
|
||||
Service string
|
||||
// Git is the git repo state.
|
||||
Git gitutil.GitState
|
||||
// Tag is the rendered DefaultTagTemplate used for images without a tag.
|
||||
Tag string
|
||||
}
|
||||
|
||||
// newImageTemplateContext creates an ImageTemplateContext for the given project, service, and git state.
|
||||
func newImageTemplateContext(
|
||||
projectName, serviceName string, gitState gitutil.GitState,
|
||||
) (ImageTemplateContext, error) {
|
||||
ctx := ImageTemplateContext{
|
||||
Project: projectName,
|
||||
Service: serviceName,
|
||||
Git: gitState,
|
||||
}
|
||||
|
||||
tag, err := processImageTemplate(DefaultTagTemplate, ctx)
|
||||
if err != nil {
|
||||
return ImageTemplateContext{}, fmt.Errorf("process default tag template: %w", err)
|
||||
}
|
||||
ctx.Tag = tag
|
||||
|
||||
return ctx, nil
|
||||
}
|
||||
|
||||
// ProcessImageTemplates processes image names in services to expand Go template expressions using
|
||||
// git metadata. If a service has no image specified, uses the default template {{.Project}}/{{.Service}}:{{.Tag}}.
|
||||
// If a service has a build section and an image without a tag, appends the default tag template.
|
||||
// Fully specified images are left unchanged.
|
||||
//
|
||||
// Template context: ImageTemplateContext
|
||||
// Template functions: gitsha(length?), gitdate(format, timezone?), date(format, timezone?)
|
||||
//
|
||||
// Examples:
|
||||
//
|
||||
// image: myapp:{{.Tag}} → myapp:2025-08-16-130734.84d33bb
|
||||
// image: registry.io/api:v1.0-{{gitsha 7}} → registry.io/api:v1.0-84d33bb
|
||||
// image: worker:{{gitdate "2006-01-02"}} → worker:2025-08-16
|
||||
// build: . / image: mybackend → mybackend:2025-08-16-130734.84d33bb
|
||||
// build: . / (no image) → myproject/myservice:2025-08-16-130734.84d33bb
|
||||
// image: postgres → postgres (unchanged)
|
||||
func ProcessImageTemplates(project *types.Project) (*types.Project, error) {
|
||||
gitState, err := gitutil.InspectGitState(project.WorkingDir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("inspect git state at '%s': %w", project.WorkingDir, err)
|
||||
}
|
||||
ctx, err := newImageTemplateContext(project.Name, "", gitState)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Process each service's image.
|
||||
for name, service := range project.Services {
|
||||
ctx.Service = service.Name
|
||||
|
||||
processedImage := ""
|
||||
image := service.Image
|
||||
|
||||
if image == "" {
|
||||
image = DefaultImageTemplate
|
||||
} else {
|
||||
// Check if image contains any template markers.
|
||||
processed, err := processImageTemplate(image, ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("process image template for service '%s': %w", name, err)
|
||||
}
|
||||
|
||||
if processed == image {
|
||||
// Image doesn't contain template markers.
|
||||
// If it doesn't have a tag and has a build section, use the default template for the tag.
|
||||
tagged, err := hasTag(image)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !tagged && service.Build != nil {
|
||||
image = image + ":" + DefaultTagTemplate
|
||||
}
|
||||
} else {
|
||||
// Image contains a template which has been processed successfully so use the result.
|
||||
processedImage = processed
|
||||
}
|
||||
}
|
||||
|
||||
if processedImage == "" {
|
||||
if processedImage, err = processImageTemplate(image, ctx); err != nil {
|
||||
return nil, fmt.Errorf("process image template for service '%s': %w", name, err)
|
||||
}
|
||||
}
|
||||
|
||||
service.Image = processedImage
|
||||
project.Services[name] = service
|
||||
}
|
||||
|
||||
return project, nil
|
||||
}
|
||||
|
||||
// processImageTemplate processes a single image string through the template engine.
|
||||
func processImageTemplate(image string, ctx ImageTemplateContext) (string, error) {
|
||||
funcMap := template.FuncMap{
|
||||
"gitsha": func(length ...int) string {
|
||||
l := 0
|
||||
if len(length) > 0 {
|
||||
l = length[0]
|
||||
}
|
||||
return ctx.Git.ShortSHA(l)
|
||||
},
|
||||
"gitdate": func(layout string, timezone ...string) string {
|
||||
if !ctx.Git.IsRepo || ctx.Git.Date.IsZero() {
|
||||
return ""
|
||||
}
|
||||
|
||||
date := ctx.Git.Date
|
||||
if len(timezone) > 0 {
|
||||
if loc, err := time.LoadLocation(timezone[0]); err == nil {
|
||||
date = date.In(loc)
|
||||
}
|
||||
}
|
||||
|
||||
return date.Format(layout)
|
||||
},
|
||||
"date": func(layout string, timezone ...string) string {
|
||||
t := time.Now().UTC()
|
||||
if len(timezone) > 0 {
|
||||
if loc, err := time.LoadLocation(timezone[0]); err == nil {
|
||||
t = t.In(loc)
|
||||
}
|
||||
}
|
||||
|
||||
return t.Format(layout)
|
||||
},
|
||||
}
|
||||
|
||||
// Parse and execute the template.
|
||||
tmpl, err := template.New("image").Funcs(funcMap).Parse(image)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("parse image template '%s': %w", image, err)
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err = tmpl.Execute(&buf, ctx); err != nil {
|
||||
return "", fmt.Errorf("execute image template: %w", err)
|
||||
}
|
||||
|
||||
return buf.String(), nil
|
||||
}
|
||||
|
||||
// hasTag checks if an image name already has an explicit tag or digest.
|
||||
// Returns an error if the image reference is invalid.
|
||||
func hasTag(image string) (bool, error) {
|
||||
ref, err := reference.ParseNormalizedNamed(image)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("parse image reference '%s': %w", image, err)
|
||||
}
|
||||
|
||||
// Check if it has an explicit tag (e.g., "myapp:v1.0").
|
||||
if _, ok := ref.(reference.Tagged); ok {
|
||||
return true, nil
|
||||
}
|
||||
// Check if it has a digest (e.g., "myapp@sha256:...").
|
||||
if _, ok := ref.(reference.Digested); ok {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
@@ -0,0 +1,466 @@
|
||||
package compose
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/compose-spec/compose-go/v2/types"
|
||||
"github.com/psviderski/uncloud/internal/gitutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestProcessImageTemplates_Integration(t *testing.T) {
|
||||
// Create a temporary directory for the test git repository.
|
||||
tmpDir := t.TempDir()
|
||||
initGitRepo(t, tmpDir)
|
||||
|
||||
// Create an initial commit.
|
||||
testFile := filepath.Join(tmpDir, "test.txt")
|
||||
err := os.WriteFile(testFile, []byte("initial content"), 0o644)
|
||||
require.NoError(t, err)
|
||||
runGitCommand(t, tmpDir, "add", "test.txt")
|
||||
runGitCommand(t, tmpDir, "commit", "-m", "Initial commit")
|
||||
|
||||
gitState, err := gitutil.InspectGitState(tmpDir)
|
||||
require.NoError(t, err)
|
||||
require.True(t, gitState.IsRepo)
|
||||
require.False(t, gitState.IsDirty)
|
||||
require.NotEmpty(t, gitState.SHA)
|
||||
|
||||
expectedTag := gitState.Date.UTC().Format("2006-01-02-150405") + "." + gitState.SHA[:7]
|
||||
expectedDate := gitState.Date.UTC().Format("2006-01-02")
|
||||
|
||||
t.Run("clean repository", func(t *testing.T) {
|
||||
project := &types.Project{
|
||||
Name: "myproject",
|
||||
WorkingDir: tmpDir,
|
||||
Services: types.Services{
|
||||
"service1": {
|
||||
Name: "service1",
|
||||
Image: "myapp:{{.Tag}}",
|
||||
Build: &types.BuildConfig{Context: "."},
|
||||
},
|
||||
"service2": {
|
||||
Name: "service2",
|
||||
Image: `myapp:v1.0-{{gitsha 7}}`,
|
||||
Build: &types.BuildConfig{Context: "."},
|
||||
},
|
||||
"service3": {
|
||||
Name: "service3",
|
||||
Image: "myapp", // No tag, has build, should get default tag template appended.
|
||||
Build: &types.BuildConfig{Context: "."},
|
||||
},
|
||||
"service4": {
|
||||
Name: "service4",
|
||||
Image: "", // Empty image, has build, should use default template.
|
||||
Build: &types.BuildConfig{Context: "."},
|
||||
},
|
||||
"service5": {
|
||||
Name: "service5",
|
||||
Image: "{{.Project}}/{{.Service}}:custom", // Custom template.
|
||||
Build: &types.BuildConfig{Context: "."},
|
||||
},
|
||||
"service6": {
|
||||
Name: "service6",
|
||||
Image: `registry.example.com/{{.Project}}/{{.Service}}:{{gitdate "2006-01-02"}}-{{gitsha 7}}`,
|
||||
Build: &types.BuildConfig{Context: "."},
|
||||
},
|
||||
"service7": {
|
||||
Name: "service7",
|
||||
Image: `myapp:{{if .Git.IsRepo}}git-{{.Git.SHA}}{{else}}no-git{{end}}`,
|
||||
Build: &types.BuildConfig{Context: "."},
|
||||
},
|
||||
"service8": {
|
||||
Name: "service8",
|
||||
Image: `myapp:{{gitdate "20060102"}}.{{gitsha 10}}{{if .Git.IsDirty}}.dirty{{end}}`,
|
||||
Build: &types.BuildConfig{Context: "."},
|
||||
},
|
||||
"postgres": {
|
||||
Name: "postgres",
|
||||
Image: "postgres", // No build section, should NOT get tag appended.
|
||||
},
|
||||
"redis": {
|
||||
Name: "redis",
|
||||
Image: "redis:7.2", // Third-party with tag, should be unchanged.
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
p, err := ProcessImageTemplates(project)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "myapp:"+expectedTag, p.Services["service1"].Image)
|
||||
assert.Equal(t, "myapp:v1.0-"+gitState.SHA[:7], p.Services["service2"].Image)
|
||||
assert.Equal(t, "myapp:"+expectedTag, p.Services["service3"].Image)
|
||||
assert.Equal(t, "myproject/service4:"+expectedTag, p.Services["service4"].Image)
|
||||
assert.Equal(t, "myproject/service5:custom", p.Services["service5"].Image)
|
||||
expectedService6 := "registry.example.com/myproject/service6:" + expectedDate + "-" + gitState.SHA[:7]
|
||||
assert.Equal(t, expectedService6, p.Services["service6"].Image)
|
||||
assert.Equal(t, "myapp:git-"+gitState.SHA, p.Services["service7"].Image)
|
||||
expectedService8 := "myapp:" + gitState.Date.UTC().Format("20060102") + "." + gitState.SHA[:10]
|
||||
assert.Equal(t, expectedService8, p.Services["service8"].Image)
|
||||
|
||||
// Images without build section should be unchanged.
|
||||
assert.Equal(t, "postgres", p.Services["postgres"].Image, "postgres image should not have tag appended")
|
||||
assert.Equal(t, "redis:7.2", p.Services["redis"].Image, "redis image should be unchanged")
|
||||
})
|
||||
|
||||
t.Run("dirty repository", func(t *testing.T) {
|
||||
// Make the repository dirty by creating an uncommitted file.
|
||||
dirtyFile := filepath.Join(tmpDir, "uncommitted.txt")
|
||||
err := os.WriteFile(dirtyFile, []byte("uncommitted changes"), 0o644)
|
||||
require.NoError(t, err)
|
||||
|
||||
dirtyGitState, err := gitutil.InspectGitState(tmpDir)
|
||||
require.NoError(t, err)
|
||||
require.True(t, dirtyGitState.IsDirty)
|
||||
require.Equal(t, gitState.SHA, dirtyGitState.SHA)
|
||||
|
||||
project := &types.Project{
|
||||
Name: "myproject",
|
||||
WorkingDir: tmpDir,
|
||||
Services: types.Services{
|
||||
"default": {
|
||||
Name: "default",
|
||||
Build: &types.BuildConfig{Context: "."},
|
||||
},
|
||||
"app": {
|
||||
Name: "app",
|
||||
Image: `myapp:{{gitdate "2006-01-02"}}-{{gitsha 7}}{{if .Git.IsDirty}}.dirty{{end}}`,
|
||||
Build: &types.BuildConfig{Context: "."},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
p, err := ProcessImageTemplates(project)
|
||||
require.NoError(t, err)
|
||||
|
||||
expectedDirtyDate := dirtyGitState.Date.UTC().Format("2006-01-02")
|
||||
|
||||
assert.Equal(t, "myproject/default:"+expectedTag+".dirty", p.Services["default"].Image)
|
||||
expectedApp := "myapp:" + expectedDirtyDate + "-" + dirtyGitState.SHA[:7] + ".dirty"
|
||||
assert.Equal(t, expectedApp, p.Services["app"].Image)
|
||||
})
|
||||
|
||||
t.Run("non-git directory", func(t *testing.T) {
|
||||
nonGitDir := t.TempDir()
|
||||
|
||||
project := &types.Project{
|
||||
Name: "myproject",
|
||||
WorkingDir: nonGitDir,
|
||||
Services: types.Services{
|
||||
"default": {
|
||||
Name: "default",
|
||||
Build: &types.BuildConfig{Context: "."},
|
||||
},
|
||||
"app": {
|
||||
Name: "app",
|
||||
Image: `myapp:{{if .Git.IsRepo}}git{{else}}{{date "2006-01-02"}}{{end}}`,
|
||||
Build: &types.BuildConfig{Context: "."},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
p, err := ProcessImageTemplates(project)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Regexp(t, regexp.MustCompile("^myproject/default:"+expectedDate+`-\d{6}$`), p.Services["default"].Image)
|
||||
assert.Equal(t, "myapp:"+expectedDate, p.Services["app"].Image)
|
||||
})
|
||||
}
|
||||
|
||||
func TestProcessImageTemplate(t *testing.T) {
|
||||
// Fixed time for testing: 2025-08-16 13:07:34 UTC.
|
||||
fixedTime := time.Date(2025, 8, 16, 13, 7, 34, 0, time.UTC)
|
||||
|
||||
gitState := gitutil.GitState{
|
||||
Date: fixedTime,
|
||||
IsDirty: false,
|
||||
IsRepo: true,
|
||||
SHA: "84d33bb1234567890abcdef1234567890abcdef",
|
||||
}
|
||||
ctx, err := newImageTemplateContext("myproject", "myservice", gitState)
|
||||
require.NoError(t, err)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
image string
|
||||
expected string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "no template markers",
|
||||
image: "myapp:v1.0",
|
||||
expected: "myapp:v1.0",
|
||||
},
|
||||
{
|
||||
name: "git SHA function with length",
|
||||
image: "myapp:{{gitsha 10}}",
|
||||
expected: "myapp:84d33bb123",
|
||||
},
|
||||
{
|
||||
name: "git SHA function full length",
|
||||
image: "myapp:{{gitsha}}",
|
||||
expected: "myapp:84d33bb1234567890abcdef1234567890abcdef",
|
||||
},
|
||||
{
|
||||
name: "git SHA function negative length",
|
||||
image: "myapp:{{gitsha -1}}",
|
||||
expected: "myapp:84d33bb1234567890abcdef1234567890abcdef",
|
||||
},
|
||||
{
|
||||
name: "git date",
|
||||
image: `myapp:{{gitdate "2006-01-02"}}`,
|
||||
expected: "myapp:2025-08-16",
|
||||
},
|
||||
{
|
||||
name: "git date with time",
|
||||
image: `myapp:{{gitdate "2006-01-02-150405"}}`,
|
||||
expected: "myapp:2025-08-16-130734",
|
||||
},
|
||||
{
|
||||
name: "git date with timezone",
|
||||
image: `myapp:{{gitdate "2006-01-02-150405" "Australia/Brisbane"}}`,
|
||||
expected: "myapp:2025-08-16-230734",
|
||||
},
|
||||
{
|
||||
name: "current date",
|
||||
image: `myapp:{{date "2006-01-02"}}`,
|
||||
expected: `^myapp:\d{4}-\d{2}-\d{2}$`,
|
||||
},
|
||||
{
|
||||
name: "current date with time",
|
||||
image: `myapp:{{date "20060102-150405"}}`,
|
||||
expected: `^myapp:\d{8}-\d{6}$`,
|
||||
},
|
||||
{
|
||||
name: "current date with timezone",
|
||||
image: `myapp:{{date "20060102-150405" "Local"}}`,
|
||||
expected: `^myapp:\d{8}-\d{6}$`,
|
||||
},
|
||||
{
|
||||
name: "multiple template functions",
|
||||
image: `myapp:v1.0-{{gitdate "2006-01-02"}}-{{gitsha 7}}`,
|
||||
expected: "myapp:v1.0-2025-08-16-84d33bb",
|
||||
},
|
||||
{
|
||||
name: "project and service variables",
|
||||
image: "{{.Project}}/{{.Service}}:{{gitsha 7}}",
|
||||
expected: "myproject/myservice:84d33bb",
|
||||
},
|
||||
{
|
||||
name: "git SHA variable",
|
||||
image: "myapp:{{.Git.SHA}}",
|
||||
expected: "myapp:84d33bb1234567890abcdef1234567890abcdef",
|
||||
},
|
||||
{
|
||||
name: "tag variable",
|
||||
image: "myapp:{{.Tag}}",
|
||||
expected: "myapp:2025-08-16-130734.84d33bb",
|
||||
},
|
||||
{
|
||||
name: "default image template short",
|
||||
image: "{{.Project}}/{{.Service}}:{{.Tag}}",
|
||||
expected: "myproject/myservice:2025-08-16-130734.84d33bb",
|
||||
},
|
||||
{
|
||||
name: "default image template long",
|
||||
image: `{{.Project}}/{{.Service}}:{{if .Git.IsRepo}}{{gitdate "2006-01-02-150405"}}.{{gitsha 7}}{{if .Git.IsDirty}}.dirty{{end}}{{else}}{{date "2006-01-02-150405"}}{{end}}`,
|
||||
expected: "myproject/myservice:2025-08-16-130734.84d33bb",
|
||||
},
|
||||
{
|
||||
name: "whitespace only",
|
||||
image: " ",
|
||||
expected: " ",
|
||||
},
|
||||
{
|
||||
name: "invalid template syntax",
|
||||
image: "myapp:{{.Git.NonExistent.Field}}",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "unclosed template",
|
||||
image: "myapp:{{.Tag",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid gitsha argument - not a number",
|
||||
image: `myapp:{{gitsha "abc"}}`,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid date format - missing quotes",
|
||||
image: "myapp:{{date 2006-01-02}}",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "division by zero in template",
|
||||
image: "myapp:{{divide 1 0}}",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "undefined function",
|
||||
image: "myapp:{{undefined_func}}",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result, err := processImageTemplate(tt.image, ctx)
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
if strings.HasPrefix(tt.expected, "^") {
|
||||
assert.Regexp(t, tt.expected, result)
|
||||
} else {
|
||||
assert.Equal(t, tt.expected, result)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHasTag(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
image string
|
||||
want bool
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "image with tag",
|
||||
image: "myapp:v1.0",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "image without tag",
|
||||
image: "myapp",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "registry with port and tag",
|
||||
image: "localhost:5000/myapp:v1.0",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "registry with port, no tag",
|
||||
image: "localhost:5000/myapp",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "registry path with tag",
|
||||
image: "registry.example.com/org/myapp:v1.0",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "registry path without tag",
|
||||
image: "registry.example.com/org/myapp",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "image with digest",
|
||||
image: "myapp@sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "registry with tag and digest",
|
||||
image: "registry.example.com/myapp:v1.0@sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
|
||||
want: true,
|
||||
},
|
||||
// Invalid image references.
|
||||
{
|
||||
name: "image with empty tag",
|
||||
image: "myapp:",
|
||||
want: false,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "empty string",
|
||||
image: "",
|
||||
want: false,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid characters - spaces",
|
||||
image: "my app:v1.0",
|
||||
want: false,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid characters - uppercase in name",
|
||||
image: "MyApp:v1.0",
|
||||
want: false,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid digest format",
|
||||
image: "myapp@invalid-digest",
|
||||
want: false,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "double colon",
|
||||
image: "myapp::tag",
|
||||
want: false,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "starts with slash",
|
||||
image: "/myapp:v1.0",
|
||||
want: false,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "ends with slash",
|
||||
image: "myapp/:v1.0",
|
||||
want: false,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid tag characters",
|
||||
image: "myapp:v1.0@latest",
|
||||
want: false,
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result, err := hasTag(tt.image)
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.want, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Helper functions.
|
||||
|
||||
func initGitRepo(t *testing.T, dir string) {
|
||||
t.Helper()
|
||||
|
||||
// Initialize git repo.
|
||||
runGitCommand(t, dir, "init")
|
||||
runGitCommand(t, dir, "config", "user.email", "test@example.com")
|
||||
runGitCommand(t, dir, "config", "user.name", "Test User")
|
||||
}
|
||||
|
||||
func runGitCommand(t *testing.T, dir string, args ...string) {
|
||||
t.Helper()
|
||||
|
||||
cmd := exec.Command("git", args...)
|
||||
cmd.Dir = dir
|
||||
output, err := cmd.CombinedOutput()
|
||||
require.NoError(t, err, "git command failed: %s", output)
|
||||
}
|
||||
@@ -3,17 +3,14 @@ package compose
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
composecli "github.com/compose-spec/compose-go/v2/cli"
|
||||
"github.com/compose-spec/compose-go/v2/types"
|
||||
)
|
||||
|
||||
// FakeProjectName is a placeholder name for the project to be able to strip it from the resource names used as prefix.
|
||||
const FakeProjectName = "f-a-k-e"
|
||||
|
||||
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.
|
||||
composecli.WithOsEnv,
|
||||
// Set the local .env file to be loaded by WithDotEnv. COMPOSE_DISABLE_ENV_FILE can disable it.
|
||||
@@ -43,6 +40,7 @@ func LoadProject(ctx context.Context, paths []string, opts ...composecli.Project
|
||||
return nil, err
|
||||
}
|
||||
|
||||
removeProjectPrefixFromNames(project)
|
||||
if project, err = transformServicesCaddyExtension(project); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -55,5 +53,19 @@ func LoadProject(ctx context.Context, paths []string, opts ...composecli.Project
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Process image templates in services to expand Go template expressions using git repo state.
|
||||
if project, err = ProcessImageTemplates(project); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return project, nil
|
||||
}
|
||||
|
||||
// removeProjectPrefixFromNames removes the project name prefix from volume names.
|
||||
func removeProjectPrefixFromNames(project *types.Project) {
|
||||
prefix := project.Name + "_"
|
||||
for name, vol := range project.Volumes {
|
||||
vol.Name = strings.TrimPrefix(vol.Name, prefix)
|
||||
project.Volumes[name] = vol
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,9 +5,9 @@ import (
|
||||
"maps"
|
||||
"os"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/compose-spec/compose-go/v2/types"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/mount"
|
||||
"github.com/opencontainers/go-digest"
|
||||
"github.com/psviderski/uncloud/pkg/api"
|
||||
@@ -124,6 +124,10 @@ func resourcesFromCompose(service types.ServiceConfig) api.ContainerResources {
|
||||
MemoryReservation: int64(service.MemReservation),
|
||||
}
|
||||
|
||||
// Convert GPU device requests from compose format, appending "gpu" capability.
|
||||
resources.DeviceReservations = append(resources.DeviceReservations,
|
||||
deviceReservationsFromCompose(service.Gpus, "gpu")...)
|
||||
|
||||
// Map resources from deploy section if specified.
|
||||
if service.Deploy != nil {
|
||||
if service.Deploy.Resources.Limits != nil {
|
||||
@@ -139,12 +143,40 @@ func resourcesFromCompose(service types.ServiceConfig) api.ContainerResources {
|
||||
if service.Deploy.Resources.Reservations.MemoryBytes > 0 {
|
||||
resources.MemoryReservation = int64(service.Deploy.Resources.Reservations.MemoryBytes)
|
||||
}
|
||||
// Handle arbitrary device reservations (same structure as Gpus above).
|
||||
resources.DeviceReservations = append(resources.DeviceReservations,
|
||||
deviceReservationsFromCompose(service.Deploy.Resources.Reservations.Devices)...)
|
||||
}
|
||||
}
|
||||
|
||||
return resources
|
||||
}
|
||||
|
||||
// Converts compose-go DeviceRequest format to Docker API DeviceRequest format.
|
||||
// Additional capabilities can be appended via extraCapabilities (e.g., "gpu" for service.Gpus).
|
||||
func deviceReservationsFromCompose(devices []types.DeviceRequest, extraCapabilities ...string) []container.DeviceRequest {
|
||||
if devices == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
requests := make([]container.DeviceRequest, 0, len(devices))
|
||||
for _, deviceRequest := range devices {
|
||||
// Docker expects an OR'd list of AND'd capabilities (e.g. [][]string),
|
||||
// but compose-go provides a single AND'd list (e.g. []string).
|
||||
capabilities := [][]string{append(deviceRequest.Capabilities, extraCapabilities...)}
|
||||
|
||||
spec := container.DeviceRequest{
|
||||
Driver: deviceRequest.Driver,
|
||||
Count: int(deviceRequest.Count),
|
||||
DeviceIDs: deviceRequest.IDs,
|
||||
Capabilities: capabilities,
|
||||
Options: deviceRequest.Options,
|
||||
}
|
||||
requests = append(requests, spec)
|
||||
}
|
||||
return requests
|
||||
}
|
||||
|
||||
func volumeSpecsFromCompose(
|
||||
volumes types.Volumes, serviceVolumes []types.ServiceVolumeConfig,
|
||||
) ([]api.VolumeSpec, []api.VolumeMount, error) {
|
||||
@@ -207,7 +239,7 @@ func dockerVolumeSpecFromCompose(serviceVolume types.ServiceVolumeConfig, volume
|
||||
Name: serviceVolume.Source,
|
||||
Type: api.VolumeTypeVolume,
|
||||
VolumeOptions: &api.VolumeOptions{
|
||||
Name: strings.TrimPrefix(volume.Name, FakeProjectName+"_"),
|
||||
Name: volume.Name,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
|
||||
"github.com/compose-spec/compose-go/v2/loader"
|
||||
"github.com/compose-spec/compose-go/v2/types"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/mount"
|
||||
"github.com/docker/go-units"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
@@ -19,7 +20,8 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// loadProjectFromContent loads a compose project from YAML content
|
||||
// loadProjectFromContent loads a compose project from YAML content.
|
||||
// Keep the implementation in sync with LoadProject.
|
||||
func loadProjectFromContent(t *testing.T, content string) (*types.Project, error) {
|
||||
t.Helper()
|
||||
ctx := context.Background()
|
||||
@@ -47,6 +49,7 @@ func loadProjectFromContent(t *testing.T, content string) (*types.Project, error
|
||||
return nil, err
|
||||
}
|
||||
|
||||
removeProjectPrefixFromNames(project)
|
||||
// Apply extension transformations since we're not using LoadProject.
|
||||
if project, err = transformServicesCaddyExtension(project); err != nil {
|
||||
return nil, err
|
||||
@@ -60,6 +63,11 @@ func loadProjectFromContent(t *testing.T, content string) (*types.Project, error
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Process image templates in services to expand Go template expressions using git repo state.
|
||||
if project, err = ProcessImageTemplates(project); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return project, nil
|
||||
}
|
||||
|
||||
@@ -434,6 +442,193 @@ services:
|
||||
}
|
||||
}
|
||||
|
||||
func TestServiceSpecFromCompose_GPUs(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
composeYAML string
|
||||
expectedDeviceReqs []container.DeviceRequest
|
||||
}{
|
||||
{
|
||||
name: "gpus_all_shorthand",
|
||||
composeYAML: `
|
||||
services:
|
||||
ai:
|
||||
image: nvidia/cuda
|
||||
gpus: all
|
||||
`,
|
||||
expectedDeviceReqs: []container.DeviceRequest{
|
||||
{
|
||||
Count: -1,
|
||||
Capabilities: [][]string{{"gpu"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "gpus_device_ids",
|
||||
composeYAML: `
|
||||
services:
|
||||
ai:
|
||||
image: nvidia/cuda
|
||||
gpus:
|
||||
- device_ids: ['0', '1']
|
||||
capabilities: [compute]
|
||||
`,
|
||||
expectedDeviceReqs: []container.DeviceRequest{
|
||||
{
|
||||
DeviceIDs: []string{"0", "1"},
|
||||
Capabilities: [][]string{{"compute", "gpu"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "gpus_count",
|
||||
composeYAML: `
|
||||
services:
|
||||
ai:
|
||||
image: nvidia/cuda
|
||||
gpus:
|
||||
- count: 2
|
||||
capabilities: [utility]
|
||||
`,
|
||||
expectedDeviceReqs: []container.DeviceRequest{
|
||||
{
|
||||
Count: 2,
|
||||
Capabilities: [][]string{{"utility", "gpu"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "gpus_driver_and_options",
|
||||
composeYAML: `
|
||||
services:
|
||||
ai:
|
||||
image: nvidia/cuda
|
||||
gpus:
|
||||
- driver: nvidia
|
||||
count: 1
|
||||
capabilities: [compute, utility]
|
||||
options:
|
||||
key1: value1
|
||||
key2: value2
|
||||
`,
|
||||
expectedDeviceReqs: []container.DeviceRequest{
|
||||
{
|
||||
Driver: "nvidia",
|
||||
Count: 1,
|
||||
Capabilities: [][]string{{"compute", "utility", "gpu"}},
|
||||
Options: map[string]string{
|
||||
"key1": "value1",
|
||||
"key2": "value2",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "deploy_resources_reservations_devices",
|
||||
composeYAML: `
|
||||
services:
|
||||
ai:
|
||||
image: nvidia/cuda
|
||||
deploy:
|
||||
resources:
|
||||
reservations:
|
||||
devices:
|
||||
- driver: nvidia
|
||||
count: 1
|
||||
capabilities: [gpu]
|
||||
`,
|
||||
expectedDeviceReqs: []container.DeviceRequest{
|
||||
{
|
||||
Driver: "nvidia",
|
||||
Count: 1,
|
||||
Capabilities: [][]string{{"gpu"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "gpus_with_existing_gpu_capability",
|
||||
composeYAML: `
|
||||
services:
|
||||
ai:
|
||||
image: nvidia/cuda
|
||||
gpus:
|
||||
- capabilities: [gpu, compute]
|
||||
`,
|
||||
expectedDeviceReqs: []container.DeviceRequest{
|
||||
{
|
||||
// defaults to "all" when count not specified
|
||||
Count: -1,
|
||||
// gpu appended even if already present (matching Docker Compose behavior)
|
||||
Capabilities: [][]string{{"gpu", "compute", "gpu"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "multiple_device_reservations",
|
||||
composeYAML: `
|
||||
services:
|
||||
ai:
|
||||
image: nvidia/cuda
|
||||
deploy:
|
||||
resources:
|
||||
reservations:
|
||||
devices:
|
||||
- driver: nvidia
|
||||
count: 2
|
||||
capabilities: [gpu, compute]
|
||||
- capabilities: [tpu]
|
||||
count: 1
|
||||
`,
|
||||
expectedDeviceReqs: []container.DeviceRequest{
|
||||
{
|
||||
Driver: "nvidia",
|
||||
Count: 2,
|
||||
Capabilities: [][]string{{"gpu", "compute"}},
|
||||
},
|
||||
{
|
||||
Count: 1,
|
||||
Capabilities: [][]string{{"tpu"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "tpu_reservation",
|
||||
composeYAML: `
|
||||
services:
|
||||
ai:
|
||||
image: tensorflow/tensorflow:latest
|
||||
deploy:
|
||||
resources:
|
||||
reservations:
|
||||
devices:
|
||||
- capabilities: [tpu]
|
||||
count: 4
|
||||
driver: google
|
||||
`,
|
||||
expectedDeviceReqs: []container.DeviceRequest{
|
||||
{
|
||||
Driver: "google",
|
||||
Count: 4,
|
||||
Capabilities: [][]string{{"tpu"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
project, err := loadProjectFromContent(t, tt.composeYAML)
|
||||
require.NoError(t, err)
|
||||
|
||||
spec, err := ServiceSpecFromCompose(project, "ai")
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, tt.expectedDeviceReqs, spec.Container.Resources.DeviceReservations,
|
||||
"DeviceReservations should match expected")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestServiceSpecFromCompose_XMachinesPlacement(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
package connector
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
|
||||
"github.com/docker/cli/cli/connhelper/commandconn"
|
||||
"github.com/psviderski/uncloud/internal/machine"
|
||||
"golang.org/x/net/proxy"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
)
|
||||
|
||||
// SSHCLIConnector establishes a connection to the machine API by executing SSH CLI
|
||||
// and running `uncloudd dial-stdio` on the remote machine.
|
||||
type SSHCLIConnector struct {
|
||||
config SSHConnectorConfig
|
||||
conn net.Conn
|
||||
}
|
||||
|
||||
func NewSSHCLIConnector(cfg *SSHConnectorConfig) *SSHCLIConnector {
|
||||
return &SSHCLIConnector{config: *cfg}
|
||||
}
|
||||
|
||||
// sshCLIDialer implements proxy.ContextDialer by spawning SSH processes with -W flag.
|
||||
type sshCLIDialer struct {
|
||||
config SSHConnectorConfig
|
||||
}
|
||||
|
||||
// buildDialArgs constructs SSH command arguments for -W flag dialing.
|
||||
func (d *sshCLIDialer) buildDialArgs(address string) []string {
|
||||
args := []string{}
|
||||
|
||||
// Add connection timeout to fail fast when node is down.
|
||||
args = append(args, "-o", "ConnectTimeout=5")
|
||||
|
||||
// Add port if non-standard.
|
||||
if d.config.Port != 0 && d.config.Port != 22 {
|
||||
args = append(args, "-p", strconv.Itoa(d.config.Port))
|
||||
}
|
||||
|
||||
// Add identity file if specified.
|
||||
if d.config.KeyPath != "" {
|
||||
args = append(args, "-i", d.config.KeyPath)
|
||||
}
|
||||
|
||||
// Add -W flag for stdin/stdout forwarding to target address.
|
||||
args = append(args, "-W", address)
|
||||
|
||||
// Add user@host.
|
||||
args = append(args, d.config.User+"@"+d.config.Host)
|
||||
|
||||
return args
|
||||
}
|
||||
|
||||
// DialContext establishes a connection to the target address through an SSH tunnel using -W flag.
|
||||
func (d *sshCLIDialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) {
|
||||
// Only support TCP connections.
|
||||
if network != "tcp" {
|
||||
return nil, fmt.Errorf("unsupported network type: %s", network)
|
||||
}
|
||||
|
||||
// Build SSH command arguments.
|
||||
args := d.buildDialArgs(address)
|
||||
|
||||
// Create connection using commandconn.
|
||||
conn, err := commandconn.New(ctx, "ssh", args...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("SSH connection to %s@%s for dialing %s: %w", d.config.User, d.config.Host, address, err)
|
||||
}
|
||||
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
func (c *SSHCLIConnector) Connect(ctx context.Context) (*grpc.ClientConn, error) {
|
||||
// Build SSH command arguments.
|
||||
args := c.buildSSHArgs()
|
||||
|
||||
// Create connection using commandconn.
|
||||
conn, err := commandconn.New(ctx, "ssh", args...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("SSH connection to %s@%s: %w", c.config.User, c.config.Host, err)
|
||||
}
|
||||
c.conn = conn
|
||||
|
||||
// Create gRPC client over the connection.
|
||||
// Use a custom dialer that returns our existing connection.
|
||||
grpcConn, err := grpc.NewClient(
|
||||
"passthrough:///", // Dummy target since we're using a custom dialer.
|
||||
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
||||
grpc.WithContextDialer(func(ctx context.Context, _ string) (net.Conn, error) {
|
||||
return c.conn, nil
|
||||
}),
|
||||
)
|
||||
if err != nil {
|
||||
c.conn.Close()
|
||||
return nil, fmt.Errorf("create machine API client: %w", err)
|
||||
}
|
||||
|
||||
return grpcConn, nil
|
||||
}
|
||||
|
||||
// buildSSHArgs constructs the SSH command arguments.
|
||||
func (c *SSHCLIConnector) buildSSHArgs() []string {
|
||||
args := []string{}
|
||||
|
||||
// Add connection timeout to fail fast when node is down.
|
||||
args = append(args, "-o", "ConnectTimeout=5")
|
||||
|
||||
// Add port if non-standard.
|
||||
if c.config.Port != 0 && c.config.Port != 22 {
|
||||
args = append(args, "-p", strconv.Itoa(c.config.Port))
|
||||
}
|
||||
|
||||
// Add identity file if specified (backward compatibility with SSHKeyFile).
|
||||
if c.config.KeyPath != "" {
|
||||
args = append(args, "-i", c.config.KeyPath)
|
||||
}
|
||||
|
||||
// Add user@host.
|
||||
args = append(args, c.config.User+"@"+c.config.Host)
|
||||
|
||||
// Add remote command: uncloudd dial-stdio
|
||||
args = append(args, "uncloudd", "dial-stdio")
|
||||
|
||||
// Add socket path if non-default.
|
||||
if c.config.SockPath != "" && c.config.SockPath != machine.DefaultUncloudSockPath {
|
||||
args = append(args, "--socket", c.config.SockPath)
|
||||
}
|
||||
|
||||
return args
|
||||
}
|
||||
|
||||
// Dialer returns a proxy dialer for establishing connections within the cluster through SSH tunnels.
|
||||
func (c *SSHCLIConnector) Dialer() (proxy.ContextDialer, error) {
|
||||
if c.config == (SSHConnectorConfig{}) {
|
||||
return nil, fmt.Errorf("SSH connector not configured")
|
||||
}
|
||||
|
||||
return &sshCLIDialer{
|
||||
config: c.config,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *SSHCLIConnector) Close() error {
|
||||
if c.conn != nil {
|
||||
err := c.conn.Close()
|
||||
c.conn = nil
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
package connector
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/psviderski/uncloud/internal/machine"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestSSHCLIConnector_buildSSHArgs(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
config SSHConnectorConfig
|
||||
expected []string
|
||||
}{
|
||||
{
|
||||
name: "basic connection",
|
||||
config: SSHConnectorConfig{
|
||||
User: "root",
|
||||
Host: "example.com",
|
||||
},
|
||||
expected: []string{"-o", "ConnectTimeout=5", "root@example.com", "uncloudd", "dial-stdio"},
|
||||
},
|
||||
{
|
||||
name: "with custom port",
|
||||
config: SSHConnectorConfig{
|
||||
User: "root",
|
||||
Host: "example.com",
|
||||
Port: 2222,
|
||||
},
|
||||
expected: []string{"-o", "ConnectTimeout=5", "-p", "2222", "root@example.com", "uncloudd", "dial-stdio"},
|
||||
},
|
||||
{
|
||||
name: "with identity file",
|
||||
config: SSHConnectorConfig{
|
||||
User: "root",
|
||||
Host: "example.com",
|
||||
KeyPath: "/path/to/key",
|
||||
},
|
||||
expected: []string{"-o", "ConnectTimeout=5", "-i", "/path/to/key", "root@example.com", "uncloudd", "dial-stdio"},
|
||||
},
|
||||
{
|
||||
name: "with custom socket path",
|
||||
config: SSHConnectorConfig{
|
||||
User: "root",
|
||||
Host: "example.com",
|
||||
SockPath: "/custom/path/uncloud.sock",
|
||||
},
|
||||
expected: []string{"-o", "ConnectTimeout=5", "root@example.com", "uncloudd", "dial-stdio", "--socket", "/custom/path/uncloud.sock"},
|
||||
},
|
||||
{
|
||||
name: "with default socket path (not included)",
|
||||
config: SSHConnectorConfig{
|
||||
User: "root",
|
||||
Host: "example.com",
|
||||
SockPath: machine.DefaultUncloudSockPath,
|
||||
},
|
||||
expected: []string{"-o", "ConnectTimeout=5", "root@example.com", "uncloudd", "dial-stdio"},
|
||||
},
|
||||
{
|
||||
name: "all options combined",
|
||||
config: SSHConnectorConfig{
|
||||
User: "root",
|
||||
Host: "example.com",
|
||||
Port: 2222,
|
||||
KeyPath: "/path/to/key",
|
||||
SockPath: "/custom/path/uncloud.sock",
|
||||
},
|
||||
expected: []string{"-o", "ConnectTimeout=5", "-p", "2222", "-i", "/path/to/key", "root@example.com", "uncloudd", "dial-stdio", "--socket", "/custom/path/uncloud.sock"},
|
||||
},
|
||||
{
|
||||
name: "port 22 not included (default)",
|
||||
config: SSHConnectorConfig{
|
||||
User: "root",
|
||||
Host: "example.com",
|
||||
Port: 0,
|
||||
},
|
||||
expected: []string{"-o", "ConnectTimeout=5", "root@example.com", "uncloudd", "dial-stdio"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
c := &SSHCLIConnector{config: tt.config}
|
||||
got := c.buildSSHArgs()
|
||||
assert.Equal(t, tt.expected, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSSHCLIDialer_buildDialArgs(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
config SSHConnectorConfig
|
||||
address string
|
||||
expected []string
|
||||
}{
|
||||
{
|
||||
name: "basic connection",
|
||||
config: SSHConnectorConfig{
|
||||
User: "root",
|
||||
Host: "example.com",
|
||||
},
|
||||
address: "10.210.1.1:5000",
|
||||
expected: []string{"-o", "ConnectTimeout=5", "-W", "10.210.1.1:5000", "root@example.com"},
|
||||
},
|
||||
{
|
||||
name: "custom port",
|
||||
config: SSHConnectorConfig{
|
||||
User: "root",
|
||||
Host: "example.com",
|
||||
Port: 2222,
|
||||
},
|
||||
address: "10.210.1.1:5000",
|
||||
expected: []string{"-o", "ConnectTimeout=5", "-p", "2222", "-W", "10.210.1.1:5000", "root@example.com"},
|
||||
},
|
||||
{
|
||||
name: "with identity file",
|
||||
config: SSHConnectorConfig{
|
||||
User: "root",
|
||||
Host: "example.com",
|
||||
Port: 22,
|
||||
KeyPath: "/home/user/.ssh/id_rsa",
|
||||
},
|
||||
address: "10.210.1.1:5000",
|
||||
expected: []string{"-o", "ConnectTimeout=5", "-i", "/home/user/.ssh/id_rsa", "-W", "10.210.1.1:5000", "root@example.com"},
|
||||
},
|
||||
{
|
||||
name: "custom port with identity file",
|
||||
config: SSHConnectorConfig{
|
||||
User: "root",
|
||||
Host: "example.com",
|
||||
Port: 2222,
|
||||
KeyPath: "/home/user/.ssh/id_rsa",
|
||||
},
|
||||
address: "10.210.1.1:5000",
|
||||
expected: []string{"-o", "ConnectTimeout=5", "-p", "2222", "-i", "/home/user/.ssh/id_rsa", "-W", "10.210.1.1:5000", "root@example.com"},
|
||||
},
|
||||
{
|
||||
name: "zero port defaults to 22",
|
||||
config: SSHConnectorConfig{
|
||||
User: "root",
|
||||
Host: "example.com",
|
||||
Port: 0,
|
||||
},
|
||||
address: "10.210.1.1:5000",
|
||||
expected: []string{"-o", "ConnectTimeout=5", "-W", "10.210.1.1:5000", "root@example.com"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
d := &sshCLIDialer{config: tt.config}
|
||||
got := d.buildDialArgs(tt.address)
|
||||
assert.Equal(t, tt.expected, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
+61
-1
@@ -199,6 +199,7 @@ func toPullProgressEvent(jm jsonmessage.JSONMessage) *progress.Event {
|
||||
}
|
||||
|
||||
// InspectContainer returns the information about the specified container within the service.
|
||||
// containerNameOrID can be name, full ID, or ID prefix of the container.
|
||||
func (cli *Client) InspectContainer(
|
||||
ctx context.Context, serviceNameOrID, containerNameOrID string,
|
||||
) (api.MachineServiceContainer, error) {
|
||||
@@ -207,10 +208,23 @@ func (cli *Client) InspectContainer(
|
||||
return api.MachineServiceContainer{}, fmt.Errorf("inspect service: %w", err)
|
||||
}
|
||||
|
||||
prefixMatchCandidates := []api.MachineServiceContainer{}
|
||||
for _, c := range svc.Containers {
|
||||
if c.Container.ID == containerNameOrID || c.Container.Name == containerNameOrID {
|
||||
if c.Container.ID == containerNameOrID ||
|
||||
c.Container.Name == containerNameOrID {
|
||||
return c, nil
|
||||
}
|
||||
|
||||
if strings.HasPrefix(c.Container.ID, containerNameOrID) {
|
||||
prefixMatchCandidates = append(prefixMatchCandidates, c)
|
||||
}
|
||||
}
|
||||
|
||||
if len(prefixMatchCandidates) == 1 {
|
||||
return prefixMatchCandidates[0], nil
|
||||
} else if len(prefixMatchCandidates) > 1 {
|
||||
return api.MachineServiceContainer{}, fmt.Errorf(
|
||||
"multiple containers found with ID prefix '%s'", containerNameOrID)
|
||||
}
|
||||
|
||||
return api.MachineServiceContainer{}, api.ErrNotFound
|
||||
@@ -294,3 +308,49 @@ func (cli *Client) RemoveContainer(
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ExecContainer executes a command in a container within the service.
|
||||
// If containerNameOrID is empty, the first container in the service will be used.
|
||||
func (cli *Client) ExecContainer(
|
||||
ctx context.Context, serviceNameOrID, containerNameOrID string, execOpts api.ExecOptions,
|
||||
) (int, error) {
|
||||
var ctr api.MachineServiceContainer
|
||||
|
||||
if containerNameOrID == "" {
|
||||
// Find the first (random) container in the service
|
||||
service, err := cli.InspectService(ctx, serviceNameOrID)
|
||||
if err != nil {
|
||||
return -1, fmt.Errorf("inspect service: %w", err)
|
||||
}
|
||||
if len(service.Containers) == 0 {
|
||||
return -1, fmt.Errorf("no containers found in service %s", serviceNameOrID)
|
||||
}
|
||||
ctr = service.Containers[0]
|
||||
} else {
|
||||
// Find the specific container
|
||||
var err error
|
||||
ctr, err = cli.InspectContainer(ctx, serviceNameOrID, containerNameOrID)
|
||||
if err != nil {
|
||||
return -1, fmt.Errorf("inspect container: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
machine, err := cli.InspectMachine(ctx, ctr.MachineID)
|
||||
if err != nil {
|
||||
return -1, fmt.Errorf("inspect machine '%s': %w", ctr.MachineID, err)
|
||||
}
|
||||
|
||||
// Proxy Docker gRPC requests to the machine hosting the container
|
||||
ctx = proxyToMachine(ctx, machine.Machine)
|
||||
|
||||
// Execute the command in the container
|
||||
exitCode, err := cli.Docker.ExecContainer(ctx, machinedocker.ExecConfig{
|
||||
ContainerID: ctr.Container.ID,
|
||||
Options: execOpts,
|
||||
})
|
||||
if err != nil {
|
||||
return exitCode, fmt.Errorf("exec in container %s: %w", ctr.Container.Name, err)
|
||||
}
|
||||
|
||||
return exitCode, nil
|
||||
}
|
||||
|
||||
@@ -83,11 +83,17 @@ func EvalContainerSpecChange(current api.ServiceSpec, new api.ServiceSpec) Conta
|
||||
}
|
||||
}
|
||||
|
||||
// Device reservations are immutable, so we'll need to recreate if any have changed
|
||||
if !reflect.DeepEqual(current.Container.Resources.DeviceReservations, newResources.DeviceReservations) {
|
||||
return ContainerNeedsRecreate
|
||||
}
|
||||
|
||||
// Check if any mutable properties changed.
|
||||
if !current.Caddy.Equals(new.Caddy) {
|
||||
return ContainerNeedsRecreate
|
||||
}
|
||||
|
||||
// Remaining resources are mutable.
|
||||
if !reflect.DeepEqual(current.Container.Resources, newResources) {
|
||||
return ContainerNeedsUpdate
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package deploy
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/mount"
|
||||
"github.com/psviderski/uncloud/pkg/api"
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -1350,6 +1351,398 @@ func TestEvalContainerSpecChange_Volumes(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvalContainerSpecChange_DeviceReservations(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
current api.ContainerResources
|
||||
new api.ContainerResources
|
||||
want ContainerSpecStatus
|
||||
}{
|
||||
{
|
||||
name: "empty",
|
||||
current: api.ContainerResources{},
|
||||
new: api.ContainerResources{},
|
||||
want: ContainerUpToDate,
|
||||
},
|
||||
{
|
||||
name: "both nil",
|
||||
current: api.ContainerResources{DeviceReservations: nil},
|
||||
new: api.ContainerResources{DeviceReservations: nil},
|
||||
want: ContainerUpToDate,
|
||||
},
|
||||
{
|
||||
name: "both empty",
|
||||
current: api.ContainerResources{DeviceReservations: []container.DeviceRequest{}},
|
||||
new: api.ContainerResources{DeviceReservations: []container.DeviceRequest{}},
|
||||
want: ContainerUpToDate,
|
||||
},
|
||||
{
|
||||
name: "set GPU request",
|
||||
current: api.ContainerResources{},
|
||||
new: api.ContainerResources{
|
||||
DeviceReservations: []container.DeviceRequest{
|
||||
{
|
||||
Count: -1, // all
|
||||
Capabilities: [][]string{{"gpu"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: ContainerNeedsRecreate,
|
||||
},
|
||||
{
|
||||
name: "unset GPU request",
|
||||
current: api.ContainerResources{
|
||||
DeviceReservations: []container.DeviceRequest{
|
||||
{
|
||||
Count: -1,
|
||||
Capabilities: [][]string{{"gpu"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
new: api.ContainerResources{},
|
||||
want: ContainerNeedsRecreate,
|
||||
},
|
||||
{
|
||||
name: "identical GPU request",
|
||||
current: api.ContainerResources{
|
||||
DeviceReservations: []container.DeviceRequest{
|
||||
{
|
||||
Driver: "nvidia",
|
||||
Count: 2,
|
||||
Capabilities: [][]string{{"gpu", "compute"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
new: api.ContainerResources{
|
||||
DeviceReservations: []container.DeviceRequest{
|
||||
{
|
||||
Driver: "nvidia",
|
||||
Count: 2,
|
||||
Capabilities: [][]string{{"gpu", "compute"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: ContainerUpToDate,
|
||||
},
|
||||
{
|
||||
name: "change GPU count",
|
||||
current: api.ContainerResources{
|
||||
DeviceReservations: []container.DeviceRequest{
|
||||
{
|
||||
Count: 1,
|
||||
Capabilities: [][]string{{"gpu"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
new: api.ContainerResources{
|
||||
DeviceReservations: []container.DeviceRequest{
|
||||
{
|
||||
Count: 2,
|
||||
Capabilities: [][]string{{"gpu"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: ContainerNeedsRecreate,
|
||||
},
|
||||
{
|
||||
name: "change GPU driver",
|
||||
current: api.ContainerResources{
|
||||
DeviceReservations: []container.DeviceRequest{
|
||||
{
|
||||
Driver: "nvidia",
|
||||
Count: 1,
|
||||
Capabilities: [][]string{{"gpu"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
new: api.ContainerResources{
|
||||
DeviceReservations: []container.DeviceRequest{
|
||||
{
|
||||
Driver: "amd",
|
||||
Count: 1,
|
||||
Capabilities: [][]string{{"gpu"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: ContainerNeedsRecreate,
|
||||
},
|
||||
{
|
||||
name: "change device IDs",
|
||||
current: api.ContainerResources{
|
||||
DeviceReservations: []container.DeviceRequest{
|
||||
{
|
||||
DeviceIDs: []string{"0"},
|
||||
Capabilities: [][]string{{"gpu"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
new: api.ContainerResources{
|
||||
DeviceReservations: []container.DeviceRequest{
|
||||
{
|
||||
DeviceIDs: []string{"0", "1"},
|
||||
Capabilities: [][]string{{"gpu"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: ContainerNeedsRecreate,
|
||||
},
|
||||
{
|
||||
name: "change capabilities",
|
||||
current: api.ContainerResources{
|
||||
DeviceReservations: []container.DeviceRequest{
|
||||
{
|
||||
Count: 1,
|
||||
Capabilities: [][]string{{"gpu"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
new: api.ContainerResources{
|
||||
DeviceReservations: []container.DeviceRequest{
|
||||
{
|
||||
Count: 1,
|
||||
Capabilities: [][]string{{"gpu", "compute"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: ContainerNeedsRecreate,
|
||||
},
|
||||
{
|
||||
name: "set options",
|
||||
current: api.ContainerResources{
|
||||
DeviceReservations: []container.DeviceRequest{
|
||||
{
|
||||
Count: 1,
|
||||
Capabilities: [][]string{{"gpu"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
new: api.ContainerResources{
|
||||
DeviceReservations: []container.DeviceRequest{
|
||||
{
|
||||
Count: 1,
|
||||
Capabilities: [][]string{{"gpu"}},
|
||||
Options: map[string]string{
|
||||
"key": "value",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: ContainerNeedsRecreate,
|
||||
},
|
||||
{
|
||||
name: "change options",
|
||||
current: api.ContainerResources{
|
||||
DeviceReservations: []container.DeviceRequest{
|
||||
{
|
||||
Count: 1,
|
||||
Capabilities: [][]string{{"gpu"}},
|
||||
Options: map[string]string{
|
||||
"key": "value1",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
new: api.ContainerResources{
|
||||
DeviceReservations: []container.DeviceRequest{
|
||||
{
|
||||
Count: 1,
|
||||
Capabilities: [][]string{{"gpu"}},
|
||||
Options: map[string]string{
|
||||
"key": "value2",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: ContainerNeedsRecreate,
|
||||
},
|
||||
{
|
||||
name: "unset options",
|
||||
current: api.ContainerResources{
|
||||
DeviceReservations: []container.DeviceRequest{
|
||||
{
|
||||
Count: 1,
|
||||
Capabilities: [][]string{{"gpu"}},
|
||||
Options: map[string]string{
|
||||
"key": "value",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
new: api.ContainerResources{
|
||||
DeviceReservations: []container.DeviceRequest{
|
||||
{
|
||||
Count: 1,
|
||||
Capabilities: [][]string{{"gpu"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: ContainerNeedsRecreate,
|
||||
},
|
||||
{
|
||||
name: "add device request",
|
||||
current: api.ContainerResources{
|
||||
DeviceReservations: []container.DeviceRequest{
|
||||
{
|
||||
Count: 1,
|
||||
Capabilities: [][]string{{"gpu"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
new: api.ContainerResources{
|
||||
DeviceReservations: []container.DeviceRequest{
|
||||
{
|
||||
Count: 1,
|
||||
Capabilities: [][]string{{"gpu"}},
|
||||
},
|
||||
{
|
||||
Count: 1,
|
||||
Capabilities: [][]string{{"tpu"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: ContainerNeedsRecreate,
|
||||
},
|
||||
{
|
||||
name: "remove device request",
|
||||
current: api.ContainerResources{
|
||||
DeviceReservations: []container.DeviceRequest{
|
||||
{
|
||||
Count: 1,
|
||||
Capabilities: [][]string{{"gpu"}},
|
||||
},
|
||||
{
|
||||
Count: 1,
|
||||
Capabilities: [][]string{{"tpu"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
new: api.ContainerResources{
|
||||
DeviceReservations: []container.DeviceRequest{
|
||||
{
|
||||
Count: 1,
|
||||
Capabilities: [][]string{{"gpu"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: ContainerNeedsRecreate,
|
||||
},
|
||||
{
|
||||
name: "multiple identical device requests",
|
||||
current: api.ContainerResources{
|
||||
DeviceReservations: []container.DeviceRequest{
|
||||
{
|
||||
Driver: "nvidia",
|
||||
Count: 2,
|
||||
Capabilities: [][]string{{"gpu", "compute"}},
|
||||
},
|
||||
{
|
||||
Count: 1,
|
||||
Capabilities: [][]string{{"tpu"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
new: api.ContainerResources{
|
||||
DeviceReservations: []container.DeviceRequest{
|
||||
{
|
||||
Driver: "nvidia",
|
||||
Count: 2,
|
||||
Capabilities: [][]string{{"gpu", "compute"}},
|
||||
},
|
||||
{
|
||||
Count: 1,
|
||||
Capabilities: [][]string{{"tpu"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: ContainerUpToDate,
|
||||
},
|
||||
{
|
||||
name: "reordered device requests",
|
||||
current: api.ContainerResources{
|
||||
DeviceReservations: []container.DeviceRequest{
|
||||
{
|
||||
Driver: "nvidia",
|
||||
Count: 2,
|
||||
Capabilities: [][]string{{"gpu"}},
|
||||
},
|
||||
{
|
||||
Count: 1,
|
||||
Capabilities: [][]string{{"tpu"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
new: api.ContainerResources{
|
||||
DeviceReservations: []container.DeviceRequest{
|
||||
{
|
||||
Count: 1,
|
||||
Capabilities: [][]string{{"tpu"}},
|
||||
},
|
||||
{
|
||||
Driver: "nvidia",
|
||||
Count: 2,
|
||||
Capabilities: [][]string{{"gpu"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: ContainerNeedsRecreate,
|
||||
},
|
||||
{
|
||||
name: "complex GPU configuration identical",
|
||||
current: api.ContainerResources{
|
||||
DeviceReservations: []container.DeviceRequest{
|
||||
{
|
||||
Driver: "nvidia",
|
||||
Count: 2,
|
||||
DeviceIDs: []string{"0", "1"},
|
||||
Capabilities: [][]string{{"gpu", "compute", "utility"}},
|
||||
Options: map[string]string{
|
||||
"runtime": "nvidia",
|
||||
"compute": "exclusive",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
new: api.ContainerResources{
|
||||
DeviceReservations: []container.DeviceRequest{
|
||||
{
|
||||
Driver: "nvidia",
|
||||
Count: 2,
|
||||
DeviceIDs: []string{"0", "1"},
|
||||
Capabilities: [][]string{{"gpu", "compute", "utility"}},
|
||||
Options: map[string]string{
|
||||
"runtime": "nvidia",
|
||||
"compute": "exclusive",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: ContainerUpToDate,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
currentSpec := api.ServiceSpec{
|
||||
Container: api.ContainerSpec{
|
||||
Image: "nvidia/cuda:latest",
|
||||
Resources: tt.current,
|
||||
},
|
||||
}
|
||||
newSpec := api.ServiceSpec{
|
||||
Container: api.ContainerSpec{
|
||||
Image: "nvidia/cuda:latest",
|
||||
Resources: tt.new,
|
||||
},
|
||||
}
|
||||
|
||||
result := EvalContainerSpecChange(currentSpec, newSpec)
|
||||
assert.Equal(t, tt.want, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvalContainerSpecChange_Mixed(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -1399,6 +1792,32 @@ func TestEvalContainerSpecChange_Mixed(t *testing.T) {
|
||||
},
|
||||
want: ContainerNeedsRecreate,
|
||||
},
|
||||
{
|
||||
name: "mutable memory change with immutable device reservation change",
|
||||
current: api.ServiceSpec{
|
||||
Container: api.ContainerSpec{
|
||||
Image: "nvidia/cuda:latest",
|
||||
Resources: api.ContainerResources{
|
||||
Memory: 100 * 1024 * 1024,
|
||||
},
|
||||
},
|
||||
},
|
||||
new: api.ServiceSpec{
|
||||
Container: api.ContainerSpec{
|
||||
Image: "nvidia/cuda:latest",
|
||||
Resources: api.ContainerResources{
|
||||
Memory: 200 * 1024 * 1024,
|
||||
DeviceReservations: []container.DeviceRequest{
|
||||
{
|
||||
Count: 1,
|
||||
Capabilities: [][]string{{"gpu"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: ContainerNeedsRecreate,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
||||
@@ -29,6 +29,8 @@ type NameResolver interface {
|
||||
ContainerName(containerID string) string
|
||||
}
|
||||
|
||||
// TODO: pass api.ServiceContainer to operations to simplify operation formatting in the plan.
|
||||
|
||||
// RunContainerOperation creates and starts a new container on a specific machine.
|
||||
type RunContainerOperation struct {
|
||||
ServiceID string
|
||||
@@ -56,8 +58,8 @@ func (o *RunContainerOperation) Format(resolver NameResolver) string {
|
||||
}
|
||||
|
||||
func (o *RunContainerOperation) String() string {
|
||||
return fmt.Sprintf("RunContainerOperation[service_id=%s, image=%s, machine_id=%s]",
|
||||
o.ServiceID, o.Spec.Container.Image, o.MachineID)
|
||||
return fmt.Sprintf("RunContainerOperation[machine_id=%s service_id=%s image=%s]",
|
||||
o.MachineID, o.ServiceID, o.Spec.Container.Image)
|
||||
}
|
||||
|
||||
// StopContainerOperation stops a container on a specific machine.
|
||||
@@ -76,26 +78,26 @@ func (o *StopContainerOperation) Execute(ctx context.Context, cli Client) error
|
||||
|
||||
func (o *StopContainerOperation) Format(resolver NameResolver) string {
|
||||
machineName := resolver.MachineName(o.MachineID)
|
||||
return fmt.Sprintf("%s: Stop container [name=%s]", machineName, resolver.ContainerName(o.ContainerID))
|
||||
return fmt.Sprintf("%s: Stop container [id=%s name=%s]", machineName,
|
||||
o.ContainerID[:12], resolver.ContainerName(o.ContainerID))
|
||||
}
|
||||
|
||||
func (o *StopContainerOperation) String() string {
|
||||
return fmt.Sprintf("StopContainerOperation[service_id=%s, container_id=%s, machine_id=%s]",
|
||||
o.ServiceID, o.ContainerID, o.MachineID)
|
||||
return fmt.Sprintf("StopContainerOperation[machine_id=%s service_id=%s container_id=%s]",
|
||||
o.MachineID, o.ServiceID, o.ContainerID)
|
||||
}
|
||||
|
||||
// RemoveContainerOperation stops and removes a container from a specific machine.
|
||||
type RemoveContainerOperation struct {
|
||||
ServiceID string
|
||||
ContainerID string
|
||||
MachineID string
|
||||
Container api.ServiceContainer
|
||||
}
|
||||
|
||||
func (o *RemoveContainerOperation) Execute(ctx context.Context, cli Client) error {
|
||||
if err := cli.StopContainer(ctx, o.ServiceID, o.ContainerID, container.StopOptions{}); err != nil {
|
||||
if err := cli.StopContainer(ctx, o.Container.ServiceID(), o.Container.ID, container.StopOptions{}); err != nil {
|
||||
return fmt.Errorf("stop container: %w", err)
|
||||
}
|
||||
if err := cli.RemoveContainer(ctx, o.ServiceID, o.ContainerID, container.RemoveOptions{
|
||||
if err := cli.RemoveContainer(ctx, o.Container.ServiceID(), o.Container.ID, container.RemoveOptions{
|
||||
// Remove anonymous volumes created by the container.
|
||||
RemoveVolumes: true,
|
||||
}); err != nil {
|
||||
@@ -107,12 +109,13 @@ func (o *RemoveContainerOperation) Execute(ctx context.Context, cli Client) erro
|
||||
|
||||
func (o *RemoveContainerOperation) Format(resolver NameResolver) string {
|
||||
machineName := resolver.MachineName(o.MachineID)
|
||||
return fmt.Sprintf("%s: Remove container [name=%s]", machineName, resolver.ContainerName(o.ContainerID))
|
||||
return fmt.Sprintf("%s: Remove container [id=%s image=%s]",
|
||||
machineName, o.Container.ShortID(), o.Container.Config.Image)
|
||||
}
|
||||
|
||||
func (o *RemoveContainerOperation) String() string {
|
||||
return fmt.Sprintf("RemoveContainerOperation[service_id=%s, container_id=%s, machine_id=%s]",
|
||||
o.ServiceID, o.ContainerID, o.MachineID)
|
||||
return fmt.Sprintf("RemoveContainerOperation[machine_id=%s service_id=%s container_id=%s]",
|
||||
o.MachineID, o.Container.ServiceID(), o.Container.ID)
|
||||
}
|
||||
|
||||
// CreateVolumeOperation creates a volume on a specific machine.
|
||||
@@ -151,8 +154,8 @@ func (o *CreateVolumeOperation) Format(_ NameResolver) string {
|
||||
}
|
||||
|
||||
func (o *CreateVolumeOperation) String() string {
|
||||
return fmt.Sprintf("CreateVolumeOperation[volume=%s, machine_id=%s]",
|
||||
o.VolumeSpec.DockerVolumeName(), o.MachineID)
|
||||
return fmt.Sprintf("CreateVolumeOperation[machine_id=%s volume=%s]",
|
||||
o.MachineID, o.VolumeSpec.DockerVolumeName())
|
||||
}
|
||||
|
||||
// SequenceOperation is a composite operation that executes a sequence of operations in order.
|
||||
|
||||
@@ -182,9 +182,8 @@ func (s *RollingStrategy) planReplicated(svc *api.Service, spec api.ServiceSpec)
|
||||
|
||||
// Remove the old container.
|
||||
plan.Operations = append(plan.Operations, &RemoveContainerOperation{
|
||||
ServiceID: plan.ServiceID,
|
||||
ContainerID: ctr.ID,
|
||||
MachineID: m.Id,
|
||||
Container: ctr,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -192,9 +191,8 @@ func (s *RollingStrategy) planReplicated(svc *api.Service, spec api.ServiceSpec)
|
||||
for mid, containers := range containersOnMachine {
|
||||
for _, c := range containers {
|
||||
plan.Operations = append(plan.Operations, &RemoveContainerOperation{
|
||||
ServiceID: plan.ServiceID,
|
||||
ContainerID: c.ID,
|
||||
MachineID: mid,
|
||||
Container: c,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -244,9 +242,8 @@ func (s *RollingStrategy) planGlobal(svc *api.Service, spec api.ServiceSpec) (Pl
|
||||
for _, containers := range containersOnMachine {
|
||||
for _, c := range containers {
|
||||
plan.Operations = append(plan.Operations, &RemoveContainerOperation{
|
||||
ServiceID: plan.ServiceID,
|
||||
ContainerID: c.Container.ID,
|
||||
MachineID: c.MachineID,
|
||||
Container: c.Container,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -295,9 +292,8 @@ func reconcileGlobalContainer(
|
||||
continue
|
||||
}
|
||||
ops = append(ops, &RemoveContainerOperation{
|
||||
ServiceID: serviceID,
|
||||
ContainerID: old.Container.ID,
|
||||
MachineID: old.MachineID,
|
||||
Container: old.Container,
|
||||
})
|
||||
}
|
||||
break
|
||||
@@ -338,9 +334,8 @@ func reconcileGlobalContainer(
|
||||
// Remove the old containers.
|
||||
for _, c := range containers {
|
||||
ops = append(ops, &RemoveContainerOperation{
|
||||
ServiceID: serviceID,
|
||||
ContainerID: c.Container.ID,
|
||||
MachineID: c.MachineID,
|
||||
Container: c.Container,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
+7
-2
@@ -102,11 +102,11 @@ func (cli *Client) ListImages(ctx context.Context, filter api.ImageFilter) ([]ap
|
||||
}
|
||||
|
||||
type PushImageOptions struct {
|
||||
// AllMachines pushes the image to all machines in the cluster. Takes precedence over Machines field.
|
||||
AllMachines bool
|
||||
// Machines is a list of machine names or IDs to push the image to. If empty and AllMachines is false,
|
||||
// pushes to the machine the client is connected to.
|
||||
Machines []string
|
||||
// AllMachines pushes the image to all machines in the cluster. Takes precedence over Machines field.
|
||||
AllMachines bool
|
||||
// Platform to push for a multi-platform image. Local Docker must use containerd image store
|
||||
// to support multi-platform images.
|
||||
Platform *ocispec.Platform
|
||||
@@ -472,6 +472,11 @@ func toPushProgressEvent(jm jsonmessage.JSONMessage) *progress.Event {
|
||||
|
||||
if jm.Progress.Total > 0 {
|
||||
percent = int(jm.Progress.Current * 100 / jm.Progress.Total)
|
||||
// Cap percent at 100 to prevent index out of bounds in progress display.
|
||||
// Docker can report Current > Total in some cases (e.g., compression).
|
||||
if percent > 100 {
|
||||
percent = 100
|
||||
}
|
||||
}
|
||||
|
||||
switch jm.Status {
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@ UNCLOUD_GROUP_ADD_USER=${UNCLOUD_GROUP_ADD_USER:-}
|
||||
UNCLOUD_DATA_DIR=${UNCLOUD_DATA_DIR:-/var/lib/uncloud}
|
||||
|
||||
CORROSION_GITHUB_URL="https://github.com/psviderski/corrosion"
|
||||
CORROSION_VERSION=${CORROSION_VERSION:-latest}
|
||||
CORROSION_VERSION=${CORROSION_VERSION:-v0.2.2}
|
||||
|
||||
DOCKER_ALREADY_INSTALLED=false
|
||||
CONTAINERD_IMAGE_STORE_ENABLED=false
|
||||
|
||||
@@ -21,7 +21,11 @@ print_manual_install() {
|
||||
|
||||
fetch_latest_version() {
|
||||
api_url="https://api.github.com/repos/${GITHUB_REPO}/releases/latest"
|
||||
VERSION=$(curl -fsSL "$api_url" | grep -o '"tag_name": "[^"]*' | cut -d'"' -f4)
|
||||
auth_header=""
|
||||
if [ -n "$GITHUB_TOKEN" ]; then
|
||||
auth_header="Authorization: Bearer $GITHUB_TOKEN"
|
||||
fi
|
||||
VERSION=$(curl -fsSL -H "$auth_header" "$api_url" | grep -o '"tag_name": "[^"]*' | cut -d'"' -f4)
|
||||
if [ -z "$VERSION" ]; then
|
||||
echo "Failed to fetch the latest version from GitHub."
|
||||
print_manual_install
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
composecli "github.com/compose-spec/compose-go/v2/cli"
|
||||
@@ -15,7 +16,6 @@ import (
|
||||
"github.com/google/go-containerregistry/pkg/name"
|
||||
"github.com/google/go-containerregistry/pkg/v1/remote"
|
||||
"github.com/psviderski/uncloud/internal/cli"
|
||||
cliInternal "github.com/psviderski/uncloud/internal/cli"
|
||||
"github.com/psviderski/uncloud/internal/ucind"
|
||||
"github.com/psviderski/uncloud/pkg/api"
|
||||
"github.com/psviderski/uncloud/pkg/client/compose"
|
||||
@@ -97,8 +97,9 @@ func TestComposeBuild(t *testing.T) {
|
||||
}),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
servicesToBuild := cliInternal.GetServicesThatNeedBuild(project)
|
||||
serviceImage1 := fmt.Sprintf("127.0.0.1:%d/service-first", registryHostPort)
|
||||
servicesToBuild, err := cli.ServicesThatNeedBuild(project, nil, false)
|
||||
require.NoError(t, err)
|
||||
serviceImage1 := project.Services["service-first"].Image // contains auto-generated default tag
|
||||
serviceImage2 := fmt.Sprintf("127.0.0.1:%d/service-second:version2", registryHostPort)
|
||||
t.Cleanup(func() {
|
||||
// Remove the images after the test
|
||||
@@ -109,8 +110,8 @@ func TestComposeBuild(t *testing.T) {
|
||||
assert.NoErrorf(t, err, "failed to remove image %s on test cleanup", serviceImage2)
|
||||
})
|
||||
|
||||
servicesToBuildExpected := map[string]types.ServiceConfig{
|
||||
"service-first": {
|
||||
servicesToBuildExpected := []types.ServiceConfig{
|
||||
{
|
||||
Name: "service-first",
|
||||
Build: &types.BuildConfig{
|
||||
Context: path.Join(project.WorkingDir, "service-first-dir"),
|
||||
@@ -122,7 +123,7 @@ func TestComposeBuild(t *testing.T) {
|
||||
"default": nil,
|
||||
},
|
||||
},
|
||||
"service-second": {
|
||||
{
|
||||
Name: "service-second",
|
||||
Build: &types.BuildConfig{
|
||||
Context: path.Join(project.WorkingDir, "service-second-dir"),
|
||||
@@ -135,22 +136,27 @@ func TestComposeBuild(t *testing.T) {
|
||||
},
|
||||
},
|
||||
}
|
||||
assert.Equal(t, servicesToBuildExpected, servicesToBuild)
|
||||
assert.ElementsMatch(t, servicesToBuildExpected, servicesToBuild)
|
||||
|
||||
// Build and push the images
|
||||
buildOpts := cli.BuildOptions{
|
||||
Push: true,
|
||||
NoCache: false,
|
||||
// Build and push the images to the test registry.
|
||||
// Create a minimal CLI instance for the build function (it's only used for cluster push, which we're not using here).
|
||||
uncli := &cli.CLI{}
|
||||
buildOpts := cli.BuildServicesOptions{
|
||||
PushRegistry: true,
|
||||
}
|
||||
cli.BuildServices(context.Background(), servicesToBuild, buildOpts)
|
||||
err = uncli.BuildServices(context.Background(), project, buildOpts)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Check the image of the first service
|
||||
ref1, err := name.NewRepository(fmt.Sprintf("127.0.0.1:%d/service-first", registryHostPort))
|
||||
tagSeparatorIdx := strings.LastIndex(serviceImage1, ":")
|
||||
serviceRepo1, serviceTag1 := serviceImage1[:tagSeparatorIdx], serviceImage1[tagSeparatorIdx+1:]
|
||||
|
||||
ref1, err := name.NewRepository(serviceRepo1)
|
||||
require.NoError(t, err)
|
||||
tags, err := remote.List(ref1)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, tags, []string{"latest"}, "Tags for service service-first do not match")
|
||||
assert.Equal(t, tags, []string{serviceTag1}, "Tags for service service-first do not match")
|
||||
|
||||
// Check the image of the second service
|
||||
ref2, err := name.NewRepository(fmt.Sprintf("127.0.0.1:%d/service-second", registryHostPort))
|
||||
|
||||
@@ -51,7 +51,7 @@ func TestComposeConfigs(t *testing.T) {
|
||||
Name: name,
|
||||
Mode: api.ServiceModeReplicated,
|
||||
Container: api.ContainerSpec{
|
||||
Command: []string{"true"},
|
||||
Command: []string{"sleep", "600"},
|
||||
Image: "busybox:1.37.0-uclibc",
|
||||
ConfigMounts: []api.ConfigMount{
|
||||
{
|
||||
@@ -93,18 +93,22 @@ func TestComposeConfigs(t *testing.T) {
|
||||
// Verify the config files are actually created in the container and contain expected content
|
||||
containerName := svc.Containers[0].Container.Name
|
||||
|
||||
configContentFirst, err := readFileInfoInContainer(t, &machine, containerName, "/etc/config-from-file.conf")
|
||||
configContentFirst, err := readFileInfoInContainer(t, cli, name, containerName, "/etc/config-from-file.conf")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, fileInfo{
|
||||
permissions: 0o644,
|
||||
content: "this is file config\n",
|
||||
userId: 0,
|
||||
groupId: 0,
|
||||
}, configContentFirst)
|
||||
|
||||
configContentSecond, err := readFileInfoInContainer(t, &machine, containerName, "/etc/config-inline.conf")
|
||||
configContentSecond, err := readFileInfoInContainer(t, cli, name, containerName, "/etc/config-inline.conf")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, fileInfo{
|
||||
permissions: 0o600,
|
||||
content: "this is inline config\n",
|
||||
userId: 1000,
|
||||
groupId: 1000,
|
||||
}, configContentSecond)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package e2e
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -224,4 +225,38 @@ func TestInternalDNS(t *testing.T) {
|
||||
|
||||
assertNoDNSErrors(t, dnsOutput)
|
||||
})
|
||||
|
||||
t.Run("nearest mode prioritizes local subnet IPs", func(t *testing.T) {
|
||||
// Test the "nearest" mode which should sort local subnet IPs first
|
||||
dnsOutput := runDNSQuery(t, "dns-test-nearest", "nearest."+serviceName+".internal", "/tmp/dns_result_nearest.txt")
|
||||
t.Logf("Nearest mode DNS query output:\n%s", dnsOutput)
|
||||
|
||||
// Find which machine ran the query
|
||||
querySvc, err := cli.InspectService(ctx, "dns-test-nearest")
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, querySvc.Containers, "Query service should have a container")
|
||||
queryMachineID := querySvc.Containers[0].MachineID
|
||||
|
||||
// Find the local container IP (on the same machine as the query)
|
||||
var localIP string
|
||||
for _, ctr := range svc.Containers {
|
||||
if ctr.MachineID == queryMachineID {
|
||||
localIP = ctr.Container.UncloudNetworkIP().String()
|
||||
break
|
||||
}
|
||||
}
|
||||
require.NotEmpty(t, localIP, "Should find local container IP on query machine %s", queryMachineID)
|
||||
|
||||
// Extract the first IP address from the DNS output using regex
|
||||
// Pattern matches: "Name: nearest.test-dns-service.internal" followed by "Address: X.X.X.X"
|
||||
re := regexp.MustCompile(`(?m)Name:\s+[\w\.\-]+\s+Address:\s+([\d\.]+)`)
|
||||
matches := re.FindStringSubmatch(dnsOutput)
|
||||
require.Len(t, matches, 2, "Should find Name's Address in DNS output")
|
||||
firstIP := matches[1]
|
||||
assert.Equal(t, localIP, firstIP,
|
||||
"Nearest mode should return local subnet IP first (query machine: %s, local IP: %s, first DNS result: %s)",
|
||||
queryMachineID, localIP, firstIP)
|
||||
|
||||
assertNoDNSErrors(t, dnsOutput)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,244 @@
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/psviderski/uncloud/internal/ucind"
|
||||
"github.com/psviderski/uncloud/pkg/api"
|
||||
"github.com/psviderski/uncloud/pkg/client"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// deployTestService is a helper function to deploy a simple alpine service for testing exec
|
||||
func deployTestService(t *testing.T, ctx context.Context, cli *client.Client, name string, replicas uint) {
|
||||
t.Helper()
|
||||
|
||||
spec := api.ServiceSpec{
|
||||
Name: name,
|
||||
Replicas: replicas,
|
||||
Container: api.ContainerSpec{
|
||||
Image: "alpine:3.20",
|
||||
Command: []string{"sleep", "3600"},
|
||||
},
|
||||
}
|
||||
|
||||
deployment := cli.NewDeployment(spec, nil)
|
||||
err := deployment.Validate(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = deployment.Run(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Wait for all replicas to be running
|
||||
require.Eventually(t, func() bool {
|
||||
service, err := cli.InspectService(ctx, name)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if uint(len(service.Containers)) != replicas {
|
||||
return false
|
||||
}
|
||||
for _, ctr := range service.Containers {
|
||||
if ctr.Container.State.Status != "running" {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}, 30*time.Second, 1*time.Second, fmt.Sprintf("service %s should have %d running replicas", name, replicas))
|
||||
}
|
||||
|
||||
// TestExecBasicCommands tests basic command execution, errors, and stderr handling
|
||||
func TestExecBasicCommands(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
clusterName := "ucind-test.exec-basic"
|
||||
ctx := context.Background()
|
||||
c, _ := createTestCluster(t, clusterName, ucind.CreateClusterOptions{Machines: 1}, true)
|
||||
|
||||
cli, err := c.Machines[0].Connect(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Test: Non-existent service should fail
|
||||
t.Run("non-existent service", func(t *testing.T) {
|
||||
execOptions := api.ExecOptions{
|
||||
Command: []string{"echo", "test"},
|
||||
AttachStdout: true,
|
||||
AttachStderr: true,
|
||||
}
|
||||
|
||||
_, err := cli.ExecContainer(ctx, "non-existent-service", "", execOptions)
|
||||
assert.Error(t, err, "should fail for non-existent service")
|
||||
assert.Contains(t, strings.ToLower(err.Error()), "inspect service: not found")
|
||||
})
|
||||
|
||||
// Deploy a simple service for all remaining tests
|
||||
serviceName := "test-exec-service"
|
||||
deployTestService(t, ctx, cli, serviceName, 1)
|
||||
|
||||
// Test: Execute a simple echo command
|
||||
t.Run("echo command", func(t *testing.T) {
|
||||
var stdout, stderr bytes.Buffer
|
||||
execOptions := api.ExecOptions{
|
||||
Command: []string{"echo", "hello world"},
|
||||
AttachStdout: true,
|
||||
AttachStderr: true,
|
||||
Stdout: &stdout,
|
||||
Stderr: &stderr,
|
||||
}
|
||||
|
||||
exitCode, err := cli.ExecContainer(ctx, serviceName, "", execOptions)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 0, exitCode, "command should exit with code 0")
|
||||
assert.Equal(t, "hello world\n", stdout.String(), "should capture stdout")
|
||||
assert.Empty(t, stderr.String(), "stderr should be empty")
|
||||
})
|
||||
|
||||
// Test: Command with non-zero exit code
|
||||
t.Run("non-zero exit code", func(t *testing.T) {
|
||||
execOptions := api.ExecOptions{
|
||||
Command: []string{"sh", "-c", "exit 42"},
|
||||
AttachStdout: true,
|
||||
AttachStderr: true,
|
||||
}
|
||||
|
||||
exitCode, err := cli.ExecContainer(ctx, serviceName, "", execOptions)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 42, exitCode, "should return actual exit code")
|
||||
})
|
||||
|
||||
// Test: Invalid command should fail
|
||||
t.Run("invalid command", func(t *testing.T) {
|
||||
var stdout, stderr bytes.Buffer
|
||||
execOptions := api.ExecOptions{
|
||||
Command: []string{"nonexistent-command"},
|
||||
AttachStdout: true,
|
||||
AttachStderr: true,
|
||||
Stdout: &stdout,
|
||||
Stderr: &stderr,
|
||||
}
|
||||
|
||||
exitCode, err := cli.ExecContainer(ctx, serviceName, "", execOptions)
|
||||
require.NoError(t, err, "exec should not fail, but command should return non-zero exit")
|
||||
assert.Equal(t, 126, exitCode, "invalid command should return non-zero exit code")
|
||||
assert.Contains(t, stdout.String(), "executable file not found")
|
||||
assert.Equal(t, "", stderr.String(), "stderr should be empty")
|
||||
})
|
||||
|
||||
// Test: Non-existent container ID
|
||||
t.Run("non-existent container", func(t *testing.T) {
|
||||
execOptions := api.ExecOptions{
|
||||
Command: []string{"echo", "test"},
|
||||
AttachStdout: true,
|
||||
AttachStderr: true,
|
||||
}
|
||||
|
||||
_, err := cli.ExecContainer(ctx, serviceName, "non-existent-container-id", execOptions)
|
||||
assert.Error(t, err, "should fail for non-existent container")
|
||||
})
|
||||
|
||||
// Test: Empty command
|
||||
t.Run("empty command", func(t *testing.T) {
|
||||
execOptions := api.ExecOptions{
|
||||
Command: []string{},
|
||||
AttachStdout: true,
|
||||
AttachStderr: true,
|
||||
}
|
||||
|
||||
_, err := cli.ExecContainer(ctx, serviceName, "", execOptions)
|
||||
assert.Error(t, err, "should fail for empty command")
|
||||
})
|
||||
|
||||
// Test: Command with both stdout and stderr
|
||||
t.Run("mixed stdout/stderr", func(t *testing.T) {
|
||||
var stdout, stderr bytes.Buffer
|
||||
execOptions := api.ExecOptions{
|
||||
Command: []string{"sh", "-c", "echo 'stdout'; echo 'stderr' >&2"},
|
||||
AttachStdout: true,
|
||||
AttachStderr: true,
|
||||
Stdout: &stdout,
|
||||
Stderr: &stderr,
|
||||
}
|
||||
|
||||
exitCode, err := cli.ExecContainer(ctx, serviceName, "", execOptions)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 0, exitCode)
|
||||
assert.Equal(t, "stdout\n", stdout.String(), "should capture stdout")
|
||||
assert.Equal(t, "stderr\n", stderr.String(), "should capture stderr")
|
||||
})
|
||||
|
||||
// Test: Detached command should return immediately
|
||||
t.Run("detached command", func(t *testing.T) {
|
||||
var stdout, stderr bytes.Buffer
|
||||
start := time.Now()
|
||||
execOptions := api.ExecOptions{
|
||||
Command: []string{"sh", "-c", "sleep 10; echo hello"}, // Long-running command
|
||||
Stdout: &stdout,
|
||||
Stderr: &stderr,
|
||||
Detach: true,
|
||||
}
|
||||
|
||||
exitCode, err := cli.ExecContainer(ctx, serviceName, "", execOptions)
|
||||
elapsed := time.Since(start)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 0, exitCode)
|
||||
// Should return immediately, not wait for command to complete
|
||||
assert.Less(t, elapsed, 5*time.Second, "detached command should return quickly")
|
||||
assert.Empty(t, stdout.String(), "stdout should be empty for detached command")
|
||||
assert.Empty(t, stderr.String(), "stderr should be empty for detached command")
|
||||
})
|
||||
|
||||
// Test: Detached mode with invalid command
|
||||
t.Run("detached invalid command", func(t *testing.T) {
|
||||
var stdout, stderr bytes.Buffer
|
||||
execOptions := api.ExecOptions{
|
||||
Command: []string{"nonexistent-detached-command"},
|
||||
Stdout: &stdout,
|
||||
Stderr: &stderr,
|
||||
Detach: true,
|
||||
}
|
||||
|
||||
exitCode, err := cli.ExecContainer(ctx, serviceName, "", execOptions)
|
||||
|
||||
require.ErrorContains(t, err, "executable file not found")
|
||||
assert.Equal(t, 1, exitCode)
|
||||
assert.Empty(t, stdout.String(), "stdout should be empty for detached command")
|
||||
assert.Empty(t, stderr.String(), "stderr should be empty for detached command")
|
||||
})
|
||||
|
||||
// Deploy a service with multiple replicas
|
||||
multiServiceName := "multi-replica-service"
|
||||
deployTestService(t, ctx, cli, multiServiceName, 2)
|
||||
|
||||
// Test: Execute command on specific container
|
||||
t.Run("exec on specific container", func(t *testing.T) {
|
||||
service, err := cli.InspectService(ctx, multiServiceName)
|
||||
require.NoError(t, err)
|
||||
require.GreaterOrEqual(t, len(service.Containers), 2, "should have at least 2 containers")
|
||||
|
||||
var stdout, stderr bytes.Buffer
|
||||
|
||||
// Use container prefix
|
||||
containerIDPrefix := service.Containers[1].Container.ID[:3]
|
||||
execOptions := api.ExecOptions{
|
||||
Command: []string{"hostname"},
|
||||
AttachStdout: true,
|
||||
AttachStderr: true,
|
||||
Stdout: &stdout,
|
||||
Stderr: &stderr,
|
||||
}
|
||||
|
||||
exitCode, err := cli.ExecContainer(ctx, multiServiceName, containerIDPrefix, execOptions)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 0, exitCode)
|
||||
assert.Greater(t, len(stdout.String()), 4)
|
||||
assert.Equal(t, service.Containers[1].Container.Name+"\n", stdout.String())
|
||||
})
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
services:
|
||||
web:
|
||||
image: busybox:1.37.0-uclibc
|
||||
command: ["true"]
|
||||
command: ["sleep", "600"]
|
||||
configs:
|
||||
- source: from-file
|
||||
target: /etc/config-from-file.conf
|
||||
|
||||
+63
-70
@@ -1,105 +1,98 @@
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/docker/api/types/container"
|
||||
dockerclient "github.com/docker/docker/client"
|
||||
"github.com/docker/docker/pkg/stdcopy"
|
||||
"github.com/psviderski/uncloud/internal/ucind"
|
||||
"github.com/psviderski/uncloud/pkg/api"
|
||||
"github.com/psviderski/uncloud/pkg/client"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type fileInfo struct {
|
||||
permissions os.FileMode
|
||||
content string
|
||||
userId int
|
||||
groupId int
|
||||
}
|
||||
|
||||
// a helper function that takes a machine, container name inside it and file path, and returns the file contents along with metadata
|
||||
//
|
||||
// Uncloud API does not currently expose exec functionality to run commands inside containers.
|
||||
// Instead this helper function uses "docker cp" inside the ucind container to copy the file from the target container
|
||||
// to a temporary location (also inside the ucind container), and then inspect its content and permissions.
|
||||
func readFileInfoInContainer(t *testing.T, machine *ucind.Machine, containerName, filePath string) (fileInfo, error) {
|
||||
// execInContainerAndReadOutput is a helper that executes a command in a container and returns stdout output.
|
||||
// It validates the exit code is 0 and that there's no stderr output.
|
||||
func execInContainerAndReadOutput(
|
||||
t *testing.T,
|
||||
ctx context.Context,
|
||||
cli *client.Client,
|
||||
serviceNameOrID, containerNameOrID string,
|
||||
command []string,
|
||||
) (string, error) {
|
||||
t.Helper()
|
||||
|
||||
var stdout, stderr bytes.Buffer
|
||||
execOpts := api.ExecOptions{
|
||||
Command: command,
|
||||
AttachStdout: true,
|
||||
AttachStderr: true,
|
||||
Stdout: &stdout,
|
||||
Stderr: &stderr,
|
||||
}
|
||||
|
||||
commandName := command[0]
|
||||
|
||||
exitCode, err := cli.ExecContainer(ctx, serviceNameOrID, containerNameOrID, execOpts)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("exec %s: %w", commandName, err)
|
||||
}
|
||||
|
||||
assert.Equal(t, 0, exitCode, "Expected exit code 0 from %s command", commandName)
|
||||
|
||||
if stderr.Len() > 0 {
|
||||
return "", fmt.Errorf("%s stderr output: %s", commandName, stderr.String())
|
||||
}
|
||||
|
||||
return stdout.String(), nil
|
||||
}
|
||||
|
||||
// readFileInfoInContainer reads file information from a container using the Uncloud client ExecContainer API.
|
||||
// Information read: permissions, uid, gid, content.
|
||||
func readFileInfoInContainer(t *testing.T, cli *client.Client, serviceNameOrID, containerNameOrID, filePath string) (fileInfo, error) {
|
||||
t.Helper()
|
||||
ctx := context.Background()
|
||||
|
||||
dockerCli, err := dockerclient.NewClientWithOpts(dockerclient.FromEnv, dockerclient.WithAPIVersionNegotiation())
|
||||
if err != nil {
|
||||
return fileInfo{}, err
|
||||
}
|
||||
defer dockerCli.Close()
|
||||
|
||||
machineContainerName := fmt.Sprintf("%s-%s", machine.ClusterName, machine.Name)
|
||||
|
||||
// 1. Create an exec instance
|
||||
fileLocation := fmt.Sprintf("%s:%s", containerName, filePath)
|
||||
tmpFileLocation := fmt.Sprintf("/tmp/%s-%s", containerName, filepath.Base(filePath))
|
||||
|
||||
cmdDocker := fmt.Sprintf("docker cp %s %s", fileLocation, tmpFileLocation)
|
||||
cmdPermissions := fmt.Sprintf("stat -c %%a %s", tmpFileLocation)
|
||||
cmdCat := fmt.Sprintf("cat %s", tmpFileLocation)
|
||||
cmdCombined := fmt.Sprintf("%s; %s; %s", cmdDocker, cmdPermissions, cmdCat)
|
||||
|
||||
execConfig := container.ExecOptions{
|
||||
Cmd: []string{"sh", "-ec", cmdCombined},
|
||||
AttachStdout: true,
|
||||
AttachStderr: true,
|
||||
}
|
||||
resp, err := dockerCli.ContainerExecCreate(ctx, machineContainerName, execConfig)
|
||||
// Get file permissions
|
||||
permOutput, err := execInContainerAndReadOutput(t, ctx, cli, serviceNameOrID, containerNameOrID,
|
||||
[]string{"stat", "-c", "%a %u %g", filePath})
|
||||
if err != nil {
|
||||
return fileInfo{}, err
|
||||
}
|
||||
|
||||
// 2. Attach to the exec session
|
||||
hijackResp, err := dockerCli.ContainerExecAttach(ctx, resp.ID, container.ExecAttachOptions{})
|
||||
// Parse permissions
|
||||
permissions := strings.TrimSpace(permOutput)
|
||||
fmt.Printf("Permissions output: %s\n", permissions)
|
||||
|
||||
// Parse three numbers: permissions, uid, gid
|
||||
var permissionsOctal, uid, gid int
|
||||
_, err = fmt.Sscanf(permissions, "%o %d %d", &permissionsOctal, &uid, &gid)
|
||||
if err != nil {
|
||||
return fileInfo{}, err
|
||||
}
|
||||
defer hijackResp.Close()
|
||||
|
||||
// 3. Inspect result
|
||||
inspectResp, err := dockerCli.ContainerExecInspect(ctx, resp.ID)
|
||||
if err != nil {
|
||||
return fileInfo{}, err
|
||||
return fileInfo{}, fmt.Errorf("parse stat output: %w", err)
|
||||
}
|
||||
|
||||
assert.Equal(t, 0, inspectResp.ExitCode, "Expected exit code 0 from exec command")
|
||||
|
||||
// 4. Read output to string using stdcopy to demultiplex the Docker stream
|
||||
var stdout, stderr strings.Builder
|
||||
_, err = stdcopy.StdCopy(&stdout, &stderr, hijackResp.Reader)
|
||||
if err != nil {
|
||||
return fileInfo{}, err
|
||||
}
|
||||
|
||||
if stderr.Len() > 0 {
|
||||
return fileInfo{}, fmt.Errorf("stderr output: %s", stderr.String())
|
||||
}
|
||||
|
||||
// 5. Parse output: permissions, content
|
||||
outputLines := strings.SplitN(stdout.String(), "\n", 2)
|
||||
if len(outputLines) < 2 {
|
||||
return fileInfo{}, fmt.Errorf("unexpected output format, expected at least 2 lines, got %d", len(outputLines))
|
||||
}
|
||||
permissions := outputLines[0]
|
||||
// convert to octal number
|
||||
permissionsOctal, err := strconv.ParseInt(permissions, 8, 32)
|
||||
if err != nil {
|
||||
return fileInfo{}, fmt.Errorf("parse file permissions: %w", err)
|
||||
}
|
||||
mode := os.FileMode(permissionsOctal)
|
||||
|
||||
fileContent := outputLines[1]
|
||||
// Get file content
|
||||
fileContent, err := execInContainerAndReadOutput(t, ctx, cli, serviceNameOrID, containerNameOrID,
|
||||
[]string{"cat", filePath})
|
||||
if err != nil {
|
||||
return fileInfo{}, err
|
||||
}
|
||||
|
||||
return fileInfo{
|
||||
permissions: mode,
|
||||
userId: uid,
|
||||
groupId: gid,
|
||||
content: fileContent,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
services:
|
||||
uncloud-website:
|
||||
image: ghcr.io/psviderski/uncloud-website:latest
|
||||
pull_policy: always
|
||||
image: ghcr.io/psviderski/uncloud-website
|
||||
build:
|
||||
cache_from:
|
||||
- type=gha,scope=uncloud-website
|
||||
cache_to:
|
||||
- type=gha,mode=max,scope=uncloud-website
|
||||
platforms:
|
||||
- linux/amd64
|
||||
- linux/arm64
|
||||
user: nobody
|
||||
x-ports:
|
||||
- uncloud.run:8000/https
|
||||
|
||||
@@ -42,7 +42,7 @@ Some of the common use cases Uncloud is a great fit for:
|
||||
- **Dev/staging environments**: Spin up additional environments for development and testing that mirror production
|
||||
reusing the same Compose configuration.
|
||||
|
||||
## What makes Uncloud different
|
||||
## What makes Uncloud special
|
||||
|
||||
Here are the design decisions that make Uncloud stand out:
|
||||
|
||||
|
||||
@@ -59,3 +59,75 @@ Address: 10.210.1.3
|
||||
Name: worker.internal
|
||||
Address: 10.210.1.4
|
||||
```
|
||||
|
||||
## IP Ordering Mode
|
||||
|
||||
Additionally, the IP ordering preference can be specified with a `rr` (round-robin) or `nearest` subdomain prefix.
|
||||
|
||||
### `rr` (round-robin) *current default*
|
||||
Randomly shuffled order on each lookup.
|
||||
|
||||
```
|
||||
$ nslookup rr.worker.internal
|
||||
Server: 127.0.0.11
|
||||
Address: 127.0.0.11#53
|
||||
|
||||
Name: rr.worker.internal
|
||||
Address: 10.210.0.3
|
||||
Name: rr.worker.internal
|
||||
Address: 10.210.0.4
|
||||
Name: rr.worker.internal
|
||||
Address: 10.210.1.3
|
||||
Name: rr.worker.internal
|
||||
Address: 10.210.1.4
|
||||
|
||||
$ nslookup rr.worker.internal
|
||||
Server: 127.0.0.11
|
||||
Address: 127.0.0.11#53
|
||||
|
||||
Name: rr.worker.internal
|
||||
Address: 10.210.0.4
|
||||
Name: rr.worker.internal
|
||||
Address: 10.210.1.3
|
||||
Name: rr.worker.internal
|
||||
Address: 10.210.1.4
|
||||
Name: rr.worker.internal
|
||||
Address: 10.210.0.3
|
||||
```
|
||||
|
||||
## Nearest scope
|
||||
Returns machine-local instances first.
|
||||
|
||||
`machine-a`:
|
||||
```
|
||||
$ nslookup nearest.worker.internal
|
||||
Server: 127.0.0.11
|
||||
Address: 127.0.0.11#53
|
||||
|
||||
Name: nearest.worker.internal
|
||||
Address: 10.210.0.3
|
||||
Name: nearest.worker.internal
|
||||
Address: 10.210.0.4
|
||||
Name: nearest.worker.internal
|
||||
Address: 10.210.1.3
|
||||
Name: nearest.worker.internal
|
||||
Address: 10.210.1.4
|
||||
```
|
||||
|
||||
`machine-b`:
|
||||
```
|
||||
$ nslookup nearest.worker.internal
|
||||
Server: 127.0.0.11
|
||||
Address: 127.0.0.11#53
|
||||
|
||||
Name: nearest.worker.internal
|
||||
Address: 10.210.1.3
|
||||
Name: nearest.worker.internal
|
||||
Address: 10.210.1.4
|
||||
Name: nearest.worker.internal
|
||||
Address: 10.210.0.3
|
||||
Name: nearest.worker.internal
|
||||
Address: 10.210.0.4
|
||||
```
|
||||
|
||||
The prefixes can be used with service ID and machine-scoped service names, as well (e.g. `nearest.3ecb3a8bbec5fd3f46efb056a934714a.internal` or `rr.0903f0ee483aa97d559eeeaac5e22283.m.worker.internal`).
|
||||
|
||||
@@ -0,0 +1,362 @@
|
||||
# Deploy an app
|
||||
|
||||
Deploy a containerised application to your Uncloud cluster using either source code or pre-built images.
|
||||
|
||||
This guide covers both scenarios:
|
||||
|
||||
- **[Deploy from source code](#deploy-from-source-code)**: Build Docker images from your application code and use them
|
||||
to deploy service containers
|
||||
- **[Deploy pre-built images](#deploy-pre-built-images)**: Deploy service containers using existing images from a
|
||||
registry or your local machine
|
||||
|
||||
Uncloud uses [Compose Specification](https://compose-spec.io/) for defining the deployment configuration of your app's
|
||||
services. It implements the most common Compose features with some Uncloud-specific extensions. See
|
||||
[Compose support matrix](../../8-compose-file-reference/1-support-matrix.md) for details.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- `uc` CLI [installed](../../2-getting-started/1-install-cli.md) on your local machine
|
||||
- An Uncloud cluster with at least one machine (see [Quick start](../../2-getting-started/2-deploy-demo-app.md))
|
||||
- Basic knowledge of [Compose Specification](https://compose-spec.io/)
|
||||
|
||||
## Deploy from source code
|
||||
|
||||
Use this scenario when you want to build and deploy your application from
|
||||
a [Dockerfile](https://docs.docker.com/reference/dockerfile/) and source code available on your local machine.
|
||||
|
||||
### 1. Create a Compose file
|
||||
|
||||
Create a `compose.yaml` file in your application directory with a
|
||||
[`build`](https://github.com/compose-spec/compose-spec/blob/main/spec.md#build) section for each service you want to
|
||||
build. Here is a minimal example of a Compose file that builds and deploys a web app that consists of a single service
|
||||
called `web`:
|
||||
|
||||
```yaml title="compose.yaml"
|
||||
services:
|
||||
web:
|
||||
# Build an image from the Dockerfile in the current directory
|
||||
build: .
|
||||
# Publish the container port 8000 as https://app.example.com
|
||||
x-ports:
|
||||
- app.example.com:8000/https
|
||||
```
|
||||
|
||||
If you don't have a Dockerfile for building an image from your source code, create one in the same directory.
|
||||
|
||||
### 2. Build and deploy your app
|
||||
|
||||
To build and deploy services defined in your Compose file, navigate to the directory with `compose.yaml` and run:
|
||||
|
||||
```shell
|
||||
uc deploy
|
||||
```
|
||||
|
||||
This command looks for a Compose file in your current working directory and:
|
||||
|
||||
1. **Builds images** for services with a `build` section using your local Docker and tags them with the current Git
|
||||
version
|
||||
2. **Pushes built images** directly **to cluster machines** using
|
||||
[unregistry](https://github.com/psviderski/unregistry), transferring only the missing layers
|
||||
3. **Plans the deployment** and shows you what will change, asking for confirmation
|
||||
4. **Creates any missing volumes** on target machines
|
||||
5. **Deploys service containers** using the built images and latest configuration changes with zero-downtime rolling
|
||||
updates
|
||||
|
||||
See the [uc deploy](../../9-cli-reference/uc_deploy.md) reference for all available options.
|
||||
|
||||
Watch `uc deploy` building and deploying a demo app (18 seconds):
|
||||
|
||||
<video controls width="100%">
|
||||
<source src="https://media.uncloud.run/docs/uc-deploy-demo.mp4"/>
|
||||
</video>
|
||||
|
||||
### Customise the build configuration
|
||||
|
||||
If you need more control over the build configuration, you can specify additional attributes in the `build` section of
|
||||
your Compose file. For example, to specify a custom Dockerfile location, set build-time arguments, or build
|
||||
multi-platform images.
|
||||
|
||||
```yaml
|
||||
services:
|
||||
web:
|
||||
build:
|
||||
# Relative path to the directory with your Dockerfile
|
||||
context: ./backend
|
||||
# Set build-time variables defined as ARG in your Dockerfile
|
||||
args:
|
||||
ALPINE_VERSION: 3.22
|
||||
BUILD_ENV: prod
|
||||
# Build a multi-platform image
|
||||
platforms:
|
||||
- linux/amd64
|
||||
- linux/arm64
|
||||
```
|
||||
|
||||
You can pass additional build arguments or override existing ones using the `--build-arg` flag with `uc deploy`:
|
||||
|
||||
```shell
|
||||
uc deploy --build-arg BUILD_ENV=dev
|
||||
```
|
||||
|
||||
You can also use advanced features like build caches, secrets, and SSH access.
|
||||
See [Compose Build Specification](https://github.com/compose-spec/compose-spec/blob/main/build.md) for all supported
|
||||
attributes.
|
||||
|
||||
:::info note
|
||||
|
||||
To build multi-platform images, you need to configure your local Docker to use the
|
||||
[containerd image store](https://docs.docker.com/desktop/features/containerd/).
|
||||
|
||||
:::
|
||||
|
||||
### Customise image tags
|
||||
|
||||
If you don't specify the `image` attribute, `uc deploy` tags built images with a Git-based version like
|
||||
|
||||
```yaml
|
||||
# <project>/<service>:<git datetime>.<short git sha>
|
||||
myapp/web:2025-10-30-223604.84d33bb
|
||||
```
|
||||
|
||||
It uses your local date/time if the working directory is not a Git repository.
|
||||
|
||||
You can customise the image name and tag format using the `image` attribute. It can be a static name or a dynamic
|
||||
template using [environment variables](https://github.com/compose-spec/compose-spec/blob/main/spec.md#interpolation) and
|
||||
the [Go template](https://pkg.go.dev/text/template) syntax.
|
||||
|
||||
```yaml
|
||||
services:
|
||||
web:
|
||||
build: .
|
||||
# Custom image name and tag format
|
||||
image: myapp:{{gitdate "20060102"}}.{{gitsha 7}}.${GITHUB_RUN_ID:-local}{{if .Git.IsDirty}}.dirty{{end}}
|
||||
```
|
||||
|
||||
This generates tags like:
|
||||
|
||||
- `myapp:20251030.84d33bb.local.dirty` when building locally with uncommitted changes
|
||||
- `myapp:20251030.84d33bb.1234` when building in CI with `GITHUB_RUN_ID=1234` and a clean repo
|
||||
|
||||
`uc deploy` renders the image templates when it loads the Compose file and then uses the resulting names for the build
|
||||
and deploy stages.
|
||||
|
||||
See the [Image tag template](../../8-compose-file-reference/2-image-tag-template.md) reference for all available template
|
||||
variables and functions.
|
||||
|
||||
### Separate build and deploy steps
|
||||
|
||||
You might want to build and deploy services as separate commands. For example, to run them as separate steps in your
|
||||
CI/CD pipeline or have more control over the build and deploy process. Here are the commands that are equivalent to
|
||||
`uc deploy`:
|
||||
|
||||
```shell
|
||||
# Build images and push to cluster machines
|
||||
uc build --push
|
||||
|
||||
# Deploy services using the built images
|
||||
uc deploy --no-build
|
||||
```
|
||||
|
||||
### Deploy configuration changes only
|
||||
|
||||
You can use the `--no-build` flag with `uc deploy` to deploy only the configuration changes in your Compose file if your
|
||||
source code and images haven't changed. However, the image tag may still change if you're using
|
||||
[dynamic tags](#customise-image-tags) based on the Git state (default). In that case, the deploy will likely fail
|
||||
because the new tag won't be found on cluster machines.
|
||||
|
||||
Use less sensitive dynamic tags or specify the built and pushed image tags you want to deploy explicitly to avoid this
|
||||
issue.
|
||||
|
||||
The recommended approach though is to commit all your changes, including the configuration ones, to the repo. Then
|
||||
rebuild and deploy your services from a clean repo state. This way, the image tags will always reflect the exact source
|
||||
code and configuration used for the deployment.
|
||||
|
||||
## Deploy pre-built images
|
||||
|
||||
Use this scenario when you want to deploy your application using pre-built images from a container registry (for
|
||||
example, Docker Hub or GitHub Container Registry) or your local Docker.
|
||||
|
||||
### 1. Create a Compose file
|
||||
|
||||
Create a `compose.yaml` file that references an image from a registry using the
|
||||
[`image`](https://github.com/compose-spec/compose-spec/blob/main/spec.md#image) attribute. It's important that you don't
|
||||
include a `build` section for services using pre-built images. Here is a minimal example of a Compose file that deploys
|
||||
an app that consists of a single `nginx` service:
|
||||
|
||||
```yaml title="compose.yaml"
|
||||
services:
|
||||
nginx:
|
||||
# Use the nginx image from Docker Hub
|
||||
image: nginx:latest
|
||||
# Publish the container port 80 as https://nginx.example.com
|
||||
x-ports:
|
||||
- nginx.example.com:80/https
|
||||
```
|
||||
|
||||
### 2. Deploy your app
|
||||
|
||||
To deploy services defined in your Compose file, navigate to the directory with `compose.yaml` and run:
|
||||
|
||||
```shell
|
||||
uc deploy
|
||||
```
|
||||
|
||||
This command looks for a Compose file in your current working directory and:
|
||||
|
||||
1. **Plans the deployment** and shows you what will change, asking for confirmation
|
||||
2. **Creates any missing volumes** on target machines
|
||||
3. **Pulls images** from a registry on cluster machines where services are deployed according to the
|
||||
[`pull_policy`](https://github.com/compose-spec/compose-spec/blob/main/spec.md#pull_policy)
|
||||
4. **Deploys service containers** using the pulled images and latest configuration changes with zero-downtime rolling
|
||||
updates
|
||||
|
||||
### Control image pulling
|
||||
|
||||
By default, `uc deploy` pulls an image from a registry only if it's missing on a target machine. You can change this
|
||||
behavior using the [`pull_policy`](https://github.com/compose-spec/compose-spec/blob/main/spec.md#pull_policy)
|
||||
attribute. For example, to always pull the latest version of an image before deploying.
|
||||
|
||||
```yaml
|
||||
services:
|
||||
nginx:
|
||||
image: nginx:alpine
|
||||
# Always pull the latest :alpine tag before deploying
|
||||
pull_policy: always
|
||||
```
|
||||
|
||||
Available `pull_policy` values:
|
||||
|
||||
- `always`: Always pull the image from the registry before deploying
|
||||
- `missing` (default): Pull only if the image isn't available on the target machine
|
||||
- `never`: Never pull, the image must be present on the target machine or the deploy will fail
|
||||
|
||||
### Pull from a private registry
|
||||
|
||||
If your images are in a private registry, `uc deploy` needs an authentication token to pull them. You can provide it by
|
||||
either:
|
||||
|
||||
- Logging in to the registry using your local Docker (recommended)
|
||||
- Logging in to the registry using Docker on each cluster machine you want to deploy to
|
||||
|
||||
When you're logged in using your local Docker, `uc deploy` automatically passes your local Docker credentials for the
|
||||
private registry to cluster machines when pulling images. This way, you don't need to log in on each machine manually.
|
||||
|
||||
See [`docker login`](https://docs.docker.com/reference/cli/docker/login/) for instructions on how to log in to a private
|
||||
registry.
|
||||
|
||||
### Push local images to cluster machines
|
||||
|
||||
You can also deploy pre-built images that exist only in your local Docker and are not available for pulling on cluster
|
||||
machines. For example, images you built locally outside of the Compose workflow or pulled from a private registry that
|
||||
is unreachable from your cluster machines. This is useful for deploying to air-gapped or restricted environments.
|
||||
|
||||
You can push these local images directly to your cluster machines using the
|
||||
[`uc image push`](../../9-cli-reference/uc_image_push.md) command:
|
||||
|
||||
```shell
|
||||
# Push to *all* cluster machines
|
||||
uc image push myapp:latest
|
||||
|
||||
# Push to specific machines only
|
||||
uc image push myapp:latest -m machine1,machine2
|
||||
```
|
||||
|
||||
This command uploads the image from your local Docker to the cluster machines using
|
||||
[unregistry](https://github.com/psviderski/unregistry) running as part of the Uncloud daemon on each machine. It
|
||||
efficiently transfers only the image layers that don't already exist on the target machines.
|
||||
|
||||
After pushing the image, update your Compose file to reference it:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
web:
|
||||
# Reference the image you just pushed
|
||||
image: myapp:latest
|
||||
# Never try to pull from a registry since the image is only available locally
|
||||
pull_policy: never
|
||||
```
|
||||
|
||||
Run [`uc images`](../../9-cli-reference/uc_images.md) to verify that the image is available on the target machines.
|
||||
|
||||
Then deploy as usual:
|
||||
|
||||
```shell
|
||||
uc deploy
|
||||
```
|
||||
|
||||
:::tip
|
||||
|
||||
Set `pull_policy: never` when using local images to prevent `uc deploy` from trying to pull them from a registry.
|
||||
|
||||
:::
|
||||
|
||||
## Mix source builds and pre-built images
|
||||
|
||||
Define multiple services in your Compose file, mixing source builds and pre-built images:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
web:
|
||||
# Build web service from source
|
||||
build: .
|
||||
x-ports:
|
||||
- app.example.com:8000/https
|
||||
|
||||
db:
|
||||
# Pull a pre-built PostgreSQL image from Docker Hub
|
||||
image: postgres:18
|
||||
environment:
|
||||
POSTGRES_PASSWORD: ${DB_PASSWORD}
|
||||
```
|
||||
|
||||
`uc deploy` builds and pushes images for services with a `build` section and pulls images from a registry for services
|
||||
with only an `image` attribute.
|
||||
|
||||
:::warning
|
||||
|
||||
**Service names must be globally unique** across all Compose files deployed to the same cluster.
|
||||
|
||||
Unlike Docker Compose or Docker Swarm, Uncloud doesn't automatically prefix service names with project or stack names.
|
||||
Choose unique names to avoid conflicts with services deployed from other Compose files.
|
||||
|
||||
:::
|
||||
|
||||
## Use a different Compose file location
|
||||
|
||||
If your Compose file has a different name or location, use the `-f/--file` flag to specify its path:
|
||||
|
||||
```shell
|
||||
uc deploy -f path/to/your-compose.yaml
|
||||
```
|
||||
|
||||
You can also specify multiple Compose files to merge configurations:
|
||||
|
||||
```shell
|
||||
uc deploy -f compose.yaml -f compose.prod.yaml
|
||||
```
|
||||
|
||||
See [Use multiple Compose files](https://docs.docker.com/compose/how-tos/multiple-compose-files/) for details on how you
|
||||
can customise your Compose application for different environments or workflows.
|
||||
|
||||
## Verify your deployment
|
||||
|
||||
After deploying your app, you can verify that your services are running as expected by listing all deployed services in
|
||||
the cluster:
|
||||
|
||||
```shell
|
||||
uc ls
|
||||
```
|
||||
|
||||
and inspecting the status of containers for a specific service:
|
||||
|
||||
```shell
|
||||
uc inspect web
|
||||
```
|
||||
|
||||
# See also
|
||||
|
||||
- [Deploy to specific machines](2-deploy-specific-machines.md): Deploy services to specific machines in your cluster
|
||||
- [Deploy a global service](3-deploy-global-services.md): Deploy one service replica on each cluster machine
|
||||
- [Compose Specification](https://compose-spec.io/): Official specification for the Compose file format
|
||||
- [Compose support matrix](../../8-compose-file-reference/1-support-matrix.md): Supported Compose features and Uncloud
|
||||
extensions
|
||||
@@ -0,0 +1,84 @@
|
||||
# Deploy to specific machines
|
||||
|
||||
Deploy services to specific machines in your cluster using the
|
||||
[`x-machines`](../../8-compose-file-reference/1-support-matrix.md#x-machines) extension in your Compose file.
|
||||
|
||||
## When to target specific machines
|
||||
|
||||
By default, Uncloud randomly chooses available machines to run your services on, evenly spreading multiple replicas of a
|
||||
service across all machines for high availability. You can restrict which machines can run your service using the
|
||||
[`x-machines`](../../8-compose-file-reference/1-support-matrix.md#x-machines) extension in your Compose file.
|
||||
|
||||
This is useful when you want to:
|
||||
|
||||
- Deploy services to machines in a **specific region** or **data center**
|
||||
- Run services only on machines with **specific hardware** (for example, GPUs or ARM processors)
|
||||
- Specify where to deploy **stateful services** and create their data volumes
|
||||
- Keep certain services isolated to **dedicated machines**
|
||||
- Deploy to a subset of machines for **testing** before rolling out cluster-wide
|
||||
|
||||
## Target machines in a Compose file
|
||||
|
||||
Set `x-machines` to a list of machine names or a single machine name to restrict which machines a service can run on.
|
||||
|
||||
```yaml title="compose.yaml"
|
||||
services:
|
||||
web:
|
||||
build: .
|
||||
x-ports:
|
||||
- app.example.com:8000/https
|
||||
# Spread 3 replicas across machine-1 and machine-2 only
|
||||
x-machines:
|
||||
- machine-1
|
||||
- machine-2
|
||||
scale: 3
|
||||
|
||||
db:
|
||||
image: postgres:18
|
||||
environment:
|
||||
POSTGRES_PASSWORD: ${DB_PASSWORD}
|
||||
volumes:
|
||||
- db-data:/var/lib/postgresql
|
||||
# Create the db-data volume and run the DB container on machine-db
|
||||
x-machines: machine-db
|
||||
|
||||
volumes:
|
||||
db-data:
|
||||
```
|
||||
|
||||
When you deploy this Compose file with [`uc deploy`](../../9-cli-reference/uc_deploy.md):
|
||||
|
||||
- The `web` service will create and spread its 3 replicas only across `machine-1` and `machine-2`
|
||||
- The `db` service will create its `db-data` volume and run only on `machine-db`
|
||||
|
||||
:::tip
|
||||
|
||||
Use [`uc machine ls`](../../9-cli-reference/uc_machine_ls.md) to see available machines in your cluster.
|
||||
|
||||
:::
|
||||
|
||||
## Push images to specific machines only
|
||||
|
||||
When [building from source](1-deploy-app.md#deploy-from-source-code), `uc deploy` and `uc build --push` automatically
|
||||
push built images to **all** cluster machines by default. This ensures images are available wherever services might be
|
||||
deployed.
|
||||
|
||||
If you're using `x-machines` to restrict deployments to specific machines, `uc deploy` and `uc build --push` push images
|
||||
**only to those** machines. This saves time and bandwidth by uploading images only where they're needed.
|
||||
|
||||
You can also manually push your local Docker images to specific machines using the [
|
||||
`uc image push`](../../9-cli-reference/uc_image_push.md) command:
|
||||
|
||||
```shell
|
||||
# Push the local Docker image 'myapp:latest' to machine-1 and machine-2
|
||||
uc image push myapp:latest -m machine-1,machine-2
|
||||
```
|
||||
|
||||
See [Push local images to cluster machines](1-deploy-app.md#push-local-images-to-cluster-machines) for more details.
|
||||
|
||||
## See also
|
||||
|
||||
- [Deploy an app](1-deploy-app.md): Deploy from source code or prebuilt images
|
||||
- [Deploy a global service](3-deploy-global-services.md): Deploy one service replica on each cluster machine
|
||||
- [Compose support matrix](../../8-compose-file-reference/1-support-matrix.md): Supported Compose features and Uncloud
|
||||
extensions
|
||||
@@ -0,0 +1,69 @@
|
||||
# Deploy a global service
|
||||
|
||||
Deploy exactly one replica of a service on each machine in your cluster.
|
||||
|
||||
This is useful for cluster-wide infrastructure services like monitoring or security agents, log collectors, or reverse
|
||||
proxies.
|
||||
|
||||
## Deploy to all machines
|
||||
|
||||
To deploy exactly one replica on each machine in your cluster for a specific service, set
|
||||
[`mode: global`](https://github.com/compose-spec/compose-spec/blob/main/deploy.md#mode) under the `deploy` section in
|
||||
your Compose file:
|
||||
|
||||
```yaml title="compose.yaml"
|
||||
services:
|
||||
monitoring:
|
||||
image: quay.io/prometheus/node-exporter:latest
|
||||
deploy:
|
||||
# Run one container on each machine in the cluster
|
||||
mode: global
|
||||
```
|
||||
|
||||
Then deploy:
|
||||
|
||||
```shell
|
||||
uc deploy
|
||||
```
|
||||
|
||||
Before creating replicas on cluster machines, it will show you a deployment plan and ask for confirmation.
|
||||
|
||||
If you add more machines to the cluster later, you need to run `uc deploy` again to create replicas on the new machines.
|
||||
Uncloud doesn't automatically scale global services to new machines.
|
||||
|
||||
## Deploy to a subset of machines
|
||||
|
||||
You can combine the `global` mode with [`x-machines`](../../8-compose-file-reference/1-support-matrix.md#x-machines)
|
||||
to deploy one container to each specified machine:
|
||||
|
||||
```yaml title="compose.yaml"
|
||||
services:
|
||||
caddy:
|
||||
image: caddy:2
|
||||
deploy:
|
||||
# Run one container on each of the three specified machines
|
||||
mode: global
|
||||
x-machines:
|
||||
- ingress-1
|
||||
- ingress-2
|
||||
- ingress-3
|
||||
```
|
||||
|
||||
This is useful when you want a service on a specific group of machines (for example, ingress or GPU machines) but still
|
||||
want the one-per-machine guarantee that global mode provides.
|
||||
|
||||
## Global vs replicated mode
|
||||
|
||||
The default mode is `replicated`, where you specify the number of replicas.
|
||||
|
||||
| Mode | Replicas | Placement |
|
||||
|------------------------|-----------------------------------------------|---------------------------------------------------------------------|
|
||||
| `replicated` (default) | You specify with `scale` or `deploy.replicas` | Uncloud evenly spreads replicas across all machines or `x-machines` |
|
||||
| `global` | Always one per machine | One replica on each machine or each `x-machines` machine |
|
||||
|
||||
## See also
|
||||
|
||||
- [Deploy an app](1-deploy-app.md): Deploy from source code or prebuilt images
|
||||
- [Deploy to specific machines](2-deploy-specific-machines.md): Deploy services to specific machines in your cluster
|
||||
- [Compose Specification: deploy.mode](https://github.com/compose-spec/compose-spec/blob/main/deploy.md#mode):
|
||||
Compose specification for deployment modes
|
||||
@@ -0,0 +1,4 @@
|
||||
label: Deployments
|
||||
collapsed: false # keep the category open by default
|
||||
link:
|
||||
type: generated-index
|
||||
@@ -0,0 +1,4 @@
|
||||
label: Guides
|
||||
collapsed: false # keep the category open by default
|
||||
link:
|
||||
type: generated-index
|
||||
+4
-3
@@ -16,6 +16,7 @@ The following table shows the support status for main Compose features:
|
||||
| `entrypoint` | ✅ Supported | Override container entrypoint |
|
||||
| `env_file` | ✅ Supported | Environment file |
|
||||
| `environment` | ✅ Supported | Environment variables |
|
||||
| `gpus` | ✅ Supported | GPU device access |
|
||||
| `image` | ✅ Supported | Container image specification |
|
||||
| `init` | ✅ Supported | Run init process in container |
|
||||
| `labels` | ❌ Not supported | |
|
||||
@@ -39,7 +40,7 @@ The following table shows the support status for main Compose features:
|
||||
| `mode` | ✅ Supported | Either `global` or `replicated` |
|
||||
| `placement` | ❌ Not supported | Use `x-machines` extension |
|
||||
| `replicas` | ✅ Supported | Number of container replicas |
|
||||
| `resources` | ⚠️ Limited | CPU and memory limits only |
|
||||
| `resources` | ⚠️ Limited | CPU, memory limits and device reservations |
|
||||
| `restart_policy` | ❌ Not supported | Defaults to `unless-stopped` |
|
||||
| **Volumes** | | |
|
||||
| Named volumes | ✅ Supported | Docker volumes |
|
||||
@@ -82,7 +83,7 @@ services:
|
||||
- 8080:80/tcp@host
|
||||
```
|
||||
|
||||
See [Publishing services](../1-ingress/2-publishing-services.md) for more details.
|
||||
See [Publishing services](../3-concepts/1-ingress/2-publishing-services.md) for more details.
|
||||
|
||||
### `x-caddy`
|
||||
|
||||
@@ -98,7 +99,7 @@ services:
|
||||
}
|
||||
```
|
||||
|
||||
See [Publishing services](../1-ingress/2-publishing-services.md) for more details.
|
||||
See [Publishing services](../3-concepts/1-ingress/2-publishing-services.md) for more details.
|
||||
|
||||
### `x-machines`
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
# Image tag template
|
||||
|
||||
Template syntax for tagging built images.
|
||||
|
||||
## Overview
|
||||
|
||||
When building service images as part of [`uc build`](../9-cli-reference/uc_build.md) or
|
||||
[`uc deploy`](../9-cli-reference/uc_deploy.md) commands, Uncloud automatically generates image tags based on the current
|
||||
Git repository state. You can customise the image name and tag format for the built images using the
|
||||
[Go template](https://pkg.go.dev/text/template) syntax environment variables.
|
||||
|
||||
## Default template
|
||||
|
||||
If you **don't specify** an `image` attribute for a service with a `build` section, Uncloud uses the following Go
|
||||
template for tagging the built image:
|
||||
|
||||
```go
|
||||
{{.Project}}/{{.Service}}:{{if.Git.IsRepo}}{{gitdate "2006-01-02-150405"}}.{{gitsha 7}}{{if.Git.IsDirty}}.dirty{{end}}{{else}}{{date "2006-01-02-150405"}}{{end}}
|
||||
```
|
||||
|
||||
```yaml title="compose.yaml"
|
||||
services:
|
||||
web:
|
||||
build: .
|
||||
```
|
||||
|
||||
This generates image tags as follows:
|
||||
|
||||
- Git repository (clean): `myapp/web:2025-10-30-223604.84d33bb`
|
||||
- Git repository (with uncommitted changes): `myapp/web:2025-10-30-223604.84d33bb.dirty`
|
||||
- Non-Git directory: `myapp/web:2025-10-31-120651`
|
||||
|
||||
If you specify only an **image name without a tag** in the `image` attribute, Uncloud appends the tag portion of the
|
||||
default template to your image name.
|
||||
|
||||
```yaml title="compose.yaml"
|
||||
services:
|
||||
web:
|
||||
build: .
|
||||
image: webapp # → webapp:2025-10-30-223604.84d33bb
|
||||
```
|
||||
|
||||
If you specify a full **image name with tag** in the `image` attribute, Uncloud uses it as-is without modification.
|
||||
|
||||
```yaml title="compose.yaml"
|
||||
services:
|
||||
web:
|
||||
build: .
|
||||
image: webapp:1.2.3 # → webapp:1.2.3
|
||||
```
|
||||
|
||||
## Template functions
|
||||
|
||||
### `gitsha [length]`
|
||||
|
||||
Returns the Git commit SHA, optionally truncated to the specified length.
|
||||
|
||||
```yaml
|
||||
image: myapp:{{gitsha 7}} # → myapp:84d33bb
|
||||
image: myapp:{{gitsha}} # → myapp:84d33bbf0dbb37f96e7df6a5010aed7bab00b089
|
||||
```
|
||||
|
||||
Returns empty string if the working directory is not a Git repository.
|
||||
|
||||
### `gitdate "format" ["timezone"]`
|
||||
|
||||
Returns the current Git commit date/time formatted using [Go time layout format](#date-format-reference). The `timezone`
|
||||
parameter is optional and defaults to UTC.
|
||||
Use [IANA timezone names](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) like `America/New_York` or
|
||||
`Europe/London`.
|
||||
|
||||
```yaml
|
||||
image: myapp:{{gitdate "2006-01-02"}} # → myapp:2025-10-30
|
||||
image: myapp:{{gitdate "20060102-150405"}} # → myapp:20251030-223604
|
||||
image: myapp:{{gitdate "2006-01-02-150405" "Australia/Brisbane"}} # → myapp:2025-10-31-083604
|
||||
```
|
||||
|
||||
Returns empty string if the working directory is not a Git repository.
|
||||
|
||||
### `date "format" ["timezone"]`
|
||||
|
||||
Returns the current local date/time formatted using [Go time layout format](#date-format-reference). The `timezone`
|
||||
parameter is optional and defaults to UTC.
|
||||
Use [IANA timezone names](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) like `America/New_York` or
|
||||
`Europe/London`.
|
||||
|
||||
```yaml
|
||||
image: myapp:{{date "2006-01-02"}} # → myapp:2025-10-31
|
||||
image: myapp:{{date "20060102-150405"}} # → myapp:20251031-120651
|
||||
image: myapp:{{date "20060102-150405" "Local"}} # → myapp:20251031-220651
|
||||
```
|
||||
|
||||
### Date format reference
|
||||
|
||||
Go uses a reference time (`Mon Jan 2 15:04:05 MST 2006`) for formatting. Replace reference components with desired
|
||||
format:
|
||||
|
||||
| Component | Reference | Example |
|
||||
|-----------|-----------|-------------|
|
||||
| Year | `2006` | `2025` |
|
||||
| Month | `01` | `10` |
|
||||
| Day | `02` | `30` |
|
||||
| Hour | `15` | `22` (24hr) |
|
||||
| Minute | `04` | `36` |
|
||||
| Second | `05` | `04` |
|
||||
|
||||
**Common patterns:**
|
||||
|
||||
| Format | Pattern | Example |
|
||||
|------------------------|---------------------|---------------------|
|
||||
| ISO 8601 date | `2006-01-02` | `2025-10-30` |
|
||||
| Compact date | `20060102` | `20251030` |
|
||||
| Date with compact time | `2006-01-02-150405` | `2025-10-30-223604`
|
||||
|
||||
See Go [time.Format documentation](https://pkg.go.dev/time#Time.Format) for all formatting options.
|
||||
|
||||
## Template fields
|
||||
|
||||
Access metadata about your project, service, and Git state:
|
||||
|
||||
| Field | Type | Description | Example |
|
||||
|----------------|-----------|--------------------------------------------------------------------|-----------------------------|
|
||||
| `.Project` | string | Project name from `name` in Compose file or working directory name | `myapp` |
|
||||
| `.Service` | string | Service name | `web` |
|
||||
| `.Tag` | string | Pre-rendered default tag (without image name) | `2025-10-30-223604.84d33bb` |
|
||||
| `.Git.IsRepo` | bool | Whether working directory is a Git repository | `true` or `false` |
|
||||
| `.Git.IsDirty` | bool | Whether there are uncommitted changes | `true` or `false` |
|
||||
| `.Git.SHA` | string | Full SHA (40 characters) of the latest Git commit | `84d33bb1234567...` |
|
||||
| `.Git.Date` | time.Time | Git commit date/time (use `gitdate` function to format) | - |
|
||||
|
||||
## Environment variable interpolation
|
||||
|
||||
Combine templates with environment variable
|
||||
[interpolation](https://github.com/compose-spec/compose-spec/blob/main/spec.md#interpolation) using Bash-like syntax.
|
||||
The environment variables are expanded before rendering the template.
|
||||
|
||||
```yaml
|
||||
# CI build number from environment
|
||||
image: myapp:{{gitdate "20060102"}}.{{gitsha 7}}.${GITHUB_RUN_ID} # → myapp:20251030.84d33bb.1234
|
||||
|
||||
# With default value
|
||||
image: myapp:{{gitsha 7}}.${GITHUB_RUN_ID:-local} # GITHUB_RUN_ID not set → myapp:84d33bb.local
|
||||
```
|
||||
|
||||
## See also
|
||||
|
||||
- [Deploy an app](../4-guides/1-deployments/1-deploy-app.md): Deploy from source code or prebuilt images
|
||||
- [Compose Build Specification](https://github.com/compose-spec/compose-spec/blob/main/build.md)
|
||||
- [Compose Specification: image](https://github.com/compose-spec/compose-spec/blob/main/spec.md#image)
|
||||
- [Go template documentation](https://pkg.go.dev/text/template)
|
||||
- [Go Time.Format documentation](https://pkg.go.dev/time#Time.Format)
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
label: Compose features
|
||||
label: Compose file reference
|
||||
collapsed: true # keep the category closed by default
|
||||
link:
|
||||
type: generated-index
|
||||
@@ -6,7 +6,7 @@ A CLI tool for managing Uncloud resources such as machines, services, and volume
|
||||
|
||||
```
|
||||
--connect string Connect to a remote cluster machine without using the Uncloud configuration file. [$UNCLOUD_CONNECT]
|
||||
Format: [ssh://]user@host[:port] or tcp://host:port
|
||||
Format: [ssh://]user@host[:port], ssh+cli://user@host[:port], or tcp://host:port
|
||||
-h, --help help for uc
|
||||
--uncloud-config string Path to the Uncloud configuration file. [$UNCLOUD_CONFIG] (default "~/.config/uncloud/config.yaml")
|
||||
```
|
||||
@@ -18,6 +18,7 @@ A CLI tool for managing Uncloud resources such as machines, services, and volume
|
||||
* [uc ctx](uc_ctx.md) - Switch between different cluster contexts. Contains subcommands to manage contexts.
|
||||
* [uc deploy](uc_deploy.md) - Deploy services from a Compose file.
|
||||
* [uc dns](uc_dns.md) - Manage cluster domain in Uncloud DNS.
|
||||
* [uc exec](uc_exec.md) - Execute a command in a running service container
|
||||
* [uc image](uc_image.md) - Manage images on machines in the cluster.
|
||||
* [uc images](uc_images.md) - List images on machines in the cluster.
|
||||
* [uc inspect](uc_inspect.md) - Display detailed information on a service.
|
||||
|
||||
@@ -2,25 +2,64 @@
|
||||
|
||||
Build services from a Compose file.
|
||||
|
||||
## Synopsis
|
||||
|
||||
Build images for services from a Compose file using local Docker.
|
||||
|
||||
By default, built images remain on the local Docker host. Use --push to upload them
|
||||
to cluster machines or --push-registry to upload them to external registries.
|
||||
|
||||
```
|
||||
uc build [FLAGS] [SERVICE...] [flags]
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
```
|
||||
# Build all services that have a build section in compose.yaml.
|
||||
uc build
|
||||
|
||||
# Build specific services that have a build section.
|
||||
uc build web api
|
||||
|
||||
# Build services and push images to all cluster machines or service x-machines if specified.
|
||||
uc build --push
|
||||
|
||||
# Build services and push images to specific machines.
|
||||
uc build --push -m machine1,machine2
|
||||
|
||||
# Build services and push images to external registries (e.g., Docker Hub).
|
||||
uc build --push-registry
|
||||
|
||||
# Build services with build arguments, pull newer base images before building, and don't use cache.
|
||||
uc build --build-arg NODE_VERSION=24 --build-arg ENV=production --no-cache --pull
|
||||
```
|
||||
|
||||
## Options
|
||||
|
||||
```
|
||||
-f, --file strings One or more Compose files to build (default compose.yaml)
|
||||
--build-arg stringArray Set a build-time variable for services. Used in Dockerfiles that declare the variable with ARG.
|
||||
Can be specified multiple times. Format: --build-arg VAR=VALUE
|
||||
--check Check the build configuration for services without building them.
|
||||
-c, --context string Name of the cluster context. (default is the current context)
|
||||
--deps Also build services declared as dependencies of the selected services.
|
||||
-f, --file strings One or more Compose files to build. (default compose.yaml)
|
||||
-h, --help help for build
|
||||
-n, --no-cache Do not use cache when building images. (default false)
|
||||
-m, --machine strings Machine names or IDs to push the built images to (requires --push).
|
||||
Can be specified multiple times or as a comma-separated list. (default is all machines or x-machines)
|
||||
--no-cache Do not use cache when building images.
|
||||
-p, --profile strings One or more Compose profiles to enable.
|
||||
-P, --push Push built images to the registry after building. (default false)
|
||||
--pull Always attempt to pull newer versions of base images before building.
|
||||
--push Upload the built images to cluster machines after building.
|
||||
Use --machine to specify which machines. (default is all machines)
|
||||
--push-registry Upload the built images to external registries (e.g., Docker Hub) after building.
|
||||
```
|
||||
|
||||
## Options inherited from parent commands
|
||||
|
||||
```
|
||||
--connect string Connect to a remote cluster machine without using the Uncloud configuration file. [$UNCLOUD_CONNECT]
|
||||
Format: [ssh://]user@host[:port] or tcp://host:port
|
||||
Format: [ssh://]user@host[:port], ssh+cli://user@host[:port], or tcp://host:port
|
||||
--uncloud-config string Path to the Uncloud configuration file. [$UNCLOUD_CONFIG] (default "~/.config/uncloud/config.yaml")
|
||||
```
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ Manage Caddy reverse proxy service.
|
||||
|
||||
```
|
||||
--connect string Connect to a remote cluster machine without using the Uncloud configuration file. [$UNCLOUD_CONNECT]
|
||||
Format: [ssh://]user@host[:port] or tcp://host:port
|
||||
Format: [ssh://]user@host[:port], ssh+cli://user@host[:port], or tcp://host:port
|
||||
--uncloud-config string Path to the Uncloud configuration file. [$UNCLOUD_CONFIG] (default "~/.config/uncloud/config.yaml")
|
||||
```
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ uc caddy config [flags]
|
||||
|
||||
```
|
||||
--connect string Connect to a remote cluster machine without using the Uncloud configuration file. [$UNCLOUD_CONNECT]
|
||||
Format: [ssh://]user@host[:port] or tcp://host:port
|
||||
Format: [ssh://]user@host[:port], ssh+cli://user@host[:port], or tcp://host:port
|
||||
--uncloud-config string Path to the Uncloud configuration file. [$UNCLOUD_CONFIG] (default "~/.config/uncloud/config.yaml")
|
||||
```
|
||||
|
||||
|
||||
@@ -18,14 +18,14 @@ uc caddy deploy [flags]
|
||||
-c, --context string Name of the cluster context to deploy to. (default is the current context)
|
||||
-h, --help help for deploy
|
||||
--image string Caddy Docker image to deploy. (default caddy:LATEST_VERSION)
|
||||
-m, --machine strings Machine names to deploy to. Can be specified multiple times or as a comma-separated list of machine names. (default is all machines)
|
||||
-m, --machine strings Machine names or IDs to deploy to. Can be specified multiple times or as a comma-separated list. (default is all machines)
|
||||
```
|
||||
|
||||
## Options inherited from parent commands
|
||||
|
||||
```
|
||||
--connect string Connect to a remote cluster machine without using the Uncloud configuration file. [$UNCLOUD_CONNECT]
|
||||
Format: [ssh://]user@host[:port] or tcp://host:port
|
||||
Format: [ssh://]user@host[:port], ssh+cli://user@host[:port], or tcp://host:port
|
||||
--uncloud-config string Path to the Uncloud configuration file. [$UNCLOUD_CONFIG] (default "~/.config/uncloud/config.yaml")
|
||||
```
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ uc ctx [flags]
|
||||
|
||||
```
|
||||
--connect string Connect to a remote cluster machine without using the Uncloud configuration file. [$UNCLOUD_CONNECT]
|
||||
Format: [ssh://]user@host[:port] or tcp://host:port
|
||||
Format: [ssh://]user@host[:port], ssh+cli://user@host[:port], or tcp://host:port
|
||||
--uncloud-config string Path to the Uncloud configuration file. [$UNCLOUD_CONFIG] (default "~/.config/uncloud/config.yaml")
|
||||
```
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ uc ctx ls [flags]
|
||||
|
||||
```
|
||||
--connect string Connect to a remote cluster machine without using the Uncloud configuration file. [$UNCLOUD_CONNECT]
|
||||
Format: [ssh://]user@host[:port] or tcp://host:port
|
||||
Format: [ssh://]user@host[:port], ssh+cli://user@host[:port], or tcp://host:port
|
||||
--uncloud-config string Path to the Uncloud configuration file. [$UNCLOUD_CONFIG] (default "~/.config/uncloud/config.yaml")
|
||||
```
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ uc ctx use [CONTEXT] [flags]
|
||||
|
||||
```
|
||||
--connect string Connect to a remote cluster machine without using the Uncloud configuration file. [$UNCLOUD_CONNECT]
|
||||
Format: [ssh://]user@host[:port] or tcp://host:port
|
||||
Format: [ssh://]user@host[:port], ssh+cli://user@host[:port], or tcp://host:port
|
||||
--uncloud-config string Path to the Uncloud configuration file. [$UNCLOUD_CONFIG] (default "~/.config/uncloud/config.yaml")
|
||||
```
|
||||
|
||||
|
||||
@@ -9,10 +9,14 @@ uc deploy [FLAGS] [SERVICE...] [flags]
|
||||
## Options
|
||||
|
||||
```
|
||||
--build-arg stringArray Set a build-time variable for services. Used in Dockerfiles that declare the variable with ARG.
|
||||
Can be specified multiple times. Format: --build-arg VAR=VALUE
|
||||
--build-pull Always attempt to pull newer versions of base images before building service images.
|
||||
-c, --context string Name of the cluster context to deploy to (default is the current context)
|
||||
-f, --file strings One or more Compose files to deploy services from. (default compose.yaml)
|
||||
-h, --help help for deploy
|
||||
-n, --no-build Do not build images before deploying services. (default false)
|
||||
--no-build Do not build new images before deploying services.
|
||||
--no-cache Do not use cache when building images.
|
||||
-p, --profile strings One or more Compose profiles to enable.
|
||||
--recreate Recreate containers even if their configuration and image haven't changed.
|
||||
-y, --yes Auto-confirm deployment plan. Should be explicitly set when running non-interactively,
|
||||
@@ -23,7 +27,7 @@ uc deploy [FLAGS] [SERVICE...] [flags]
|
||||
|
||||
```
|
||||
--connect string Connect to a remote cluster machine without using the Uncloud configuration file. [$UNCLOUD_CONNECT]
|
||||
Format: [ssh://]user@host[:port] or tcp://host:port
|
||||
Format: [ssh://]user@host[:port], ssh+cli://user@host[:port], or tcp://host:port
|
||||
--uncloud-config string Path to the Uncloud configuration file. [$UNCLOUD_CONFIG] (default "~/.config/uncloud/config.yaml")
|
||||
```
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ DNS commands allow you to reserve or release a unique '\<id>.cluster.uncloud.run
|
||||
|
||||
```
|
||||
--connect string Connect to a remote cluster machine without using the Uncloud configuration file. [$UNCLOUD_CONNECT]
|
||||
Format: [ssh://]user@host[:port] or tcp://host:port
|
||||
Format: [ssh://]user@host[:port], ssh+cli://user@host[:port], or tcp://host:port
|
||||
--uncloud-config string Path to the Uncloud configuration file. [$UNCLOUD_CONFIG] (default "~/.config/uncloud/config.yaml")
|
||||
```
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ uc dns release [flags]
|
||||
|
||||
```
|
||||
--connect string Connect to a remote cluster machine without using the Uncloud configuration file. [$UNCLOUD_CONNECT]
|
||||
Format: [ssh://]user@host[:port] or tcp://host:port
|
||||
Format: [ssh://]user@host[:port], ssh+cli://user@host[:port], or tcp://host:port
|
||||
--uncloud-config string Path to the Uncloud configuration file. [$UNCLOUD_CONFIG] (default "~/.config/uncloud/config.yaml")
|
||||
```
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ uc dns reserve [flags]
|
||||
|
||||
```
|
||||
--connect string Connect to a remote cluster machine without using the Uncloud configuration file. [$UNCLOUD_CONNECT]
|
||||
Format: [ssh://]user@host[:port] or tcp://host:port
|
||||
Format: [ssh://]user@host[:port], ssh+cli://user@host[:port], or tcp://host:port
|
||||
--uncloud-config string Path to the Uncloud configuration file. [$UNCLOUD_CONFIG] (default "~/.config/uncloud/config.yaml")
|
||||
```
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ uc dns show [flags]
|
||||
|
||||
```
|
||||
--connect string Connect to a remote cluster machine without using the Uncloud configuration file. [$UNCLOUD_CONNECT]
|
||||
Format: [ssh://]user@host[:port] or tcp://host:port
|
||||
Format: [ssh://]user@host[:port], ssh+cli://user@host[:port], or tcp://host:port
|
||||
--uncloud-config string Path to the Uncloud configuration file. [$UNCLOUD_CONFIG] (default "~/.config/uncloud/config.yaml")
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
# uc exec
|
||||
|
||||
Execute a command in a running service container
|
||||
|
||||
## Synopsis
|
||||
|
||||
Execute a command (interactive shell by default) in a running container within a service.
|
||||
If the service has multiple replicas and no container ID is specified, the command will be executed in a random container.
|
||||
|
||||
|
||||
```
|
||||
uc exec [OPTIONS] SERVICE [COMMAND ARGS...] [flags]
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
```
|
||||
|
||||
# Start an interactive shell ("bash" or "sh" will be tried by default)
|
||||
uc exec web-service
|
||||
|
||||
# Start an interactive shell with explicit command
|
||||
uc exec web-service /bin/zsh
|
||||
|
||||
# List files in the specific container of the service; --container accepts full ID or a (unique) prefix
|
||||
uc exec --container d792e web-service ls -la
|
||||
|
||||
# Pipe input to a command inside the service container
|
||||
cat backup.sql | uc exec -T db-service psql -U postgres mydb
|
||||
|
||||
# Run a task in the background (detached mode)
|
||||
uc exec -d web-service /scripts/cleanup.sh
|
||||
```
|
||||
|
||||
## Options
|
||||
|
||||
```
|
||||
--container string ID of the container to exec into. Accepts full ID or a unique prefix (default is the random container of the service)
|
||||
-c, --context string Name of the cluster context. (default is the current context)
|
||||
-d, --detach Detached mode: run command in the background
|
||||
-h, --help help for exec
|
||||
-T, --no-tty Disable pseudo-TTY allocation. By default 'uc exec' allocates a TTY when connected to a terminal.
|
||||
```
|
||||
|
||||
## Options inherited from parent commands
|
||||
|
||||
```
|
||||
--connect string Connect to a remote cluster machine without using the Uncloud configuration file. [$UNCLOUD_CONNECT]
|
||||
Format: [ssh://]user@host[:port], ssh+cli://user@host[:port], or tcp://host:port
|
||||
--uncloud-config string Path to the Uncloud configuration file. [$UNCLOUD_CONFIG] (default "~/.config/uncloud/config.yaml")
|
||||
```
|
||||
|
||||
## See also
|
||||
|
||||
* [uc](uc.md) - A CLI tool for managing Uncloud resources such as machines, services, and volumes.
|
||||
|
||||
@@ -12,7 +12,7 @@ Manage images on machines in the cluster.
|
||||
|
||||
```
|
||||
--connect string Connect to a remote cluster machine without using the Uncloud configuration file. [$UNCLOUD_CONNECT]
|
||||
Format: [ssh://]user@host[:port] or tcp://host:port
|
||||
Format: [ssh://]user@host[:port], ssh+cli://user@host[:port], or tcp://host:port
|
||||
--uncloud-config string Path to the Uncloud configuration file. [$UNCLOUD_CONFIG] (default "~/.config/uncloud/config.yaml")
|
||||
```
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ uc image ls [REPO:[TAG]] [flags]
|
||||
|
||||
```
|
||||
--connect string Connect to a remote cluster machine without using the Uncloud configuration file. [$UNCLOUD_CONNECT]
|
||||
Format: [ssh://]user@host[:port] or tcp://host:port
|
||||
Format: [ssh://]user@host[:port], ssh+cli://user@host[:port], or tcp://host:port
|
||||
--uncloud-config string Path to the Uncloud configuration file. [$UNCLOUD_CONFIG] (default "~/.config/uncloud/config.yaml")
|
||||
```
|
||||
|
||||
|
||||
@@ -35,8 +35,7 @@ uc image push IMAGE [flags]
|
||||
```
|
||||
-c, --context string Name of the cluster context. (default is the current context)
|
||||
-h, --help help for push
|
||||
-m, --machine strings Machine names to push the image to. Can be specified multiple times or as a comma-separated list of machine names.
|
||||
Use 'all' to push to all machines. (default is connected machine)
|
||||
-m, --machine strings Machine names or IDs to push the image to. Can be specified multiple times or as a comma-separated list. (default is all machines)
|
||||
--platform string Push a specific platform of a multi-platform image (e.g., linux/amd64, linux/arm64).
|
||||
Local Docker must be configured to use containerd image store to support multi-platform images.
|
||||
```
|
||||
@@ -45,7 +44,7 @@ uc image push IMAGE [flags]
|
||||
|
||||
```
|
||||
--connect string Connect to a remote cluster machine without using the Uncloud configuration file. [$UNCLOUD_CONNECT]
|
||||
Format: [ssh://]user@host[:port] or tcp://host:port
|
||||
Format: [ssh://]user@host[:port], ssh+cli://user@host[:port], or tcp://host:port
|
||||
--uncloud-config string Path to the Uncloud configuration file. [$UNCLOUD_CONFIG] (default "~/.config/uncloud/config.yaml")
|
||||
```
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ uc images [IMAGE] [flags]
|
||||
|
||||
```
|
||||
--connect string Connect to a remote cluster machine without using the Uncloud configuration file. [$UNCLOUD_CONNECT]
|
||||
Format: [ssh://]user@host[:port] or tcp://host:port
|
||||
Format: [ssh://]user@host[:port], ssh+cli://user@host[:port], or tcp://host:port
|
||||
--uncloud-config string Path to the Uncloud configuration file. [$UNCLOUD_CONFIG] (default "~/.config/uncloud/config.yaml")
|
||||
```
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ uc inspect SERVICE [flags]
|
||||
|
||||
```
|
||||
--connect string Connect to a remote cluster machine without using the Uncloud configuration file. [$UNCLOUD_CONNECT]
|
||||
Format: [ssh://]user@host[:port] or tcp://host:port
|
||||
Format: [ssh://]user@host[:port], ssh+cli://user@host[:port], or tcp://host:port
|
||||
--uncloud-config string Path to the Uncloud configuration file. [$UNCLOUD_CONFIG] (default "~/.config/uncloud/config.yaml")
|
||||
```
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ uc ls [flags]
|
||||
|
||||
```
|
||||
--connect string Connect to a remote cluster machine without using the Uncloud configuration file. [$UNCLOUD_CONNECT]
|
||||
Format: [ssh://]user@host[:port] or tcp://host:port
|
||||
Format: [ssh://]user@host[:port], ssh+cli://user@host[:port], or tcp://host:port
|
||||
--uncloud-config string Path to the Uncloud configuration file. [$UNCLOUD_CONFIG] (default "~/.config/uncloud/config.yaml")
|
||||
```
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ Manage machines in an Uncloud cluster.
|
||||
|
||||
```
|
||||
--connect string Connect to a remote cluster machine without using the Uncloud configuration file. [$UNCLOUD_CONNECT]
|
||||
Format: [ssh://]user@host[:port] or tcp://host:port
|
||||
Format: [ssh://]user@host[:port], ssh+cli://user@host[:port], or tcp://host:port
|
||||
--uncloud-config string Path to the Uncloud configuration file. [$UNCLOUD_CONFIG] (default "~/.config/uncloud/config.yaml")
|
||||
```
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ uc machine add [USER@]HOST[:PORT] [flags]
|
||||
|
||||
```
|
||||
--connect string Connect to a remote cluster machine without using the Uncloud configuration file. [$UNCLOUD_CONNECT]
|
||||
Format: [ssh://]user@host[:port] or tcp://host:port
|
||||
Format: [ssh://]user@host[:port], ssh+cli://user@host[:port], or tcp://host:port
|
||||
--uncloud-config string Path to the Uncloud configuration file. [$UNCLOUD_CONFIG] (default "~/.config/uncloud/config.yaml")
|
||||
```
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ uc machine init [USER@HOST:PORT] [flags]
|
||||
|
||||
```
|
||||
--connect string Connect to a remote cluster machine without using the Uncloud configuration file. [$UNCLOUD_CONNECT]
|
||||
Format: [ssh://]user@host[:port] or tcp://host:port
|
||||
Format: [ssh://]user@host[:port], ssh+cli://user@host[:port], or tcp://host:port
|
||||
--uncloud-config string Path to the Uncloud configuration file. [$UNCLOUD_CONFIG] (default "~/.config/uncloud/config.yaml")
|
||||
```
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ uc machine ls [flags]
|
||||
|
||||
```
|
||||
--connect string Connect to a remote cluster machine without using the Uncloud configuration file. [$UNCLOUD_CONNECT]
|
||||
Format: [ssh://]user@host[:port] or tcp://host:port
|
||||
Format: [ssh://]user@host[:port], ssh+cli://user@host[:port], or tcp://host:port
|
||||
--uncloud-config string Path to the Uncloud configuration file. [$UNCLOUD_CONFIG] (default "~/.config/uncloud/config.yaml")
|
||||
```
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ uc machine rename OLD_NAME NEW_NAME [flags]
|
||||
|
||||
```
|
||||
--connect string Connect to a remote cluster machine without using the Uncloud configuration file. [$UNCLOUD_CONNECT]
|
||||
Format: [ssh://]user@host[:port] or tcp://host:port
|
||||
Format: [ssh://]user@host[:port], ssh+cli://user@host[:port], or tcp://host:port
|
||||
--uncloud-config string Path to the Uncloud configuration file. [$UNCLOUD_CONFIG] (default "~/.config/uncloud/config.yaml")
|
||||
```
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ uc machine rm MACHINE [flags]
|
||||
|
||||
```
|
||||
--connect string Connect to a remote cluster machine without using the Uncloud configuration file. [$UNCLOUD_CONNECT]
|
||||
Format: [ssh://]user@host[:port] or tcp://host:port
|
||||
Format: [ssh://]user@host[:port], ssh+cli://user@host[:port], or tcp://host:port
|
||||
--uncloud-config string Path to the Uncloud configuration file. [$UNCLOUD_CONFIG] (default "~/.config/uncloud/config.yaml")
|
||||
```
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ uc machine token [flags]
|
||||
|
||||
```
|
||||
--connect string Connect to a remote cluster machine without using the Uncloud configuration file. [$UNCLOUD_CONNECT]
|
||||
Format: [ssh://]user@host[:port] or tcp://host:port
|
||||
Format: [ssh://]user@host[:port], ssh+cli://user@host[:port], or tcp://host:port
|
||||
--uncloud-config string Path to the Uncloud configuration file. [$UNCLOUD_CONFIG] (default "~/.config/uncloud/config.yaml")
|
||||
```
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ uc machine update [flags]
|
||||
|
||||
```
|
||||
--connect string Connect to a remote cluster machine without using the Uncloud configuration file. [$UNCLOUD_CONNECT]
|
||||
Format: [ssh://]user@host[:port] or tcp://host:port
|
||||
Format: [ssh://]user@host[:port], ssh+cli://user@host[:port], or tcp://host:port
|
||||
--uncloud-config string Path to the Uncloud configuration file. [$UNCLOUD_CONFIG] (default "~/.config/uncloud/config.yaml")
|
||||
```
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user