Skip to content

systemstart/many-templates

Repository files navigation

Many Templates

Many Templates

A swiss army knife for Kubernetes manifests — multi-stage pipelines for fetching, processing, and rendering.

CI codecov Go Report Card License: GPL-3.0 Release


many is a GitOps tool to process Kubernetes manifests from many sources in many formats.

Sources can live locally, in an OCI registry, behind an HTTPS URL, or in an OCM component --- many fetches and processes them in a pipeline.

many discovers .many.yaml pipeline definitions in a directory tree, executes them and writes the results to a output directory.

Each pipeline composes steps --- Go templating (with Sprig), Kustomize builds, Helm renders, file generation, and YAML splitting --- to turn a template repository into ready-to-apply Kubernetes manifests.

many can fan out outputs to many instances, check the examples.

Installation

go install github.com/systemstart/many-templates/cmd/many@latest

Or build from source:

make build   # produces ./many

Examples

1 --- Fetch and Render Go Templates

examples/source-https/ fetches a YAML file from a GitHub release and renders it as a Go template.

# examples/source-https/.many.yaml
pipeline:
  - name: fetch-and-render
    type: template
    source:
      # renovate: datasource=github-releases depName=kubernetes-sigs/metrics-server
      https: "https://github.com/kubernetes-sigs/metrics-server/releases/download/v0.7.2/components.yaml"
      sha256: "f103539a54ed72efe66616afc74a8bfaed651703cb3918797599046af5617441"
    template:
      files:
        include: ["**/*.yaml"]

The source on the step fetches files into the working directory before the step executes. Here an HTTPS source is downloaded (tarballs are auto-extracted), then all YAML files are rendered as Go templates with Sprig functions.

SHA-256 verification --- the sha256 field pins a source to a known checksum. When set, many verifies the download matches before proceeding. An empty string disables verification, useful during development. The checksum will be set if empty, unless -no-sha256-update is provided. Note: that doesn't update an existing checksum; to update, empty it manually first.

Renovate integration --- the # renovate: comment is a Renovate annotation. Renovate can be configured to update both the URL and the sha256 checksum.

many -input examples/source-https -output-directory ./output

2 --- Fetch, Split, and Kustomize

examples/kubevirt/ downloads upstream release YAML files, splits each into individual resources, and generates a kustomization.

# examples/kubevirt/source/.many.yaml (abbreviated --- 4 similar split steps)
pipeline:
  - name: split-kubevirt-operator
    type: split
    source:
      # renovate: datasource=github-releases depName=kubevirt/kubevirt
      - https: "https://github.com/kubevirt/kubevirt/releases/download/v1.7.2/kubevirt-operator.yaml"
        sha256: "b74106509bbce107a01b228083b4030890b2278cf918fe5c2cf380fdc2a27aef"
        path: build/
    split:
      input: build/kubevirt-operator.yaml
      by: resource
      outputDir: manifests/operator/
    exclude:
      - "build/**"

  # ... split-kubevirt-cr, split-cdi-operator, split-cdi-cr ...

  - name: create-kustomization
    type: kustomize-create
    kustomize-create:
      autodetect: true
      recursive: true

Each source fetches a release YAML into build/. The split step breaks it into one file per resource under manifests/, and exclude cleans up the intermediate build/ directory. The final kustomize-create step generates a kustomization.yaml referencing all discovered manifests:

output/
├── kustomization.yaml
└── manifests/
    ├── cdi/
    │   ├── cdi-cdi.yaml
    │   ├── deployment-cdi-operator.yaml
    │   └── ...
    └── operator/
        ├── kubevirt-kubevirt.yaml
        ├── deployment-virt-operator.yaml
        └── ...
many -input examples/kubevirt/source -output-directory ./output

3 --- Instances

examples/many-sites/ renders the same services for multiple environments. Each instance selects a subset of services and provides per-instance context values.

instances.yaml defines two instances with different service selections:

instances:
  - name: fediverse
    input: "services"
    output: fediverse.example/
    include: [dex, eso, lldap, mastodon, matrix, mobilizon, pixelfed]
    context:
      domain: "fediverse.example"
      siteName: "fediverse"

  - name: development
    input: "services"
    output: development.example/
    include: [dex, eso, lldap, forgejo, harbor, woodpecker]
    context:
      domain: "development.example"
      siteName: "development"

context.yaml provides shared configuration. String values can reference other context keys via Go template interpolation (single pass after merge):

subdomains:
  mastodon: "social"
  forgejo: "git"
  # ...

smtp:
  from: "noreply@{{ .domain }}"

eso:
  remotePathPrefix: "site/{{ .siteName }}"

Services follow two patterns. Helm-based services (dex, mastodon, ...) use kustomize-build with Helm and split the output:

# services/dex/.many.yaml
context:
  namespace: dex

pipeline:
  - name: render-templates
    type: template
    source:
      - file: kustomization.yaml
      - file: values.yaml
      - file: externalsecret.yaml
    template:
      files:
        include: ["**/*.yaml"]

  - name: build
    type: kustomize-build
    kustomize-build:
      enableHelm: true
      outputFile: kustomize-output.yaml

  - name: split-output
    type: split
    exclude: ["kustomize-output.yaml"]
    split:
      input: kustomize-output.yaml
      by: resource
      outputDir: manifests/

  - name: create-kustomization
    type: kustomize-create
    kustomize-create:
      autodetect: true
      recursive: true
      dir: manifests/

Raw-manifest services (lldap, woodpecker, ...) render templates and generate a kustomization:

# services/lldap/.many.yaml
pipeline:
  - name: render-templates
    type: template
    source:
      file: manifests/
      path: manifests/
    template:
      files:
        include: ["**/*.yaml"]

  - name: create-kustomization
    type: kustomize-create
    kustomize-create:
      autodetect: true
      recursive: true
      namespace: lldap

Context is merged in layers --- global (-context-file) → instance (context in instances.yaml) → pipeline-local (context in .many.yaml) --- with later layers overriding earlier ones via deep merge.

many \
  -input examples/many-sites \
  -output-directory output \
  -instances examples/many-sites/instances.yaml \
  -context-file examples/many-sites/context.yaml

Output:

output/
├── fediverse.example/
│   ├── dex/
│   │   ├── kustomization.yaml
│   │   └── manifests/...
│   ├── mastodon/...
│   └── ...
└── development.example/
    ├── dex/...
    ├── forgejo/...
    └── ...

.many.yaml Reference

A .many.yaml file placed anywhere in the input tree defines a pipeline. Below is the complete structure with all available fields:

# Optional: pipeline-local context variables, available as {{ .key }} in templates.
# Deep-merged on top of global and instance context (see Context).
context:
  key: "value"
  nested:
    key: "value"

# Required: at least one step.
pipeline:
  - name: step-name                     # required, must be unique within pipeline
    type: template                      # required: template | kustomize-build | kustomize-create
                                        #           helm | split | generate | copy

    # --- Source (optional) ---------------------------------------------------
    # Fetch files into the working directory before the step runs.
    # Single entry:
    source:
      https: https://example.com/v1.tar.gz
      sha256: "abc123..."               # 64-char hex; empty string disables verification
      path: subdir/                     # target subdirectory
    # Or a list of entries:
    source:
      - oci: ghcr.io/org/manifests:v1
      - file: ./local-overrides
        path: patches/

    # --- Exclude (optional) --------------------------------------------------
    # Glob patterns removed from the working directory after the step runs.
    exclude: ["build/**", "*.tmp"]

    # --- Step-type config (exactly one, matching `type`) ---------------------

    template:                           # type: template
      files:
        include: ["**/*.yaml"]          # default: ["**/*"]
        exclude: ["kustomization.yaml"] # default: []

    kustomize-build:                    # type: kustomize-build
      dir: "."                          # default: "."
      enableHelm: false                 # default: false
      outputFile: out.yaml              # required

    kustomize-create:                   # type: kustomize-create
      dir: "."                          # default: "."
      autodetect: true                  # at least one of autodetect / resources required
      recursive: true                   # requires autodetect
      resources: []                     # explicit resource list
      namespace: "staging"
      nameprefix: "acme-"
      namesuffix: "-v2"
      annotations: {}
      labels: {}

    helm:                               # type: helm
      chart: ./charts/my-app            # required
      releaseName: my-app               # required
      namespace: default                # default: "default"
      valuesFiles: ["values.yaml"]
      set: { image.tag: "v1.2.3" }
      outputFile: helm-output.yaml

    split:                              # type: split
      input: multi-doc.yaml             # required
      by: resource                      # kind | resource | group | kind-dir | custom
      outputDir: manifests/             # default: "."
      fileNameTemplate: ""              # required when by=custom
      canonicalKeyOrder: true           # default: true

    generate:                           # type: generate
      output: manifests/config.yaml     # required
      template: |                       # required, inline Go template
        apiVersion: v1
        kind: ConfigMap
        metadata:
          name: {{ .app_name }}

    copy:                               # type: copy
      files:
        include: ["manifests/**/*.yaml"] # default: ["**/*"]
        exclude: []
      dest: manifests/                  # default: "."

CLI Reference

Flag Description Default
-input, -input-directory Source directory (or remote URI) to process required
-output-directory Destination for rendered output required
-overwrite-output-directory Delete and recreate output directory false
-context-file Global context YAML file (removed from output if inside input) none
-max-depth Max directory recursion depth (-1 = unlimited, 0 = root only) -1
-processing Single .many.yaml to run (skips directory discovery) none
-instances Instances YAML file for matrix mode none
-env-file Load environment variables from the specified file none
-no-sha256-update Disable sha256 writeback to .many.yaml files false
-log-level debug, info, warn, error info
-logging-type json, text, tint tint
-version Print version and exit

Discovery Mode (default)

When neither -processing nor -instances is given, many walks the input directory collecting all .many.yaml files, sorts them by depth (parents before children), and executes each pipeline independently.

many \
  -input ./infrastructure \
  -output-directory ./output \
  -max-depth 2

Single Pipeline Mode

Run one specific .many.yaml (must be within the input directory):

many \
  -processing ./infrastructure/cert-manager/.many.yaml \
  -input ./infrastructure \
  -output-directory ./output

Instances Mode

Run the same input tree multiple times with different contexts, producing separate output directories. Incompatible with -processing.

many \
  -input ./services \
  -output-directory ./output \
  -instances instances.yaml \
  -context-file global.yaml

Instance file format:

instances:
  - name: prod-east
    output: prod-east/          # required --- subdirectory of -output-directory
    input: ""                   # optional --- subdirectory of -input (or remote URI)
    include: [ api, frontend ]    # optional --- filter immediate subdirectories (empty = all)
    context: # optional --- merged on top of global context
      region: us-east-1
      replicas: 3

For each instance, many copies the input tree (filtered by include), merges global + instance context, discovers and runs pipelines, then removes .many.yaml files. If an instance fails, remaining instances still run. The exit code is non-zero if any instance failed.

Pull

Fetch a remote source directly to a local directory, without running any pipeline:

many pull <ref> <dir>

The <ref> supports the same schemes as -input: bare paths, file://, oci://, https://, and ocm://.

many pull oci://ghcr.io/myorg/manifests:v1 ./local-copy
many pull https://example.com/archive.tar.gz ./extracted

Pipeline Steps

Each step has a name (unique within the pipeline) and a type. Steps execute sequentially.

Common Step Fields

Every step supports these fields regardless of type:

Field Description Default
name Unique identifier within the pipeline required
type Step type: template, kustomize-build, kustomize-create, helm, split, generate, copy required
source Fetch files before the step runs (single entry or list --- see Sources) none
exclude Glob patterns to remove from the working directory after the step completes []

In addition, each step has a type-specific config block (e.g. template:, split:) documented below.

template

Renders files in-place using Go's text/template with Sprig functions.

- name: render
  type: template
  template:
    files:
      include: [ "**/*.yaml" ]
      exclude: [ "kustomization.yaml" ]
Field Description Default
files.include Glob patterns for files to template ["**/*"]
files.exclude Glob patterns for files to skip []

Globs are relative to the pipeline directory and support ** for recursive matching via doublestar.

kustomize-build

Runs kustomize build and captures the multi-document YAML output. Requires kustomize on PATH.

- name: build
  type: kustomize-build
  kustomize-build:
    enableHelm: true
    outputFile: kustomize-output.yaml
Field Description Default
dir Directory containing kustomization.yaml "."
enableHelm Pass --enable-helm to kustomize false
outputFile File to write the build output to required

kustomize-create

Runs kustomize create to auto-generate a kustomization.yaml file. Useful when pulling sources from OCI/HTTPS and you want to avoid hand-writing the kustomization file. Requires kustomize on PATH.

- name: create-kustomization
  type: kustomize-create
  kustomize-create:
    dir: "."
    autodetect: true
    recursive: true
    resources:
      - deployment.yaml
      - ../base
    namespace: "staging"
    nameprefix: "acme-"
    namesuffix: "-v2"
    annotations:
      app.kubernetes.io/managed-by: many
    labels:
      env: production
Field Description Default
dir Directory in which to create kustomization.yaml "."
autodetect Pass --autodetect to discover resources false
recursive Pass --recursive (requires autodetect) false
resources Explicit list of resources to include []
namespace Set namespace in the generated kustomization none
nameprefix Set name prefix none
namesuffix Set name suffix none
annotations Map of annotations to add {}
labels Map of labels to add {}

At least one of autodetect or resources must be set. recursive requires autodetect to be enabled.

helm

Runs helm template to render a chart. Requires helm on PATH.

- name: render-chart
  type: helm
  helm:
    chart: ./charts/my-app
    releaseName: my-app
    namespace: default
    valuesFiles: [ "values.yaml" ]
    set:
      image.tag: "v1.2.3"
Field Description Default
chart Path to chart directory or chart reference required
releaseName Helm release name required
namespace Target namespace "default"
valuesFiles List of values files []
set Map of --set overrides {}
outputFile File to write the rendered output to none

generate

Creates a file from an inline Go template rendered against the pipeline context. Unlike template (which renders existing files in-place), generate synthesizes new files purely from context data.

- name: gen-config
  type: generate
  generate:
    output: manifests/config.yaml
    template: |
      apiVersion: v1
      kind: ConfigMap
      metadata:
        name: {{ .app_name }}
      data:
        domain: {{ .domain }}
Field Description Default
output Output file path relative to the pipeline directory required
template Inline Go template string required

Parent directories are created automatically.

copy

Copies files from the original source/input directory into the pipeline working directory. Useful for pulling in static manifests, CRs, or other files that don't need templating.

- name: copy-cr-files
  type: copy
  copy:
    files:
      include: ["manifests/**/*.yaml"]
      exclude: ["manifests/tmp/**"]
    dest: manifests/
Field Description Default
files.include Glob patterns for files to copy from the source directory ["**/*"]
files.exclude Glob patterns for files to skip []
dest Destination subdirectory within the working directory "."

Files are copied preserving their relative directory structure. Globs use doublestar syntax (** for recursive matching).

split

Takes a multi-document YAML file and splits it into individual files.

- name: split-manifests
  type: split
  split:
    input: kustomize-output.yaml
    by: resource
    outputDir: manifests/
Field Description Default
input File path to the multi-document YAML to split required
by Splitting strategy (see below) "kind"
outputDir Directory to write split files into "."
fileNameTemplate Go template for file paths (only with custom strategy) ---
canonicalKeyOrder Reorder keys: apiVersion, kind, metadata first true

Splitting strategies:

Strategy Layout
kind One file per Kind (deployment.yaml, service.yaml). Multiple resources of the same Kind share a file.
resource One file per resource (deployment-api.yaml, service-api.yaml).
group Directories per API group (apps/deployment-api.yaml, core/service-api.yaml).
kind-dir Directories per Kind, pluralized (deployments/api.yaml, services/api.yaml).
custom File paths from a Go template: `fileNameTemplate: "{{ .metadata.namespace }}/{{ .kind

Sources

Each step can declare a source to fetch files into the working directory before execution. A source is a single entry or a list of entries, applied in order:

source:
  https: https://example.com/archive.tar.gz
  sha256: "abc123..."
  path: subdir/
source:
  - oci: ghcr.io/myorg/manifests:v1.0.0
  - file: ./local-overrides
    path: patches/
Scheme Description Example
file Local file or directory (relative to .many.yaml) file: ../shared/postgres.yaml
https URL to a single file or tarball (auto-extracted) https: https://example.com/v1.tar.gz
oci OCI image reference oci: ghcr.io/myorg/config:v1
ocm OCM component version ocm: github.com/myorg/component//res
helm Helm chart (requires repo) helm: my-chart
Option Description
path Target subdirectory to place fetched files into
sha256 SHA-256 checksum for https sources (see below)
recursive Recursively resolve OCM references (OCM only)
repo Helm chart repository URL (Helm only)
version Helm chart version (Helm only)

SHA-256 verification --- when sha256 is set to a hex digest, many verifies the downloaded content matches before proceeding. On the first run you can leave it empty (sha256: "") to disable verification; many computes the actual checksum and writes it back to the .many.yaml file so you can pin it. Use -no-sha256-update to disable this writeback. Once pinned, any mismatch (e.g. an upstream release change) fails the pipeline. Combined with a # renovate: comment, Renovate updates both the URL and the checksum automatically.

Context

Pipeline-Local Context

Each .many.yaml can define a context block. These values are available as template data in all template and generate steps:

context:
  domain: "example.com"
  replicas: 3

pipeline:
  - name: render
    type: template

Templates reference values with {{ .domain }}, {{ .replicas }}, etc.

Global Context

A global context file provided via -context-file applies to all pipelines:

many -input ./infra -output-directory ./output -context-file global.yaml

Context Merge Order

Context is merged in layers (later layers override earlier ones, deep-merged for nested maps):

  1. -context-file (global)
  2. Instance context (instances mode only)
  3. .many.yaml context (pipeline-local)
# global.yaml
domain: "default.example.com"
database:
  host: "db.default"

# .many.yaml --- domain overridden, database.host inherited, database.port added
context:
  domain: "app.example.com"
  database:
    port: 5432

Context Value Interpolation

After merging, all string values are rendered as Go templates against the full context (single pass). This lets context values reference other context values:

domain: "example.com"
forgejo_url: "https://forgejo.{{ .domain }}"
smtp_from: "noreply@{{ .domain }}"

After interpolation, forgejo_url becomes https://forgejo.example.com. Sprig functions are available (e.g. {{ .name | upper }}). Non-string values (ints, bools) are left unchanged.

Execution Model

  1. The source tree is copied to the output directory.
  2. .many.yaml files are discovered, sorted by directory depth (parents first).
  3. Each pipeline executes in-place within the output tree.
  4. template steps modify files in-place. kustomize-build/helm steps write their output to a file (outputFile). split steps read a multi-document YAML file and write individual files.
  5. .many.yaml files and the context file are removed from the output.

A failing step aborts its pipeline. Other pipelines continue. The exit code is non-zero if any pipeline failed.

Environment Variables

Use -env-file to load environment variables from a file via godotenv:

many -env-file .env -input ./infra -output-directory ./output

Loaded variables are available to kustomize-build, kustomize-create, and helm steps via environment inheritance.

About

A pipeline-based CLI tool for processing Kubernetes manifests and configuration files.

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors