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

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions