Skip to content

feat(connect): opt-in local Kubernetes service deployment - #2404

Draft
agarwal-ishaan wants to merge 20 commits into
promptdriven:mainfrom
agarwal-ishaan:feat/connect-k8s
Draft

feat(connect): opt-in local Kubernetes service deployment#2404
agarwal-ishaan wants to merge 20 commits into
promptdriven:mainfrom
agarwal-ishaan:feat/connect-k8s

Conversation

@agarwal-ishaan

Copy link
Copy Markdown
Collaborator

Tracks #2403.

Adds an opt-in local deployment layer to PDD Connect. Dev Units are implementation units, not pods — a developer explicitly maps one or more Dev Units onto an independently runnable service. Projects that are plain scripts, CLIs or libraries are entirely unaffected.

Note on scope: this branch is stacked on feat/connect-observability (#2402), so the diff below includes that PR's commits as ancestors. Review only 19e4536 — the single commit that adds the Kubernetes work. GitHub will not let this target #2402's branch directly, because the base of a cross-fork PR must exist in promptdriven/pdd.

What's here

pdd-k8s/ — companion package

Deliberately a separate distribution. Core PDD never imports it; it is loaded lazily and its absence is a normal, reported state.

# .pdd/deployments.yaml
version: 1
cluster:
  name: pdd-local
  namespace: pdd-local
services:
  api:
    dev_units: [router, parser, analyzer, formatter, webpage]
    dockerfile: deploy/Dockerfile
    port: 8000
    health:
      path: /

doctor · services · up · status · logs · down · manifest

Connect integration

  • /api/v1/deployments — registered unconditionally, degrades to plugin_installed: false with an install hint. Action endpoints return 501 rather than failing to import.
  • Deploys are background operations polled via /operations — container builds take minutes, so a synchronous POST would block.
  • DeploymentsPanel renders null unless the plugin and a manifest both exist.

Safety properties

  • Every kubectl call is pinned to the manifest's own context (kind-<name>), so an unrelated cluster cannot be touched — asserted by a test that inspects every recorded invocation.
  • Only objects labelled app.kubernetes.io/managed-by=pdd are deleted.
  • Images are side-loaded into kind and tagged :local; nothing is pushed to a registry.
  • down leaves the cluster running unless --cluster is passed.
  • A plugin exception fails its operation with the error text — it never returns a 500 or takes the server down.

Deliberate non-goals

  • No Helm. Plain manifests are inspectable and diffable; Helm earns its place later, with environment variants and dependencies.
  • PDD never writes a Dockerfile. It detects yours and reports what is missing. Generating one means guessing at build steps, secrets, databases and runtime commands.
  • Local only. No registry pushes, no remote clusters, no credentials.

Verification

Exercised end to end against a real kind cluster, deploying a FastAPI service whose five Dev Units ship as one container:

$ pdd-k8s up
  Building image pdd-api:local…
  Loading pdd-api:local into cluster 'pdd-local'…
  Applying manifests for 'api'…
  Waiting for 'api' to become ready…
✔ api is ready

$ pdd-k8s status
api              running      1/1 ready  0 restarts
  dev units: router, parser, analyzer, formatter, webpage
  · api-5cb88bbc49-qdgwp  ready  health=passing  node=pdd-local-control-plane

The service served real traffic in-cluster, and Connect's API returned the live mapping and pod health.

  • pdd-k8s: 54 tests, every external command faked — needs no Docker, kind or cluster.
  • Connect: 469 server tests, frontend builds clean.

Two real bugs were caught and fixed during this work: nodeName was read from pod.status instead of pod.spec, and generated YAML emitted anchors (&id001) that made single objects hard to copy out.

Known blockers — please read before merging

1. Sync-ownership gate. test_estimate_contract_rotations_are_exact_and_dormant fails on 8 tracked paths with no rule in .pdd/sync-ownership.json. All 8 are pre-existing observability paths — verified identical with these changes stashed. The new deployments paths will need rules too. Since that file is itself digest-pinned, this reads as maintainer work rather than a contributor edit, so I have left it untouched.

2. No fast-fail on a wedged Docker daemon. Requests return correct results but hang on subprocess timeouts (~20s for status, ~10s for logs). runtime.py needs a cheap liveness probe. Tracked in #2403.

Open question for maintainers

The real question is not "should PDD support Kubernetes?" It is whether PDD should offer an optional, project-aware local deployment layer at all. If yes, the abstraction should stay "local service runner" so a Docker Compose backend can slot in later. Discussion in #2403.

agarwal-ishaan and others added 19 commits August 3, 2026 17:57
…t regeneration

pdd generate had an asymmetry: `_verify_public_surface_regression` only
ast.parse()s generated Python when existing_code is present (i.e. only
when regenerating a mature module). A brand-new file has no existing
code to compare against, so that gate is skipped entirely -- a prompt
whose content doesn't match its declared `_<language>` filename suffix
(e.g. `game_python.prompt` whose content actually describes an HTML
page) can silently write unparseable content into a freshly created
`.py` file with no error at all.

Add `_verify_generated_syntax`, called unconditionally alongside the
existing `_verify_architecture_conformance` check (which already runs
on every generation, first-time included). It ast.parse()s the output
against its declared language and raises ArchitectureConformanceError
on mismatch, reusing the same error type/UX the codebase already has
for conformance failures. Respects PDD_SKIP_CONFORMANCE. Tolerates a
surface markdown code fence (some response paths intentionally leave
one un-stripped) so it validates the real code rather than tripping on
cosmetic ``` wrapping.

Also updates a handful of existing incremental-generation tests whose
mock "generated code" fixtures were plain English placeholder strings
("Updated code", "Base-ref updated code", etc.) rather than valid
Python -- harmless before this fix since nothing validated first-time
output, but now correctly caught by the new check. Replaced with
trivial valid-Python snippets; the tests' actual assertions (control
flow, kwargs passed, cost/model returned) are unchanged.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…k their prompt

The repair_directive already told an agentic repair loop to check the
prompt against its declared language, but the actual user-facing
message (what a human sees on the CLI) didn't. Add an explicit,
plain-language prompt to double-check the requested content matches
the declared language suffix, since that's the most common real cause
of this error (verified via manual end-to-end testing against a real
prompt/response pair).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
_verify_generated_syntax was added directly to code_generator_main.py
(17048b3, 72afd9f) without updating its source-of-truth prompt. Add
section 5a1 to code_generator_main_python.prompt spelling out the gate
exactly as implemented, so the prompt and code stay in sync.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Dev Units are implementation units, not pods. This adds an explicit mapping
layer so a developer declares which Dev Units combine into an independently
runnable service, then runs those services on a named local cluster.

pdd-k8s companion package (pdd-k8s/):
- .pdd/deployments.yaml maps dev_units -> dockerfile/port/health per service
- doctor / services / up / status / logs / down / manifest commands
- plain generated manifests, no Helm, no Dockerfile generation
- kubectl pinned to the manifest's own context; only objects labelled
  app.kubernetes.io/managed-by=pdd are ever deleted
- pdd_k8s.api is the stable facade Connect imports; it never raises

Connect integration:
- /api/v1/deployments router, registered unconditionally but degrading to
  plugin_installed: false so core PDD never depends on Docker or Kubernetes
- deploys run as background operations polled via /operations, since container
  builds take minutes
- DeploymentsPanel renders nothing unless the plugin and a manifest both exist

Also corrects a stale expected __all__ in test_routes_init.py that omitted the
observability exports.
@agarwal-ishaan

Copy link
Copy Markdown
Collaborator Author

Why Kubernetes, and why it might be the wrong call

Recording the trade-offs explicitly, since #2403 asks whether this layer should exist at all rather than how to build it.

Advantages

The declarative model already matches what PDD does. PDD's whole premise is that a prompt is the source of truth and the artifact is derived. Kubernetes has the same shape: a manifest is the source of truth and the running pod is derived. dev_units → service → manifest → pod is one more link in a chain the project already believes in. A procedural runner (docker run, a Procfile) would sit awkwardly against that.

Readiness is a real signal, not a guess. A readiness probe is the difference between "the process started" and "the service actually works." That is exactly the question a developer has after regenerating a Dev Unit, and it is genuinely hard to answer without an orchestrator. This PR gets it almost free — health.path becomes an httpGet probe, and Connect reports the result.

Restarts and events turn silent failures into evidence. When a regenerated Dev Unit crashes on import, the pod restarts and Kubernetes records why. That trail is what makes the Connect panel useful rather than decorative — CrashLoopBackOff with the reason beats a dead terminal.

Multi-service projects stop needing a README ritual. Once a project has an API and a worker and a frontend, "how do I run this" becomes tribal knowledge. pdd-k8s up replaces that with something checked into the repo.

The labelling model gives safe blast-radius control. Everything PDD creates carries managed-by=pdd, so teardown can be precise. That is much harder to guarantee with loose docker run containers.

It is a credible path to parity with deployment. If a team later runs on Kubernetes for real, the local manifests are the same object model. Nothing learned locally is wasted.

Disadvantages

The dependency chain is heavy and fragile. Docker Desktop, plus kind, plus kubectl, plus a downloaded node image — before a single line of the user's code runs. This session proved the point: the Docker daemon wedged mid-demo and took the cluster with it, and every kubectl call hung until its timeout. doctor softens the first-run experience but cannot make the dependency go away.

It is enormously more machinery than most projects need. For a single-process FastAPI app, uvicorn main:app is one command with no daemon, no cluster and no image build. Kubernetes buys nothing there. This is precisely why the layer must stay opt-in — and why the Dev-Unit-is-not-a-pod boundary matters more than any feature in this PR.

The iteration loop is slow. Build the image, side-load it into kind, apply, wait for rollout. That is tens of seconds per change against sub-second hot reload. A developer iterating on one Dev Unit will not want this in the loop, so it competes with the workflow it is meant to support.

It leaks a large vocabulary into PDD. Namespaces, selectors, probes, replicas, rollout status, events. The panel is deliberately narrow, but the moment something breaks, the user is debugging Kubernetes, not PDD. Every error message this layer surfaces is one PDD did not write and cannot control.

Kubernetes semantics are not local-dev semantics. imagePullPolicy, image tags that must not be :latest, side-loading instead of pulling — several sharp edges exist purely because a production-shaped tool is being run locally. Two real bugs in this PR came from exactly that gap (nodeName living on spec, not status; generated YAML anchors).

It risks defining the abstraction around one backend. The interesting idea in #2403 is "local service runner." If the schema, the panel and the vocabulary all harden around Kubernetes, adding Docker Compose later means retrofitting, not extending.

Where that leaves it

The honest summary: Kubernetes is a good first backend and a bad only backend.

It is the right choice for validating the concept, because it forces the hard questions — health, readiness, restarts, teardown scope — to be answered properly rather than approximated. It would be the wrong choice as a permanent assumption, because most PDD projects will never justify the dependency.

Two concrete implications for review:

  1. .pdd/deployments.yaml should stay backend-neutral. Today it already is: dev_units, dockerfile, port, health.path describe a service, not a pod. The cluster: block is the only Kubernetes-specific part, and it is separable. That property is worth defending in review — it is what keeps a Compose backend cheap later.
  2. A Docker Compose backend is the test of whether "local service runner" is a real abstraction or just a description of this implementation. Worth attempting before the schema is treated as stable.

The routes __init__ prompt still specified only the four original routers,
while the module exports nine submodules, eight routers and two project-scoped
factories. The file is .pddignore'd so nothing regenerates it, which is how the
drift went unnoticed, but an inaccurate spec is worse than none.

Also documents why observability and deployments export factories rather than
router objects: both are built per project root, not at import time.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant