diff --git a/cmd/uc/service/inspect.go b/cmd/uc/service/inspect.go index c7500c9a..b7ee143e 100644 --- a/cmd/uc/service/inspect.go +++ b/cmd/uc/service/inspect.go @@ -3,6 +3,7 @@ package service import ( "context" "fmt" + "maps" "slices" "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("Name: %s\n", svc.Name) 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() // Combine regular and hook containers. @@ -130,3 +141,10 @@ func inspect(ctx context.Context, uncli *cli.CLI, opts inspectOptions) error { lipgloss.Println(t) 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]) + } +} diff --git a/cmd/uc/service/run.go b/cmd/uc/service/run.go index e7427f76..7fb1f69a 100644 --- a/cmd/uc/service/run.go +++ b/cmd/uc/service/run.go @@ -21,13 +21,16 @@ import ( type runOptions struct { caddyfile string command []string + containerLabels []string cpu dockeropts.NanoCPUs entrypoint string entrypointChanged bool env []string image string + labels []string machines []string memory dockeropts.MemBytes + memoryReservation dockeropts.MemBytes mode string shmSize dockeropts.MemBytes name string @@ -64,6 +67,9 @@ func NewRunCommand(groupID string) *cobra.Command { cmd.Flags().StringVar(&opts.caddyfile, "caddyfile", "", "Path to a custom Caddy config (Caddyfile) for the service. "+ "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", "", "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.") @@ -72,6 +78,9 @@ func NewRunCommand(groupID string) *cobra.Command { cmd.Flags().StringSliceVarP(&opts.env, "env", "e", nil, "Set an environment variable for service containers. Can be specified multiple times.\n"+ "Format: VAR=value or just VAR to use the value from the local environment.") + cmd.Flags().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, fmt.Sprintf("Replication mode of the service: either '%s' (a specified number of containers across "+ "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"+ "Default unit is bytes if no suffix specified.\n"+ "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", "", "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"+ @@ -192,6 +204,15 @@ func prepareServiceSpec(opts runOptions) (api.ServiceSpec, error) { 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 { case api.ServiceModeReplicated, api.ServiceModeGlobal: default: @@ -232,17 +253,20 @@ func prepareServiceSpec(opts runOptions) (api.ServiceSpec, error) { Command: opts.command, Env: env, Image: opts.image, + Labels: containerLabels, Privileged: opts.privileged, PullPolicy: opts.pull, Resources: api.ContainerResources{ - CPU: opts.cpu.Value(), - Memory: opts.memory.Value(), - SharedMemory: opts.shmSize.Value(), - Ulimits: ulimits, + CPU: opts.cpu.Value(), + Memory: opts.memory.Value(), + MemoryReservation: opts.memoryReservation.Value(), + SharedMemory: opts.shmSize.Value(), + Ulimits: ulimits, }, User: opts.user, VolumeMounts: mounts, }, + Labels: labels, Mode: opts.mode, Name: opts.name, Placement: placement, @@ -301,6 +325,24 @@ func parseEnv(env []string) (api.EnvVars, error) { 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. // It handles both named volumes (volume_name:/container/path[:ro|volume-nocopy]) // and bind mounts (/host/path:/container/path[:ro]). diff --git a/cmd/uc/service/run_test.go b/cmd/uc/service/run_test.go new file mode 100644 index 00000000..e0daaaaf --- /dev/null +++ b/cmd/uc/service/run_test.go @@ -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) + }) + } +} diff --git a/pkg/api/service.go b/pkg/api/service.go index 4bd8ac0d..671683b1 100644 --- a/pkg/api/service.go +++ b/pkg/api/service.go @@ -8,6 +8,7 @@ import ( "slices" "strings" "time" + "unicode" mapset "github.com/deckarep/golang-set/v2" "github.com/distribution/reference" @@ -443,12 +444,16 @@ func (s *ContainerSpec) Clone() ContainerSpec { 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 { for k := range labels { if strings.TrimSpace(k) == "" { 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) { return fmt.Errorf("label '%s' cannot use the reserved prefix '%s' or '%s'", k, LabelReservedPrefix, LabelDaemonReservedPrefix) diff --git a/pkg/api/service_test.go b/pkg/api/service_test.go index 0870393a..8bd3fbe8 100644 --- a/pkg/api/service_test.go +++ b/pkg/api/service_test.go @@ -74,7 +74,14 @@ func TestServiceSpec_Validate_Labels(t *testing.T) { {name: "nil labels"}, {name: "valid labels", labels: map[string]string{"com.example.team": "web", "tier": ""}}, {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: "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 daemon prefix", labels: map[string]string{"uncloudd.foo": "bar"}, wantErr: "reserved prefix"}, } diff --git a/website/docs/9-cli-reference/uc_run.md b/website/docs/9-cli-reference/uc_run.md index c28a7fc3..fa472d39 100644 --- a/website/docs/9-cli-reference/uc_run.md +++ b/website/docs/9-cli-reference/uc_run.md @@ -9,49 +9,55 @@ uc run IMAGE [COMMAND...] [flags] ## Options ``` - --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. - --entrypoint string Overwrite the default ENTRYPOINT of the image. Pass an empty string "" to reset it. - -e, --env strings Set an environment variable for service containers. Can be specified multiple times. - Format: VAR=value or just VAR to use the value from the local environment. - -h, --help help for run - -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) - --memory bytes Maximum amount of memory a service container can use. Value is a positive integer with optional unit suffix (b, k, m, g). - Default unit is bytes if no suffix specified. - Examples: 1073741824, 1024m, 1g (all equal 1 gibibyte) - --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") - -n, --name string Assign a name to the service. A random name is generated if not specified. - --privileged Give extended privileges to service containers. This is a security risk and should be used with caution. - -p, --publish strings Publish a service port to make it accessible outside the cluster. Can be specified multiple times. - Format: [hostname:]container_port[/protocol] or [host_ip|host_prefix:]host_port:container_port[/protocol]@host - Supported protocols: tcp, udp, http, https (default is tcp). If a hostname for http(s) port is not specified - and a cluster domain is reserved, service-name.cluster-domain will be used as the hostname. - Examples: - -p 8080/https Publish port 8080 as HTTPS via reverse proxy with default service-name.cluster-domain hostname - -p app.example.com:8080/https Publish port 8080 as HTTPS via reverse proxy with custom hostname - -p 53:5353/udp@host Bind UDP port 5353 to host port 53 - -p 192.168.76.0/24:53:5353/udp@host Bind UDP port 5353 to host port 53 on every host IP address - contained in the prefix 192.168.76.0/24 - --pull string Pull image from the registry before running service containers ('always', 'missing', 'never'). (default "missing") - --replicas uint Number of containers to run for the service. Only valid for a replicated service. (default 1) - --shm-size bytes Maximum amount of shared memory (mounted at /dev/shm) a service container can use. Value is a positive integer - with optional unit suffix (b, k, m, g). Default unit is bytes if no suffix specified. - Examples: 1073741824, 1024m, 1g (all equal 1 gibibyte) - --ulimit strings Set resource limits for service containers. Can be specified multiple times. - Format: type=soft_limit[:hard_limit]. If hard limit is not specified, soft limit is used for both. - Examples: - --ulimit nofile=1024:2048 Set soft limit to 1024 and hard limit to 2048 for number of open files - --ulimit nproc=65535 Set both soft and hard limits to 65535 for number of processes - -u, --user string User name or UID and optionally group name or GID used for running the command inside service containers. - Format: USER[:GROUP] or UID[:GID]. If not specified, the user is set to the default user of the image. - -v, --volume strings Mount a data volume or host path into service containers. Service containers will be scheduled on the machine(s) where - 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 + --caddyfile string Path to a custom Caddy config (Caddyfile) for the service. Cannot be used together with non-@host published ports. + --container-label stringArray Set a Docker label on service containers. Can be specified multiple times. + Format: key=value or just key for an empty value. + --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. + --entrypoint string Overwrite the default ENTRYPOINT of the image. Pass an empty string "" to reset it. + -e, --env strings Set an environment variable for service containers. Can be specified multiple times. + Format: VAR=value or just VAR to use the value from the local environment. + -h, --help help for run + -l, --label stringArray Set a service label. Service labels aren't applied to Docker containers, use --container-label for that. + Can be specified multiple times. Format: key=value or just key for an empty value. + -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) + --memory bytes Maximum amount of memory a service container can use. Value is a positive integer with optional unit suffix (b, k, m, g). + Default unit is bytes if no suffix specified. + Examples: 1073741824, 1024m, 1g (all equal 1 gibibyte) + --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). + Default unit is bytes if no suffix specified. + --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") + -n, --name string Assign a name to the service. A random name is generated if not specified. + --privileged Give extended privileges to service containers. This is a security risk and should be used with caution. + -p, --publish strings Publish a service port to make it accessible outside the cluster. Can be specified multiple times. + Format: [hostname:]container_port[/protocol] or [host_ip|host_prefix:]host_port:container_port[/protocol]@host + Supported protocols: tcp, udp, http, https (default is tcp). If a hostname for http(s) port is not specified + and a cluster domain is reserved, service-name.cluster-domain will be used as the hostname. + Examples: + -p 8080/https Publish port 8080 as HTTPS via reverse proxy with default service-name.cluster-domain hostname + -p app.example.com:8080/https Publish port 8080 as HTTPS via reverse proxy with custom hostname + -p 53:5353/udp@host Bind UDP port 5353 to host port 53 + -p 192.168.76.0/24:53:5353/udp@host Bind UDP port 5353 to host port 53 on every host IP address + contained in the prefix 192.168.76.0/24 + --pull string Pull image from the registry before running service containers ('always', 'missing', 'never'). (default "missing") + --replicas uint Number of containers to run for the service. Only valid for a replicated service. (default 1) + --shm-size bytes Maximum amount of shared memory (mounted at /dev/shm) a service container can use. Value is a positive integer + with optional unit suffix (b, k, m, g). Default unit is bytes if no suffix specified. + Examples: 1073741824, 1024m, 1g (all equal 1 gibibyte) + --ulimit strings Set resource limits for service containers. Can be specified multiple times. + Format: type=soft_limit[:hard_limit]. If hard limit is not specified, soft limit is used for both. + Examples: + --ulimit nofile=1024:2048 Set soft limit to 1024 and hard limit to 2048 for number of open files + --ulimit nproc=65535 Set both soft and hard limits to 65535 for number of processes + -u, --user string User name or UID and optionally group name or GID used for running the command inside service containers. + Format: USER[:GROUP] or UID[:GID]. If not specified, the user is set to the default user of the image. + -v, --volume strings Mount a data volume or host path into service containers. Service containers will be scheduled on the machine(s) where + 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 diff --git a/website/docs/9-cli-reference/uc_service_run.md b/website/docs/9-cli-reference/uc_service_run.md index 446959f7..06e90e89 100644 --- a/website/docs/9-cli-reference/uc_service_run.md +++ b/website/docs/9-cli-reference/uc_service_run.md @@ -9,49 +9,55 @@ uc service run IMAGE [COMMAND...] [flags] ## Options ``` - --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. - --entrypoint string Overwrite the default ENTRYPOINT of the image. Pass an empty string "" to reset it. - -e, --env strings Set an environment variable for service containers. Can be specified multiple times. - Format: VAR=value or just VAR to use the value from the local environment. - -h, --help help for run - -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) - --memory bytes Maximum amount of memory a service container can use. Value is a positive integer with optional unit suffix (b, k, m, g). - Default unit is bytes if no suffix specified. - Examples: 1073741824, 1024m, 1g (all equal 1 gibibyte) - --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") - -n, --name string Assign a name to the service. A random name is generated if not specified. - --privileged Give extended privileges to service containers. This is a security risk and should be used with caution. - -p, --publish strings Publish a service port to make it accessible outside the cluster. Can be specified multiple times. - Format: [hostname:]container_port[/protocol] or [host_ip|host_prefix:]host_port:container_port[/protocol]@host - Supported protocols: tcp, udp, http, https (default is tcp). If a hostname for http(s) port is not specified - and a cluster domain is reserved, service-name.cluster-domain will be used as the hostname. - Examples: - -p 8080/https Publish port 8080 as HTTPS via reverse proxy with default service-name.cluster-domain hostname - -p app.example.com:8080/https Publish port 8080 as HTTPS via reverse proxy with custom hostname - -p 53:5353/udp@host Bind UDP port 5353 to host port 53 - -p 192.168.76.0/24:53:5353/udp@host Bind UDP port 5353 to host port 53 on every host IP address - contained in the prefix 192.168.76.0/24 - --pull string Pull image from the registry before running service containers ('always', 'missing', 'never'). (default "missing") - --replicas uint Number of containers to run for the service. Only valid for a replicated service. (default 1) - --shm-size bytes Maximum amount of shared memory (mounted at /dev/shm) a service container can use. Value is a positive integer - with optional unit suffix (b, k, m, g). Default unit is bytes if no suffix specified. - Examples: 1073741824, 1024m, 1g (all equal 1 gibibyte) - --ulimit strings Set resource limits for service containers. Can be specified multiple times. - Format: type=soft_limit[:hard_limit]. If hard limit is not specified, soft limit is used for both. - Examples: - --ulimit nofile=1024:2048 Set soft limit to 1024 and hard limit to 2048 for number of open files - --ulimit nproc=65535 Set both soft and hard limits to 65535 for number of processes - -u, --user string User name or UID and optionally group name or GID used for running the command inside service containers. - Format: USER[:GROUP] or UID[:GID]. If not specified, the user is set to the default user of the image. - -v, --volume strings Mount a data volume or host path into service containers. Service containers will be scheduled on the machine(s) where - 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 + --caddyfile string Path to a custom Caddy config (Caddyfile) for the service. Cannot be used together with non-@host published ports. + --container-label stringArray Set a Docker label on service containers. Can be specified multiple times. + Format: key=value or just key for an empty value. + --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. + --entrypoint string Overwrite the default ENTRYPOINT of the image. Pass an empty string "" to reset it. + -e, --env strings Set an environment variable for service containers. Can be specified multiple times. + Format: VAR=value or just VAR to use the value from the local environment. + -h, --help help for run + -l, --label stringArray Set a service label. Service labels aren't applied to Docker containers, use --container-label for that. + Can be specified multiple times. Format: key=value or just key for an empty value. + -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) + --memory bytes Maximum amount of memory a service container can use. Value is a positive integer with optional unit suffix (b, k, m, g). + Default unit is bytes if no suffix specified. + Examples: 1073741824, 1024m, 1g (all equal 1 gibibyte) + --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). + Default unit is bytes if no suffix specified. + --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") + -n, --name string Assign a name to the service. A random name is generated if not specified. + --privileged Give extended privileges to service containers. This is a security risk and should be used with caution. + -p, --publish strings Publish a service port to make it accessible outside the cluster. Can be specified multiple times. + Format: [hostname:]container_port[/protocol] or [host_ip|host_prefix:]host_port:container_port[/protocol]@host + Supported protocols: tcp, udp, http, https (default is tcp). If a hostname for http(s) port is not specified + and a cluster domain is reserved, service-name.cluster-domain will be used as the hostname. + Examples: + -p 8080/https Publish port 8080 as HTTPS via reverse proxy with default service-name.cluster-domain hostname + -p app.example.com:8080/https Publish port 8080 as HTTPS via reverse proxy with custom hostname + -p 53:5353/udp@host Bind UDP port 5353 to host port 53 + -p 192.168.76.0/24:53:5353/udp@host Bind UDP port 5353 to host port 53 on every host IP address + contained in the prefix 192.168.76.0/24 + --pull string Pull image from the registry before running service containers ('always', 'missing', 'never'). (default "missing") + --replicas uint Number of containers to run for the service. Only valid for a replicated service. (default 1) + --shm-size bytes Maximum amount of shared memory (mounted at /dev/shm) a service container can use. Value is a positive integer + with optional unit suffix (b, k, m, g). Default unit is bytes if no suffix specified. + Examples: 1073741824, 1024m, 1g (all equal 1 gibibyte) + --ulimit strings Set resource limits for service containers. Can be specified multiple times. + Format: type=soft_limit[:hard_limit]. If hard limit is not specified, soft limit is used for both. + Examples: + --ulimit nofile=1024:2048 Set soft limit to 1024 and hard limit to 2048 for number of open files + --ulimit nproc=65535 Set both soft and hard limits to 65535 for number of processes + -u, --user string User name or UID and optionally group name or GID used for running the command inside service containers. + Format: USER[:GROUP] or UID[:GID]. If not specified, the user is set to the default user of the image. + -v, --volume strings Mount a data volume or host path into service containers. Service containers will be scheduled on the machine(s) where + 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