Skip to content

Commit

Permalink
feat(k8s): manual port forward config for helm and kubernetes modules
Browse files Browse the repository at this point in the history
This allows users to explicitly configure the ports to forward for
kubernetes and helm modules, including which local port to forward to.

Example:

```yaml
kind: Module
type: kubernetes
...
portForwards:
  - name: http
    resource: Service/my-service
    targetPort: 80
    localPort: 8080
```
  • Loading branch information
edvald authored and thsig committed Aug 20, 2021
1 parent c7404c0 commit 9111a48
Show file tree
Hide file tree
Showing 10 changed files with 433 additions and 178 deletions.
33 changes: 33 additions & 0 deletions core/src/plugins/kubernetes/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -780,6 +780,39 @@ export const hotReloadArgsSchema = () =>
.description("If specified, overrides the arguments for the main container when running in hot-reload mode.")
.example(["nodemon", "my-server.js"])

export interface PortForwardSpec {
name?: string
resource: string
targetPort: number
localPort?: number
}

const portForwardSpecSchema = () =>
joi.object().keys({
name: joiIdentifier().description("An identifier to describe the port forward."),
resource: joi
.string()
.required()
.description(
"The full resource kind and name to forward to, e.g. Service/my-service or Deployment/my-deployment. Note that Garden will not validate this ahead of attempting to start the port forward, so you need to make sure this is correctly set. The types of resources supported will match that of the `kubectl port-forward` CLI command."
),
targetPort: joi.number().integer().required().description("The port number on the remote resource to forward to."),
localPort: joi
.number()
.integer()
.description(
"The _preferred_ local port to forward from. If none is set, a random port is chosen. If the specified port is not available, a warning is shown and a random port chosen instead."
),
})

export const portForwardsSchema = () =>
joi
.array()
.items(portForwardSpecSchema())
.description(
"Manually specify port forwards that Garden should set up when deploying in dev or watch mode. If specified, these override the auto-detection of forwardable ports, so you'll need to specify the full list of port forwards to create."
)

const runPodSpecWhitelistDescription = runPodSpecIncludeFields.map((f) => `* \`${f}\``).join("\n")

export const kubernetesTaskSchema = () =>
Expand Down
18 changes: 11 additions & 7 deletions core/src/plugins/kubernetes/helm/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ import {
containerModuleSchema,
hotReloadArgsSchema,
serviceResourceDescription,
portForwardsSchema,
PortForwardSpec,
} from "../config"
import { posix } from "path"
import { runPodSpecIncludeFields } from "../run"
Expand All @@ -57,6 +59,7 @@ export interface HelmServiceSpec {
dependencies: string[]
devMode?: KubernetesDevModeSpec
namespace?: string
portForwards?: PortForwardSpec[]
releaseName?: string
repo?: string
serviceResource?: ServiceResourceSpec
Expand Down Expand Up @@ -169,7 +172,15 @@ export const helmModuleSpecSchema = () =>
"List of names of services that should be deployed before this chart."
),
devMode: kubernetesDevModeSchema(),
include: joiModuleIncludeDirective(dedent`
If neither \`include\` nor \`exclude\` is set, and the module has local chart sources, Garden
automatically sets \`include\` to: \`["*", "charts/**/*", "templates/**/*"]\`.
If neither \`include\` nor \`exclude\` is set and the module specifies a remote chart, Garden
automatically sets \`ìnclude\` to \`[]\`.
`),
namespace: namespaceNameSchema(),
portForwards: portForwardsSchema(),
releaseName: joiIdentifier().description(
"Optionally override the release name used when installing (defaults to the module name)."
),
Expand All @@ -190,13 +201,6 @@ export const helmModuleSpecSchema = () =>
deline`Set this to true if the chart should only be built, but not deployed as a service.
Use this, for example, if the chart should only be used as a base for other modules.`
),
include: joiModuleIncludeDirective(dedent`
If neither \`include\` nor \`exclude\` is set, and the module has local chart sources, Garden
automatically sets \`include\` to: \`["*", "charts/**/*", "templates/**/*"]\`.
If neither \`include\` nor \`exclude\` is set and the module specifies a remote chart, Garden
automatically sets \`ìnclude\` to \`[]\`.
`),
tasks: joiSparseArray(helmTaskSchema()).description("The task definitions for this module."),
tests: joiSparseArray(helmTestSchema()).description("The test suite definitions for this module."),
timeout: joi
Expand Down
2 changes: 1 addition & 1 deletion core/src/plugins/kubernetes/helm/deployment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ export async function deployHelmService({
timeoutSec: module.spec.timeout,
})

const forwardablePorts = getForwardablePorts(manifests)
const forwardablePorts = getForwardablePorts(manifests, service)

// Make sure port forwards work after redeployment
killPortForwards(service, forwardablePorts || [], log)
Expand Down
2 changes: 1 addition & 1 deletion core/src/plugins/kubernetes/helm/status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ export async function getServiceStatus({

if (state !== "missing") {
const deployedResources = await getDeployedResources({ ctx: k8sCtx, module, releaseName, log })
forwardablePorts = getForwardablePorts(deployedResources)
forwardablePorts = getForwardablePorts(deployedResources, service)

if (state === "ready" && devMode && service.spec.devMode) {
// Need to start the dev-mode sync here, since the deployment handler won't be called.
Expand Down
18 changes: 11 additions & 7 deletions core/src/plugins/kubernetes/kubernetes-module/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import {
containerModuleSchema,
hotReloadArgsSchema,
serviceResourceDescription,
portForwardsSchema,
PortForwardSpec,
} from "../config"
import { ContainerModule } from "../../container/config"
import { kubernetesDevModeSchema, KubernetesDevModeSpec } from "../dev-mode"
Expand All @@ -41,8 +43,9 @@ export interface KubernetesServiceSpec {
dependencies: string[]
devMode?: KubernetesDevModeSpec
files: string[]
namespace?: string
manifests: KubernetesResource[]
namespace?: string
portForwards?: PortForwardSpec[]
serviceResource?: ServiceResourceSpec
tasks: KubernetesTaskSpec[]
tests: KubernetesTestSpec[]
Expand Down Expand Up @@ -71,20 +74,21 @@ export const kubernetesModuleSpecSchema = () =>
joi.object().keys({
build: baseBuildSpecSchema(),
dependencies: dependenciesSchema(),
manifests: joiSparseArray(kubernetesResourceSchema()).description(
deline`
List of Kubernetes resource manifests to deploy. Use this instead of the \`files\` field if you need to
resolve template strings in any of the manifests.`
),
devMode: kubernetesDevModeSchema(),
files: joiSparseArray(joi.posixPath().subPathOnly()).description(
"POSIX-style paths to YAML files to load manifests from. Each can contain multiple manifests, and can include any Garden template strings, which will be resolved before applying the manifests."
),
include: joiModuleIncludeDirective(dedent`
If neither \`include\` nor \`exclude\` is set, Garden automatically sets \`include\` to equal the
\`files\` directive so that only the Kubernetes manifests get included.
`),
manifests: joiSparseArray(kubernetesResourceSchema()).description(
deline`
List of Kubernetes resource manifests to deploy. Use this instead of the \`files\` field if you need to
resolve template strings in any of the manifests.`
),
namespace: namespaceNameSchema(),
devMode: kubernetesDevModeSchema(),
portForwards: portForwardsSchema(),
serviceResource: serviceResourceSchema()
.description(
dedent`
Expand Down
2 changes: 1 addition & 1 deletion core/src/plugins/kubernetes/kubernetes-module/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ export async function getKubernetesServiceStatus({

let { state, remoteResources } = await compareDeployedResources(k8sCtx, api, namespace, prepareResult.manifests, log)

const forwardablePorts = getForwardablePorts(remoteResources)
const forwardablePorts = getForwardablePorts(remoteResources, service)

if (state === "ready" && devMode && service.spec.devMode) {
// Need to start the dev-mode sync here, since the deployment handler won't be called.
Expand Down
17 changes: 16 additions & 1 deletion core/src/plugins/kubernetes/port-forward.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import { isBuiltIn, matchSelector } from "./util"
import { LogEntry } from "../../logger/log-entry"
import { RuntimeError } from "../../exceptions"
import execa = require("execa")
import { KubernetesService } from "./kubernetes-module/config"
import { HelmService } from "./helm/config"

// TODO: implement stopPortForward handler

Expand Down Expand Up @@ -209,7 +211,20 @@ function getTargetResource(service: GardenService, targetName?: string) {
/**
* Returns a list of forwardable ports based on the specified resources.
*/
export function getForwardablePorts(resources: KubernetesResource[]) {
export function getForwardablePorts(
resources: KubernetesResource[],
parentService: KubernetesService | HelmService | undefined
): ForwardablePort[] {
if (parentService?.spec.portForwards) {
return parentService?.spec.portForwards.map((p) => ({
name: p.name,
protocol: "TCP",
targetName: p.resource,
targetPort: p.targetPort,
preferredLocalPort: p.localPort,
}))
}

const ports: ForwardablePort[] = []

// Start by getting ports defined by Service resources
Expand Down
Loading

0 comments on commit 9111a48

Please sign in to comment.