From ba9838bfb984c71759b34a85d422985ac08b9c23 Mon Sep 17 00:00:00 2001 From: Jon Edvald Date: Mon, 7 Jun 2021 13:33:48 +0000 Subject: [PATCH] feat(container): allow specifying preferred local port for port-forwards This adds a `localPort` field to the `services[].ports` field, to make it easier to control the local port used for port forwards. This is handy for a variety of reasons, one of which is to have a predictable port for debuggers. --- core/src/plugins/container/config.ts | 10 +++++ .../plugins/kubernetes/container/status.ts | 1 + core/src/proxy.ts | 37 +++++++++++++------ core/src/types/service.ts | 2 + docs/reference/commands.md | 21 +++++++++++ docs/reference/module-types/container.md | 24 ++++++++++++ .../reference/module-types/maven-container.md | 24 ++++++++++++ 7 files changed, 107 insertions(+), 12 deletions(-) diff --git a/core/src/plugins/container/config.ts b/core/src/plugins/container/config.ts index 7c7425c22d..b6ca291a44 100644 --- a/core/src/plugins/container/config.ts +++ b/core/src/plugins/container/config.ts @@ -59,6 +59,7 @@ export interface ServicePortSpec { name: string protocol: ServicePortProtocol containerPort: number + localPort?: number // Defaults to containerPort servicePort: number hostPort?: number @@ -387,6 +388,15 @@ export const portSchema = () => The service port maps to the container port: \`servicePort:80 -> containerPort:8080 -> process:8080\``), + localPort: joi + .number() + .example(10080) + .description( + dedent` + Specify a preferred local port to attach to when creating a port-forward to the service port. If this port is + busy, a warning will be shown and an alternative port chosen. + ` + ), servicePort: joi .number() .default((context) => context.containerPort) diff --git a/core/src/plugins/kubernetes/container/status.ts b/core/src/plugins/kubernetes/container/status.ts index cabc4e89f8..53640142cf 100644 --- a/core/src/plugins/kubernetes/container/status.ts +++ b/core/src/plugins/kubernetes/container/status.ts @@ -65,6 +65,7 @@ export async function getContainerServiceStatus({ name: p.name, protocol: "TCP", targetPort: p.servicePort, + preferredLocalPort: p.localPort, // TODO: this needs to be configurable // urlProtocol: "http", } diff --git a/core/src/proxy.ts b/core/src/proxy.ts index f6ef03f97a..19748d780f 100644 --- a/core/src/proxy.ts +++ b/core/src/proxy.ts @@ -28,6 +28,8 @@ interface PortProxy { spec: ForwardablePort } +const defaultLocalIp = "127.0.0.1" + const activeProxies: { [key: string]: PortProxy } = {} registerCleanupFunction("kill-service-port-proxies", () => { @@ -77,14 +79,6 @@ async function createProxy( const key = getPortKey(service, spec) let fwd: GetPortForwardResult | null = null - // get preferred IP from db - const localAddress = await LocalAddress.resolve({ - projectName: garden.projectName, - moduleName: service.module.name, - serviceName: service.name, - hostname: getHostname(service, spec), - }) - const getPortForward = async () => { if (fwd) { return fwd @@ -203,18 +197,31 @@ async function createProxy( }) } - let localIp = localAddress.getIp() + let localIp = defaultLocalIp let localPort: number | undefined + const preferredLocalPort = spec.preferredLocalPort || spec.targetPort + + if (!spec.preferredLocalPort) { + // Only try a non-default IP if a preferred port isn't set + const preferredLocalAddress = await LocalAddress.resolve({ + projectName: garden.projectName, + moduleName: service.module.name, + serviceName: service.name, + hostname: getHostname(service, spec), + }) + + localIp = preferredLocalAddress.getIp() + } while (true) { try { - localPort = await getPort({ host: localIp, port: spec.targetPort }) + localPort = await getPort({ host: localIp, port: preferredLocalPort }) } catch (err) { if (err.errno === "EADDRNOTAVAIL") { // If we're not allowed to bind to other 127.x.x.x addresses, we fall back to localhost. This will almost always // be the case on Mac, until we come up with something more clever (that doesn't require sudo). - localIp = "127.0.0.1" - localPort = await getPort({ host: localIp, port: spec.targetPort }) + localIp = defaultLocalIp + localPort = await getPort({ host: localIp, port: preferredLocalPort }) } else { throw err } @@ -244,6 +251,12 @@ async function createProxy( }) if (started) { + if (spec.preferredLocalPort && (localIp !== defaultLocalIp || localPort !== spec.preferredLocalPort)) { + log.warn( + chalk.yellow(`→ Unable to bind port forward ${key} to preferred local port ${spec.preferredLocalPort}`) + ) + } + return { key, server, service, spec, localPort, localUrl } } else { // Need to retry on different port diff --git a/core/src/types/service.ts b/core/src/types/service.ts index 849dd6cc7e..a1648a8486 100644 --- a/core/src/types/service.ts +++ b/core/src/types/service.ts @@ -157,6 +157,7 @@ export const serviceIngressSchema = () => export interface ForwardablePort { name?: string // TODO: support other protocols + preferredLocalPort?: number protocol: "TCP" targetName?: string targetPort: number @@ -167,6 +168,7 @@ export const forwardablePortKeys = () => ({ name: joiIdentifier().description( "A descriptive name for the port. Should correspond to user-configured ports where applicable." ), + preferredLocalPort: joi.number().integer().description("The preferred local port to use for forwarding."), protocol: joi.string().allow("TCP").default("TCP").description("The protocol of the port."), targetName: joi.string().description("The target name/hostname to forward to (defaults to the service name)."), targetPort: joi.number().integer().required().description("The target port on the service."), diff --git a/docs/reference/commands.md b/docs/reference/commands.md index 3f3af351bb..2ce3ec904d 100644 --- a/docs/reference/commands.md +++ b/docs/reference/commands.md @@ -132,6 +132,9 @@ deployments: - # A descriptive name for the port. Should correspond to user-configured ports where applicable. name: + # The preferred local port to use for forwarding. + preferredLocalPort: + # The protocol of the port. protocol: @@ -485,6 +488,9 @@ serviceStatuses: - # A descriptive name for the port. Should correspond to user-configured ports where applicable. name: + # The preferred local port to use for forwarding. + preferredLocalPort: + # The protocol of the port. protocol: @@ -593,6 +599,9 @@ Examples: - # A descriptive name for the port. Should correspond to user-configured ports where applicable. name: + # The preferred local port to use for forwarding. + preferredLocalPort: + # The protocol of the port. protocol: @@ -752,6 +761,9 @@ deployments: - # A descriptive name for the port. Should correspond to user-configured ports where applicable. name: + # The preferred local port to use for forwarding. + preferredLocalPort: + # The protocol of the port. protocol: @@ -2351,6 +2363,9 @@ services: - # A descriptive name for the port. Should correspond to user-configured ports where applicable. name: + # The preferred local port to use for forwarding. + preferredLocalPort: + # The protocol of the port. protocol: @@ -2945,6 +2960,9 @@ deployments: - # A descriptive name for the port. Should correspond to user-configured ports where applicable. name: + # The preferred local port to use for forwarding. + preferredLocalPort: + # The protocol of the port. protocol: @@ -3510,6 +3528,9 @@ deployments: - # A descriptive name for the port. Should correspond to user-configured ports where applicable. name: + # The preferred local port to use for forwarding. + preferredLocalPort: + # The protocol of the port. protocol: diff --git a/docs/reference/module-types/container.md b/docs/reference/module-types/container.md index d49e30df77..fc577bdde8 100644 --- a/docs/reference/module-types/container.md +++ b/docs/reference/module-types/container.md @@ -340,6 +340,11 @@ services: # `servicePort:80 -> containerPort:8080 -> process:8080` containerPort: + # Specify a preferred local port to attach to when creating a port-forward to the service port. If this port + # is + # busy, a warning will be shown and an alternative port chosen. + localPort: + # The port exposed on the service. Defaults to `containerPort` if not specified. # This is the port you use when calling a service from another service within the cluster. For example, if # your service name is my-service and the service port is 8090, you would call it with: @@ -1556,6 +1561,25 @@ services: - containerPort: 8080 ``` +### `services[].ports[].localPort` + +[services](#services) > [ports](#servicesports) > localPort + +Specify a preferred local port to attach to when creating a port-forward to the service port. If this port is +busy, a warning will be shown and an alternative port chosen. + +| Type | Required | +| -------- | -------- | +| `number` | No | + +Example: + +```yaml +services: + - ports: + - localPort: 10080 +``` + ### `services[].ports[].servicePort` [services](#services) > [ports](#servicesports) > servicePort diff --git a/docs/reference/module-types/maven-container.md b/docs/reference/module-types/maven-container.md index fed9f6e55f..f2cfbeb119 100644 --- a/docs/reference/module-types/maven-container.md +++ b/docs/reference/module-types/maven-container.md @@ -338,6 +338,11 @@ services: # `servicePort:80 -> containerPort:8080 -> process:8080` containerPort: + # Specify a preferred local port to attach to when creating a port-forward to the service port. If this port + # is + # busy, a warning will be shown and an alternative port chosen. + localPort: + # The port exposed on the service. Defaults to `containerPort` if not specified. # This is the port you use when calling a service from another service within the cluster. For example, if # your service name is my-service and the service port is 8090, you would call it with: @@ -1564,6 +1569,25 @@ services: - containerPort: 8080 ``` +### `services[].ports[].localPort` + +[services](#services) > [ports](#servicesports) > localPort + +Specify a preferred local port to attach to when creating a port-forward to the service port. If this port is +busy, a warning will be shown and an alternative port chosen. + +| Type | Required | +| -------- | -------- | +| `number` | No | + +Example: + +```yaml +services: + - ports: + - localPort: 10080 +``` + ### `services[].ports[].servicePort` [services](#services) > [ports](#servicesports) > servicePort