Skip to content

demo: use cdk8s for deployment#5994

Draft
theosanderson-agent wants to merge 6 commits intomainfrom
demo/cdk8s-deployment
Draft

demo: use cdk8s for deployment#5994
theosanderson-agent wants to merge 6 commits intomainfrom
demo/cdk8s-deployment

Conversation

@theosanderson-agent
Copy link
Collaborator

@theosanderson-agent theosanderson-agent commented Feb 17, 2026

Summary

This is a proof of concept demonstrating the use of CDK8s (Cloud Development Kit for Kubernetes) as a replacement for Helm chart templating in Loculus.

CDK8s lets us define Kubernetes manifests in TypeScript instead of Go templates, giving us type safety, IDE support, and the full power of a programming language for configuration logic. The generated YAML is functionally identical to what Helm produces.

What's included

  • kubernetes/cdk8s/ — Full CDK8s implementation (~2,500 lines of TypeScript) with constructs for all Loculus components: backend, website, keycloak, database, LAPIS/SILO, preprocessing, ingest, ENA submission, MinIO, ingress, secrets, and docs
  • deploy.py — Updated to synthesize manifests via npx ts-node instead of helm template, and apply via kubectl apply --server-side
  • .github/workflows/integration-tests.yml — CI updated to install Node.js and CDK8s dependencies before the deploy step
  • Prettier formatting configured for the CDK8s source

Validation

  • YAML output was structurally compared against Helm output — functionally identical (only difference: 4 CROSSREF env vars where CDK8s omits null value keys vs Helm emitting value: null, which is semantically equivalent for Kubernetes)
  • 92/98 integration tests pass on a local k3d deployment
    • 4 browser test failures: Playwright trace artifact ENOENT errors (infrastructure flakiness)
    • 2 CLI test failures: pre-existing "Only singleReference organisms are supported currently" bug, unrelated to deployment method

Not yet included

  • Removal of the Helm chart (kubernetes/loculus/templates/)
  • Updates to other CI workflows (e.g. helm-schema-lint.yaml)
  • CDK8s unit tests
  • Server/production deployment configuration (ArgoCD integration)

🤖 Generated with Claude Code

🚀 Preview: Add preview label to enable

theosanderson and others added 2 commits February 17, 2026 15:35
Document critical gotchas discovered when running integration tests
against a local k3d cluster:

- CoreDNS fix for host.k3d.internal DNS resolution from pods (S3/MinIO
  pre-signed URLs are unreachable from pods without this override)
- Anaconda Python version conflict with deploy.py
- Disk pressure thresholds causing pod scheduling failures
- CLI venv setup: keyrings.alt installation and PATH configuration
- Updated step-by-step instructions with correct Helm deploy flags
- Added troubleshooting section for common failure modes

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
…eration

This is a proof of concept demonstrating CDK8s (TypeScript) as a replacement
for Helm chart templating. The CDK8s implementation generates functionally
identical Kubernetes manifests, validated by passing 92/98 integration tests
(the 6 failures are pre-existing issues unrelated to the migration).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@claude claude bot added the deployment Code changes targetting the deployment infrastructure label Feb 17, 2026
@claude
Copy link
Contributor

claude bot commented Feb 17, 2026

This PR may be related to: #5952 (Alternatives to Helm)

}
current = current[parts[i]];
}
current[parts[parts.length - 1]] = value;

Check warning

Code scanning / CodeQL

Prototype-polluting function Medium

The property chain
here
is recursively assigned to
current
without guarding against prototype pollution.

Copilot Autofix

AI about 1 month ago

In general, prototype pollution in deep-assignment functions like setNestedValue is fixed by blocking dangerous property names (__proto__, constructor, prototype) in the property chain, or by ensuring that assignments never target an object’s prototype (for example, by using Object.create(null) so there is no prototype). The minimal, behavior-preserving change here is to keep using plain objects but reject or ignore path segments that could pollute prototypes.

The best fix for this code is to add a guard that detects and rejects dangerous keys before they are used to index into current. Specifically:

  • Define a small helper, e.g. isSafeKey(key: string): boolean, that returns false for "__proto__", "constructor", and "prototype".
  • In setNestedValue, before using any parts[i] as a property name (both when creating intermediate objects and when assigning the final value), check isSafeKey. If any segment is unsafe, we should skip the assignment altogether to avoid creating any prototype-polluting structures. That preserves existing behavior for all normal keys and just ignores malicious or invalid paths.
  • All changes are confined to kubernetes/cdk8s/src/values.ts, within the shown snippet: we add the helper function near setNestedValue and adjust the loop in setNestedValue to call it. No imports are needed; we only use basic string comparisons.

This approach avoids changing how normal configuration keys are handled and adds a straightforward, well-known protection against prototype pollution.


Suggested changeset 1
kubernetes/cdk8s/src/values.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/kubernetes/cdk8s/src/values.ts b/kubernetes/cdk8s/src/values.ts
--- a/kubernetes/cdk8s/src/values.ts
+++ b/kubernetes/cdk8s/src/values.ts
@@ -323,16 +323,32 @@
   return value;
 }
 
+function isSafeKey(key: string): boolean {
+  // Prevent prototype pollution by disallowing dangerous keys
+  return key !== '__proto__' && key !== 'constructor' && key !== 'prototype';
+}
+
 function setNestedValue(obj: any, path: string, value: any): void {
   const parts = path.split('.');
   let current = obj;
+  // Build intermediate objects, guarding against prototype-polluting keys
   for (let i = 0; i < parts.length - 1; i++) {
-    if (!(parts[i] in current) || typeof current[parts[i]] !== 'object') {
-      current[parts[i]] = {};
+    const part = parts[i];
+    if (!isSafeKey(part)) {
+      // Unsafe path segment; do not perform this assignment
+      return;
     }
-    current = current[parts[i]];
+    if (!(part in current) || typeof current[part] !== 'object') {
+      current[part] = {};
+    }
+    current = current[part];
   }
-  current[parts[parts.length - 1]] = value;
+  const lastKey = parts[parts.length - 1];
+  if (!isSafeKey(lastKey)) {
+    // Unsafe final key; do not perform this assignment
+    return;
+  }
+  current[lastKey] = value;
 }
 
 export function deepMerge(target: any, source: any): any {
EOF
@@ -323,16 +323,32 @@
return value;
}

function isSafeKey(key: string): boolean {
// Prevent prototype pollution by disallowing dangerous keys
return key !== '__proto__' && key !== 'constructor' && key !== 'prototype';
}

function setNestedValue(obj: any, path: string, value: any): void {
const parts = path.split('.');
let current = obj;
// Build intermediate objects, guarding against prototype-polluting keys
for (let i = 0; i < parts.length - 1; i++) {
if (!(parts[i] in current) || typeof current[parts[i]] !== 'object') {
current[parts[i]] = {};
const part = parts[i];
if (!isSafeKey(part)) {
// Unsafe path segment; do not perform this assignment
return;
}
current = current[parts[i]];
if (!(part in current) || typeof current[part] !== 'object') {
current[part] = {};
}
current = current[part];
}
current[parts[parts.length - 1]] = value;
const lastKey = parts[parts.length - 1];
if (!isSafeKey(lastKey)) {
// Unsafe final key; do not perform this assignment
return;
}
current[lastKey] = value;
}

export function deepMerge(target: any, source: any): any {
Copilot is powered by AI and may make mistakes. Always verify output.
theosanderson and others added 4 commits February 17, 2026 17:16
The dev server test workflow runs deploy.py config which now uses CDK8s
instead of Helm for manifest generation. Add Node.js setup and CDK8s
npm ci before config generation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Delete all 46 Helm template files from kubernetes/loculus/templates/
- Delete Chart.yaml, Chart.lock, CONTRIBUTING.md
- Delete helm-schema-lint.yaml workflow
- Remove helm lint from pre-commit config
- Remove setup-helm from website dev test workflow (not needed for config gen)
- Rename deploy.py subcommand: helm -> deploy
- Rename HELM_CHART_DIR -> VALUES_DIR in deploy.py
- Update all references in workflows, scripts, READMEs, and AGENTS.md

Values files (values.yaml, values_e2e_and_dev.yaml, values_preview_server.yaml,
values.schema.json) are retained as they are consumed by CDK8s.
Helm itself is still installed in CI for the secret-generator dependency.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…efault'

Replace hardcoded `const ns = 'default'` in ingress.ts and lapis.ts with
the actual deployment namespace. Add --namespace/-n CLI flag to main.ts
and thread it through LoculusChart -> values.releaseNamespace.

This matches Helm's $.Release.Namespace behavior so deployments to
non-default namespaces generate correct Traefik middleware references.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…construct

Export commonMetadataFields() from config-generation.ts and import it
in silo.ts instead of duplicating 158 lines. No circular dependency
exists despite the previous comment. Reduces silo.ts from 360 to 200
lines.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

deployment Code changes targetting the deployment infrastructure

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants