mirror of
https://github.com/psviderski/uncloud.git
synced 2026-11-10 20:54:17 +00:00
feat(labels): add support for service and container labels in uc run and inspect commands
This commit is contained in:
@@ -3,6 +3,7 @@ package service
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"maps"
|
||||||
"slices"
|
"slices"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -58,6 +59,16 @@ func inspect(ctx context.Context, uncli *cli.CLI, opts inspectOptions) error {
|
|||||||
fmt.Printf("Service ID: %s\n", svc.ID)
|
fmt.Printf("Service ID: %s\n", svc.ID)
|
||||||
fmt.Printf("Name: %s\n", svc.Name)
|
fmt.Printf("Name: %s\n", svc.Name)
|
||||||
fmt.Printf("Mode: %s\n", svc.Mode)
|
fmt.Printf("Mode: %s\n", svc.Mode)
|
||||||
|
|
||||||
|
spec := svc.Spec()
|
||||||
|
if len(spec.Labels) > 0 {
|
||||||
|
fmt.Println("Labels:")
|
||||||
|
printLabels(spec.Labels)
|
||||||
|
}
|
||||||
|
if len(spec.Container.Labels) > 0 {
|
||||||
|
fmt.Println("Container labels:")
|
||||||
|
printLabels(spec.Container.Labels)
|
||||||
|
}
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
|
|
||||||
// Combine regular and hook containers.
|
// Combine regular and hook containers.
|
||||||
@@ -130,3 +141,10 @@ func inspect(ctx context.Context, uncli *cli.CLI, opts inspectOptions) error {
|
|||||||
lipgloss.Println(t)
|
lipgloss.Println(t)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// printLabels prints the labels sorted by key followed by a blank line.
|
||||||
|
func printLabels(labels map[string]string) {
|
||||||
|
for _, key := range slices.Sorted(maps.Keys(labels)) {
|
||||||
|
lipgloss.Println(" " + key + tui.Faint.Render("=") + labels[key])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+46
-4
@@ -21,13 +21,16 @@ import (
|
|||||||
type runOptions struct {
|
type runOptions struct {
|
||||||
caddyfile string
|
caddyfile string
|
||||||
command []string
|
command []string
|
||||||
|
containerLabels []string
|
||||||
cpu dockeropts.NanoCPUs
|
cpu dockeropts.NanoCPUs
|
||||||
entrypoint string
|
entrypoint string
|
||||||
entrypointChanged bool
|
entrypointChanged bool
|
||||||
env []string
|
env []string
|
||||||
image string
|
image string
|
||||||
|
labels []string
|
||||||
machines []string
|
machines []string
|
||||||
memory dockeropts.MemBytes
|
memory dockeropts.MemBytes
|
||||||
|
memoryReservation dockeropts.MemBytes
|
||||||
mode string
|
mode string
|
||||||
shmSize dockeropts.MemBytes
|
shmSize dockeropts.MemBytes
|
||||||
name string
|
name string
|
||||||
@@ -64,6 +67,9 @@ func NewRunCommand(groupID string) *cobra.Command {
|
|||||||
cmd.Flags().StringVar(&opts.caddyfile, "caddyfile", "",
|
cmd.Flags().StringVar(&opts.caddyfile, "caddyfile", "",
|
||||||
"Path to a custom Caddy config (Caddyfile) for the service. "+
|
"Path to a custom Caddy config (Caddyfile) for the service. "+
|
||||||
"Cannot be used together with non-@host published ports.")
|
"Cannot be used together with non-@host published ports.")
|
||||||
|
cmd.Flags().StringArrayVar(&opts.containerLabels, "container-label", nil,
|
||||||
|
"Set a Docker label on service containers. Can be specified multiple times.\n"+
|
||||||
|
"Format: key=value or just key for an empty value.")
|
||||||
cmd.Flags().VarP(&opts.cpu, "cpu", "",
|
cmd.Flags().VarP(&opts.cpu, "cpu", "",
|
||||||
"Maximum number of CPU cores a service container can use. Fractional values are allowed: "+
|
"Maximum number of CPU cores a service container can use. Fractional values are allowed: "+
|
||||||
"0.5 for half a core or 2.25 for two and a quarter cores.")
|
"0.5 for half a core or 2.25 for two and a quarter cores.")
|
||||||
@@ -72,6 +78,9 @@ func NewRunCommand(groupID string) *cobra.Command {
|
|||||||
cmd.Flags().StringSliceVarP(&opts.env, "env", "e", nil,
|
cmd.Flags().StringSliceVarP(&opts.env, "env", "e", nil,
|
||||||
"Set an environment variable for service containers. Can be specified multiple times.\n"+
|
"Set an environment variable for service containers. Can be specified multiple times.\n"+
|
||||||
"Format: VAR=value or just VAR to use the value from the local environment.")
|
"Format: VAR=value or just VAR to use the value from the local environment.")
|
||||||
|
cmd.Flags().StringArrayVarP(&opts.labels, "label", "l", nil,
|
||||||
|
"Set a service label. Service labels aren't applied to Docker containers, use --container-label for that.\n"+
|
||||||
|
"Can be specified multiple times. Format: key=value or just key for an empty value.")
|
||||||
cmd.Flags().StringVar(&opts.mode, "mode", api.ServiceModeReplicated,
|
cmd.Flags().StringVar(&opts.mode, "mode", api.ServiceModeReplicated,
|
||||||
fmt.Sprintf("Replication mode of the service: either '%s' (a specified number of containers across "+
|
fmt.Sprintf("Replication mode of the service: either '%s' (a specified number of containers across "+
|
||||||
"the machines) or '%s' (one container on every machine).",
|
"the machines) or '%s' (one container on every machine).",
|
||||||
@@ -83,6 +92,9 @@ func NewRunCommand(groupID string) *cobra.Command {
|
|||||||
"Maximum amount of memory a service container can use. Value is a positive integer with optional unit suffix (b, k, m, g).\n"+
|
"Maximum amount of memory a service container can use. Value is a positive integer with optional unit suffix (b, k, m, g).\n"+
|
||||||
"Default unit is bytes if no suffix specified.\n"+
|
"Default unit is bytes if no suffix specified.\n"+
|
||||||
"Examples: 1073741824, 1024m, 1g (all equal 1 gibibyte)")
|
"Examples: 1073741824, 1024m, 1g (all equal 1 gibibyte)")
|
||||||
|
cmd.Flags().Var(&opts.memoryReservation, "memory-reservation",
|
||||||
|
"Minimum amount of memory reserved for a service container. Value is a positive integer with optional unit suffix (b, k, m, g).\n"+
|
||||||
|
"Default unit is bytes if no suffix specified.")
|
||||||
cmd.Flags().VarP(&opts.shmSize, "shm-size", "",
|
cmd.Flags().VarP(&opts.shmSize, "shm-size", "",
|
||||||
"Maximum amount of shared memory (mounted at /dev/shm) a service container can use. Value is a positive integer\n"+
|
"Maximum amount of shared memory (mounted at /dev/shm) a service container can use. Value is a positive integer\n"+
|
||||||
"with optional unit suffix (b, k, m, g). Default unit is bytes if no suffix specified.\n"+
|
"with optional unit suffix (b, k, m, g). Default unit is bytes if no suffix specified.\n"+
|
||||||
@@ -192,6 +204,15 @@ func prepareServiceSpec(opts runOptions) (api.ServiceSpec, error) {
|
|||||||
return spec, err
|
return spec, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
labels, err := parseLabels(opts.labels)
|
||||||
|
if err != nil {
|
||||||
|
return spec, err
|
||||||
|
}
|
||||||
|
containerLabels, err := parseLabels(opts.containerLabels)
|
||||||
|
if err != nil {
|
||||||
|
return spec, err
|
||||||
|
}
|
||||||
|
|
||||||
switch opts.mode {
|
switch opts.mode {
|
||||||
case api.ServiceModeReplicated, api.ServiceModeGlobal:
|
case api.ServiceModeReplicated, api.ServiceModeGlobal:
|
||||||
default:
|
default:
|
||||||
@@ -232,17 +253,20 @@ func prepareServiceSpec(opts runOptions) (api.ServiceSpec, error) {
|
|||||||
Command: opts.command,
|
Command: opts.command,
|
||||||
Env: env,
|
Env: env,
|
||||||
Image: opts.image,
|
Image: opts.image,
|
||||||
|
Labels: containerLabels,
|
||||||
Privileged: opts.privileged,
|
Privileged: opts.privileged,
|
||||||
PullPolicy: opts.pull,
|
PullPolicy: opts.pull,
|
||||||
Resources: api.ContainerResources{
|
Resources: api.ContainerResources{
|
||||||
CPU: opts.cpu.Value(),
|
CPU: opts.cpu.Value(),
|
||||||
Memory: opts.memory.Value(),
|
Memory: opts.memory.Value(),
|
||||||
SharedMemory: opts.shmSize.Value(),
|
MemoryReservation: opts.memoryReservation.Value(),
|
||||||
Ulimits: ulimits,
|
SharedMemory: opts.shmSize.Value(),
|
||||||
|
Ulimits: ulimits,
|
||||||
},
|
},
|
||||||
User: opts.user,
|
User: opts.user,
|
||||||
VolumeMounts: mounts,
|
VolumeMounts: mounts,
|
||||||
},
|
},
|
||||||
|
Labels: labels,
|
||||||
Mode: opts.mode,
|
Mode: opts.mode,
|
||||||
Name: opts.name,
|
Name: opts.name,
|
||||||
Placement: placement,
|
Placement: placement,
|
||||||
@@ -301,6 +325,24 @@ func parseEnv(env []string) (api.EnvVars, error) {
|
|||||||
return envVars, nil
|
return envVars, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// parseLabels parses labels from the command line arguments in the format "key=value" or just "key" for an empty value.
|
||||||
|
func parseLabels(labels []string) (map[string]string, error) {
|
||||||
|
if len(labels) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
result := make(map[string]string, len(labels))
|
||||||
|
for _, label := range labels {
|
||||||
|
key, value, _ := strings.Cut(label, "=")
|
||||||
|
if strings.TrimSpace(key) == "" {
|
||||||
|
return nil, fmt.Errorf("invalid label '%s': key must not be empty", label)
|
||||||
|
}
|
||||||
|
result[key] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
// parseVolumeFlags parses volume flag values in Docker CLI format and returns VolumeSpecs and VolumeMounts.
|
// parseVolumeFlags parses volume flag values in Docker CLI format and returns VolumeSpecs and VolumeMounts.
|
||||||
// It handles both named volumes (volume_name:/container/path[:ro|volume-nocopy])
|
// It handles both named volumes (volume_name:/container/path[:ro|volume-nocopy])
|
||||||
// and bind mounts (/host/path:/container/path[:ro]).
|
// and bind mounts (/host/path:/container/path[:ro]).
|
||||||
|
|||||||
@@ -0,0 +1,423 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/docker/go-units"
|
||||||
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
// validRunOptions returns the minimal options that produce a valid service spec.
|
||||||
|
func validRunOptions() runOptions {
|
||||||
|
return runOptions{
|
||||||
|
image: "nginx:1.27",
|
||||||
|
name: "web",
|
||||||
|
mode: api.ServiceModeReplicated,
|
||||||
|
pull: api.PullPolicyMissing,
|
||||||
|
replicas: 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseLabels(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
labels, err := parseLabels(nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Nil(t, labels)
|
||||||
|
|
||||||
|
labels, err = parseLabels([]string{"tier=frontend", "empty", "list=a,b", "expr=a=b", "tier=backend"})
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, map[string]string{
|
||||||
|
"tier": "backend",
|
||||||
|
"empty": "",
|
||||||
|
"list": "a,b",
|
||||||
|
"expr": "a=b",
|
||||||
|
}, labels)
|
||||||
|
|
||||||
|
_, err = parseLabels([]string{"=value"})
|
||||||
|
require.ErrorContains(t, err, "key must not be empty")
|
||||||
|
|
||||||
|
_, err = parseLabels([]string{" \t =value"})
|
||||||
|
require.ErrorContains(t, err, "key must not be empty")
|
||||||
|
|
||||||
|
labels, err = parseLabels([]string{"key=value with spaces"})
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, map[string]string{"key": "value with spaces"}, labels)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPrepareServiceSpec(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
t.Run("minimal options", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
spec, err := prepareServiceSpec(validRunOptions())
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, "web", spec.Name)
|
||||||
|
assert.Equal(t, api.ServiceModeReplicated, spec.Mode)
|
||||||
|
assert.EqualValues(t, 1, spec.Replicas)
|
||||||
|
assert.Equal(t, "nginx:1.27", spec.Container.Image)
|
||||||
|
assert.Equal(t, api.PullPolicyMissing, spec.Container.PullPolicy)
|
||||||
|
assert.Nil(t, spec.Caddy)
|
||||||
|
assert.Nil(t, spec.Labels)
|
||||||
|
assert.Nil(t, spec.Container.Labels)
|
||||||
|
assert.Nil(t, spec.Container.Entrypoint)
|
||||||
|
assert.Nil(t, spec.Container.Resources.Ulimits)
|
||||||
|
assert.Nil(t, spec.Placement.Machines)
|
||||||
|
assert.Empty(t, spec.Container.Env)
|
||||||
|
assert.Empty(t, spec.Ports)
|
||||||
|
assert.Empty(t, spec.Volumes)
|
||||||
|
assert.Empty(t, spec.Container.VolumeMounts)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("all options", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
opts := validRunOptions()
|
||||||
|
opts.command = []string{"nginx", "-g", "daemon off;"}
|
||||||
|
opts.env = []string{"FOO=bar", "EMPTY=", "EXPR=a=b"}
|
||||||
|
opts.labels = []string{"tier=frontend"}
|
||||||
|
opts.containerLabels = []string{"logging=enabled"}
|
||||||
|
opts.machines = []string{"m1,m2", "m3"}
|
||||||
|
opts.privileged = true
|
||||||
|
opts.pull = api.PullPolicyAlways
|
||||||
|
opts.publish = []string{"app.example.com:8080/https", "53:5353/udp@host"}
|
||||||
|
opts.replicas = 3
|
||||||
|
opts.ulimits = []string{"nofile=1024:2048", "nproc=65535"}
|
||||||
|
opts.user = "1000:1000"
|
||||||
|
opts.volumes = []string{"data:/var/lib/data:ro"}
|
||||||
|
require.NoError(t, opts.cpu.Set("0.5"))
|
||||||
|
require.NoError(t, opts.memory.Set("128m"))
|
||||||
|
require.NoError(t, opts.memoryReservation.Set("32m"))
|
||||||
|
require.NoError(t, opts.shmSize.Set("64m"))
|
||||||
|
|
||||||
|
spec, err := prepareServiceSpec(opts)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, api.ServiceSpec{
|
||||||
|
Container: api.ContainerSpec{
|
||||||
|
Command: []string{"nginx", "-g", "daemon off;"},
|
||||||
|
Env: api.EnvVars{"FOO": "bar", "EMPTY": "", "EXPR": "a=b"},
|
||||||
|
Image: "nginx:1.27",
|
||||||
|
Labels: map[string]string{"logging": "enabled"},
|
||||||
|
Privileged: true,
|
||||||
|
PullPolicy: api.PullPolicyAlways,
|
||||||
|
Resources: api.ContainerResources{
|
||||||
|
CPU: 500 * api.MilliCore,
|
||||||
|
Memory: 128 * units.MiB,
|
||||||
|
MemoryReservation: 32 * units.MiB,
|
||||||
|
SharedMemory: 64 * units.MiB,
|
||||||
|
Ulimits: map[string]api.Ulimit{
|
||||||
|
"nofile": {Soft: 1024, Hard: 2048},
|
||||||
|
"nproc": {Soft: 65535, Hard: 65535},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
User: "1000:1000",
|
||||||
|
VolumeMounts: []api.VolumeMount{
|
||||||
|
{VolumeName: "data", ContainerPath: "/var/lib/data", ReadOnly: true},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Labels: map[string]string{"tier": "frontend"},
|
||||||
|
Mode: api.ServiceModeReplicated,
|
||||||
|
Name: "web",
|
||||||
|
Placement: api.Placement{Machines: []string{"m1", "m2", "m3"}},
|
||||||
|
Ports: []api.PortSpec{
|
||||||
|
{
|
||||||
|
Hostname: "app.example.com",
|
||||||
|
ContainerPort: 8080,
|
||||||
|
Protocol: api.ProtocolHTTPS,
|
||||||
|
Mode: api.PortModeIngress,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
PublishedPort: 53,
|
||||||
|
ContainerPort: 5353,
|
||||||
|
Protocol: api.ProtocolUDP,
|
||||||
|
Mode: api.PortModeHost,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Replicas: 3,
|
||||||
|
Volumes: []api.VolumeSpec{
|
||||||
|
{
|
||||||
|
Name: "data",
|
||||||
|
Type: api.VolumeTypeVolume,
|
||||||
|
VolumeOptions: &api.VolumeOptions{Name: "data"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, spec)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("generates a name when not specified", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
opts := validRunOptions()
|
||||||
|
opts.name = ""
|
||||||
|
opts.image = "bitnami/nginx:latest"
|
||||||
|
|
||||||
|
spec, err := prepareServiceSpec(opts)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Regexp(t, `^nginx-[a-z0-9]{4}$`, spec.Name)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("keeps the specified name", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
opts := validRunOptions()
|
||||||
|
opts.name = "custom"
|
||||||
|
|
||||||
|
spec, err := prepareServiceSpec(opts)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, "custom", spec.Name)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("global mode", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
opts := validRunOptions()
|
||||||
|
opts.mode = api.ServiceModeGlobal
|
||||||
|
|
||||||
|
spec, err := prepareServiceSpec(opts)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, api.ServiceModeGlobal, spec.Mode)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("entrypoint", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
opts := validRunOptions()
|
||||||
|
opts.entrypoint = "/bin/sh"
|
||||||
|
opts.entrypointChanged = true
|
||||||
|
spec, err := prepareServiceSpec(opts)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, []string{"/bin/sh"}, spec.Container.Entrypoint)
|
||||||
|
|
||||||
|
// An explicitly empty entrypoint resets the one from the image.
|
||||||
|
opts = validRunOptions()
|
||||||
|
opts.entrypointChanged = true
|
||||||
|
spec, err = prepareServiceSpec(opts)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, []string{""}, spec.Container.Entrypoint)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("env without a value is omitted when not set locally", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
opts := validRunOptions()
|
||||||
|
opts.env = []string{"UNCLOUD_TEST_ENV_THAT_IS_NOT_SET"}
|
||||||
|
|
||||||
|
spec, err := prepareServiceSpec(opts)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.NotContains(t, spec.Container.Env, "UNCLOUD_TEST_ENV_THAT_IS_NOT_SET")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("bind mount", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
opts := validRunOptions()
|
||||||
|
opts.volumes = []string{"/data/uploads:/app/uploads"}
|
||||||
|
|
||||||
|
spec, err := prepareServiceSpec(opts)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.Len(t, spec.Volumes, 1)
|
||||||
|
assert.Regexp(t, `^bind-[a-z0-9]{4}$`, spec.Volumes[0].Name)
|
||||||
|
assert.Equal(t, api.VolumeTypeBind, spec.Volumes[0].Type)
|
||||||
|
require.NotNil(t, spec.Volumes[0].BindOptions)
|
||||||
|
assert.Equal(t, "/data/uploads", spec.Volumes[0].BindOptions.HostPath)
|
||||||
|
assert.True(t, spec.Volumes[0].BindOptions.CreateHostPath)
|
||||||
|
|
||||||
|
require.Len(t, spec.Container.VolumeMounts, 1)
|
||||||
|
assert.Equal(t, spec.Volumes[0].Name, spec.Container.VolumeMounts[0].VolumeName)
|
||||||
|
assert.Equal(t, "/app/uploads", spec.Container.VolumeMounts[0].ContainerPath)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("named volume mounted twice creates one volume", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
opts := validRunOptions()
|
||||||
|
opts.volumes = []string{"data:/a", "data:/b"}
|
||||||
|
|
||||||
|
spec, err := prepareServiceSpec(opts)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Len(t, spec.Volumes, 1)
|
||||||
|
assert.Len(t, spec.Container.VolumeMounts, 2)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("caddyfile content is trimmed", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
path := filepath.Join(t.TempDir(), "Caddyfile")
|
||||||
|
require.NoError(t, os.WriteFile(path, []byte("\nexample.com {\n}\n\n"), 0o600))
|
||||||
|
|
||||||
|
opts := validRunOptions()
|
||||||
|
opts.caddyfile = path
|
||||||
|
|
||||||
|
spec, err := prepareServiceSpec(opts)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, spec.Caddy)
|
||||||
|
assert.Equal(t, "example.com {\n}", spec.Caddy.Config)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("empty caddyfile is ignored", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
path := filepath.Join(t.TempDir(), "Caddyfile")
|
||||||
|
require.NoError(t, os.WriteFile(path, []byte(" \n"), 0o600))
|
||||||
|
|
||||||
|
opts := validRunOptions()
|
||||||
|
opts.caddyfile = path
|
||||||
|
|
||||||
|
spec, err := prepareServiceSpec(opts)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Nil(t, spec.Caddy)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("caddyfile can be used with host ports", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
path := filepath.Join(t.TempDir(), "Caddyfile")
|
||||||
|
require.NoError(t, os.WriteFile(path, []byte("example.com {\n}"), 0o600))
|
||||||
|
|
||||||
|
opts := validRunOptions()
|
||||||
|
opts.caddyfile = path
|
||||||
|
opts.publish = []string{"53:5353/udp@host"}
|
||||||
|
|
||||||
|
_, err := prepareServiceSpec(opts)
|
||||||
|
require.NoError(t, err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPrepareServiceSpec_LocalEnv(t *testing.T) {
|
||||||
|
// Not parallel because t.Setenv changes the environment of the whole process.
|
||||||
|
t.Setenv("UNCLOUD_TEST_LOCAL_ENV", "from-local")
|
||||||
|
|
||||||
|
opts := validRunOptions()
|
||||||
|
opts.env = []string{"UNCLOUD_TEST_LOCAL_ENV"}
|
||||||
|
|
||||||
|
spec, err := prepareServiceSpec(opts)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, "from-local", spec.Container.Env["UNCLOUD_TEST_LOCAL_ENV"])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPrepareServiceSpec_Invalid(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
caddyfile := filepath.Join(t.TempDir(), "Caddyfile")
|
||||||
|
require.NoError(t, os.WriteFile(caddyfile, []byte("example.com {\n}"), 0o600))
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
modify func(opts *runOptions)
|
||||||
|
wantErr string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "invalid image",
|
||||||
|
modify: func(opts *runOptions) { opts.image = "INVALID IMAGE" },
|
||||||
|
wantErr: "invalid image",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid name",
|
||||||
|
modify: func(opts *runOptions) { opts.name = "Not_Valid" },
|
||||||
|
wantErr: "invalid service name",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid mode",
|
||||||
|
modify: func(opts *runOptions) { opts.mode = "unknown" },
|
||||||
|
wantErr: "invalid replication mode: 'unknown'",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid pull policy",
|
||||||
|
modify: func(opts *runOptions) { opts.pull = "sometimes" },
|
||||||
|
wantErr: "invalid pull policy: 'sometimes'",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid env",
|
||||||
|
modify: func(opts *runOptions) { opts.env = []string{"=value"} },
|
||||||
|
wantErr: "invalid environment variable: '=value'",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid port",
|
||||||
|
modify: func(opts *runOptions) { opts.publish = []string{"not-a-port"} },
|
||||||
|
wantErr: "invalid service port 'not-a-port'",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid ulimit",
|
||||||
|
modify: func(opts *runOptions) { opts.ulimits = []string{"nofile"} },
|
||||||
|
wantErr: "invalid ulimit 'nofile'",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "volume without a container path",
|
||||||
|
modify: func(opts *runOptions) { opts.volumes = []string{"data"} },
|
||||||
|
wantErr: "invalid volume mount 'data'",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "volume with a relative container path",
|
||||||
|
modify: func(opts *runOptions) { opts.volumes = []string{"data:relative"} },
|
||||||
|
wantErr: "must be absolute path",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "volume with an unknown option",
|
||||||
|
modify: func(opts *runOptions) { opts.volumes = []string{"data:/data:unknown"} },
|
||||||
|
wantErr: "invalid option: 'unknown'",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing caddyfile",
|
||||||
|
modify: func(opts *runOptions) { opts.caddyfile = filepath.Join(os.TempDir(), "uncloud-test-no-such-file") },
|
||||||
|
wantErr: "read Caddyfile",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "caddyfile with an ingress port",
|
||||||
|
modify: func(opts *runOptions) {
|
||||||
|
opts.caddyfile = caddyfile
|
||||||
|
opts.publish = []string{"app.example.com:8080/https"}
|
||||||
|
},
|
||||||
|
wantErr: "cannot be specified simultaneously",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "service label without a key",
|
||||||
|
modify: func(opts *runOptions) { opts.labels = []string{"=value"} },
|
||||||
|
wantErr: "key must not be empty",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "container label without a key",
|
||||||
|
modify: func(opts *runOptions) { opts.containerLabels = []string{"=value"} },
|
||||||
|
wantErr: "key must not be empty",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "service label with whitespace",
|
||||||
|
modify: func(opts *runOptions) { opts.labels = []string{"my label=value"} },
|
||||||
|
wantErr: "invalid service labels: label 'my label' must not contain whitespace",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "container label with whitespace",
|
||||||
|
modify: func(opts *runOptions) { opts.containerLabels = []string{"my label=value"} },
|
||||||
|
wantErr: "invalid container labels: label 'my label' must not contain whitespace",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "reserved service label",
|
||||||
|
modify: func(opts *runOptions) { opts.labels = []string{api.LabelManaged + "=true"} },
|
||||||
|
wantErr: "invalid service labels",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "reserved container label",
|
||||||
|
modify: func(opts *runOptions) { opts.containerLabels = []string{api.LabelManaged + "=true"} },
|
||||||
|
wantErr: "invalid container labels",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
opts := validRunOptions()
|
||||||
|
tt.modify(&opts)
|
||||||
|
|
||||||
|
_, err := prepareServiceSpec(opts)
|
||||||
|
require.ErrorContains(t, err, tt.wantErr)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
+6
-1
@@ -8,6 +8,7 @@ import (
|
|||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
"unicode"
|
||||||
|
|
||||||
mapset "github.com/deckarep/golang-set/v2"
|
mapset "github.com/deckarep/golang-set/v2"
|
||||||
"github.com/distribution/reference"
|
"github.com/distribution/reference"
|
||||||
@@ -443,12 +444,16 @@ func (s *ContainerSpec) Clone() ContainerSpec {
|
|||||||
return spec
|
return spec
|
||||||
}
|
}
|
||||||
|
|
||||||
// ValidateLabels checks that label keys are non-empty and don't use the reserved Uncloud prefixes.
|
// ValidateLabels checks that label keys are non-empty, don't contain whitespace, and don't use the reserved Uncloud
|
||||||
|
// prefixes.
|
||||||
func ValidateLabels(labels map[string]string) error {
|
func ValidateLabels(labels map[string]string) error {
|
||||||
for k := range labels {
|
for k := range labels {
|
||||||
if strings.TrimSpace(k) == "" {
|
if strings.TrimSpace(k) == "" {
|
||||||
return fmt.Errorf("label key must not be empty")
|
return fmt.Errorf("label key must not be empty")
|
||||||
}
|
}
|
||||||
|
if strings.ContainsFunc(k, unicode.IsSpace) {
|
||||||
|
return fmt.Errorf("label '%s' must not contain whitespace", k)
|
||||||
|
}
|
||||||
if strings.HasPrefix(k, LabelReservedPrefix) || strings.HasPrefix(k, LabelDaemonReservedPrefix) {
|
if strings.HasPrefix(k, LabelReservedPrefix) || strings.HasPrefix(k, LabelDaemonReservedPrefix) {
|
||||||
return fmt.Errorf("label '%s' cannot use the reserved prefix '%s' or '%s'",
|
return fmt.Errorf("label '%s' cannot use the reserved prefix '%s' or '%s'",
|
||||||
k, LabelReservedPrefix, LabelDaemonReservedPrefix)
|
k, LabelReservedPrefix, LabelDaemonReservedPrefix)
|
||||||
|
|||||||
@@ -74,7 +74,14 @@ func TestServiceSpec_Validate_Labels(t *testing.T) {
|
|||||||
{name: "nil labels"},
|
{name: "nil labels"},
|
||||||
{name: "valid labels", labels: map[string]string{"com.example.team": "web", "tier": ""}},
|
{name: "valid labels", labels: map[string]string{"com.example.team": "web", "tier": ""}},
|
||||||
{name: "prefix without dot", labels: map[string]string{"uncloudfoo": "bar"}},
|
{name: "prefix without dot", labels: map[string]string{"uncloudfoo": "bar"}},
|
||||||
|
{name: "whitespace in value", labels: map[string]string{"tier": "value with spaces"}},
|
||||||
{name: "empty key", labels: map[string]string{"": "bar"}, wantErr: "must not be empty"},
|
{name: "empty key", labels: map[string]string{"": "bar"}, wantErr: "must not be empty"},
|
||||||
|
{name: "only whitespace in key", labels: map[string]string{" ": "bar"}, wantErr: "must not be empty"},
|
||||||
|
{name: "space in key", labels: map[string]string{"my label": "bar"}, wantErr: "must not contain whitespace"},
|
||||||
|
{name: "leading space in key", labels: map[string]string{" tier": "bar"}, wantErr: "must not contain whitespace"},
|
||||||
|
{name: "trailing space in key", labels: map[string]string{"tier ": "bar"}, wantErr: "must not contain whitespace"},
|
||||||
|
{name: "tab in key", labels: map[string]string{"my\tlabel": "bar"}, wantErr: "must not contain whitespace"},
|
||||||
|
{name: "newline in key", labels: map[string]string{"my\nlabel": "bar"}, wantErr: "must not contain whitespace"},
|
||||||
{name: "reserved prefix", labels: map[string]string{LabelManaged: ""}, wantErr: "reserved prefix"},
|
{name: "reserved prefix", labels: map[string]string{LabelManaged: ""}, wantErr: "reserved prefix"},
|
||||||
{name: "reserved daemon prefix", labels: map[string]string{"uncloudd.foo": "bar"}, wantErr: "reserved prefix"},
|
{name: "reserved daemon prefix", labels: map[string]string{"uncloudd.foo": "bar"}, wantErr: "reserved prefix"},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,49 +9,55 @@ uc run IMAGE [COMMAND...] [flags]
|
|||||||
## Options
|
## Options
|
||||||
|
|
||||||
```
|
```
|
||||||
--caddyfile string Path to a custom Caddy config (Caddyfile) for the service. Cannot be used together with non-@host published ports.
|
--caddyfile string Path to a custom Caddy config (Caddyfile) for the service. Cannot be used together with non-@host published ports.
|
||||||
--cpu decimal Maximum number of CPU cores a service container can use. Fractional values are allowed: 0.5 for half a core or 2.25 for two and a quarter cores.
|
--container-label stringArray Set a Docker label on service containers. Can be specified multiple times.
|
||||||
--entrypoint string Overwrite the default ENTRYPOINT of the image. Pass an empty string "" to reset it.
|
Format: key=value or just key for an empty value.
|
||||||
-e, --env strings Set an environment variable for service containers. Can be specified multiple times.
|
--cpu decimal Maximum number of CPU cores a service container can use. Fractional values are allowed: 0.5 for half a core or 2.25 for two and a quarter cores.
|
||||||
Format: VAR=value or just VAR to use the value from the local environment.
|
--entrypoint string Overwrite the default ENTRYPOINT of the image. Pass an empty string "" to reset it.
|
||||||
-h, --help help for run
|
-e, --env strings Set an environment variable for service containers. Can be specified multiple times.
|
||||||
-m, --machine strings Placement constraint by machine names, limiting which machines the service can run on. Can be specified multiple times or as a comma-separated list of machine names. (default is any suitable machine)
|
Format: VAR=value or just VAR to use the value from the local environment.
|
||||||
--memory bytes Maximum amount of memory a service container can use. Value is a positive integer with optional unit suffix (b, k, m, g).
|
-h, --help help for run
|
||||||
Default unit is bytes if no suffix specified.
|
-l, --label stringArray Set a service label. Service labels aren't applied to Docker containers, use --container-label for that.
|
||||||
Examples: 1073741824, 1024m, 1g (all equal 1 gibibyte)
|
Can be specified multiple times. Format: key=value or just key for an empty value.
|
||||||
--mode string Replication mode of the service: either 'replicated' (a specified number of containers across the machines) or 'global' (one container on every machine). (default "replicated")
|
-m, --machine strings Placement constraint by machine names, limiting which machines the service can run on. Can be specified multiple times or as a comma-separated list of machine names. (default is any suitable machine)
|
||||||
-n, --name string Assign a name to the service. A random name is generated if not specified.
|
--memory bytes Maximum amount of memory a service container can use. Value is a positive integer with optional unit suffix (b, k, m, g).
|
||||||
--privileged Give extended privileges to service containers. This is a security risk and should be used with caution.
|
Default unit is bytes if no suffix specified.
|
||||||
-p, --publish strings Publish a service port to make it accessible outside the cluster. Can be specified multiple times.
|
Examples: 1073741824, 1024m, 1g (all equal 1 gibibyte)
|
||||||
Format: [hostname:]container_port[/protocol] or [host_ip|host_prefix:]host_port:container_port[/protocol]@host
|
--memory-reservation bytes Minimum amount of memory reserved for a service container. Value is a positive integer with optional unit suffix (b, k, m, g).
|
||||||
Supported protocols: tcp, udp, http, https (default is tcp). If a hostname for http(s) port is not specified
|
Default unit is bytes if no suffix specified.
|
||||||
and a cluster domain is reserved, service-name.cluster-domain will be used as the hostname.
|
--mode string Replication mode of the service: either 'replicated' (a specified number of containers across the machines) or 'global' (one container on every machine). (default "replicated")
|
||||||
Examples:
|
-n, --name string Assign a name to the service. A random name is generated if not specified.
|
||||||
-p 8080/https Publish port 8080 as HTTPS via reverse proxy with default service-name.cluster-domain hostname
|
--privileged Give extended privileges to service containers. This is a security risk and should be used with caution.
|
||||||
-p app.example.com:8080/https Publish port 8080 as HTTPS via reverse proxy with custom hostname
|
-p, --publish strings Publish a service port to make it accessible outside the cluster. Can be specified multiple times.
|
||||||
-p 53:5353/udp@host Bind UDP port 5353 to host port 53
|
Format: [hostname:]container_port[/protocol] or [host_ip|host_prefix:]host_port:container_port[/protocol]@host
|
||||||
-p 192.168.76.0/24:53:5353/udp@host Bind UDP port 5353 to host port 53 on every host IP address
|
Supported protocols: tcp, udp, http, https (default is tcp). If a hostname for http(s) port is not specified
|
||||||
contained in the prefix 192.168.76.0/24
|
and a cluster domain is reserved, service-name.cluster-domain will be used as the hostname.
|
||||||
--pull string Pull image from the registry before running service containers ('always', 'missing', 'never'). (default "missing")
|
Examples:
|
||||||
--replicas uint Number of containers to run for the service. Only valid for a replicated service. (default 1)
|
-p 8080/https Publish port 8080 as HTTPS via reverse proxy with default service-name.cluster-domain hostname
|
||||||
--shm-size bytes Maximum amount of shared memory (mounted at /dev/shm) a service container can use. Value is a positive integer
|
-p app.example.com:8080/https Publish port 8080 as HTTPS via reverse proxy with custom hostname
|
||||||
with optional unit suffix (b, k, m, g). Default unit is bytes if no suffix specified.
|
-p 53:5353/udp@host Bind UDP port 5353 to host port 53
|
||||||
Examples: 1073741824, 1024m, 1g (all equal 1 gibibyte)
|
-p 192.168.76.0/24:53:5353/udp@host Bind UDP port 5353 to host port 53 on every host IP address
|
||||||
--ulimit strings Set resource limits for service containers. Can be specified multiple times.
|
contained in the prefix 192.168.76.0/24
|
||||||
Format: type=soft_limit[:hard_limit]. If hard limit is not specified, soft limit is used for both.
|
--pull string Pull image from the registry before running service containers ('always', 'missing', 'never'). (default "missing")
|
||||||
Examples:
|
--replicas uint Number of containers to run for the service. Only valid for a replicated service. (default 1)
|
||||||
--ulimit nofile=1024:2048 Set soft limit to 1024 and hard limit to 2048 for number of open files
|
--shm-size bytes Maximum amount of shared memory (mounted at /dev/shm) a service container can use. Value is a positive integer
|
||||||
--ulimit nproc=65535 Set both soft and hard limits to 65535 for number of processes
|
with optional unit suffix (b, k, m, g). Default unit is bytes if no suffix specified.
|
||||||
-u, --user string User name or UID and optionally group name or GID used for running the command inside service containers.
|
Examples: 1073741824, 1024m, 1g (all equal 1 gibibyte)
|
||||||
Format: USER[:GROUP] or UID[:GID]. If not specified, the user is set to the default user of the image.
|
--ulimit strings Set resource limits for service containers. Can be specified multiple times.
|
||||||
-v, --volume strings Mount a data volume or host path into service containers. Service containers will be scheduled on the machine(s) where
|
Format: type=soft_limit[:hard_limit]. If hard limit is not specified, soft limit is used for both.
|
||||||
the volume is located. Can be specified multiple times.
|
Examples:
|
||||||
Format: volume_name:/container/path[:ro|volume-nocopy] or /host/path:/container/path[:ro]
|
--ulimit nofile=1024:2048 Set soft limit to 1024 and hard limit to 2048 for number of open files
|
||||||
Host and container paths support runtime templates such as {{.Container.Name}}.
|
--ulimit nproc=65535 Set both soft and hard limits to 65535 for number of processes
|
||||||
Examples:
|
-u, --user string User name or UID and optionally group name or GID used for running the command inside service containers.
|
||||||
-v postgres-data:/var/lib/postgresql/data Mount volume 'postgres-data' to /var/lib/postgresql/data in container
|
Format: USER[:GROUP] or UID[:GID]. If not specified, the user is set to the default user of the image.
|
||||||
-v /data/uploads:/app/uploads Bind mount /data/uploads host directory to /app/uploads in container
|
-v, --volume strings Mount a data volume or host path into service containers. Service containers will be scheduled on the machine(s) where
|
||||||
-v /host/path:/container/path:ro Bind mount a host directory or file as read-only
|
the volume is located. Can be specified multiple times.
|
||||||
|
Format: volume_name:/container/path[:ro|volume-nocopy] or /host/path:/container/path[:ro]
|
||||||
|
Host and container paths support runtime templates such as {{.Container.Name}}.
|
||||||
|
Examples:
|
||||||
|
-v postgres-data:/var/lib/postgresql/data Mount volume 'postgres-data' to /var/lib/postgresql/data in container
|
||||||
|
-v /data/uploads:/app/uploads Bind mount /data/uploads host directory to /app/uploads in container
|
||||||
|
-v /host/path:/container/path:ro Bind mount a host directory or file as read-only
|
||||||
```
|
```
|
||||||
|
|
||||||
## Options inherited from parent commands
|
## Options inherited from parent commands
|
||||||
|
|||||||
@@ -9,49 +9,55 @@ uc service run IMAGE [COMMAND...] [flags]
|
|||||||
## Options
|
## Options
|
||||||
|
|
||||||
```
|
```
|
||||||
--caddyfile string Path to a custom Caddy config (Caddyfile) for the service. Cannot be used together with non-@host published ports.
|
--caddyfile string Path to a custom Caddy config (Caddyfile) for the service. Cannot be used together with non-@host published ports.
|
||||||
--cpu decimal Maximum number of CPU cores a service container can use. Fractional values are allowed: 0.5 for half a core or 2.25 for two and a quarter cores.
|
--container-label stringArray Set a Docker label on service containers. Can be specified multiple times.
|
||||||
--entrypoint string Overwrite the default ENTRYPOINT of the image. Pass an empty string "" to reset it.
|
Format: key=value or just key for an empty value.
|
||||||
-e, --env strings Set an environment variable for service containers. Can be specified multiple times.
|
--cpu decimal Maximum number of CPU cores a service container can use. Fractional values are allowed: 0.5 for half a core or 2.25 for two and a quarter cores.
|
||||||
Format: VAR=value or just VAR to use the value from the local environment.
|
--entrypoint string Overwrite the default ENTRYPOINT of the image. Pass an empty string "" to reset it.
|
||||||
-h, --help help for run
|
-e, --env strings Set an environment variable for service containers. Can be specified multiple times.
|
||||||
-m, --machine strings Placement constraint by machine names, limiting which machines the service can run on. Can be specified multiple times or as a comma-separated list of machine names. (default is any suitable machine)
|
Format: VAR=value or just VAR to use the value from the local environment.
|
||||||
--memory bytes Maximum amount of memory a service container can use. Value is a positive integer with optional unit suffix (b, k, m, g).
|
-h, --help help for run
|
||||||
Default unit is bytes if no suffix specified.
|
-l, --label stringArray Set a service label. Service labels aren't applied to Docker containers, use --container-label for that.
|
||||||
Examples: 1073741824, 1024m, 1g (all equal 1 gibibyte)
|
Can be specified multiple times. Format: key=value or just key for an empty value.
|
||||||
--mode string Replication mode of the service: either 'replicated' (a specified number of containers across the machines) or 'global' (one container on every machine). (default "replicated")
|
-m, --machine strings Placement constraint by machine names, limiting which machines the service can run on. Can be specified multiple times or as a comma-separated list of machine names. (default is any suitable machine)
|
||||||
-n, --name string Assign a name to the service. A random name is generated if not specified.
|
--memory bytes Maximum amount of memory a service container can use. Value is a positive integer with optional unit suffix (b, k, m, g).
|
||||||
--privileged Give extended privileges to service containers. This is a security risk and should be used with caution.
|
Default unit is bytes if no suffix specified.
|
||||||
-p, --publish strings Publish a service port to make it accessible outside the cluster. Can be specified multiple times.
|
Examples: 1073741824, 1024m, 1g (all equal 1 gibibyte)
|
||||||
Format: [hostname:]container_port[/protocol] or [host_ip|host_prefix:]host_port:container_port[/protocol]@host
|
--memory-reservation bytes Minimum amount of memory reserved for a service container. Value is a positive integer with optional unit suffix (b, k, m, g).
|
||||||
Supported protocols: tcp, udp, http, https (default is tcp). If a hostname for http(s) port is not specified
|
Default unit is bytes if no suffix specified.
|
||||||
and a cluster domain is reserved, service-name.cluster-domain will be used as the hostname.
|
--mode string Replication mode of the service: either 'replicated' (a specified number of containers across the machines) or 'global' (one container on every machine). (default "replicated")
|
||||||
Examples:
|
-n, --name string Assign a name to the service. A random name is generated if not specified.
|
||||||
-p 8080/https Publish port 8080 as HTTPS via reverse proxy with default service-name.cluster-domain hostname
|
--privileged Give extended privileges to service containers. This is a security risk and should be used with caution.
|
||||||
-p app.example.com:8080/https Publish port 8080 as HTTPS via reverse proxy with custom hostname
|
-p, --publish strings Publish a service port to make it accessible outside the cluster. Can be specified multiple times.
|
||||||
-p 53:5353/udp@host Bind UDP port 5353 to host port 53
|
Format: [hostname:]container_port[/protocol] or [host_ip|host_prefix:]host_port:container_port[/protocol]@host
|
||||||
-p 192.168.76.0/24:53:5353/udp@host Bind UDP port 5353 to host port 53 on every host IP address
|
Supported protocols: tcp, udp, http, https (default is tcp). If a hostname for http(s) port is not specified
|
||||||
contained in the prefix 192.168.76.0/24
|
and a cluster domain is reserved, service-name.cluster-domain will be used as the hostname.
|
||||||
--pull string Pull image from the registry before running service containers ('always', 'missing', 'never'). (default "missing")
|
Examples:
|
||||||
--replicas uint Number of containers to run for the service. Only valid for a replicated service. (default 1)
|
-p 8080/https Publish port 8080 as HTTPS via reverse proxy with default service-name.cluster-domain hostname
|
||||||
--shm-size bytes Maximum amount of shared memory (mounted at /dev/shm) a service container can use. Value is a positive integer
|
-p app.example.com:8080/https Publish port 8080 as HTTPS via reverse proxy with custom hostname
|
||||||
with optional unit suffix (b, k, m, g). Default unit is bytes if no suffix specified.
|
-p 53:5353/udp@host Bind UDP port 5353 to host port 53
|
||||||
Examples: 1073741824, 1024m, 1g (all equal 1 gibibyte)
|
-p 192.168.76.0/24:53:5353/udp@host Bind UDP port 5353 to host port 53 on every host IP address
|
||||||
--ulimit strings Set resource limits for service containers. Can be specified multiple times.
|
contained in the prefix 192.168.76.0/24
|
||||||
Format: type=soft_limit[:hard_limit]. If hard limit is not specified, soft limit is used for both.
|
--pull string Pull image from the registry before running service containers ('always', 'missing', 'never'). (default "missing")
|
||||||
Examples:
|
--replicas uint Number of containers to run for the service. Only valid for a replicated service. (default 1)
|
||||||
--ulimit nofile=1024:2048 Set soft limit to 1024 and hard limit to 2048 for number of open files
|
--shm-size bytes Maximum amount of shared memory (mounted at /dev/shm) a service container can use. Value is a positive integer
|
||||||
--ulimit nproc=65535 Set both soft and hard limits to 65535 for number of processes
|
with optional unit suffix (b, k, m, g). Default unit is bytes if no suffix specified.
|
||||||
-u, --user string User name or UID and optionally group name or GID used for running the command inside service containers.
|
Examples: 1073741824, 1024m, 1g (all equal 1 gibibyte)
|
||||||
Format: USER[:GROUP] or UID[:GID]. If not specified, the user is set to the default user of the image.
|
--ulimit strings Set resource limits for service containers. Can be specified multiple times.
|
||||||
-v, --volume strings Mount a data volume or host path into service containers. Service containers will be scheduled on the machine(s) where
|
Format: type=soft_limit[:hard_limit]. If hard limit is not specified, soft limit is used for both.
|
||||||
the volume is located. Can be specified multiple times.
|
Examples:
|
||||||
Format: volume_name:/container/path[:ro|volume-nocopy] or /host/path:/container/path[:ro]
|
--ulimit nofile=1024:2048 Set soft limit to 1024 and hard limit to 2048 for number of open files
|
||||||
Host and container paths support runtime templates such as {{.Container.Name}}.
|
--ulimit nproc=65535 Set both soft and hard limits to 65535 for number of processes
|
||||||
Examples:
|
-u, --user string User name or UID and optionally group name or GID used for running the command inside service containers.
|
||||||
-v postgres-data:/var/lib/postgresql/data Mount volume 'postgres-data' to /var/lib/postgresql/data in container
|
Format: USER[:GROUP] or UID[:GID]. If not specified, the user is set to the default user of the image.
|
||||||
-v /data/uploads:/app/uploads Bind mount /data/uploads host directory to /app/uploads in container
|
-v, --volume strings Mount a data volume or host path into service containers. Service containers will be scheduled on the machine(s) where
|
||||||
-v /host/path:/container/path:ro Bind mount a host directory or file as read-only
|
the volume is located. Can be specified multiple times.
|
||||||
|
Format: volume_name:/container/path[:ro|volume-nocopy] or /host/path:/container/path[:ro]
|
||||||
|
Host and container paths support runtime templates such as {{.Container.Name}}.
|
||||||
|
Examples:
|
||||||
|
-v postgres-data:/var/lib/postgresql/data Mount volume 'postgres-data' to /var/lib/postgresql/data in container
|
||||||
|
-v /data/uploads:/app/uploads Bind mount /data/uploads host directory to /app/uploads in container
|
||||||
|
-v /host/path:/container/path:ro Bind mount a host directory or file as read-only
|
||||||
```
|
```
|
||||||
|
|
||||||
## Options inherited from parent commands
|
## Options inherited from parent commands
|
||||||
|
|||||||
Reference in New Issue
Block a user