-
Notifications
You must be signed in to change notification settings - Fork 26
docs: TypeScript charts specification #500
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,64 @@ | ||
| # TypeScript Charts for Nelm | ||
|
|
||
| Alternative to Go templates for generating Kubernetes manifests. | ||
|
|
||
| ## Documents | ||
|
|
||
| - [decisions.md](./decisions.md) — Accepted design decisions | ||
| - [api.md](./api.md) — HelmContext API and types | ||
| - [sdk.md](./sdk.md) — npm packages and types | ||
| - [workflow.md](./workflow.md) — Development and deployment workflow | ||
| - [cli.md](./cli.md) — CLI commands | ||
| - [data-mechanism.md](./data-mechanism.md) — External data fetching | ||
|
|
||
| ## Overview | ||
|
|
||
| TypeScript charts provide a type-safe, scalable alternative to Go templates while maintaining Helm compatibility. | ||
|
|
||
| ```typescript | ||
| import { HelmContext, Manifest } from '@nelm/types' | ||
| import { Deployment } from '@nelm/types/apps/v1' | ||
| import { Values } from './generated/values.types' | ||
|
|
||
| export default function render(ctx: HelmContext<Values>): Manifest[] { | ||
| var deployment: Deployment = { | ||
| apiVersion: 'apps/v1', | ||
| kind: 'Deployment', | ||
| metadata: { | ||
| name: ctx.Release.Name, | ||
| namespace: ctx.Release.Namespace, | ||
| }, | ||
| spec: { | ||
| replicas: ctx.Values.replicas, | ||
| selector: { matchLabels: { app: ctx.Release.Name } }, | ||
| template: { | ||
| metadata: { labels: { app: ctx.Release.Name } }, | ||
| spec: { | ||
| containers: [{ | ||
| name: 'app', | ||
| image: ctx.Values.image.repository + ':' + ctx.Values.image.tag, | ||
| }], | ||
| }, | ||
| }, | ||
| }, | ||
| } | ||
|
|
||
| return [deployment] | ||
| } | ||
| ``` | ||
|
|
||
| ## Key Principles | ||
|
|
||
| 1. **Pure functions** — `render(ctx) → Manifest[]` | ||
| 2. **Deterministic** — no network/fs in render, external data via data mechanism | ||
| 3. **Type safety** — types from `@nelm/types` + generators | ||
| 4. **Isolation** — subcharts render independently | ||
| 5. **ES5 target** — goja compatibility, no async/await | ||
|
|
||
| ## npm Packages | ||
|
|
||
| | Package | Purpose | | ||
| |---------|---------| | ||
| | `@nelm/types` | HelmContext, Manifest, K8s resources | | ||
| | `@nelm/crd-to-ts` | Generate types from CRD | | ||
| | `json-schema-to-typescript` | Generate Values types | |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,223 @@ | ||||||
| # HelmContext API | ||||||
|
|
||||||
| ## Main Interface | ||||||
|
|
||||||
| ```typescript | ||||||
| interface HelmContext<V = unknown, D = DataResults> { | ||||||
| // Data only, no functions | ||||||
| Values: V | ||||||
| Release: Release | ||||||
| Chart: Chart | ||||||
| Capabilities: Capabilities | ||||||
| Files: Files | ||||||
| Data: D // Results from data() phase | ||||||
| } | ||||||
| ``` | ||||||
|
|
||||||
| **Note:** No helper functions in ctx. Define your own as needed. | ||||||
|
|
||||||
| ## Release | ||||||
|
|
||||||
| ```typescript | ||||||
| interface Release { | ||||||
| Name: string | ||||||
| Namespace: string | ||||||
| IsUpgrade: boolean | ||||||
| IsInstall: boolean | ||||||
| Revision: number | ||||||
| Service: string // "Helm" or "Nelm" | ||||||
| } | ||||||
| ``` | ||||||
|
|
||||||
| ## Chart | ||||||
|
|
||||||
| ```typescript | ||||||
| interface Chart { | ||||||
| Name: string | ||||||
| Version: string | ||||||
| AppVersion: string | ||||||
| Description: string | ||||||
| Keywords: string[] | ||||||
| Home: string | ||||||
| Sources: string[] | ||||||
| Icon: string | ||||||
| Deprecated: boolean | ||||||
| Type: string // "application" or "library" | ||||||
| } | ||||||
| ``` | ||||||
|
|
||||||
| ## Capabilities | ||||||
|
|
||||||
| ```typescript | ||||||
| interface Capabilities { | ||||||
| KubeVersion: KubeVersion | ||||||
| APIVersions: APIVersions | ||||||
| HelmVersion: HelmVersion | ||||||
| } | ||||||
|
|
||||||
| interface KubeVersion { | ||||||
| Major: string | ||||||
| Minor: string | ||||||
| GitVersion: string // e.g., "v1.28.3" | ||||||
| } | ||||||
|
|
||||||
| interface APIVersions { | ||||||
| list: string[] | ||||||
| } | ||||||
|
|
||||||
| interface HelmVersion { | ||||||
| Version: string | ||||||
| GitCommit: string | ||||||
| GoVersion: string | ||||||
| } | ||||||
| ``` | ||||||
|
|
||||||
| ## Files | ||||||
|
|
||||||
| ```typescript | ||||||
| interface Files { | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's make it We don't really need all these methods, the user can do it himself. And we have all the Data from files already at this point (Helm reads all of them into the memory, even if not used). |
||||||
| get(path: string): string | ||||||
| getBytes(path: string): Uint8Array | ||||||
| glob(pattern: string): Record<string, string> // path -> content | ||||||
| lines(path: string): string[] | ||||||
| } | ||||||
| ``` | ||||||
|
|
||||||
| ## Data (from data mechanism) | ||||||
|
|
||||||
| ```typescript | ||||||
| type DataResults = Record<string, DataResult> | ||||||
|
|
||||||
| type DataResult = | ||||||
| | KubernetesResource | ||||||
| | KubernetesList | ||||||
| | boolean | ||||||
| | null | ||||||
| ``` | ||||||
|
|
||||||
| See [data-mechanism.md](./data-mechanism.md) for details. | ||||||
|
|
||||||
| ## Manifest | ||||||
|
|
||||||
| ```typescript | ||||||
| interface Manifest { | ||||||
| apiVersion: string | ||||||
| kind: string | ||||||
| metadata: ObjectMeta | ||||||
| [key: string]: unknown | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not sure how can it be done in TS, but this must work for |
||||||
| } | ||||||
|
|
||||||
| interface ObjectMeta { | ||||||
| name: string | ||||||
| namespace?: string | ||||||
| labels?: Record<string, string> | ||||||
| annotations?: Record<string, string> | ||||||
| ownerReferences?: OwnerReference[] | ||||||
| finalizers?: string[] | ||||||
|
Comment on lines
+115
to
+116
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Not needed |
||||||
| } | ||||||
| ``` | ||||||
|
|
||||||
| ## Usage Example | ||||||
|
|
||||||
| ```typescript | ||||||
| import { HelmContext, Manifest } from '@nelm/types' | ||||||
| import { Deployment } from '@nelm/types/apps/v1' | ||||||
| import { Service } from '@nelm/types/core/v1' | ||||||
| import { Values } from './generated/values.types' | ||||||
|
|
||||||
| // User-defined helper | ||||||
| function when<T>(condition: boolean, items: T[]): T[] { | ||||||
| return condition ? items : [] | ||||||
| } | ||||||
|
|
||||||
| export default function render(ctx: HelmContext<Values>): Manifest[] { | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd like to see something like RenderResult here instead of Manifest[].
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also, maybe instead of ctx name it $? Will be more familiar |
||||||
| var labels = { | ||||||
| 'app.kubernetes.io/name': ctx.Chart.Name, | ||||||
| 'app.kubernetes.io/instance': ctx.Release.Name, | ||||||
| 'app.kubernetes.io/version': ctx.Chart.AppVersion, | ||||||
| } | ||||||
|
|
||||||
| var deployment: Deployment = { | ||||||
| apiVersion: 'apps/v1', | ||||||
| kind: 'Deployment', | ||||||
| metadata: { | ||||||
| name: ctx.Release.Name, | ||||||
| namespace: ctx.Release.Namespace, | ||||||
| labels: labels, | ||||||
| }, | ||||||
| spec: { | ||||||
| replicas: ctx.Values.replicas, | ||||||
| selector: { matchLabels: labels }, | ||||||
| template: { | ||||||
| metadata: { labels: labels }, | ||||||
| spec: { | ||||||
| containers: [{ | ||||||
| name: ctx.Chart.Name, | ||||||
| image: ctx.Values.image.repository + ':' + ctx.Values.image.tag, | ||||||
| }], | ||||||
| }, | ||||||
| }, | ||||||
| }, | ||||||
| } | ||||||
|
|
||||||
| var service: Service = { | ||||||
| apiVersion: 'v1', | ||||||
| kind: 'Service', | ||||||
| metadata: { | ||||||
| name: ctx.Release.Name, | ||||||
| namespace: ctx.Release.Namespace, | ||||||
| labels: labels, | ||||||
| }, | ||||||
| spec: { | ||||||
| selector: labels, | ||||||
| ports: [{ port: 80, targetPort: 8080 }], | ||||||
| }, | ||||||
| } | ||||||
|
|
||||||
| return [ | ||||||
| deployment, | ||||||
| service, | ||||||
|
|
||||||
| // Conditional based on values | ||||||
| ...when(ctx.Values.ingress.enabled, [{ | ||||||
| apiVersion: 'networking.k8s.io/v1', | ||||||
| kind: 'Ingress', | ||||||
| metadata: { | ||||||
| name: ctx.Release.Name, | ||||||
| namespace: ctx.Release.Namespace, | ||||||
| }, | ||||||
| spec: { | ||||||
| rules: [{ | ||||||
| host: ctx.Values.ingress.host, | ||||||
| http: { | ||||||
| paths: [{ | ||||||
| path: '/', | ||||||
| pathType: 'Prefix', | ||||||
| backend: { | ||||||
| service: { | ||||||
| name: ctx.Release.Name, | ||||||
| port: { number: 80 }, | ||||||
| }, | ||||||
| }, | ||||||
| }], | ||||||
| }, | ||||||
| }], | ||||||
| }, | ||||||
| }]), | ||||||
|
|
||||||
| // Conditional based on data mechanism | ||||||
| ...when(ctx.Data.serviceMonitorCRDExists === true, [{ | ||||||
| apiVersion: 'monitoring.coreos.com/v1', | ||||||
| kind: 'ServiceMonitor', | ||||||
| metadata: { | ||||||
| name: ctx.Release.Name, | ||||||
| namespace: ctx.Release.Namespace, | ||||||
| }, | ||||||
| spec: { | ||||||
| selector: { matchLabels: labels }, | ||||||
| endpoints: [{ port: 'http' }], | ||||||
| }, | ||||||
| }]), | ||||||
| ] | ||||||
| } | ||||||
| ``` | ||||||
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,102 @@ | ||||||||
| # CLI Commands | ||||||||
|
|
||||||||
| ## TypeScript Chart Commands | ||||||||
|
|
||||||||
| ### nelm chart ts init | ||||||||
|
|
||||||||
| Initialize TypeScript support in a chart. | ||||||||
|
|
||||||||
| ```bash | ||||||||
| nelm chart ts init [path] | ||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
I guess it should make a minimal chart with templates directory by default. But if you pass flag As a first version we can return an error, if |
||||||||
| ``` | ||||||||
|
|
||||||||
| **Arguments:** | ||||||||
| - `path` — Chart directory (default: current directory) | ||||||||
|
|
||||||||
| **Creates:** | ||||||||
| - `ts/package.json` | ||||||||
| - `ts/tsconfig.json` | ||||||||
| - `ts/src/index.ts` | ||||||||
|
|
||||||||
| **Output:** | ||||||||
| ``` | ||||||||
| Created ts/package.json | ||||||||
| Created ts/tsconfig.json | ||||||||
| Created ts/src/index.ts | ||||||||
|
|
||||||||
| Next steps: | ||||||||
| cd ts | ||||||||
| npm install | ||||||||
| npm run generate:values # if values.schema.json exists | ||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We don't need this now, maybe later. |
||||||||
| ``` | ||||||||
|
|
||||||||
| ### nelm chart render | ||||||||
|
|
||||||||
| Render chart manifests (Go templates + TypeScript). | ||||||||
|
|
||||||||
| ```bash | ||||||||
| nelm chart render [path] [flags] | ||||||||
| ``` | ||||||||
|
|
||||||||
| **Flags:** | ||||||||
| - `--values, -f` — Values file | ||||||||
| - `--set` — Set values on command line | ||||||||
| - `--output, -o` — Output format (yaml, json) | ||||||||
|
|
||||||||
| **Behavior:** | ||||||||
| 1. Renders Go templates (if `templates/` exists) | ||||||||
| 2. Renders TypeScript (if `ts/` exists) | ||||||||
| 3. Combines and outputs manifests | ||||||||
|
|
||||||||
| ### nelm chart publish | ||||||||
|
|
||||||||
| Package and publish chart to registry. | ||||||||
|
|
||||||||
| ```bash | ||||||||
| nelm chart publish [path] [flags] | ||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||
| ``` | ||||||||
|
|
||||||||
| **Behavior:** | ||||||||
| 1. Bundles TypeScript with embedded esbuild → `ts/vendor/bundle.js` | ||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Source maps also needed. I guess we can even embed them into the same bundle.js file. |
||||||||
| 2. Packages chart | ||||||||
| 3. Uploads to registry | ||||||||
|
|
||||||||
| **Note:** esbuild is embedded in Nelm CLI. | ||||||||
|
|
||||||||
| ## Existing Commands (unchanged) | ||||||||
|
|
||||||||
| These commands work with both Go templates and TypeScript charts: | ||||||||
|
|
||||||||
| ```bash | ||||||||
| nelm release install <chart> [flags] | ||||||||
| nelm release upgrade <release> <chart> [flags] | ||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||
| nelm release uninstall <release> [flags] | ||||||||
| nelm release list [flags] | ||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||
| ``` | ||||||||
|
|
||||||||
| ## Example Session | ||||||||
|
|
||||||||
| ```bash | ||||||||
| # Create new chart | ||||||||
| mkdir mychart && cd mychart | ||||||||
| nelm chart create . | ||||||||
|
|
||||||||
| # Add TypeScript support | ||||||||
| nelm chart ts init . | ||||||||
|
|
||||||||
| # Install dependencies | ||||||||
| cd ts && npm install | ||||||||
|
|
||||||||
| # Generate types from schema | ||||||||
| npm run generate:values | ||||||||
|
|
||||||||
| # Develop... | ||||||||
| # Edit src/index.ts | ||||||||
|
|
||||||||
| # Test render | ||||||||
| cd .. | ||||||||
| nelm chart render . --values my-values.yaml | ||||||||
|
|
||||||||
| # Publish | ||||||||
| nelm chart publish . --repo myrepo | ||||||||
| ``` | ||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We'll probably need something like this, but not now.