Skip to content
KenkoGeekPublic

About

Timonel (Spanish for “helmsman”) is a TypeScript library to programmatically generate Helm charts using cdk8s.

Resources

Code of conduct

Contributing

Security policy

Stars

8 stars

Watchers

0 watching

Forks

Latest commit

 

History

809 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Timonel

License: MIT npm version Security CodeQL CI pnpm Node.js TypeScript Maintained by KenkoGeek

Timonel is a TypeScript library for generating complete Helm charts programmatically on top of cdk8s and cdk8s-plus. Kubernetes resources stay typed in application code while Timonel handles Helm chart structure, values files, helpers, environment variants, policy validation, and final chart output.

The library API is the primary product. The tl CLI is a convenience layer for scaffolding, synthesis, Helm validation, deployment, and umbrella-chart workflows.

Architecture

Timonel is intentionally typed-first. Use Kubernetes resource APIs in this order:

  1. cdk8s-plus-33 when it already provides the resource abstraction;
  2. generated cdk8s CRD constructs when an upstream schema is available;
  3. TypedCustomResource<TBody> or another focused typed Timonel abstraction when no generated construct fits.

Rutter does not expose raw manifest ingestion APIs. addManifest(), addTemplateManifest(), addConditionalManifest(), and raw synthesized-asset injection are intentionally absent. Helm helpers remain available for names, labels, computed values, and reusable fragments, but Timonel rejects helpers that embed complete Kubernetes manifests so resources cannot bypass the typed cdk8s/cdk8s-plus path through _helpers.tpl.

Requirements

  • Node.js ^22.22.2, ^24.15.0, or >=26.0.0
  • pnpm >=9
  • Helm 3 when using tl validate, tl deploy, or manual chart validation

Installation

pnpm add timonel cdk8s cdk8s-plus-33 constructs

npm and other compatible package managers can also install the package.

Quick start

import * as kplus from 'cdk8s-plus-33';
import { Rutter } from 'timonel';

const chart = new Rutter({
  meta: {
    name: 'my-app',
    version: '1.0.0',
    description: 'Typed Helm chart generated by Timonel',
  },
  defaultValues: {
    environment: 'production',
  },
});

const deployment = new kplus.Deployment(chart.getChart(), 'App', {
  metadata: { name: 'my-app' },
  containers: [
    {
      name: 'app',
      image: 'nginx:1.27',
      portNumber: 80,
      resources: {
        cpu: { request: kplus.Cpu.millis(100) },
      },
    },
  ],
});

deployment.exposeViaService();

new kplus.HorizontalPodAutoscaler(chart.getChart(), 'AppHpa', {
  target: deployment,
  minReplicas: 1,
  maxReplicas: 5,
});

await chart.write('./dist/my-app');

The generated chart contains Chart.yaml, values.yaml, templates/, _helpers.tpl, and .helmignore.

Validate the final Helm output:

helm lint ./dist/my-app
helm template my-app ./dist/my-app

Rutter

Rutter owns a cdk8s Chart and converts its resource tree into Helm chart assets.

const chart = new Rutter({
  meta: { name: 'orders', version: '1.0.0' },
  namespace: 'orders',
  defaultValues: {
    replicas: 2,
  },
  envValues: {
    production: {
      replicas: 4,
    },
  },
});

Useful methods include:

API Purpose
getChart() Access the real cdk8s Chart for native typed constructs
when(condition, resource) Conditionally render a complete typed resource
bindHelmValue(resource, pointer, value) Bind a typed Helm value to a synthesized scalar field
write(outDir) Synthesize and write the complete Helm chart
toSynthArray() Asynchronously synthesize Helm assets
getMeta() Read chart metadata
getDefaultValues() Read default values
getEnvValues() Read environment-specific values
addChartFile(asset) Package an arbitrary non-manifest chart file
getChartFiles() Read configured non-manifest chart files

toSynthArraySync() remains for compatibility and is deprecated. It cannot be used with a policy engine.

Packaged chart files

Use chartFiles or addChartFile() for non-manifest files that must ship inside the Helm chart, such as JSON consumed through .Files.Get, scripts, dashboards, or binary assets:

const chart = new Rutter({
  meta: { name: 'orders', version: '1.0.0' },
  chartFiles: [
    {
      destination: 'files/service-inputs.json',
      content: JSON.stringify({ queue: 'orders' }),
    },
  ],
});

chart.addChartFile({
  destination: 'files/apply.mjs',
  content: 'export default true;\n',
});

Destinations are chart-relative, traversal is rejected, and generated chart files such as Chart.yaml cannot be overwritten. Uint8Array content is supported for binary files.

Existing construct tree

Pass scope when Timonel should participate in an existing cdk8s/constructs tree:

import { App } from 'cdk8s';
import * as kplus from 'cdk8s-plus-33';
import { Rutter } from 'timonel';

const app = new App();
const chart = new Rutter({
  scope: app,
  meta: { name: 'shared-tree', version: '1.0.0' },
});

new kplus.ConfigMap(chart.getChart(), 'Config', {
  metadata: { name: 'shared-tree' },
  data: { mode: 'production' },
});

Conditional typed resources

Gate a complete cdk8s/cdk8s-plus resource with a typed Helm values condition without switching to a manifest fallback:

import * as kplus from 'cdk8s-plus-33';
import { Rutter, valuesRef } from 'timonel';

const v = valuesRef<{ restart: { enabled: boolean } }>();
const chart = new Rutter({
  meta: { name: 'orders', version: '1.0.0' },
  defaultValues: { restart: { enabled: true } },
});

const account = new kplus.ServiceAccount(chart.getChart(), 'RestartAccount', {
  metadata: { name: 'restart-manager' },
});

chart.when(v.restart.enabled, account);

when() wraps the synthesized resource with a Helm if block while keeping the resource itself on the typed construct path.

Type-safe Helm values

valuesRef<T>() exposes Helm values using the shape of a TypeScript type.

import { valuesRef } from 'timonel';

interface Values {
  environment: string;
  replicas: number;
  image: {
    repository: string;
    tag: string;
  };
  autoscaling: {
    enabled: boolean;
  };
  env: Array<{
    name: string;
    value: string;
  }>;
  envByName: Record<string, string>;
  selectedEnvKey: string;
}

const v = valuesRef<Values>();

const imageTag = v.image.tag.quote();
const multipleReplicas = v.replicas.gt(1);
const production = v.environment.eq('production');
const fixedTag = v.image.tag.default('1.0.0');

const env = v.env.range((item) => ({
  name: item.name,
  value: item.value,
}));

const selectedEnv = v.envByName.index(v.selectedEnvKey);
const hasSelectedEnv = v.envByName.hasKey(v.selectedEnvKey);
const envEntries = v.envByName.rangeEntries((key, value) => ({
  name: key,
  value: value.quote(),
}));

const replicasField = v.replicas.if(v.autoscaling.enabled.not(), v.replicas);

The compiler rejects value paths that do not exist in Values. String-keyed maps also support rangeEntries(), typed index(), and hasKey() with either literal keys or HelmValueRef<string> keys. Dynamic map lookups are anchored to Helm's root context, so they remain valid inside range() and with() scopes.

Reserved values keys

Names used by the proxy API, such as default, range, and with, and root helpers such as release, chart, and capabilities, are accessed with the typed at() method when those names also exist in your values schema:

interface Values {
  release: { name: string };
  settings: { default: string };
}

const v = valuesRef<Values>();

const releaseName = v.at('release').name;
const defaultSetting = v.settings.at('default');

at() accepts only keys from the current TypeScript values type.

Custom resources

When cdk8s-plus does not expose an extension API, keep normal construct usage with TypedCustomResource<TBody> rather than falling back to a manifest body:

import { TypedCustomResource, valuesRef, type HelmExpression } from 'timonel';

interface ServiceMonitorBody {
  spec: {
    selector: { matchLabels: Record<string, string> };
    endpoints: Array<{ port: string; interval: string | HelmExpression }>;
  };
}

const v = valuesRef<{ monitoring: { interval: string } }>();

new TypedCustomResource<ServiceMonitorBody>(chart.getChart(), 'OrdersServiceMonitor', {
  apiVersion: 'monitoring.coreos.com/v1',
  kind: 'ServiceMonitor',
  metadata: { name: 'orders' },
  body: {
    spec: {
      selector: { matchLabels: { app: 'orders' } },
      endpoints: [{ port: 'http', interval: v.monitoring.interval.toExpression() }],
    },
  },
});

The generic body is preserved in the published declarations, so invalid CRD fields fail consumer compilation. It also supports APIs with non-spec top-level fields, such as OpenShift SCC, by modeling those fields in TBody.

For CRDs generated with cdk8s import, instantiate the generated construct directly under chart.getChart(). Timonel discovers the resulting ApiObject during normal synthesis with no adapter. Prefer generated CRD classes when an upstream schema is available; use TypedCustomResource<TBody> when you own or maintain the TypeScript contract.

Prometheus Operator resources that previously existed as manifest-producing helpers now have typed ServiceMonitor and PrometheusRule constructs exported by Timonel.

Do not switch a standard Kubernetes resource to raw YAML merely because one field contains Helm logic. Prefer typed constructs and Timonel's Helm-value helpers where they fit.

Umbrella charts

UmbrellaRutter combines generated Timonel subcharts, remote Helm dependencies, and already-vendored local charts:

import * as kplus from 'cdk8s-plus-33';
import { Rutter, UmbrellaRutter } from 'timonel';

function serviceChart(name: string): Rutter {
  const chart = new Rutter({
    meta: { name, version: '1.0.0' },
  });

  new kplus.ConfigMap(chart.getChart(), 'Config', {
    metadata: { name: `${name}-config` },
  });

  return chart;
}

const umbrella = new UmbrellaRutter({
  meta: {
    name: 'commerce',
    version: '1.0.0',
  },
  subcharts: [
    { name: 'catalog', version: '1.0.0', rutter: serviceChart('catalog') },
    {
      name: 'ingress-nginx',
      version: '4.15.1',
      repository: 'https://kubernetes.github.io/ingress-nginx',
      condition: 'ingress-nginx.enabled',
    },
    {
      name: 'fluent-bit',
      version: '1.2.3',
      sourceDirectory: './vendor/fluent-bit',
      condition: 'fluent-bit.enabled',
    },
  ],
});

await umbrella.write('./dist/commerce');

Entries with rutter are generated into charts/. Remote dependency-only entries are written only to the parent Chart.yaml for Helm dependency tooling. Entries with sourceDirectory are copied verbatim into charts/<name> without Timonel regenerating the third-party chart; symbolic links are rejected during the copy to avoid packaging paths outside the vendored source tree.

The CLI also supports dependency and inline umbrella synthesis modes.

AWS and EKS helpers

Timonel includes focused helpers for Kubernetes-side AWS integrations:

  • EBS and EFS StorageClass resources;
  • IRSA and ECR-oriented ServiceAccount resources;
  • ALB Ingress configuration;
  • Karpenter NodePool, NodeClaim, and EC2NodeClass resources;
  • convenience NodePool builders for disruption and scheduling policies.

Example:

chart.addAWSIRSAServiceAccount({
  name: 'orders',
  roleArn: 'arn:aws:iam::123456789012:role/orders',
});

These APIs generate Kubernetes resources. They do not create IAM roles, storage systems, ECR repositories, controllers, cluster networking, or Karpenter CRDs in AWS.

Policy Engine

The optional Policy Engine validates synthesized Kubernetes manifests through user-supplied plugins before Timonel writes the Helm chart.

import type { PolicyPlugin } from 'timonel';
import { PolicyEngine, Rutter } from 'timonel';

const requireMetadataName: PolicyPlugin = {
  name: 'require-metadata-name',
  version: '1.0.0',
  async validate(manifests) {
    return manifests.flatMap((manifest) => {
      if (!manifest || typeof manifest !== 'object') return [];

      const object = manifest as {
        kind?: string;
        metadata?: { name?: string };
      };

      if (object.metadata?.name) return [];

      return [
        {
          plugin: 'require-metadata-name',
          severity: 'error' as const,
          message: `${object.kind ?? 'Resource'} must have metadata.name`,
        },
      ];
    });
  },
};

const policyEngine = new PolicyEngine({
  timeout: 5000,
  parallel: true,
});

await policyEngine.use(requireMetadataName);

const chart = new Rutter({
  meta: { name: 'validated', version: '1.0.0' },
  policyEngine,
});

Policy options include plugin timeouts, retries, graceful degradation, caching, parallel execution, inline plugin configuration, schema validation, and environment-variable configuration.

ConfigurationLoader currently accepts file-related options, but file-backed policy configuration loading is not implemented. Do not rely on configurationFiles to load plugin configuration from disk in the current release.

Environment variable configuration

The environment-variable loader reads YAML/JSON configuration and generates Kubernetes env entries:

import { loadAndGenerateEnvVars } from 'timonel';

const env = loadAndGenerateEnvVars({
  configPath: './env-config.yaml',
  defaultScope: 'global.env',
});

It can generate literal Helm values and Kubernetes secretKeyRef entries. It does not fetch values from Vault, AWS Secrets Manager, or other external secret stores.

CLI

Run the locally installed CLI with pnpm exec tl or expose the package binary through your package manager.

tl init <chart-name>
tl synth [chartDir] [outDir]
tl validate
tl deploy <release> [namespace]
tl templates
tl umbrella init <name>
tl umbrella add <subchart>
tl umbrella synth [outDir]

Common flags:

--dry-run
--silent
--env <environment>
--set <key=value>
--mode <dependencies|inline>
--help, -h

--dry-run validates the requested operation and reports what would happen without writing chart files or invoking Helm. Combine it with --silent when the preview itself should also be suppressed.

tl validate and tl deploy execute the Helm CLI, so Helm must be installed and the current working directory must point at the chart you intend to validate or deploy.

Generated chart structure

A normal chart written by Rutter looks like:

my-app/
├── Chart.yaml
├── values.yaml
├── values-production.yaml   # when envValues.production exists
├── .helmignore
└── templates/
    ├── _helpers.tpl
    ├── App.yaml
    ├── AppHpa.yaml
    └── ...

Resource filenames use stable cdk8s construct identifiers where possible.

Documentation

The GitHub wiki contains the full documentation set:

Development

pnpm install --frozen-lockfile
pnpm ci:check
pnpm test:unit
pnpm test:integration
pnpm test:coverage
pnpm md:lint
pnpm doc:coverage:validate
pnpm security:audit
pnpm pack

Contribution and agent rules are defined in AGENTS.md.

Releases

Development follows a trunk-based model centered on main:

  • pull requests target main;
  • successful main CI can publish an npm canary build;
  • stable publication is an explicit workflow with production approval;
  • npm publication uses Trusted Publishing/OIDC and provenance.

See the release guide for the current workflow and operational constraints.

License

MIT

About

Timonel (Spanish for “helmsman”) is a TypeScript library to programmatically generate Helm charts using cdk8s.

Resources

Code of conduct

Contributing

Security policy

Stars

8 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages