Skip to content

epic: hot-reloads and tests #2

@Anmol1696

Description

@Anmol1696

The mental model (what you’re aiming for)

  • Repo: functions/<fn-name>/...

  • Cluster: one Knative Service per function

  • Dev loop:

    • Tilt/Skaffold builds the image once (or rarely)
    • then syncs only that function’s files into its pod
    • nodemon/tsx inside the container restarts immediately
  • Testing:

    • unit tests: run fast locally (watch mode)
    • integration tests: run inside cluster as a Job hitting http://<ksvc>.<ns>.svc.cluster.local

For many functions, I strongly prefer Tilt because you can script/loop resources easily.


1) Make each function “hot-reloadable” in a pod

Recommended repo layout

repo/
  functions/
    simple-email/
      src/
      package.json
    send-email-link/
      src/
      package.json
  packages/
    shared/
  pnpm-workspace.yaml
  pnpm-lock.yaml
  Dockerfile.dev
  k8s/
    knative/
      simple-email.yaml
      send-email-link.yaml
  Tiltfile

functions/<fn>/package.json

Make sure each function has a dev script that restarts on changes:

JS

{
  "scripts": {
    "dev": "nodemon --legacy-watch --watch src --ext js,json --exec node src/index.js",
    "test": "vitest run"
  },
  "devDependencies": {
    "nodemon": "^3.0.0",
    "vitest": "^2.0.0"
  }
}

TS (nice default)

{
  "scripts": {
    "dev": "tsx watch src/index.ts",
    "test": "vitest run"
  },
  "devDependencies": {
    "tsx": "^4.0.0",
    "vitest": "^2.0.0"
  }
}

File watching in containers can be flaky → polling helps. We’ll set env vars in the Knative Service.


2) Use a dev Docker image that can run any function

Dockerfile.dev (monorepo-friendly, pnpm)

FROM node:20-alpine
WORKDIR /app

RUN corepack enable

# Install deps (cached)
COPY pnpm-lock.yaml package.json pnpm-workspace.yaml ./
COPY functions ./functions
COPY packages ./packages

RUN pnpm install --frozen-lockfile

# Default; each Knative Service will override workingDir/command
CMD ["node", "-e", "console.log('dev image ready')"]

This image contains the whole repo so that live-sync can just overwrite files in-place.


3) Knative Service per function (dev-friendly)

Key points:

  • minScale: "1" so the pod stays running (hot reload needs a running process)
  • optional cluster-local visibility to simplify calling from tests/jobs
  • set polling env vars

Example: k8s/knative/simple-email.yaml

apiVersion: serving.knative.dev/v1
kind: Service
metadata:
  name: simple-email
  namespace: interweb
  annotations:
    networking.knative.dev/visibility: cluster-local
spec:
  template:
    metadata:
      annotations:
        autoscaling.knative.dev/minScale: "1"
    spec:
      containers:
        - image: fn-simple-email-dev
          workingDir: /app/functions/simple-email
          command: ["pnpm", "dev"]
          env:
            - name: NODE_ENV
              value: development
            - name: CHOKIDAR_USEPOLLING
              value: "true"
            - name: WATCHPACK_POLLING
              value: "true"
          ports:
            - containerPort: 8080

If your function listens on 3000, change containerPort. Knative generally expects your app to listen on $PORT (often 8080). If you can, have your server read process.env.PORT.


4) Hot sync into the right function pods (Tilt approach)

Tiltfile (one image per function; sync only its folder)

# Tiltfile
allow_k8s_contexts('kind-kind', 'docker-desktop')

FUNCTIONS = [
  'simple-email',
  'send-email-link',
  # add more...
]

for fn in FUNCTIONS:
  img = f"fn-{fn}-dev"

  docker_build(
    img,
    context='.',
    dockerfile='Dockerfile.dev',
    live_update=[
      # sync only this function’s code
      sync(f'functions/{fn}', f'/app/functions/{fn}'),
      # if you have shared libs, also sync them
      sync('packages/shared', '/app/packages/shared'),
      # restart process if needed (tsx/nodemon usually handles it;
      # but this is a reliable fallback)
      restart_container(),
    ],
  )

  k8s_yaml(f'k8s/knative/{fn}.yaml')
  k8s_resource(fn)  # resource name matches Knative Service metadata.name

Run:

tilt up

Now the loop is:

  • edit functions/simple-email/src/...
  • Tilt syncs only that folder into /app/functions/simple-email
  • tsx watch / nodemon restarts immediately

kind vs Docker Desktop note

  • Docker Desktop K8s: local images “just work”.
  • kind: nodes use containerd; Tilt handles loading images into kind for you (that’s a big reason Tilt is pleasant here).

5) How to test (fast + realistic)

A) Unit tests (fastest)

Run locally with pnpm filters:

# single function
pnpm -C functions/simple-email test

# all functions
pnpm -r --filter ./functions/** test

If you want watch mode:

pnpm -C functions/simple-email vitest

B) Integration tests inside the cluster (best signal)

Because your Knative services are cluster-local, you can hit them by DNS:

http://simple-email.interweb.svc.cluster.local

Create a test Job that runs a Node test runner inside the cluster.

k8s/tests/integration-job.yaml

apiVersion: batch/v1
kind: Job
metadata:
  name: integration-tests
  namespace: interweb
spec:
  backoffLimit: 0
  template:
    spec:
      restartPolicy: Never
      containers:
        - name: tests
          image: node:20-alpine
          workingDir: /work
          command: ["sh", "-lc"]
          args:
            - |
              corepack enable
              apk add --no-cache git
              # simplest: bake a tests image instead of cloning
              # but for local/dev, you can mount or clone
              echo "TODO: run your integration tests here"

Better (what I recommend): build a small tests-runner image from your repo (same Dockerfile.dev pattern) that contains your tests/ package, then the Job just runs pnpm -C tests/integration test.

C) Quick manual “does it work” test

Run a curl pod in-cluster (no ingress/domain pain):

kubectl -n interweb run -it --rm curl \
  --image=curlimages/curl --restart=Never -- \
  sh -lc 'curl -i http://simple-email.interweb.svc.cluster.local'

6) Common gotchas with Knative hot reload

  • Scale-to-zero kills hot reload → set autoscaling.knative.dev/minScale: "1".

  • Port mismatch → make sure your app listens on $PORT (Knative sets it), or align containerPort.

  • File watching → use polling env vars if you don’t see reloads.

  • Shared code (packages/shared) → either:

    • sync it into every function container (Tilt sync('packages/shared', ...)), or
    • accept rebuilds when shared changes.

If you paste (or describe) your current functions/ layout + how each Knative Service is generated (manual YAML vs your spec generator), I can adapt this into:

  • an auto-discovered Tiltfile (no manual list),
  • per-function sync rules (only src/**, not node_modules),
  • and a clean “tests runner” job/image that you can also reuse in CI with kind.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions