Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions docs/proposals/go-template-alternative/README.md
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 |
223 changes: 223 additions & 0 deletions docs/proposals/go-template-alternative/api.md
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
Copy link
Member

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.

}
```

**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 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's make it

interface File {
  Name string
  Data []byte
}

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
Copy link
Member

Choose a reason for hiding this comment

The 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 data, spec, or whatever else, or a combination of.

}

interface ObjectMeta {
name: string
namespace?: string
labels?: Record<string, string>
annotations?: Record<string, string>
ownerReferences?: OwnerReference[]
finalizers?: string[]
Comment on lines +115 to +116
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
ownerReferences?: OwnerReference[]
finalizers?: string[]

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[] {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd like to see something like RenderResult here instead of Manifest[].

interface RenderResult {
  ApiVersion: string // v1 for now
  Manifests: []Manifest
}

Copy link
Member

Choose a reason for hiding this comment

The 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' }],
},
}]),
]
}
```
102 changes: 102 additions & 0 deletions docs/proposals/go-template-alternative/cli.md
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]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
nelm chart ts init [path]
nelm chart init [path]

I guess it should make a minimal chart with templates directory by default. But if you pass flag --only-ts, then it will create ts directory in the [path], without touching anything else.

As a first version we can return an error, if --only-ts is not specified, with something like only the --only-ts mode is implemented for now. So the user can create boilerplate ts files, but we won't bother will fully implementing the command for now.

```

**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
Copy link
Member

Choose a reason for hiding this comment

The 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]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
nelm chart publish [path] [flags]
nelm chart upload [path] [flags]

```

**Behavior:**
1. Bundles TypeScript with embedded esbuild → `ts/vendor/bundle.js`
Copy link
Member

Choose a reason for hiding this comment

The 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]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
nelm release upgrade <release> <chart> [flags]

nelm release uninstall <release> [flags]
nelm release list [flags]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
nelm release list [flags]
nelm release rollback [flags]
nelm release plan intsall [flags]

```

## 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
```
Loading