diff --git a/cmd/uncloud/service/run.go b/cmd/uncloud/service/run.go index 01ff5eca..5c4ab2e8 100644 --- a/cmd/uncloud/service/run.go +++ b/cmd/uncloud/service/run.go @@ -93,15 +93,17 @@ func NewRunCommand(groupID string) *cobra.Command { "Give extended privileges to service containers. This is a security risk and should be used with caution.") cmd.Flags().StringSliceVarP(&opts.publish, "publish", "p", nil, "Publish a service port to make it accessible outside the cluster. Can be specified multiple times.\n"+ - "Format: [hostname:]container_port[/protocol] or [host_ip:]host_port:container_port[/protocol]@host\n"+ + "Format: [hostname:]container_port[/protocol] or [host_ip|host_prefix:]host_port:container_port[/protocol]@host\n"+ "Supported protocols: tcp, udp, http, https (default is tcp). If a hostname for http(s) port is not specified\n"+ "and a cluster domain is reserved, service-name.cluster-domain will be used as the hostname.\n"+ "Examples:\n"+ - " -p 8080/https Publish port 8080 as HTTPS via reverse proxy with default service-name.cluster-domain hostname\n"+ - " -p app.example.com:8080/https Publish port 8080 as HTTPS via reverse proxy with custom hostname\n"+ + " -p 8080/https Publish port 8080 as HTTPS via reverse proxy with default service-name.cluster-domain hostname\n"+ + " -p app.example.com:8080/https Publish port 8080 as HTTPS via reverse proxy with custom hostname\n"+ // TODO: add support for publishing L4 tcp/udp ports. //" -p 9000:8080 Publish port 8080 as TCP port 9000 via reverse proxy\n"+ - " -p 53:5353/udp@host Bind UDP port 5353 to host port 53") + " -p 53:5353/udp@host Bind UDP port 5353 to host port 53\n"+ + " -p 192.168.76.0/24:53:5353/udp@host Bind UDP port 5353 to host port 53 on every host IP address\n"+ + " contained in the prefix 192.168.76.0/24") cmd.Flags().StringVar(&opts.pull, "pull", api.PullPolicyMissing, fmt.Sprintf("Pull image from the registry before running service containers ('%s', '%s', '%s').", api.PullPolicyAlways, api.PullPolicyMissing, api.PullPolicyNever)) diff --git a/internal/machine/docker/interface.go b/internal/machine/docker/interface.go new file mode 100644 index 00000000..b8a5d2b3 --- /dev/null +++ b/internal/machine/docker/interface.go @@ -0,0 +1,34 @@ +package docker + +import ( + "fmt" + "net" + "net/netip" +) + +// addrOfPrefix checks the interfaces and returns the address of each interface that was contained in the prefix. +func addrOfPrefix(prefix netip.Prefix) ([]string, error) { + ifis, err := net.Interfaces() + if err != nil { + return nil, err + } + var addrs []string + for _, ifi := range ifis { + ifaddrs, _ := ifi.Addrs() + for _, addr := range ifaddrs { + ipnet, ok := addr.(*net.IPNet) + if !ok { + continue + } + nip, _ := netip.ParseAddr(ipnet.IP.String()) // round about way is needed to get ipv6 addrs, not mapped v4 in v6. + if prefix.Contains(nip) { + addrs = append(addrs, nip.String()) + } + } + } + if len(addrs) == 0 { + return nil, fmt.Errorf("no host addresses are contained in prefix '%s'", prefix) + } + + return addrs, nil +} diff --git a/internal/machine/docker/server.go b/internal/machine/docker/server.go index 813f15c1..e6e61e1c 100644 --- a/internal/machine/docker/server.go +++ b/internal/machine/docker/server.go @@ -620,7 +620,24 @@ func (s *Server) CreateServiceContainer( if p.HostIP.IsValid() { portBindings[port][0].HostIP = p.HostIP.String() } + + if p.HostPrefix.IsValid() { + addrs, err := addrOfPrefix(p.HostPrefix) + if err != nil { + return nil, err + } + // p.HostIP was not valid, so the first IP can be set in the above added PortBindings, the rest is + // just appended. + portBindings[port][0].HostIP = addrs[0] + for _, addr := range addrs[1:] { + portBindings[port] = append(portBindings[port], nat.PortBinding{ + HostPort: strconv.Itoa(int(p.PublishedPort)), + HostIP: addr, + }) + } + } } + hostConfig := &container.HostConfig{ CapAdd: spec.Container.CapAdd, CapDrop: spec.Container.CapDrop, diff --git a/pkg/api/port.go b/pkg/api/port.go index b552b88e..10c162c1 100644 --- a/pkg/api/port.go +++ b/pkg/api/port.go @@ -23,6 +23,9 @@ type PortSpec struct { Hostname string // HostIP is the host IP to bind the PublishedPort to. Only valid in host mode. HostIP netip.Addr + // HostPrefix is the host prefix to bind the PublishedPort to. Only valid in host mode. Either HostIP + // is set or HostPrefix + HostPrefix netip.Prefix // PublishedPort is the port number exposed outside the container. // In ingress mode, this is the load balancer port. In host mode, this is the port bound on the host. PublishedPort uint16 @@ -55,6 +58,9 @@ func (p *PortSpec) Validate() error { if p.HostIP.IsValid() { return fmt.Errorf("host IP cannot be specified in %s mode", PortModeIngress) } + if p.HostPrefix.IsValid() { + return fmt.Errorf("host prefix cannot be specified in %s mode", PortModeIngress) + } if p.Hostname != "" { if p.Protocol != ProtocolHTTP && p.Protocol != ProtocolHTTPS { return fmt.Errorf("hostname is only valid with '%s' or '%s' protocols", ProtocolHTTP, ProtocolHTTPS) @@ -64,6 +70,9 @@ func (p *PortSpec) Validate() error { } } case PortModeHost: + if p.HostIP.IsValid() && p.HostPrefix.IsValid() { + return fmt.Errorf("host IP and prefix cannot both be specified in %s mode", PortModeHost) + } if p.PublishedPort == 0 { return fmt.Errorf("published port is required in %s mode", PortModeHost) } @@ -111,6 +120,16 @@ func (p *PortSpec) String() (string, error) { parts = append(parts, p.HostIP.String()) } } + if p.HostPrefix.IsValid() { + if p.HostPrefix.Addr().Is6() { + // Enclose the IPv6 address part in square brackets to disambiguate its colons from the + // port separators, e.g. [2001:db8::]/64. + parts = append(parts, fmt.Sprintf("[%s]/%d", p.HostPrefix.Addr(), p.HostPrefix.Bits())) + } else { + parts = append(parts, p.HostPrefix.String()) + } + } + parts = append(parts, fmt.Sprint(p.PublishedPort)) parts = append(parts, fmt.Sprint(p.ContainerPort)) @@ -122,56 +141,70 @@ func (p *PortSpec) String() (string, error) { func ParsePortSpec(port string) (PortSpec, error) { spec := PortSpec{ - Protocol: ProtocolTCP, // Default protocol. - Mode: PortModeIngress, // Default mode. + Protocol: ProtocolTCP, // Default protocol. } - // Split off mode first. - parts := strings.Split(port, "@") - if len(parts) > 2 { + if strings.Count(port, "@") > 1 { return spec, fmt.Errorf("too many '@' symbols") } - if len(parts) == 2 { - if parts[1] != PortModeHost { - return spec, fmt.Errorf("invalid mode: '%s', only 'host' mode is supported", parts[1]) - } - spec.Mode = PortModeHost - } - port = parts[0] - // Parse protocol. - parts = strings.Split(port, "/") - if len(parts) > 2 { - return spec, fmt.Errorf("too many '/' symbols") - } - specifiedProtocol := "" - if len(parts) == 2 { - protocol := parts[1] - switch protocol { - case ProtocolHTTP, ProtocolHTTPS, ProtocolTCP, ProtocolUDP: - spec.Protocol = protocol - specifiedProtocol = protocol - default: - return spec, fmt.Errorf("unsupported protocol: '%s'", protocol) + parts := splitPortParts(port) + specifiedProtocol := "" // Save the set protocol for PortModeIngress to set the correct default later. + + mode := parts[len(parts)-1] + if i := strings.Index(mode, "@"); i > -1 { + spec.Mode = PortModeHost + + if mode[i:] != "@"+PortModeHost { + return spec, fmt.Errorf("invalid mode: '%s'", mode[i+1:]) + } + mode = mode[:i] // drop @host, leave PORT/udp PORT/tcp or PORT + if i := strings.Index(mode, "/"); i > -1 { + switch mode[i+1:] { + case ProtocolTCP: + case ProtocolUDP: + spec.Protocol = ProtocolUDP + default: + return spec, fmt.Errorf("unsupported protocol '%s' in host mode, only 'tcp' and 'udp' are supported", + mode[i+1:]) + } + + mode = mode[:i] // drop /udp or /tcp, leaving the port only + } + } else { + spec.Mode = PortModeIngress + + if i := strings.Index(mode, "/"); i > -1 { + switch mode[i+1:] { + case ProtocolTCP: + spec.Protocol = ProtocolTCP + case ProtocolUDP: + spec.Protocol = ProtocolUDP + case ProtocolHTTP: + spec.Protocol = ProtocolHTTP + case ProtocolHTTPS: + spec.Protocol = ProtocolHTTPS + default: + return spec, fmt.Errorf("unsupported protocol: '%s'", mode[i+1:]) + } + specifiedProtocol = mode[i+1:] + + mode = mode[:i] // drop /xxx, leaving the port only } } - port = parts[0] + + var err error + if spec.ContainerPort, err = parsePort(mode); err != nil { + return spec, fmt.Errorf("invalid container port '%s': %w", mode, err) + } // Parse hostname/host IP and ports. - parts = splitPortParts(port) - var err error - switch len(parts) { - case 1: // Just container port. - if spec.ContainerPort, err = parsePort(parts[0]); err != nil { - return spec, fmt.Errorf("invalid container port '%s': %w", parts[0], err) - } + case 1: + // Container port already done. case 2: // hostname:container_port or [load_balancer_port|host_port]:container_port - if spec.ContainerPort, err = parsePort(parts[1]); err != nil { - return spec, fmt.Errorf("invalid container port '%s': %w", parts[1], err) - } - + // Container port (parts[1]) already done if parts[0] == "" { return spec, fmt.Errorf("hostname or published port must be specified, format: " + "hostname:container_port or published_port:container_port") @@ -188,30 +221,32 @@ func ParsePortSpec(port string) (PortSpec, error) { } case 3: // hostname:load_balancer_port:container_port or host_ip:host_port:container_port - if spec.ContainerPort, err = parsePort(parts[2]); err != nil { - return spec, fmt.Errorf("invalid container port '%s': %w", parts[2], err) - } + // Container port (parts[2]) already done if spec.PublishedPort, err = parsePort(parts[1]); err != nil { return spec, fmt.Errorf("invalid published port '%s': %w", parts[1], err) } if spec.Mode == PortModeHost { - // In host mode, the first part must be IP. + // In host mode, the first part must be a host IP or prefix. ip := parts[0] - // Strip brackets from IPv6 address if present. + // Strip brackets from an IPv6 address if present. if strings.Contains(ip, ":") { - if !strings.HasPrefix(ip, "[") { + end := strings.Index(ip, "]") + if !strings.HasPrefix(ip, "[") || end < 0 { return spec, fmt.Errorf( "invalid host IP '%s': IPv6 address must be enclosed in square brackets", ip) } - if !strings.HasSuffix(ip, "]") { - return spec, fmt.Errorf("invalid host IP '%s': missing closing bracket", ip) - } - ip = ip[1 : len(ip)-1] + ip = ip[1:end] + ip[end+1:] } - if spec.HostIP, err = netip.ParseAddr(ip); err != nil { - return spec, fmt.Errorf("invalid host IP '%s': %w", parts[0], err) + if strings.Contains(ip, "/") { + if spec.HostPrefix, err = netip.ParsePrefix(ip); err != nil { + return spec, fmt.Errorf("invalid host prefix '%s': %w", parts[0], err) + } + } else { + if spec.HostIP, err = netip.ParseAddr(ip); err != nil { + return spec, fmt.Errorf("invalid host IP '%s': %w", parts[0], err) + } } } else { // Hostname may be empty. diff --git a/pkg/api/port_test.go b/pkg/api/port_test.go index 040f6d52..ced334b3 100644 --- a/pkg/api/port_test.go +++ b/pkg/api/port_test.go @@ -191,6 +191,16 @@ func TestPortSpec_Validate(t *testing.T) { }, wantErr: "host IP cannot be specified in ingress mode", }, + { + name: "host prefix in ingress mode", + spec: PortSpec{ + HostPrefix: netip.MustParsePrefix("127.0.0.1/8"), + ContainerPort: 8080, + Protocol: ProtocolTCP, + Mode: PortModeIngress, + }, + wantErr: "host prefix cannot be specified in ingress mode", + }, { name: "zero published port in host mode", spec: PortSpec{ @@ -231,6 +241,18 @@ func TestPortSpec_Validate(t *testing.T) { }, wantErr: "unsupported protocol 'https' in host mode", }, + { + name: "both host ip and prefix", + spec: PortSpec{ + HostIP: netip.MustParseAddr("127.0.0.1"), + HostPrefix: netip.MustParsePrefix("127.0.0.0/8"), + PublishedPort: 80, + ContainerPort: 8080, + Protocol: ProtocolTCP, + Mode: PortModeHost, + }, + wantErr: "host IP and prefix cannot both be specified in host mode", + }, } for _, tt := range tests { @@ -369,6 +391,17 @@ func TestPortSpec_String(t *testing.T) { }, expected: "127.0.0.1:80:8080/udp@host", }, + { + name: "host mode with IPv4 prefix", + spec: PortSpec{ + HostPrefix: netip.MustParsePrefix("127.0.0.1/8"), + PublishedPort: 80, + ContainerPort: 8080, + Protocol: ProtocolUDP, + Mode: PortModeHost, + }, + expected: "127.0.0.1/8:80:8080/udp@host", + }, { name: "host mode with IPv6", spec: PortSpec{ @@ -380,6 +413,17 @@ func TestPortSpec_String(t *testing.T) { }, expected: "[2001:db8::1234:5678]:80:8080/tcp@host", }, + { + name: "host mode with IPv6 prefix", + spec: PortSpec{ + HostPrefix: netip.MustParsePrefix("2001:db8::/64"), + PublishedPort: 80, + ContainerPort: 8080, + Protocol: ProtocolUDP, + Mode: PortModeHost, + }, + expected: "[2001:db8::]/64:80:8080/udp@host", + }, } for _, tt := range tests { @@ -553,6 +597,39 @@ func TestParsePortSpec(t *testing.T) { Mode: PortModeHost, }, }, + { + name: "host mode with prefix and protocol", + port: "192.168.76.0/24:80:8080/udp@host", + expected: PortSpec{ + HostPrefix: netip.MustParsePrefix("192.168.76.0/24"), + PublishedPort: 80, + ContainerPort: 8080, + Protocol: ProtocolUDP, + Mode: PortModeHost, + }, + }, + { + name: "host mode with prefix without protocol", + port: "192.168.76.0/24:80:8080@host", + expected: PortSpec{ + HostPrefix: netip.MustParsePrefix("192.168.76.0/24"), + PublishedPort: 80, + ContainerPort: 8080, + Protocol: ProtocolTCP, + Mode: PortModeHost, + }, + }, + { + name: "host mode with IPv6 prefix and protocol", + port: "[2001:db8::]/64:80:8080/udp@host", + expected: PortSpec{ + HostPrefix: netip.MustParsePrefix("2001:db8::/64"), + PublishedPort: 80, + ContainerPort: 8080, + Protocol: ProtocolUDP, + Mode: PortModeHost, + }, + }, // Error cases. { @@ -560,11 +637,31 @@ func TestParsePortSpec(t *testing.T) { port: "", wantErr: "invalid container port", }, + { + name: "slash", + port: "/", + wantErr: "unsupported protocol", + }, + { + name: "at", + port: "@", + wantErr: "invalid mode", + }, { name: "invalid container port", port: "invalid", wantErr: "invalid container port", }, + { + name: "no protocol", + port: "53/", + wantErr: "unsupported protocol", + }, + { + name: "no host modifier", + port: "53@", + wantErr: "invalid mode", + }, { name: "container port zero", port: "0", @@ -598,7 +695,7 @@ func TestParsePortSpec(t *testing.T) { { name: "multiple protocols", port: "8080/tcp/udp", - wantErr: "too many '/' symbols", + wantErr: "unsupported protocol: 'tcp/udp'", }, { name: "invalid protocol", @@ -671,6 +768,26 @@ func TestParsePortSpec(t *testing.T) { port: "app.example.com:invalid:8080@host", wantErr: "invalid published port", }, + { + name: "invalid prefix", + port: "192.168.76.0/45:53:5353/udp@host", + wantErr: "invalid host prefix", + }, + { + name: "valid prefix invalid protocol", + port: "192.168.76.0/24:53:5353/http@host", + wantErr: "unsupported protocol 'http' in host mode, only 'tcp' and 'udp' are supported", + }, + { + name: "valid prefix invalid protocol", + port: "192.168.76.0/24:53:5353/invalid@host", + wantErr: "unsupported protocol 'invalid' in host mode, only 'tcp' and 'udp' are supported", + }, + { + name: "valid prefix no protocol", + port: "192.168.76.0/24:53:5353/@host", + wantErr: "unsupported protocol '' in host mode, only 'tcp' and 'udp' are supported", + }, } for _, tt := range tests { diff --git a/pkg/client/compose/port.go b/pkg/client/compose/port.go index 26cb42a9..b1e219c4 100644 --- a/pkg/client/compose/port.go +++ b/pkg/client/compose/port.go @@ -112,10 +112,14 @@ func convertServicePortConfigToPortSpec(port types.ServicePortConfig) (api.PortS // Set host IP if specified if port.HostIP != "" { hostIP, err := netip.ParseAddr(port.HostIP) - if err != nil { - return spec, fmt.Errorf("invalid host IP %q: %w", port.HostIP, err) + if err == nil { + spec.HostIP = hostIP + } else { + spec.HostPrefix, err = netip.ParsePrefix(port.HostIP) + if err != nil { + return spec, fmt.Errorf("invalid host IP or prefix '%s': %w", port.HostIP, err) + } } - spec.HostIP = hostIP } // Validate the resulting spec diff --git a/pkg/client/compose/port_test.go b/pkg/client/compose/port_test.go index 4f7828ee..41849ddf 100644 --- a/pkg/client/compose/port_test.go +++ b/pkg/client/compose/port_test.go @@ -58,6 +58,15 @@ func TestConvertStandardPortsToPortSpecs(t *testing.T) { {ContainerPort: 8080, PublishedPort: 80, Protocol: "tcp", Mode: "host", HostIP: mustParseAddr("::1")}, }, }, + { + name: "host prefix", + ports: []types.ServicePortConfig{ + {Target: 8080, Published: "80", Protocol: "tcp", HostIP: "192.168.76.0/24", Mode: "host"}, + }, + expected: []api.PortSpec{ + {ContainerPort: 8080, PublishedPort: 80, Protocol: "tcp", Mode: "host", HostPrefix: netip.MustParsePrefix("192.168.76.0/24")}, + }, + }, } for _, tt := range tests { diff --git a/pkg/client/compose/service_test.go b/pkg/client/compose/service_test.go index 4f668f47..bd6c28d0 100644 --- a/pkg/client/compose/service_test.go +++ b/pkg/client/compose/service_test.go @@ -315,7 +315,7 @@ func TestServiceSpecFromCompose(t *testing.T) { return strings.Compare(a.Name, b.Name) }) - cmpOpts := cmp.Options{cmpopts.EquateEmpty(), cmpopts.EquateComparable(netip.Addr{})} + cmpOpts := cmp.Options{cmpopts.EquateEmpty(), cmpopts.EquateComparable(netip.Addr{}, netip.Prefix{})} assert.True(t, cmp.Equal(spec, expectedSpec, cmpOpts...), cmp.Diff(spec, expectedSpec, cmpOpts...)) } }) diff --git a/website/docs/3-concepts/2-ingress/2-publishing-services.md b/website/docs/3-concepts/2-ingress/2-publishing-services.md index 267cc431..46aa0366 100644 --- a/website/docs/3-concepts/2-ingress/2-publishing-services.md +++ b/website/docs/3-concepts/2-ingress/2-publishing-services.md @@ -45,20 +45,23 @@ extension: network interface(s). This is useful for non-HTTP services that need direct port access (bypasses Caddy): ``` -[host_ip:]host_port:container_port[/protocol]@host +[host_ip|host_prefix:]host_port:container_port[/protocol]@host ``` -- `host_ip` (optional): The IP address on the host to bind to. If omitted, binds to all interfaces. +- `host_ip` / `host_prefix` (optional): The IP address on the host to bind to. Or an IP prefix in + [CIDR](https://en.wikipedia.org/wiki/Classless_Inter-Domain_Routing) notation, which binds to every host IP address + that is contained in the prefix. If omitted, binds to all interfaces. - `host_port`: The port number on the host to bind to. - `container_port`: The port number within the container that's listening for traffic. - `protocol` (optional): `tcp` or `udp` (default: `tcp`) -| Port value | Description | -|------------------------------|--------------------------------------------------------------------------------------| -| `8000/http` | Publish port 8000 as HTTP via Caddy using hostname `.` | -| `app.example.com:8080/https` | Publish port 8080 as HTTPS via Caddy using hostname `app.example.com` | -| `127.0.0.1:5432:5432@host` | Bind TCP port 5432 to host port 5432 on loopback interface only | -| `53:5353/udp@host` | Bind UDP port 5353 to host port 53 on all network interfaces | +| Port value | Description | +|--------------------------------------|--------------------------------------------------------------------------------------| +| `8000/http` | Publish port 8000 as HTTP via Caddy using hostname `.` | +| `app.example.com:8080/https` | Publish port 8080 as HTTPS via Caddy using hostname `app.example.com` | +| `127.0.0.1:5432:5432@host` | Bind TCP port 5432 to host port 5432 on loopback interface only | +| `53:5353/udp@host` | Bind UDP port 5353 to host port 53 on all network interfaces | +| `192.168.76.0/24:5432:5432/tcp@host` | Bind TCP port 5432 to host port 5432 on every host IP contained in 192.168.76.0/24 | :::warning @@ -138,7 +141,7 @@ custom global configuration. The following functions and variables are available: | Template | Description | -|---------------------------------------|-----------------------------------------------------------------------------------------------| +| ------------------------------------- | --------------------------------------------------------------------------------------------- | | `{{upstreams [service-name] [port]}}` | A space-separated list of healthy container IPs for the current or specified service and port | | `{{.Name}}` | The name of the service the config belongs to | | `{{.Upstreams}}` | A map of all service names to their healthy container IPs | @@ -149,38 +152,49 @@ changes. **Examples:** 1. Current service upstreams, default port: + ```caddyfile reverse_proxy {{upstreams}} ``` + ↓ ```caddyfile reverse_proxy 10.210.1.3 10.210.2.5 ``` + 2. Current service upstreams, port 8000: + ```caddyfile reverse_proxy {{upstreams 8000}} ``` + ↓ ```caddyfile reverse_proxy 10.210.1.3:8000 10.210.2.5:8000 ``` + 3. Current service upstreams with `https` scheme: + ```caddyfile reverse_proxy {{- range $ip := index .Upstreams .Name}} https://{{$ip}}{{end}} ``` + ↓ ```caddyfile reverse_proxy https://10.210.1.3 https://10.210.2.5 ``` + 4. `api` service upstreams, port 9000: + ```caddyfile handle_path /api/* { reverse_proxy {{upstreams "api" 9000}} } ``` + ↓ ```caddyfile diff --git a/website/docs/9-cli-reference/uc_run.md b/website/docs/9-cli-reference/uc_run.md index c37d2050..3ca4d5c7 100644 --- a/website/docs/9-cli-reference/uc_run.md +++ b/website/docs/9-cli-reference/uc_run.md @@ -23,13 +23,15 @@ uc run IMAGE [COMMAND...] [flags] -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_port:container_port[/protocol]@host + 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 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 diff --git a/website/docs/9-cli-reference/uc_service_run.md b/website/docs/9-cli-reference/uc_service_run.md index f036c2e8..d3d73690 100644 --- a/website/docs/9-cli-reference/uc_service_run.md +++ b/website/docs/9-cli-reference/uc_service_run.md @@ -23,13 +23,15 @@ uc service run IMAGE [COMMAND...] [flags] -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_port:container_port[/protocol]@host + 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 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