Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Introduction

nyl (pronounced like “nil”) is a Kubernetes manifest generator with Helm integration, designed to simplify the management of Kubernetes resources through a powerful templating system.

Goals

nyl aims to provide:

  • High performance manifest generation
  • Lightweight binary for easy distribution
  • Clean, maintainable architecture for future development
  • Powerful templating with Helm integration

Features

  • Configuration loading (YAML, JSON)
  • File discovery with upward directory traversal
  • nyl validate command with strict mode
  • nyl new project command for project scaffolding
  • nyl new component command for component scaffolding
  • Helm integration and component discovery
  • Template rendering with Jinja2
  • Kubernetes operations (diff, apply)
  • Release state management with revision tracking

Key Features

  • Safety: Memory safety without garbage collection
  • Performance: Compiled binaries with minimal runtime overhead
  • Concurrency: Efficient concurrent operations
  • Reliability: Strong type system catches errors at compile time
  • Rich ecosystem: Extensive library support

Architecture

nyl is structured into several key modules:

  • config: Project configuration loading and validation
  • cli: Command-line interface and argument parsing
  • template: Jinja2 template rendering (Phase 3)
  • kubernetes: Kubernetes client integration (Phase 4)
  • resources: Resource definitions and transformations
  • generator: Manifest generation pipeline (Phase 3+)

Getting Started

This guide will help you get started with nyl-rs.

Installation

From Source

cd nyl-rs
cargo build --release

The binary will be available at target/release/nyl.

Install Locally

cargo install --path .

This installs nyl to ~/.cargo/bin/nyl.

Quick Start

1. Create a New Project

nyl new project my-app
cd my-app

This creates:

  • nyl.toml - Project configuration
  • components/ - Directory for components

2. Add a Component

nyl new component v1.example.io MyApp

This creates a new component at components/v1.example.io/MyApp/ with:

  • Chart.yaml - Helm chart metadata
  • values.yaml - Default values
  • values.schema.json - JSON schema for validation
  • templates/deployment.yaml - Kubernetes deployment template

3. Validate Your Project

nyl validate

Output:

✓ Found project config: /path/to/my-app/nyl.toml
✓ Components search path exists: /path/to/my-app/components
✓ Helm chart search path exists: /path/to/my-app

✓ Validation passed

4. Strict Validation

For CI/CD pipelines, use strict mode to treat warnings as errors:

nyl validate --strict

Project Structure

my-app/
├── nyl.toml                  # Project configuration
├── components/               # Component definitions
│   └── v1.example.io/
│       └── MyApp/
│           ├── Chart.yaml
│           ├── values.yaml
│           ├── values.schema.json
│           └── templates/
│               └── deployment.yaml
└── charts/                   # Optional: additional Helm chart search path

Next Steps

Configuration

nyl project settings are loaded from a single file: nyl.toml.

Configuration File Discovery

nyl searches for nyl.toml starting in the current directory and walking up parent directories.

Configuration Structure

nyl.toml supports:

  • [project] for project settings
  • [profile.values.<name>] for profile values used in templates
[project]
components_search_paths = ["components"]
helm_chart_search_paths = ["."]

[profile.values.default]
namespace = "default"
replicas = 1

[profile.values.dev]
namespace = "dev"
replicas = 2

[project.aliases]
"myapi.io/v1/MyKind" = "oci://mycharts.org/my-kind@1.0.0"

Settings

project.components_search_paths

  • Type: array of path strings
  • Default: ["components"]
  • Meaning: Direct roots for component charts. Each root is scanned as:
    • <root>/<apiVersion>/<kind>/Chart.yaml

project.helm_chart_search_paths

  • Type: array of path strings
  • Default: ["."]
  • Meaning: Search paths used for Helm chart name resolution.

project.aliases

  • Type: table/map of string to string
  • Default: empty table
  • Key format: <apiVersion>/<kind>
  • Value format: same component shortcut format accepted in kind (<repository>[#<name>][@<version>]) or a local component path
  • Meaning: Treat matching resources as component-style resources and resolve them directly to the configured target instead of components_search_paths.

profile.values.<name>

  • Type: object/map
  • Default: none
  • Meaning: Template values for profile <name> exposed as values.* during rendering.
  • Selection: --profile <name> selects a profile. If omitted, Nyl uses default when available.

Example:

[profile.values.dev]
my_value = "Hello!"
replicas = 1

[profile.values.prod]
my_value = "World!"
replicas = 3

Template usage:

{{ values.my_value }}
{% if profile == "dev" %}
# dev-specific logic
{% endif %}

Example:

[project]
components_search_paths = ["components"]

[project.aliases]
"myapi.io/v1/MyKind" = "oci://registry-1.docker.io/bitnamicharts/nginx@18.2.4"
"platform.example.io/v1/IngressStack" = "https://charts.bitnami.com/bitnami#nginx@18.2.4"

Then this manifest is resolved through the alias target:

apiVersion: myapi.io/v1
kind: MyKind
metadata:
  name: my-nginx
spec:
  replicaCount: 2

Path Resolution

Relative paths are resolved against the directory that contains nyl.toml.

Example (/home/user/my-app/nyl.toml):

[project]
components_search_paths = ["components", "/opt/shared-components"]
helm_chart_search_paths = [".", "charts"]

Resolves to:

  • components_search_paths:
    • /home/user/my-app/components
    • /opt/shared-components
  • helm_chart_search_paths:
    • /home/user/my-app
    • /home/user/my-app/charts

Validation

Use:

nyl validate

Checks:

  • nyl.toml discovery and parse validity
  • existence of configured components_search_paths
  • existence of configured helm_chart_search_paths

Use strict mode in CI:

nyl validate --strict

JSON Schema

Generate schema from the current binary:

nyl generate schema config

Published schema artifact:

Component System

Nyl’s component system lets you map Kubernetes-style resources to Helm charts with a predictable lookup model.

For most teams, the default workflow is:

  1. Define local component charts under components/<apiVersion>/<kind>/.
  2. Reference those components from manifests with:
    • apiVersion: components.nyl.niklasrosenstein.github.com/v1
    • kind: <apiVersion>/<kind>
  3. Put Helm values into spec.

What a Component Resource Does

A Component resource is a compact wrapper around Helm rendering:

  • kind identifies which chart to render.
  • metadata.name is used as Helm release name.
  • metadata.namespace is used as Helm release namespace (defaults to default if omitted).
  • spec is forwarded as Helm values.

Example:

apiVersion: components.nyl.niklasrosenstein.github.com/v1
kind: example/v1/Nginx
metadata:
  name: web
  namespace: default
spec:
  replicaCount: 2

Choosing Between Component, HelmChart, and Aliases

  • Use local Component resources for reusable, repo-owned building blocks.
  • Use HelmChart for explicit chart definitions in platform-level manifests.
  • Use project.aliases when you want stable domain apiVersion/kind names decoupled from chart source.

See:

Authoring Local Components

Local components are standard Helm charts placed under configured component search roots.

Directory Contract

Each component must exist at:

<components_search_path>/<apiVersion>/<kind>/
  Chart.yaml
  values.yaml
  templates/

With default configuration:

[project]
components_search_paths = ["components"]

A component example/v1/Nginx resolves to:

components/example/v1/Nginx/Chart.yaml

Create a New Component

nyl new component example/v1 Nginx

This scaffolds:

components/example/v1/Nginx/
  Chart.yaml
  values.yaml
  values.schema.json
  templates/deployment.yaml

Consume the Component in a Manifest

apiVersion: components.nyl.niklasrosenstein.github.com/v1
kind: example/v1/Nginx
metadata:
  name: web
  namespace: default
spec:
  replicaCount: 3
  image:
    repository: nginx

spec is passed to Helm as values, merged on top of chart defaults from values.yaml.

Operational Notes

  • Keep component manifests separate from chart templates. nyl render accepts a file path, not a directory.
  • Use nyl validate --strict in CI to catch missing search paths early.
  • Prefer stable apiVersion/kind naming for internal component APIs.

Resolution & Lookup Rules

This page describes how Nyl resolves component resources into charts.

Local Lookup

For Component resources with local kinds, Nyl checks component search roots in order:

  1. Resolve project.components_search_paths from nyl.toml.
  2. Build candidate path <root>/<kind>/Chart.yaml.
  3. Use the first existing match.

For kind: example/v1/Nginx, Nyl looks for:

<root>/example/v1/Nginx/Chart.yaml

Relative search paths are resolved relative to the directory containing nyl.toml.

Search Path Precedence

components_search_paths = ["components", "/opt/shared-components"] means:

  1. Prefer repo-local components under components/.
  2. Fall back to /opt/shared-components.

The first matching component wins.

Alias Resolution

Before local lookup, Nyl checks project.aliases for a <apiVersion>/<kind> match.

If an alias exists, the alias target is resolved directly and local lookup is skipped for that resource.

Example:

[project.aliases]
"platform.example.io/v1/IngressStack" = "oci://registry-1.docker.io/bitnamicharts/nginx@18.2.4"

Expansion and Filtering Caveat

-c/--component filters top-level input resources before expansion.

Example:

  • -c ConfigMap does not include ConfigMaps generated by Component or HelmChart expansion.
  • Use -c HelmChart or match the component resource in input when you need those expansions.

Remote Shortcuts & Aliases

Local components should be the default for repo-owned APIs. Use remote shortcuts and aliases when you need indirection or external chart sources.

Shortcut Grammar

Component kind shortcuts use:

<base>[#<name>][@<version>]
  • base: repository URL or local path
  • name: chart name or Git subpath
  • version: chart version or Git ref

Remote bases are recognized by prefix:

  • http:// or https://
  • oci://
  • git+

Remote Shortcut Examples

HTTP Helm repo:

apiVersion: components.nyl.niklasrosenstein.github.com/v1
kind: https://charts.bitnami.com/bitnami#nginx@18.2.4
metadata:
  name: nginx-http
spec:
  replicaCount: 2

OCI:

apiVersion: components.nyl.niklasrosenstein.github.com/v1
kind: oci://registry-1.docker.io/bitnamicharts/nginx@18.2.4
metadata:
  name: nginx-oci

Git:

apiVersion: components.nyl.niklasrosenstein.github.com/v1
kind: git+https://github.com/prometheus-community/helm-charts#charts/prometheus@prometheus-25.28.0
metadata:
  name: prometheus
  namespace: monitoring

Git URL Parsing Edge Case

Parsing is right-to-left for separators to avoid misinterpreting @ in SSH-style URLs.

Example:

git+git@github.com:org/repo#charts/app@main

Interpreted as:

  • base = git+git@github.com:org/repo
  • name = charts/app
  • version = main

Aliases for Stable APIs

Define stable domain resource types in nyl.toml:

[project.aliases]
"myapi.io/v1/MyKind" = "oci://registry-1.docker.io/bitnamicharts/nginx@18.2.4"

Then write manifests with standard apiVersion/kind:

apiVersion: myapi.io/v1
kind: MyKind
metadata:
  name: my-kind-release
spec:
  replicaCount: 2

When to Use What

Nyl supports three common patterns:

PatternHow you write itBest forMain benefit
Full HelmChart resourcekind: HelmChart + spec.chart.*Explicit platform manifestsMaximum clarity and explicit chart fields
Component shortcutapiVersion: components... + kind: <shortcut>Fast authoring near chart sourceMinimal boilerplate
project.aliases named kindsapiVersion/kind mapped in nyl.tomlDomain APIs across teamsStable semantic kinds decoupled from chart location
  • Local component path: internal platform APIs and reviewable chart source.
  • Remote shortcut: quick direct dependency on an external chart source.
  • Alias: stable contract for app teams while platform controls chart target centrally.

Troubleshooting Components

Component Not Found

Symptom:

  • Render/diff/apply fails to resolve a component kind.

Checks:

  1. Confirm apiVersion is exactly components.nyl.niklasrosenstein.github.com/v1 for Component resources.
  2. Confirm kind matches <apiVersion>/<kind> directory layout.
  3. Confirm Chart.yaml exists at <components_search_path>/<kind>/Chart.yaml.
  4. Confirm components_search_paths are valid from the nyl.toml location.

Example expected path for kind: example/v1/Nginx:

components/example/v1/Nginx/Chart.yaml

Wrong Chart Selected

Symptom:

  • Nyl resolves a shared component when repo-local component was expected.

Cause:

  • Search paths are ordered; first match wins.

Fix:

  • Put preferred path earlier in project.components_search_paths.

Alias Not Applied

Symptom:

  • Resource is treated as a normal Kubernetes resource instead of a chart-backed resource.

Checks:

  1. Alias key is exact: <apiVersion>/<kind>.
  2. Manifest apiVersion and kind exactly match key case and spelling.
  3. Alias target uses a valid local path or remote shortcut syntax.

Filtering Doesn’t Show Generated Resources

Symptom:

  • -c ConfigMap misses ConfigMaps produced by Helm rendering.

Cause:

  • Filtering runs before expansion.

Fix:

  • Filter top-level chart resources (-c HelmChart or matching component resources), then inspect rendered output.

Fast Validation Loop

Use:

nyl validate --strict
nyl render <manifest-file>

This catches path/config issues before cluster-facing commands.

Git Integration

Nyl provides built-in Git repository management for fetching Helm charts and scanning application manifests from Git repositories. This enables declarative infrastructure management without requiring manual repository cloning.

Features

  • Bare repositories: Minimal disk usage with shared object store
  • Worktrees: Isolated checkouts for different refs (branches, tags, commits)
  • Lazy fetching: Refs fetched first, objects downloaded on-demand
  • Automatic caching: Repositories cached locally for fast subsequent access
  • Concurrent access: Multiple refs from the same repository can be checked out simultaneously

Cache Directory

Nyl stores Git repositories in a cache directory to avoid redundant clones and improve performance.

Configuration

The cache directory is determined by:

  1. NYL_CACHE_DIR environment variable (preferred)
  2. .nyl/cache/ in the current directory (fallback)

Example:

export NYL_CACHE_DIR=/var/cache/nyl
nyl render app.yaml

Cache Structure

$NYL_CACHE_DIR/git/
├── bare/
│   ├── {url_hash}-{repo_name}/          # Bare repository
│   │   ├── objects/
│   │   ├── refs/
│   │   └── ...
│   └── {url_hash2}-{repo_name2}/
└── worktrees/
    ├── {url_hash}-{ref_hash}/            # Worktree checkout
    ├── {url_hash}-{ref_hash2}/           # Another ref from same repo
    └── {url_hash2}-{ref_hash}/           # Different repo

Key points:

  • One bare repository per Git URL (shared object store)
  • One worktree per unique URL + ref combination
  • Worktrees share objects from bare repo (disk-efficient)
  • URL and ref hashes ensure uniqueness and avoid conflicts

Using Git with HelmChart

HelmChart resources can reference Helm charts stored in Git repositories.

Basic Example

apiVersion: nyl.niklasrosenstein.github.com/v1
kind: HelmChart
metadata:
  name: nginx
  namespace: default
spec:
  chart:
    repository: git+https://github.com/bitnami/charts.git
    version: main
    name: bitnami/nginx

Parameters

  • repository: Git repository URL with git+ prefix (required)
  • version: Branch, tag, or commit (optional, defaults to HEAD)
  • name: Subdirectory within repository (optional)

Helm Dependencies

Nyl automatically detects and builds Helm chart dependencies for charts from Git repositories. When a chart contains a Chart.yaml with dependencies or a Chart.lock file, Nyl will:

  1. Detect the presence of dependencies
  2. Automatically run helm dependency build to fetch and build them
  3. Continue with normal chart rendering

This means you can use charts with dependencies from Git repositories without any additional configuration:

# Chart.yaml in Git repository
apiVersion: v2
name: my-app
version: 1.0.0
dependencies:
  - name: common
    version: "^1.0"
    repository: "oci://registry-1.docker.io/bitnamicharts"
  - name: postgresql
    version: "~12.0"
    repository: "https://charts.bitnami.com/bitnami"

The dependencies will be automatically resolved when you reference this chart:

apiVersion: nyl.niklasrosenstein.github.com/v1
kind: HelmChart
metadata:
  name: my-app
spec:
  chart:
    repository: git+https://github.com/example/charts.git
    version: main
    name: charts/my-app  # Chart with dependencies
  release:
    name: my-app
    namespace: default

Supported Ref Types

You can reference different types of Git refs:

Branch:

spec:
  chart:
    repository: git+https://github.com/example/charts.git
    version: main

Tag:

spec:
  chart:
    repository: git+https://github.com/example/charts.git
    version: v2.1.0

Commit SHA:

spec:
  chart:
    repository: git+https://github.com/example/charts.git
    version: abc123def456

HEAD (default):

spec:
  chart:
    repository: git+https://github.com/example/charts.git
    # version defaults to HEAD

Using Git with ApplicationGenerator

ApplicationGenerator resources scan Git repositories for Nyl manifests and automatically generate ArgoCD Applications.

Example

apiVersion: argocd.nyl.niklasrosenstein.github.com/v1
kind: ApplicationGenerator
metadata:
  name: cluster-apps
  namespace: argocd
spec:
  destination:
    server: https://kubernetes.default.svc
    namespace: argocd
  source:
    repoURL: https://github.com/example/gitops-demo.git
    targetRevision: main
    path: apps
    include:
      - "*.yaml"
      - "*.yml"
    exclude:
      - ".*"
      - "_*"
  project: default

When rendered, Nyl will:

  1. Resolve repository source location (override, local checkout reuse, ArgoCD checkout reuse, or cached Git worktree)
  2. Use the main revision
  3. Navigate to the apps/ directory
  4. Scan for YAML files matching the include/exclude patterns
  5. Generate ArgoCD Application manifests for each NylRelease found

ApplicationGenerator Resolution Order

For ApplicationGenerator, Nyl resolves the source repository in this order:

  1. NYL_APPGEN_REPO_PATH_OVERRIDE, if set
  2. The current local Git checkout, if the current PWD is inside a repository whose remote matches spec.source.repoURL, and spec.source.targetRevision is HEAD or the current branch name
  3. ArgoCD’s local checkout via ARGOCD_APP_SOURCE_*
  4. The normal Git cache/worktree flow

If one step does not match, Nyl falls through to the next step automatically.

Current Local Checkout Reuse

When you run Nyl from inside the same repository referenced by an ApplicationGenerator, Nyl can skip clone/worktree operations and reuse the current local checkout.

Reuse is enabled only when all of these conditions match:

  • The current PWD is inside a Git repository
  • At least one local remote matches spec.source.repoURL after normalization
  • spec.source.targetRevision is HEAD, or exactly matches the current checked-out branch name

Notes:

  • Detached HEAD only matches targetRevision: HEAD
  • If the remote or revision does not match, Nyl falls back to ArgoCD checkout reuse or the normal Git cache/worktree flow

ArgoCD Local Checkout Reuse

When running in ArgoCD plugin context, Nyl can also reuse the local checkout that ArgoCD already prepared.

Reuse is enabled only when all of these conditions match exactly:

  • spec.source.repoURL equals ARGOCD_APP_SOURCE_REPO_URL (normalized URL comparison)
  • spec.source.targetRevision equals ARGOCD_APP_SOURCE_TARGET_REVISION (exact string match)
  • ARGOCD_APP_SOURCE_PATH can be resolved relative to the current working directory

If any condition does not match, Nyl falls back to the normal Git cache/worktree resolution.

Local Worktree Override (Testing)

For local testing, you can bypass Git clone/worktree resolution and force all ApplicationGenerator resources to read from a local repository root:

export NYL_APPGEN_REPO_PATH_OVERRIDE=/path/to/local/repo
nyl render apps.yaml
export NYL_APPGEN_REPO_PATH_OVERRIDE=@git
nyl render apps.yaml

Behavior:

  • If NYL_APPGEN_REPO_PATH_OVERRIDE is set to a local path, Nyl resolves spec.source.path/spec.source.paths selectors under that path.
  • If NYL_APPGEN_REPO_PATH_OVERRIDE=@git, Nyl discovers the Git repository root from the current PWD and resolves selectors under that root.
  • If unset, Nyl tries current local checkout reuse first, then ArgoCD checkout reuse, then normal Git clone/cache/worktree resolution from spec.source.repoURL and spec.source.targetRevision.
  • If the override path is invalid, or @git is used outside a Git repository, rendering fails immediately.

Selector behavior:

  • source.path scans the selected directory non-recursively by default.
  • Use glob selectors (for example **/*.yaml) or source.paths for multi-selector/recursive workflows.
  • Include/exclude patterns are matched against paths relative to the repository root.

Performance Characteristics

Initial Clone

  • Lazy fetching: Only refs are fetched initially (~KB), not objects (~MB/GB)
  • On-demand objects: Commit objects fetched only when needed
  • Bandwidth efficient: Minimal initial download

Subsequent Access

  • Cache hit: Near-instant if ref already checked out
  • Ref update: Only fetch new refs if branch updated
  • Object reuse: Worktrees share objects from bare repo

Disk Usage

  • Bare repo: One copy of objects for all refs
  • Worktrees: Only working directory files (no .git directory)
  • Efficient: Much smaller than full clones for each ref

Example disk usage for a 100MB repository with 3 refs:

  • Traditional approach: 3 × 100MB = 300MB
  • Nyl approach: 100MB (bare) + 3 × ~10MB (worktrees) = ~130MB

Authentication

Nyl supports both public and private Git repositories through multiple authentication methods.

Automatic Credential Discovery

When running inside a Kubernetes cluster with access to ArgoCD secrets, Nyl automatically discovers repository credentials from ArgoCD repository secrets. No additional configuration required!

Example: Using a private Helm chart

apiVersion: nyl.niklasrosenstein.github.com/v1
kind: HelmChart
metadata:
  name: private-app
  namespace: default
spec:
  chart:
    repository: git+git@github.com:myorg/private-charts.git
    version: main
    name: charts/app

If an ArgoCD repository secret exists for github.com, Nyl will automatically use those credentials.

Authentication Methods

  1. ArgoCD Repository Secrets (Recommended)

    • Credentials automatically discovered from argocd namespace
    • Supports both SSH keys and HTTPS tokens
    • Zero configuration required
    • See Repository Secrets for details
  2. SSH Agent

    • Fallback for SSH URLs when no secret found
    • Works with local development workflows
    • Requires SSH agent running with key loaded
  3. Public Repositories

    • No authentication needed
    • Works out of the box

Supported Credential Types

SSH Key Authentication:

  • Private key stored in ArgoCD secret
  • Recommended for production use
  • Better security than HTTPS tokens

HTTPS Token Authentication:

  • Personal access tokens or passwords
  • Useful for HTTPS-only repositories
  • Stored in ArgoCD secret

Example: Creating Repository Secret

# SSH authentication
kubectl create secret generic github-private \
  -n argocd \
  --from-literal=url=git@github.com:myorg/charts.git \
  --from-file=sshPrivateKey=$HOME/.ssh/id_rsa

kubectl label secret github-private \
  -n argocd \
  argocd.argoproj.io/secret-type=repository

# HTTPS authentication
kubectl create secret generic github-https \
  -n argocd \
  --from-literal=url=https://github.com/myorg/charts.git \
  --from-literal=username=myuser \
  --from-literal=password=ghp_token123

kubectl label secret github-https \
  -n argocd \
  argocd.argoproj.io/secret-type=repository

For complete documentation on authentication, see Repository Secrets.

Limitations

  1. Shallow clones not supported: libgit2 (the underlying library) doesn’t support shallow clones. Full repository history is fetched.

  2. Force checkout: When reusing worktrees, local changes are discarded. Worktrees are treated as read-only checkouts.

Troubleshooting

Cache directory permissions

Problem: Permission denied when creating cache directory

Solution: Set NYL_CACHE_DIR to a writable location:

export NYL_CACHE_DIR=$HOME/.cache/nyl

Large repository performance

Problem: Initial clone is slow for large repositories

Explanation: Nyl fetches the full repository (no shallow clone support)

Workaround: Use a specific tag/commit to avoid fetching all branches:

spec:
  chart:
    repository: git+https://github.com/large/repo.git
    version: v1.2.3  # Specific tag, not a branch

Stale cache

Problem: Git repository not updating with latest changes

Solution: Clear the cache directory:

rm -rf $NYL_CACHE_DIR/git/

Or for a specific repository:

# Find the cached repo
ls $NYL_CACHE_DIR/git/bare/
# Remove it
rm -rf $NYL_CACHE_DIR/git/bare/{hash}-{repo-name}
rm -rf $NYL_CACHE_DIR/git/worktrees/{hash}-*

Authentication failures

Problem: Cannot access private repository

Solution: See Repository Secrets for authentication setup

Examples

Multi-environment Chart from Git

apiVersion: nyl.niklasrosenstein.github.com/v1
kind: HelmChart
metadata:
  name: myapp
  namespace: production
spec:
  chart:
    repository: git+https://github.com/company/charts.git
    version: stable
    name: applications/myapp
  values:
    environment: production
    replicas: 5

Development Branch

apiVersion: nyl.niklasrosenstein.github.com/v1
kind: HelmChart
metadata:
  name: myapp
  namespace: development
spec:
  chart:
    repository: git+https://github.com/company/charts.git
    version: develop
    name: applications/myapp
  values:
    environment: development
    replicas: 1

Specific Version

apiVersion: nyl.niklasrosenstein.github.com/v1
kind: HelmChart
metadata:
  name: myapp
  namespace: staging
spec:
  chart:
    repository: git+https://github.com/company/charts.git
    version: v2.1.0
    name: applications/myapp

ApplicationGenerator with Filtering

apiVersion: argocd.nyl.niklasrosenstein.github.com/v1
kind: ApplicationGenerator
metadata:
  name: monitoring-apps
  namespace: argocd
spec:
  destination:
    server: https://kubernetes.default.svc
    namespace: argocd
  source:
    repoURL: https://github.com/company/infrastructure.git
    targetRevision: main
    path: monitoring
    include:
      - "*.yaml"
      - "*.yml"
    exclude:
      - ".*"              # Hidden files
      - "_*"              # Files starting with underscore
      - "test_*"          # Test files
  project: monitoring
  labels:
    team: platform
    category: monitoring

Best Practices

  1. Pin versions in production: Use tags or commit SHAs for production deployments:

    version: v1.2.3  # Tag
    # or
    version: abc123  # Commit SHA
    
  2. Use branches for development: Use branch refs for development environments:

    version: develop  # Branch name
    
  3. Set cache directory: Configure NYL_CACHE_DIR in CI/CD environments:

    export NYL_CACHE_DIR=/cache/nyl
    
  4. Monitor cache size: Periodically clean up old worktrees if disk space is limited:

    find $NYL_CACHE_DIR/git/worktrees -mtime +30 -delete
    
  5. Use subpaths: Keep charts in subdirectories for better organization:

    spec:
      chart:
        repository: git+https://github.com/company/charts.git
        name: charts/applications/myapp
    

Commands

nyl provides several commands for managing Kubernetes manifests:

Available Commands

Phase 1 (Current)

  • new - Create new projects and components
  • validate - Validate project configuration

Phase 2+ (Coming Soon)

  • rendering-pipeline - Shared rendering pipeline used by render/diff/apply
  • render - Render Kubernetes manifests
  • diff - Show diff between rendered manifests and cluster state
  • apply - Apply rendered manifests to the cluster

Global Options

--verbose / -v

Enable verbose logging for debugging.

nyl --verbose validate
nyl -v new project my-app

--color <COLOR>

Control when to use colored output. Accepts three values:

  • auto (default) - Automatically detect if colors should be used based on TTY detection
  • always - Always use colors, even when output is redirected to a file or pipe
  • never - Never use colors

This applies to both:

  • Colored output from commands (diffs, status indicators, etc.)
  • Log messages from tracing (INFO, WARN, ERROR)

This flag is particularly useful when:

  • Redirecting output to a file where ANSI color codes are not desired
  • Working in environments where terminal color support is inconsistent
  • Forcing colored output in CI/CD pipelines that support ANSI colors
# Disable colors when piping to a file
nyl --color never diff | tee output.txt

# Force colors in CI/CD
nyl --color always diff

# Auto-detect (default behavior)
nyl diff

--help / -h

Show help information for any command.

nyl --help
nyl new --help
nyl validate --help

--version / -V

Show the version of nyl.

nyl --version

Rendering Pipeline

nyl render, nyl diff, and nyl apply share the same manifest generation pipeline.

Shared Pipeline Steps

  1. Load project configuration and select the active profile.
  2. Load secrets and build the template context.
  3. Load the input manifest file and render Jinja templates.
  4. Apply --only-source-kind filtering on top-level input resources (before expansion).
  5. Expand resources recursively (HelmChart, Component, RemoteManifest, aliases) with --max-depth.
  6. Process ApplicationGenerator resources into Argo CD Application manifests.
  7. Apply Kyverno policies (Global scope currently supported).
  8. Deduplicate final manifests (last occurrence wins).
  9. Apply post-render kind filtering with --only-kind / --exclude-kind.

Namespace Resolution (Online Mode)

In online mode, Nyl connects to Kubernetes and resolves missing metadata.namespace for namespaced resources.

Fallback order:

  1. Existing metadata.namespace
  2. Release namespace hint (NylRelease.metadata.namespace or --namespace for release commands)
  3. Kube context default namespace
  4. Error if no namespace can be determined

When --offline is used (render only), namespace resolution is skipped.

Online vs Offline

  • render --offline:
    • does not connect to the cluster for API discovery during rendering;
    • requires --kube-version and --kube-api-versions;
    • skips namespace resolution.
  • render (without --offline), diff, and apply:
    • require cluster connectivity;
    • initialize Kubernetes discovery once per command run and reuse it for scope checks.

Command-Specific Behavior After Rendering

  • render: outputs rendered YAML to stdout.
  • diff: compares desired state with live cluster state and prints differences.
  • apply: applies resources and manages release tracking/pruning.

Source Filter vs Post-Render Filters

  • --only-source-kind filters only top-level resources in the input file, before expansion.
  • --only-kind / --exclude-kind filter the final rendered manifests, after expansion.

new

Create new nyl projects and components.

Synopsis

nyl new project <dir>
nyl new component <api-version> <kind>

nyl new project

Creates:

<dir>/
├── nyl.toml
└── components/

Generated nyl.toml:

[project]
components_search_paths = ["components"]
helm_chart_search_paths = ["."]

nyl new component

Creates component chart files under:

components/<api-version>/<kind>/
├── Chart.yaml
├── values.yaml
├── values.schema.json
└── templates/deployment.yaml

validate

Validate project configuration and search paths.

Synopsis

nyl validate [path] [--strict]

Checks

  1. Project config discovery (nyl.toml)
  2. TOML parse validity
  3. Existence of each project.components_search_paths entry
  4. Existence of each project.helm_chart_search_paths entry

Examples

nyl validate
nyl validate --strict
nyl validate /path/to/project

render

Render Kubernetes manifests from nyl components and templates.

Synopsis

nyl render [OPTIONS] <FILE>

Description

The render command generates Kubernetes manifests and writes final YAML to stdout. For detailed shared pipeline behavior (also used by diff and apply), see Rendering Pipeline.

Arguments

  • <FILE> - Path to the manifest file to render (required)

Options

Common Options

  • --only-source-kind <KIND> - Filter top-level resources by kind (e.g., ConfigMap, Deployment) or by apiVersion/kind (e.g., apps/v1/Deployment) before expansion.
  • --only-kind <KIND,...> - Filter final rendered manifests to only include specific kinds (post-render).
  • --exclude-kind <KIND,...> - Filter final rendered manifests to exclude specific kinds (post-render, mutually exclusive with --only-kind).
  • -p, --profile <PROFILE> - Profile to use for rendering. If omitted, Nyl tries default; if profiles exist but default is missing, rendering fails with an error.
  • --max-depth <MAX_DEPTH> - Maximum evaluation depth for recursive resource expansion (default: 10)
  • --track-parent - Track parent resource information in annotations

Offline Mode Options

  • --offline - Skip Kubernetes discovery and use CLI-provided API information (useful for CI/CD)
  • --kube-version <KUBE_VERSION> - Kubernetes version for Helm templating (required with –offline)
  • --kube-api-versions <KUBE_API_VERSIONS> - Kubernetes API versions for Helm (required with –offline, comma-separated)

Examples

Basic Rendering

# Render a manifest file
nyl render manifest.yaml

# Render with specific profile
nyl render -p production manifest.yaml

# Filter top-level input resources
nyl render --only-source-kind ConfigMap manifest.yaml

# Filter by full apiVersion/kind
nyl render --only-source-kind apps/v1/Deployment manifest.yaml

# Filter final rendered output kinds
nyl render --only-kind Deployment,Service manifest.yaml

Offline Mode

# Render in offline mode (profile selection is unchanged)
nyl render --offline --kube-version 1.30 --kube-api-versions v1,apps/v1 manifest.yaml

Advanced Options

# Limit recursive expansion depth
nyl render --max-depth 5 manifest.yaml

# Track parent resources in annotations
nyl render --track-parent manifest.yaml

# Combine options
nyl render -p staging --max-depth 3 --track-parent manifest.yaml

Notes

  • Nyl processes single files only. Directory paths are not supported.
  • See Rendering Pipeline for namespace resolution, filter semantics, and online/offline behavior.
  • ApplicationGenerator source resolution first honors NYL_APPGEN_REPO_PATH_OVERRIDE, then tries to reuse the current local Git checkout when repoURL matches a local remote and targetRevision is HEAD or the current branch, then falls back to ArgoCD checkout reuse and normal Git cache/worktree resolution.
  • Local ApplicationGenerator testing override: set NYL_APPGEN_REPO_PATH_OVERRIDE to a local repository root (or @git to auto-detect the Git root from the current PWD) to make ApplicationGenerator scan the local filesystem instead of cloning. This affects render, diff, and apply (all use the same render pipeline). Using @git outside a Git repository fails with a configuration error.
  • ApplicationGenerator discovery semantics: source.path scans non-recursively by default; use glob selectors (or source.paths) for recursive discovery, and include/exclude patterns match relative paths.

diff

Show the difference between rendered manifests and the current cluster state.

Synopsis

nyl diff [OPTIONS] <FILE>

Description

The diff command renders manifests, compares them with live cluster state, and prints changes. For shared rendering behavior and namespace resolution details, see Rendering Pipeline.

Arguments

  • <FILE> - Path to the manifest file to diff (required)

Options

Common Options

  • --only-source-kind <KIND> - Filter top-level resources by kind (e.g., ConfigMap, Deployment) or by apiVersion/kind (e.g., apps/v1/Deployment) before expansion.
  • --only-kind <KIND,...> - Filter final rendered manifests to only include specific kinds (post-render).
  • --exclude-kind <KIND,...> - Filter final rendered manifests to exclude specific kinds (post-render, mutually exclusive with --only-kind).
  • -p, --profile <PROFILE> - Profile to use for rendering. If omitted, Nyl tries default; if profiles exist but default is missing, diff fails with an error.
  • --max-depth <MAX_DEPTH> - Maximum evaluation depth for recursive resource expansion (default: 10)
  • --track-parent - Track parent resource information in annotations

Release Options

  • --name <NAME> - Release name (required if no NylRelease in file)
  • --namespace <NAMESPACE> - Release namespace (required if no NylRelease in file)

Cluster Options

  • --context <CONTEXT> - Kubernetes context to use

Diff Options

  • --summary - Show summary only (counts, no detailed diff)
  • --mode <MODE> - Diff mode: normalized (default) or raw
    • normalized: Uses server-side apply to filter server defaults (like kubectl diff)
    • raw: Compares raw manifests without server normalization
  • --append-release - Preview diff as if current manifests were merged with the previous deployed release

Examples

Basic Diff

# Show diff for a manifest file
nyl diff manifest.yaml

# Diff with specific profile
nyl diff -p production manifest.yaml

# Diff only top-level ConfigMap resources
nyl diff --only-source-kind ConfigMap manifest.yaml

# Diff only final rendered Deployments
nyl diff --only-kind Deployment manifest.yaml

Summary Mode

# Show only the summary
nyl diff --summary manifest.yaml

Diff Modes

# Normalized mode (default) - filters server defaults
nyl diff --mode normalized manifest.yaml

# Raw mode - shows all differences including server defaults
nyl diff --mode raw manifest.yaml

Release Management

# Diff with explicit release name
nyl diff --name my-release --namespace default manifest.yaml

# Use different Kubernetes context
nyl diff --context production manifest.yaml

Output

The diff command shows:

  • Green (+): Lines that will be added
  • Red (-): Lines that will be removed
  • Summary: Count of resources to create, update, or delete

Notes

  • Nyl processes single files only. Directory paths are not supported.
  • A NylRelease resource in the manifest provides release metadata automatically.
  • Normalized mode is recommended for most use cases as it matches kubectl diff behavior.
  • If no previous release state exists, diff still compares desired resources against live state but cannot determine prune candidates; a warning is shown and to delete remains incomplete.
  • See Rendering Pipeline for namespace resolution and filter semantics.

apply

Apply rendered manifests to the Kubernetes cluster with release tracking.

Synopsis

nyl apply [OPTIONS] <FILE>

Description

The apply command renders manifests, applies them with server-side apply, and tracks release state. For shared rendering behavior and namespace resolution details, see Rendering Pipeline.

Arguments

  • <FILE> - Path to the manifest file to apply (required)

Options

Common Options

  • --only-source-kind <KIND> - Filter top-level resources by kind (e.g., ConfigMap, Deployment) or by apiVersion/kind (e.g., apps/v1/Deployment) before expansion.
  • --only-kind <KIND,...> - Filter final rendered manifests to only include specific kinds (post-render).
  • --exclude-kind <KIND,...> - Filter final rendered manifests to exclude specific kinds (post-render, mutually exclusive with --only-kind).
  • -p, --profile <PROFILE> - Profile to use for rendering. If omitted, Nyl tries default; if profiles exist but default is missing, apply fails with an error.
  • --max-depth <MAX_DEPTH> - Maximum evaluation depth for recursive resource expansion (default: 10)
  • --track-parent - Track parent resource information in annotations

Release Options

  • --name <NAME> - Release name (required if no NylRelease in file)
  • --namespace <NAMESPACE> - Release namespace (required if no NylRelease in file)
  • --append-release - Merge current resources with the previous deployed revision and skip pruning removed resources
  • --no-release - Apply resources without creating release revisions, without release metadata, and without pruning

Cluster Options

  • --context <CONTEXT> - Kubernetes context to use

Examples

Basic Apply

# Apply a manifest file
nyl apply manifest.yaml

# Apply with specific profile
nyl apply -p production manifest.yaml

# Apply only top-level ConfigMap resources
nyl apply --only-source-kind ConfigMap manifest.yaml

# Apply only final rendered Deployments
nyl apply --only-kind Deployment manifest.yaml

Release Management

# Apply with explicit release name (overrides NylRelease if present)
nyl apply --name my-release --namespace default manifest.yaml

# Use different Kubernetes context
nyl apply --context production manifest.yaml

Dry Run

Use nyl diff to preview changes before running nyl apply.

No Release Mode

# Apply resources without release tracking or pruning
nyl apply --no-release manifest.yaml

Notes

  • Nyl processes single files only. Directory paths are not supported.
  • A NylRelease resource in the manifest provides release metadata automatically.
  • Release state is tracked in Kubernetes Secrets in the release namespace.
  • --no-release disables release tracking entirely. In this mode, nyl cannot compute or prune resources removed from subsequent applies.
  • See Rendering Pipeline for namespace resolution and filter semantics.

generate

Generate auxiliary resources and configurations from Nyl manifests.

Usage

nyl generate <subcommand> [options]

Subcommands

argocd

Generate ArgoCD Application manifests from Nyl releases.

nyl generate argocd [OPTIONS] <PATH>

Arguments:

  • <PATH>: Directory to scan for Nyl release files

Options:

  • -o, --output <FILE>: Output file (default: stdout)
  • --repo-url <URL>: Git repository URL for generated Applications
  • --target-revision <REV>: Target revision (branch/tag/commit)
  • --destination-server <URL>: Kubernetes server URL (default: https://kubernetes.default.svc)
  • --destination-namespace <NS>: Namespace for Applications (default: argocd)
  • --project <PROJECT>: ArgoCD project name (default: default)

Example:

# Generate Applications for all releases in clusters/default
nyl generate argocd clusters/default \
  --repo-url https://github.com/myorg/gitops.git \
  --target-revision main \
  -o applications.yaml

schema config

Generate JSON Schema for nyl.toml to stdout.

nyl generate schema config

Relation to ApplicationGenerator

The nyl generate argocd command is a manual CLI tool for one-time generation of ArgoCD Applications from a directory of Nyl releases. It’s useful for:

  • Initial ArgoCD bootstrap
  • One-off Application generation
  • CI/CD pipelines that don’t use ApplicationGenerator

The ApplicationGenerator resource is the recommended approach for ongoing management. It provides:

  • Automatic discovery and generation during nyl render
  • Integration with ArgoCD plugin for GitOps
  • Declarative configuration
  • Self-hosting bootstrap pattern

When to use each:

Use CaseToolWhy
Bootstrap ArgoCDApplicationGeneratorDeclarative, self-hosting
Ongoing managementApplicationGeneratorAutomatic, GitOps-native
One-time bootstrapnyl generate argocdManual control
CI/CD generationnyl generate argocdExplicit generation step

Recommendation: Use ApplicationGenerator for most use cases. It’s processed during nyl render and integrates seamlessly with ArgoCD.

See Also

ArgoCD Integration

Nyl provides first-class support for ArgoCD through a custom configuration management plugin and the ApplicationGenerator resource. This integration enables GitOps-style deployments where ArgoCD can render Nyl manifests directly from Git repositories.

Key Features

  • ArgoCD Plugin: Process Nyl manifests directly within ArgoCD
  • ApplicationGenerator: Automatically generate ArgoCD Applications from Nyl releases
  • Self-Hosting Pattern: Bootstrap ArgoCD with Nyl using a declarative approach
  • Live Diffing: See differences between Git state and cluster state
  • Automated Sync: Optionally enable automatic synchronization and pruning

How It Works

The Plugin

Nyl provides an ArgoCD configuration management plugin that acts as a bridge between ArgoCD and Nyl. When ArgoCD syncs an Application that uses the Nyl plugin:

  1. ArgoCD clones the Git repository
  2. The Nyl plugin is invoked to render manifests
  3. Nyl processes the YAML files (HelmCharts, NylRelease, etc.)
  4. Rendered Kubernetes manifests are returned to ArgoCD
  5. ArgoCD applies the manifests to the cluster

ApplicationGenerator

The ApplicationGenerator resource enables automatic discovery and generation of ArgoCD Applications. When you use nyl render on a file containing an ApplicationGenerator:

  1. Nyl scans the configured directory for YAML files
  2. Each file with a NylRelease is discovered
  3. An ArgoCD Application is generated for each NylRelease
  4. The ApplicationGenerator is replaced with the generated Applications

This pattern is particularly useful for:

  • Managing multiple applications in a single repository
  • Bootstrapping ArgoCD to manage itself
  • Multi-cluster deployments
  • Directory-based organization

Getting Started

  1. Install the ArgoCD Plugin in your ArgoCD installation
  2. Follow the Bootstrapping Guide to set up Nyl with ArgoCD
  3. Use ApplicationGenerator to manage applications at scale
  4. Review Best Practices for production deployments

Use Cases

Single Application: Use the Nyl plugin directly in ArgoCD Applications to render Nyl manifests from Git.

Multi-Application: Use ApplicationGenerator to scan a directory and automatically create Applications for each discovered release.

Bootstrap Pattern: Use ApplicationGenerator to have ArgoCD manage itself and all applications declaratively.

Helm Integration: Leverage Nyl’s HelmChart resource within ArgoCD to deploy Helm charts with full templating support.

Plugin Installation

The Nyl ArgoCD plugin enables ArgoCD to render Nyl manifests directly from Git repositories. This guide covers plugin installation and configuration.

Prerequisites

  • ArgoCD 2.4+ installed in your cluster
  • Access to modify ArgoCD’s configuration
  • Nyl binary available in the ArgoCD repo-server

Installation Methods

Build a custom ArgoCD repo-server image with Nyl included:

FROM quay.io/argoproj/argocd:v2.9.3

# Switch to root to install Nyl
USER root

# Download and install Nyl
RUN curl -L https://github.com/NiklasRosenstein/nyl/releases/download/vX.Y.Z/nyl-linux-amd64 \
    -o /usr/local/bin/nyl && \
    chmod +x /usr/local/bin/nyl

# Switch back to argocd user
USER argocd

Deploy this custom image by updating the argocd-repo-server deployment:

spec:
  template:
    spec:
      containers:
      - name: argocd-repo-server
        image: your-registry/argocd-with-nyl:v2.9.3

Method 2: Init Container

Use an init container to download Nyl into a shared volume:

spec:
  template:
    spec:
      initContainers:
      - name: install-nyl
        image: alpine:3.18
        command:
        - sh
        - -c
        - |
          apk add --no-cache curl
          curl -L https://github.com/NiklasRosenstein/nyl/releases/download/vX.Y.Z/nyl-linux-amd64 \
            -o /plugins/nyl
          chmod +x /plugins/nyl
        volumeMounts:
        - name: plugins
          mountPath: /plugins
      containers:
      - name: argocd-repo-server
        volumeMounts:
        - name: plugins
          mountPath: /usr/local/bin/nyl
          subPath: nyl
      volumes:
      - name: plugins
        emptyDir: {}

Plugin Configuration

Configure the Nyl plugin in the ArgoCD ConfigMap:

apiVersion: v1
kind: ConfigMap
metadata:
  name: argocd-cm
  namespace: argocd
data:
  configManagementPlugins: |
    - name: nyl-v2
      generate:
        command: ["/bin/sh", "-c"]
        args:
        - |
          TEMPLATE_INPUT="${ARGOCD_ENV_NYL_CMP_TEMPLATE_INPUT:-${NYL_CMP_TEMPLATE_INPUT:-}}"
          test -n "$TEMPLATE_INPUT" || { echo "NYL_CMP_TEMPLATE_INPUT is required" >&2; exit 1; }
          nyl render "$TEMPLATE_INPUT"

NYL_CMP_TEMPLATE_INPUT path semantics:

  • Values that start with / are treated as repository-root-relative (for example: /gitops/system/argocd.yaml).
  • Values without a leading / are treated as relative to spec.source.path (recommended, usually just the file name).

Verification

Test the plugin installation:

  1. Create a test Application using the Nyl plugin:
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: test-nyl
  namespace: argocd
spec:
  project: default
  source:
    repoURL: https://github.com/your-org/your-repo
    targetRevision: HEAD
    path: path/to/nyl/manifests
    plugin:
      name: nyl-v2
  destination:
    server: https://kubernetes.default.svc
    namespace: default
  1. Check the Application status:
kubectl get application test-nyl -n argocd
argocd app get test-nyl
  1. Verify manifest rendering:
argocd app manifests test-nyl

Passing Environment Variables

You can pass environment variables to the Nyl plugin for configuration:

source:
  plugin:
    name: nyl-v2
    env:
    - name: NYL_CMP_TEMPLATE_INPUT
      value: apps.yaml
    - name: NYL_RELEASE_NAME
      value: my-app
    - name: NYL_RELEASE_NAMESPACE
      value: production

Use apps.yaml (source-path-relative) in most cases. Use a leading / only when you intentionally need a repository-root-relative path.

Troubleshooting

Plugin Not Found

If ArgoCD reports “plugin not found”, check:

  1. The plugin name matches exactly: nyl-v2
  2. The argocd-cm ConfigMap is in the argocd namespace
  3. The repo-server pods have been restarted after configuration changes
kubectl rollout restart deployment argocd-repo-server -n argocd

Command Not Found

If the plugin fails with “nyl: command not found”:

  1. Verify Nyl binary is in the PATH: /usr/local/bin/nyl
  2. Check file permissions (should be executable)
  3. Test manually in the repo-server pod:
kubectl exec -it deployment/argocd-repo-server -n argocd -- nyl --version

Profile Not Found

If Nyl reports “Profile ‘default’ not found”:

  1. Ensure your repository defines [profile.values.default] in nyl.toml
  2. Check the profile name matches what you’re referencing
  3. Verify nyl.toml is in the repository root or search path

Next Steps

Bootstrapping ArgoCD with Nyl

This guide demonstrates how to bootstrap ArgoCD using Nyl with a self-hosting pattern, where ArgoCD manages itself and all applications through the ApplicationGenerator resource.

Overview

The bootstrap pattern works as follows:

  1. Manually apply a bootstrap manifest to install ArgoCD
  2. The bootstrap includes an ArgoCD Application pointing to apps.yaml
  3. ArgoCD syncs the “apps” Application, running nyl render apps.yaml
  4. Nyl sees ApplicationGenerator in apps.yaml and scans for NylRelease files
  5. Nyl generates ArgoCD Applications for each found NylRelease
  6. ArgoCD receives and creates all child Applications
  7. ArgoCD now manages all applications (including itself) declaratively

Prerequisites

  • kubectl configured with cluster access
  • Git repository for storing manifests
  • Basic understanding of ArgoCD concepts

Directory Structure

Organize your repository as follows:

gitops-repo/
├── bootstrap.yaml              # Initial bootstrap (apply once manually)
├── apps.yaml                   # ApplicationGenerator definition
├── argocd/                     # ArgoCD installation manifests
│   └── argocd.yaml            # HelmChart for ArgoCD
├── clusters/
│   └── default/               # Applications for default cluster
│       ├── app1.yaml          # Application 1 with NylRelease
│       ├── app2.yaml          # Application 2 with NylRelease
│       └── app3.yaml          # Application 3 with NylRelease
├── nyl.toml                   # Nyl project + profile configuration
└── nyl-secrets.yaml           # Secrets configuration

Step 1: Create Configuration Files

nyl.toml

[project]
components_search_paths = ["components"]
helm_chart_search_paths = ["."]

[profile.values.default]
# default profile values

nyl-secrets.yaml

type: null

Step 2: Create ArgoCD Installation Manifest

Create argocd/argocd.yaml:

apiVersion: nyl.niklasrosenstein.github.com/v1
kind: NylRelease
metadata:
  name: argocd
  namespace: argocd
---
apiVersion: v1
kind: Namespace
metadata:
  name: argocd
---
apiVersion: nyl.niklasrosenstein.github.com/v1
kind: HelmChart
metadata:
  name: argocd
  namespace: argocd
spec:
  chart:
    repository: https://argoproj.github.io/argo-helm
    name: argo-cd
    version: "5.51.6"
  values:
    # ArgoCD configuration
    server:
      extraArgs:
        - --insecure  # For testing; use TLS in production

    # Configure the Nyl plugin
    repoServer:
      volumes:
        - name: plugins
          emptyDir: {}

      initContainers:
        - name: install-nyl
          image: alpine:3.18
          command: [sh, -c]
          args:
            - |
              apk add --no-cache curl
              curl -L https://github.com/NiklasRosenstein/nyl/releases/latest/download/nyl-linux-amd64 \
                -o /plugins/nyl
              chmod +x /plugins/nyl
          volumeMounts:
            - name: plugins
              mountPath: /plugins

      volumeMounts:
        - name: plugins
          mountPath: /usr/local/bin/nyl
          subPath: nyl

    # Plugin configuration
    configs:
      cm:
        configManagementPlugins: |
          - name: nyl-v2
            generate:
              command: ["/bin/sh", "-c"]
              args:
                - |
                  TEMPLATE_INPUT="${ARGOCD_ENV_NYL_CMP_TEMPLATE_INPUT:-${NYL_CMP_TEMPLATE_INPUT:-}}"
                  test -n "$TEMPLATE_INPUT" || { echo "NYL_CMP_TEMPLATE_INPUT is required" >&2; exit 1; }
                  nyl render "$TEMPLATE_INPUT"

NYL_CMP_TEMPLATE_INPUT should usually be source-path-relative (for example apps.yaml when spec.source.path points at that directory). A leading / marks a repository-root-relative path.

Step 3: Create ApplicationGenerator

Create apps.yaml with the ApplicationGenerator resource:

apiVersion: argocd.nyl.niklasrosenstein.github.com/v1
kind: ApplicationGenerator
metadata:
  name: cluster-apps
  namespace: argocd
spec:
  # Where to create generated Applications
  destination:
    server: https://kubernetes.default.svc
    namespace: argocd

  # Source repository configuration
  source:
    repoURL: https://github.com/your-org/gitops-repo.git
    targetRevision: HEAD
    path: clusters/default

    # Optional: file filtering
    include:
      - "*.yaml"
      - "*.yml"
    exclude:
      - ".*"           # Skip hidden files
      - "_*"           # Skip files starting with underscore
      - "apps.yaml"    # Avoid recursion

  # ArgoCD project for generated Applications
  project: default

  # Default sync policy for all generated Applications
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
    syncOptions:
      - CreateNamespace=true

  # Labels added to all generated Applications
  labels:
    managed-by: nyl
    generator: cluster-apps

  # Annotations added to all generated Applications
  annotations:
    docs-url: https://wiki.example.com/cluster-apps

Step 4: Create Sample Applications

Create clusters/default/nginx.yaml:

apiVersion: nyl.niklasrosenstein.github.com/v1
kind: NylRelease
metadata:
  name: nginx
  namespace: default
---
apiVersion: nyl.niklasrosenstein.github.com/v1
kind: HelmChart
metadata:
  name: nginx
  namespace: default
spec:
  chart:
    repository: https://charts.bitnami.com/bitnami
    name: nginx
    version: "15.4.4"
  values:
    replicaCount: 2
    service:
      type: ClusterIP

Create clusters/default/redis.yaml:

apiVersion: nyl.niklasrosenstein.github.com/v1
kind: NylRelease
metadata:
  name: redis
  namespace: default
---
apiVersion: nyl.niklasrosenstein.github.com/v1
kind: HelmChart
metadata:
  name: redis
  namespace: default
spec:
  chart:
    repository: https://charts.bitnami.com/bitnami
    name: redis
    version: "18.4.0"
  values:
    architecture: standalone
    auth:
      enabled: false

Step 5: Create Bootstrap Manifest

Create bootstrap.yaml:

# Namespace for ArgoCD
apiVersion: v1
kind: Namespace
metadata:
  name: argocd
---
# Application to install ArgoCD itself
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: argocd
  namespace: argocd
  finalizers:
    - resources-finalizer.argocd.argoproj.io
spec:
  project: default
  source:
    repoURL: https://github.com/your-org/gitops-repo.git
    targetRevision: HEAD
    path: argocd
    plugin:
      name: nyl-v2
  destination:
    server: https://kubernetes.default.svc
    namespace: argocd
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
    syncOptions:
      - CreateNamespace=true
---
# Application generator that creates all other Applications
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: apps
  namespace: argocd
  finalizers:
    - resources-finalizer.argocd.argoproj.io
spec:
  project: default
  source:
    repoURL: https://github.com/your-org/gitops-repo.git
    targetRevision: HEAD
    path: .
    plugin:
      name: nyl-v2
  destination:
    server: https://kubernetes.default.svc
    namespace: argocd
  syncPolicy:
    automated:
      prune: true
      selfHeal: true

Important: In the apps Application, the path is . (repository root) because apps.yaml contains the ApplicationGenerator which will scan clusters/default.

Step 6: Bootstrap the Cluster

  1. Commit all files to your Git repository:
git add .
git commit -m "Initial Nyl + ArgoCD bootstrap"
git push origin main
  1. Apply the bootstrap manifest to your cluster:
kubectl apply -f bootstrap.yaml
  1. Wait for ArgoCD to install:
kubectl wait --for=condition=available --timeout=5m \
  deployment/argocd-server -n argocd
  1. Access ArgoCD UI:
# Port forward to access UI
kubectl port-forward svc/argocd-server -n argocd 8080:443

# Get admin password
kubectl get secret argocd-initial-admin-secret -n argocd \
  -o jsonpath='{.data.password}' | base64 -d
  1. Verify Applications were created:
kubectl get applications -n argocd

Expected output:

NAME     SYNC STATUS   HEALTH STATUS
argocd   Synced        Healthy
apps     Synced        Healthy
nginx    Synced        Healthy
redis    Synced        Healthy

How It Works

When you apply bootstrap.yaml:

  1. ArgoCD Namespace Created: The argocd namespace is created
  2. ArgoCD Application Syncs: ArgoCD installs itself via the Nyl plugin
  3. Apps Application Syncs: The “apps” Application runs nyl render "$NYL_CMP_TEMPLATE_INPUT" (for example apps.yaml)
  4. ApplicationGenerator Processes: Nyl finds apps.yaml, sees ApplicationGenerator
  5. Directory Scanned: Nyl scans clusters/default/ for YAML files
  6. Applications Generated: For each NylRelease found (nginx, redis), Nyl generates an ArgoCD Application
  7. Applications Created: ArgoCD receives the generated Applications and creates them
  8. Child Applications Sync: Each generated Application syncs its resources to the cluster

Verification

Check Application Status

# View all applications
argocd app list

# Get details of a specific application
argocd app get nginx

# View application tree
argocd app get nginx --show-operation

# View rendered manifests
argocd app manifests nginx

Verify Nyl Plugin

# Check if Nyl is available in repo-server
kubectl exec -it deployment/argocd-repo-server -n argocd -- nyl --version

# View plugin configuration
kubectl get configmap argocd-cm -n argocd -o yaml | grep -A10 configManagementPlugins

View Generated Applications

# See what ApplicationGenerator produced
kubectl exec -it deployment/argocd-repo-server -n argocd -- \
  sh -c 'cd /tmp && git clone https://github.com/your-org/gitops-repo.git && \
  cd gitops-repo && nyl render apps.yaml'

Adding New Applications

To add a new application to be managed by ArgoCD:

  1. Create a new YAML file in clusters/default/:
# clusters/default/postgres.yaml
apiVersion: nyl.niklasrosenstein.github.com/v1
kind: NylRelease
metadata:
  name: postgres
  namespace: database
---
apiVersion: v1
kind: Namespace
metadata:
  name: database
---
apiVersion: nyl.niklasrosenstein.github.com/v1
kind: HelmChart
metadata:
  name: postgres
  namespace: database
spec:
  chart:
    repository: https://charts.bitnami.com/bitnami
    name: postgresql
    version: "13.2.24"
  values:
    auth:
      username: myuser
      database: mydb
  1. Commit and push to Git:
git add clusters/default/postgres.yaml
git commit -m "Add PostgreSQL application"
git push origin main
  1. ArgoCD automatically detects the change and creates the postgres Application

No manual intervention needed! The ApplicationGenerator pattern discovers new applications automatically.

Troubleshooting

Applications Not Created

If generated Applications don’t appear:

  1. Check the “apps” Application status:

    argocd app get apps
    
  2. View the rendered manifests:

    argocd app manifests apps
    
  3. Check if ApplicationGenerator is processing correctly:

    # Should show ArgoCD Applications, not the ApplicationGenerator
    argocd app manifests apps | grep "kind: Application"
    

Plugin Failures

If the Nyl plugin fails:

  1. Check repo-server logs:

    kubectl logs deployment/argocd-repo-server -n argocd -f
    
  2. Verify Nyl installation:

    kubectl exec deployment/argocd-repo-server -n argocd -- which nyl
    kubectl exec deployment/argocd-repo-server -n argocd -- nyl --version
    
  3. Test Nyl render manually:

    kubectl exec -it deployment/argocd-repo-server -n argocd -- sh
    # Inside the pod:
    cd /tmp
    git clone <your-repo>
    cd <repo-name>
    nyl render apps.yaml
    

Sync Issues

If Applications fail to sync:

  1. Check Application health:

    argocd app get <app-name>
    
  2. View sync operation details:

    argocd app get <app-name> --show-operation
    
  3. Force refresh and sync:

    argocd app sync <app-name> --force
    

Next Steps

ApplicationGenerator Reference

The ApplicationGenerator resource enables automatic discovery and generation of ArgoCD Applications from NylRelease files in a Git repository directory.

Note: Repository sources are resolved automatically by Nyl. Depending on context, Nyl may reuse an existing local checkout or clone into its Git cache. See the Git Integration guide for cache management and resolution details.

Resource Definition

apiVersion: argocd.nyl.niklasrosenstein.github.com/v1
kind: ApplicationGenerator
metadata:
  name: string              # Generator name (optional)
  namespace: string         # Namespace (optional, typically 'argocd')
spec:
  destination:              # Required
    server: string          # Kubernetes API server URL
    namespace: string       # Namespace for generated Applications
  source:                   # Required
    repoURL: string         # Git repository URL
    targetRevision: string  # Branch, tag, or commit (default: "HEAD")
    path: string            # Single selector to scan (mutually exclusive with paths)
    paths: [string]         # Multiple selectors to scan (mutually exclusive with path)
    include: [string]       # Include patterns (default: ["*.yaml", "*.yml"])
    exclude: [string]       # Exclude patterns (default: [".*", "_*", ".nyl/**"])
  project: string           # ArgoCD project (default: "default")
  syncPolicy:               # Optional sync policy for generated Applications
    automated:
      prune: bool
      selfHeal: bool
    syncOptions: [string]
  applicationNameTemplate: string  # Naming template (default: "{{ .release.name }}")
  labels: {string: string}         # Labels for generated Applications
  annotations: {string: string}    # Annotations for generated Applications
  releaseCustomization:            # Optional NylRelease override policy
    allowedPaths: [string]
    deniedPaths: [string]

Field Reference

metadata

Standard Kubernetes metadata for the ApplicationGenerator resource itself.

  • name (optional): Identifier for this generator
  • namespace (optional): Namespace where this resource lives (typically argocd)

spec.destination

Defines where generated Applications should be created and what cluster they target.

  • server (required): Kubernetes API server URL for the target cluster

    • Example: https://kubernetes.default.svc (in-cluster)
    • Example: https://my-cluster.example.com:6443 (external cluster)
  • namespace (required): Namespace where generated Applications are created

    • Typically argocd
    • Must match where ArgoCD is installed

spec.source

Configures the Git repository and directory scanning behavior.

  • repoURL (required): Git repository URL

    • HTTPS: https://github.com/org/repo.git
    • SSH: git@github.com:org/repo.git
  • targetRevision (optional, default: "HEAD"): Git reference to use

    • Branch: main, develop
    • Tag: v1.0.0
    • Commit: abc123def456
  • path (required if paths is not set): Single selector to scan

    • Relative to repository root
    • Can be a file, directory, or glob selector
    • Directory selectors are scanned non-recursively by default
  • paths (required if path is not set): Multiple selectors to scan

    • Relative to repository root
    • Can include glob selectors (for example clusters/*/apps or **/*.yaml)
    • Mutually exclusive with path
  • include (optional, default: ["*.yaml", "*.yml"]): Glob patterns for files to include

    • Patterns without / match basename
    • Patterns with / match relative path from repository root
    • Multiple patterns are OR’d together
  • exclude (optional, default: [".*", "_*", ".nyl/**"]): Glob patterns for files to exclude

    • Takes precedence over include patterns
    • Default excludes hidden files (.), underscore-prefixed files (_), and Nyl cache directories
    • Example: ["test_*", ".*", "backup*"]

spec.project

  • project (optional, default: "default"): ArgoCD project name for generated Applications
    • Must be an existing ArgoCD AppProject
    • Used for RBAC and resource restrictions

spec.syncPolicy

Optional default sync policy applied to all generated Applications.

  • automated (optional): Enable automated sync

    • prune (bool): Delete resources no longer defined in Git
    • selfHeal (bool): Force resource state to match Git
  • syncOptions (optional): List of sync options

    • CreateNamespace=true: Create destination namespace if missing
    • PruneLast=true: Prune resources after other operations
    • RespectIgnoreDifferences=true: Respect ignore differences
    • ApplyOutOfSyncOnly=true: Only apply resources that are out of sync

spec.applicationNameTemplate

  • applicationNameTemplate (optional, default: "{{ .release.name }}"): Template for Application names
    • Currently supports: {{ .release.name }}, {{ .release.namespace }}
    • Future: Full Handlebars/Tera template support

spec.labels

Key-value map of labels to add to all generated Applications.

Example:

labels:
  managed-by: nyl
  team: platform
  environment: production

spec.annotations

Key-value map of annotations to add to all generated Applications.

Example:

annotations:
  docs-url: https://wiki.example.com/apps
  team-slack: "#platform-team"

spec.releaseCustomization

Controls project-level NylRelease.spec.argocd.applicationOverride customization of generated Applications.

  • NylRelease overrides are always evaluated against effective allowedPaths/deniedPaths.
  • +<field> append overrides are matched against the canonical field path without the + prefix.
    • Example: spec.syncPolicy.+syncOptions is checked as spec.syncPolicy.syncOptions.
  • If releaseCustomization is omitted, defaults are used.
  • allowedPaths and deniedPaths use dotted field globs:
    • * matches one segment (does not cross dots)
    • ** matches multiple segments (crosses dots)
  • If both allow and deny match, deny takes precedence.
  • If allowedPaths is omitted or null, defaults are used:
    • metadata.annotations."pref.argocd.argoproj.io/*"
    • spec.info.**
    • spec.ignoreDifferences.**
    • spec.syncPolicy.**
  • If allowedPaths is an empty list, no fields are allowed.

Ignored fields (unsupported/disallowed/invalid) are not applied and are reported in generated Application.spec.info.

File Filtering

The ApplicationGenerator scans the configured directory and applies include/exclude patterns:

Pattern Matching

Patterns use standard glob syntax:

  • *.yaml - Matches files ending with .yaml
  • *.yml - Matches files ending with .yml
  • app* - Matches files starting with app
  • .* - Matches hidden files (starting with .)
  • _* - Matches files starting with underscore
  • test_*.yaml - Matches test_*.yaml files
  • **/*.yaml - Recursive match for YAML files
  • Exact match: apps.yaml - Matches only apps.yaml

Filtering Logic

  1. Expand selectors from path/paths into candidate files
  2. File must match at least one include pattern
  3. File must NOT match any exclude pattern
  4. Nyl parses the file and looks for a NylRelease resource
  5. If NylRelease found, an Application is generated

Default Patterns

By default:

  • Include: ["*.yaml", "*.yml"] - All YAML files
  • Exclude: [".*", "_*", ".nyl/**"] - Hidden files, underscore-prefixed files, and Nyl cache paths

This prevents accidental inclusion of:

  • Hidden files like .secrets.yaml, .git/
  • Backup files like _backup.yaml
  • Test files like _test_app.yaml

Examples

Basic Usage

Simplest ApplicationGenerator for scanning a directory:

apiVersion: argocd.nyl.niklasrosenstein.github.com/v1
kind: ApplicationGenerator
metadata:
  name: my-apps
  namespace: argocd
spec:
  destination:
    server: https://kubernetes.default.svc
    namespace: argocd
  source:
    repoURL: https://github.com/myorg/gitops.git
    path: apps

This scans apps/ for *.yaml and *.yml files, generates an Application for each NylRelease found.

With Automated Sync

Enable automatic synchronization and pruning:

apiVersion: argocd.nyl.niklasrosenstein.github.com/v1
kind: ApplicationGenerator
metadata:
  name: production-apps
  namespace: argocd
spec:
  destination:
    server: https://kubernetes.default.svc
    namespace: argocd
  source:
    repoURL: https://github.com/myorg/gitops.git
    targetRevision: production
    path: clusters/production
  project: production
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
    syncOptions:
      - CreateNamespace=true
      - PruneLast=true

With Release-Level +syncOptions

Generator defaults can be extended from a discovered NylRelease instead of replaced.

ApplicationGenerator:

apiVersion: argocd.nyl.niklasrosenstein.github.com/v1
kind: ApplicationGenerator
metadata:
  name: home-lab
  namespace: argocd
spec:
  destination:
    server: https://kubernetes.default.svc
    namespace: argocd
  source:
    repoURL: git@gitlab.com:NiklasRosenstein/config.git
    targetRevision: HEAD
    path: kasoku.netbird.selfhosted/gitops/home-lab
  project: home-lab
  syncPolicy:
    syncOptions:
      - ServerSideApply=true
      - ApplyOutOfSyncOnly=true

NylRelease:

apiVersion: nyl.niklasrosenstein.github.com/v1
kind: NylRelease
metadata:
  name: service-proxies
  namespace: home-proxy
spec:
  argocd:
    applicationOverride:
      spec:
        syncPolicy:
          +syncOptions:
            - RespectIgnoreDifferences=false

Generated Application excerpt:

spec:
  syncPolicy:
    syncOptions:
      - ServerSideApply=true
      - ApplyOutOfSyncOnly=true
      - RespectIgnoreDifferences=false

Custom File Filtering

Include only specific files and exclude test files:

apiVersion: argocd.nyl.niklasrosenstein.github.com/v1
kind: ApplicationGenerator
metadata:
  name: filtered-apps
  namespace: argocd
spec:
  destination:
    server: https://kubernetes.default.svc
    namespace: argocd
  source:
    repoURL: https://github.com/myorg/gitops.git
    path: apps
    include:
      - "prod-*.yaml"
      - "core-*.yaml"
    exclude:
      - ".*"
      - "_*"
      - "test-*"
      - "*-backup.yaml"

With Labels and Annotations

Add metadata to generated Applications:

apiVersion: argocd.nyl.niklasrosenstein.github.com/v1
kind: ApplicationGenerator
metadata:
  name: team-apps
  namespace: argocd
spec:
  destination:
    server: https://kubernetes.default.svc
    namespace: argocd
  source:
    repoURL: https://github.com/myorg/gitops.git
    path: teams/platform/apps
  labels:
    team: platform
    managed-by: nyl
    environment: production
  annotations:
    team-slack: "#platform-team"
    oncall-pagerduty: "P123ABC"

Multi-Cluster Setup

Generate Applications for an external cluster:

apiVersion: argocd.nyl.niklasrosenstein.github.com/v1
kind: ApplicationGenerator
metadata:
  name: staging-cluster-apps
  namespace: argocd
spec:
  destination:
    server: https://staging-cluster.example.com:6443
    namespace: argocd
  source:
    repoURL: https://github.com/myorg/gitops.git
    targetRevision: main
    path: clusters/staging
  project: staging
  syncPolicy:
    automated:
      selfHeal: true

Note: The cluster must be registered in ArgoCD first.

Multiple Generators

You can have multiple ApplicationGenerators in the same file:

apiVersion: argocd.nyl.niklasrosenstein.github.com/v1
kind: ApplicationGenerator
metadata:
  name: core-apps
spec:
  destination:
    server: https://kubernetes.default.svc
    namespace: argocd
  source:
    repoURL: https://github.com/myorg/gitops.git
    path: core
  project: core
---
apiVersion: argocd.nyl.niklasrosenstein.github.com/v1
kind: ApplicationGenerator
metadata:
  name: addon-apps
spec:
  destination:
    server: https://kubernetes.default.svc
    namespace: argocd
  source:
    repoURL: https://github.com/myorg/gitops.git
    path: addons
  project: addons

Generated Application Structure

For a NylRelease file like this:

# clusters/default/nginx.yaml
apiVersion: nyl.niklasrosenstein.github.com/v1
kind: NylRelease
metadata:
  name: nginx
  namespace: web
---
# ... other resources ...

The ApplicationGenerator produces:

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: nginx                    # From NylRelease.metadata.name
  namespace: argocd              # From generator.spec.destination.namespace
  labels:                        # From generator.spec.labels
    managed-by: nyl
  annotations:                   # From generator.spec.annotations
    docs-url: https://wiki.example.com/apps
spec:
  project: default               # From generator.spec.project
  source:
    repoURL: https://github.com/myorg/gitops.git  # From generator.spec.source.repoURL
    path: clusters/default       # Directory of the file
    targetRevision: HEAD         # From generator.spec.source.targetRevision
    plugin:
      name: nyl-v2
      env:
        - name: NYL_CMP_TEMPLATE_INPUT
          value: nginx.yaml
        - name: NYL_RELEASE_NAME
          value: nginx
        - name: NYL_RELEASE_NAMESPACE
          value: web
  destination:
    server: https://kubernetes.default.svc  # From generator.spec.destination.server
    namespace: web               # From NylRelease.metadata.namespace
  syncPolicy:                    # From generator.spec.syncPolicy
    automated:
      prune: true
      selfHeal: true
    syncOptions:
      - CreateNamespace=true

Behavior

Processing Flow

  1. When nyl render encounters an ApplicationGenerator resource:

    • The ApplicationGenerator is extracted and NOT included in output
    • The source path is resolved (from Git by default, or from local override path if configured)
    • The resolved source path is scanned for YAML files
    • Files are filtered by include/exclude patterns
    • Each file is parsed for a NylRelease resource
    • An ArgoCD Application is generated for each NylRelease
    • Generated Applications are added to the output
  2. The ApplicationGenerator acts as an “offline controller” pattern:

    • Processing happens during nyl render
    • No runtime controller or operator needed
    • Deterministic output (same input always produces same output)
    • Works in ArgoCD plugin context

Path Resolution

By default, source.path / source.paths are resolved using the following precedence:

  1. NYL_APPGEN_REPO_PATH_OVERRIDE, if set
  2. The current local Git checkout, if the current PWD is inside a Git repository and:
    • one of the local repository remotes matches source.repoURL after normalization
    • source.targetRevision is HEAD, or exactly matches the current local branch name
  3. ArgoCD’s local checkout, when the ArgoCD plugin environment variables match
  4. Nyl’s normal Git cache/worktree resolution from source.repoURL + source.targetRevision

If a higher-priority resolution strategy does not match, Nyl falls back to the next one automatically.

Current Local Repository Reuse

When you run nyl render, nyl diff, or nyl apply from inside the same Git repository referenced by an ApplicationGenerator, Nyl can reuse the current local checkout instead of creating a separate clone/worktree.

Reuse is enabled only when all of these conditions are satisfied:

  • The current PWD is inside a Git repository
  • One of that repository’s remotes matches source.repoURL (normalized URL comparison)
  • source.targetRevision is HEAD, or exactly matches the current checked-out branch name

Notes:

  • A detached HEAD only matches targetRevision: HEAD
  • Branch names must match exactly
  • If these checks fail, Nyl falls back to ArgoCD checkout reuse or normal Git resolution

This is useful for local development because ApplicationGenerator sees the files currently checked out in your working tree.

ArgoCD Local Checkout Reuse

When Nyl runs inside ArgoCD plugin context, it can also reuse ArgoCD’s local checkout instead of cloning:

  • source.repoURL must match ARGOCD_APP_SOURCE_REPO_URL (normalized URL comparison)
  • source.targetRevision must exactly match ARGOCD_APP_SOURCE_TARGET_REVISION
  • ARGOCD_APP_SOURCE_PATH must be resolvable to a local source directory

If these checks fail, Nyl falls back to normal Git cache/worktree resolution.

For local testing, set:

export NYL_APPGEN_REPO_PATH_OVERRIDE=/path/to/local/repo

Or use:

export NYL_APPGEN_REPO_PATH_OVERRIDE=@git

to resolve the repository root from the current PWD.

When this env var is set, selectors from source.path/source.paths are resolved under that local directory and no local checkout detection, ArgoCD checkout reuse, clone, or worktree checkout is performed for ApplicationGenerator processing.

If the override path is missing/invalid, @git is used outside a Git repository, or the resulting source path does not exist, nyl render fails with a configuration error.

For generated Applications:

  • The path field points to the directory containing the NylRelease file
  • Relative to the repository root

Validation

ApplicationGenerator is validated when parsed:

  • spec.destination.server must not be empty
  • spec.destination.namespace must not be empty
  • spec.source.repoURL must not be empty
  • Exactly one of spec.source.path or spec.source.paths must be set
  • spec.source.path / spec.source.paths selectors must not be empty

Invalid ApplicationGenerator resources cause nyl render to fail with a clear error message.

Limitations (Phase 1)

Current limitations:

  • No templating: applicationNameTemplate only supports basic substitution
  • Single repository: Cannot scan multiple Git repositories in one generator

These limitations are addressed in Phase 2 (future enhancement).

Next Steps

Repository Secrets

Nyl automatically discovers credentials from ArgoCD repository secrets to authenticate with private Git repositories. This enables seamless integration with private Helm charts and GitOps repositories.

Overview

When Nyl encounters a Git URL in a HelmChart or ApplicationGenerator resource, it:

  1. Queries ArgoCD repository secrets from the argocd namespace
  2. Matches the Git URL to an appropriate secret
  3. Uses the discovered credentials for authentication
  4. Falls back to SSH agent authentication if no secret matches

This approach provides:

  • Zero configuration: Credentials are discovered automatically from existing ArgoCD secrets
  • Security: Credentials remain in Kubernetes secrets, never in configuration files
  • Consistency: Same credentials used by ArgoCD and Nyl
  • Flexibility: Supports both SSH and HTTPS authentication

Secret Types

ArgoCD supports two types of repository credential secrets, both of which are supported by Nyl:

Repository Secrets (Fine-Grained)

Repository secrets (argocd.argoproj.io/secret-type=repository) provide credentials for specific repositories using exact URL matching.

Example:

apiVersion: v1
kind: Secret
metadata:
  name: myorg-charts-repo
  namespace: argocd
  labels:
    argocd.argoproj.io/secret-type: repository
type: Opaque
stringData:
  type: git  # Only "git" type secrets are used (defaults to "git" if omitted)
  url: git@github.com:myorg/charts.git
  sshPrivateKey: |
    -----BEGIN OPENSSH PRIVATE KEY-----
    ...
    -----END OPENSSH PRIVATE KEY-----

Use when:

  • Managing credentials for individual repositories
  • Need fine-grained access control
  • Different credentials for different repos on same host

Repository Credential Templates (Scalable)

Repository credential templates (argocd.argoproj.io/secret-type=repo-creds) provide credentials for multiple repositories matching a URL pattern.

Example:

apiVersion: v1
kind: Secret
metadata:
  name: github-myorg-all
  namespace: argocd
  labels:
    argocd.argoproj.io/secret-type: repo-creds
type: Opaque
stringData:
  type: git  # Only "git" type secrets are used (defaults to "git" if omitted)
  url: https://github.com/myorg/*
  username: myusername
  password: ghp_token123

Pattern Examples:

  • https://github.com/myorg/* - All repos in organization
  • https://github.com/* - All GitHub repos
  • git@gitlab.com:*/* - All GitLab repos with org and repo

Use when:

  • Managing many repositories from same provider
  • Centralized credential management
  • Single credential applies to multiple repos

Important: Both secret types must have type: git (or omit the type field, which defaults to git). Secrets with other types (e.g., helm, oci) are ignored.

Authentication Methods

SSH Key Authentication

SSH key authentication is the recommended method for private repositories.

Creating an SSH repository secret:

# Create secret with SSH private key
kubectl create secret generic my-private-repo \
  -n argocd \
  --from-literal=url=git@github.com:myorg/charts.git \
  --from-file=sshPrivateKey=$HOME/.ssh/id_rsa

# Label the secret for ArgoCD
kubectl label secret my-private-repo \
  -n argocd \
  argocd.argoproj.io/secret-type=repository

Secret structure:

apiVersion: v1
kind: Secret
metadata:
  name: my-private-repo
  namespace: argocd
  labels:
    argocd.argoproj.io/secret-type: repository
type: Opaque
data:
  url: Z2l0QGdpdGh1Yi5jb206bXlvcmcvY2hhcnRzLmdpdA==  # base64: git@github.com:myorg/charts.git
  sshPrivateKey: LS0tLS1CRUdJTi...                    # base64 encoded SSH private key

HTTPS Token Authentication

HTTPS authentication uses personal access tokens or passwords.

Creating an HTTPS repository secret:

# Create secret with username and token
kubectl create secret generic my-private-repo-https \
  -n argocd \
  --from-literal=url=https://github.com/myorg/charts.git \
  --from-literal=username=myusername \
  --from-literal=password=ghp_your_token_here

# Label the secret for ArgoCD
kubectl label secret my-private-repo-https \
  -n argocd \
  argocd.argoproj.io/secret-type=repository

Secret structure:

apiVersion: v1
kind: Secret
metadata:
  name: my-private-repo-https
  namespace: argocd
  labels:
    argocd.argoproj.io/secret-type: repository
type: Opaque
data:
  url: aHR0cHM6Ly9naXRodWIuY29tL215b3JnL2NoYXJ0cy5naXQ=  # base64: https://github.com/myorg/charts.git
  username: bXl1c2VybmFtZQ==                              # base64: myusername
  password: Z2hwX3lvdXJfdG9rZW5faGVyZQ==                  # base64: ghp_your_token_here

SSH Agent Fallback

If no ArgoCD secret matches a Git URL, Nyl automatically falls back to SSH agent authentication for SSH URLs:

# Start SSH agent
eval "$(ssh-agent -s)"

# Add your SSH key
ssh-add ~/.ssh/id_rsa

# Nyl will now use the SSH agent for authentication
nyl render my-chart.yaml

URL Matching

Nyl matches Git URLs to repository secrets using multiple strategies with specific precedence rules.

Credential Selection Precedence

When multiple secrets could apply to a repository, Nyl uses this precedence:

  1. Exact match (repository secret with exact URL match)
  2. Pattern match (repo-creds secret with matching pattern)
  3. Hostname fallback (repository secret with same hostname)

Example:

Given these secrets:

# 1. Exact match (highest precedence)
apiVersion: v1
kind: Secret
metadata:
  name: charts-exact
  namespace: argocd
  labels:
    argocd.argoproj.io/secret-type: repository
stringData:
  url: https://github.com/myorg/charts.git
  sshPrivateKey: |
    -----BEGIN OPENSSH PRIVATE KEY-----
    ...specific key for charts repo...

# 2. Pattern match (medium precedence)
apiVersion: v1
kind: Secret
metadata:
  name: myorg-pattern
  namespace: argocd
  labels:
    argocd.argoproj.io/secret-type: repo-creds
stringData:
  url: https://github.com/myorg/*
  username: myuser
  password: ghp_token_for_myorg

# 3. Hostname fallback (lowest precedence)
apiVersion: v1
kind: Secret
metadata:
  name: github-fallback
  namespace: argocd
  labels:
    argocd.argoproj.io/secret-type: repository
stringData:
  url: https://github.com/other/repo.git
  username: myuser
  password: ghp_generic_token

For URL https://github.com/myorg/charts.git:

  • Uses exact match (#1) ✅

For URL https://github.com/myorg/other-repo.git:

  • No exact match
  • Uses pattern match (#2) ✅

For URL https://github.com/different-org/repo.git:

  • No exact match
  • No pattern match
  • Uses hostname fallback (#3) ✅

Exact Match

The secret’s url field exactly matches the requested URL (after normalization):

# Secret URL (repository type)
url: https://github.com/myorg/charts.git

# Matches
- https://github.com/myorg/charts.git
- https://github.com/myorg/charts      # .git suffix optional

Pattern Match

The secret’s url field contains wildcards (*) that match the requested URL:

# Secret URL (repo-creds type)
url: https://github.com/myorg/*

# Matches
- https://github.com/myorg/charts.git
- https://github.com/myorg/other-repo.git
- https://github.com/myorg/any-repo

# Does NOT match
- https://github.com/otherorg/repo.git  # Different organization
- https://gitlab.com/myorg/repo.git     # Different host

Pattern Examples:

# Organization-level access
url: https://github.com/myorg/*
# Matches: https://github.com/myorg/repo1, https://github.com/myorg/repo2

# Host-level access (all repos on GitHub)
url: https://github.com/*
# Matches: https://github.com/anyorg/anyrepo

# SSH with pattern
url: git@github.com:myorg/*
# Matches: git@github.com:myorg/charts, git@github.com:myorg/other

# Multi-level wildcard
url: https://gitlab.com/*/*
# Matches: https://gitlab.com/group/project, https://gitlab.com/org/repo

Hostname Fallback

If no exact or pattern match is found, Nyl matches by hostname. This allows a single repository secret to authenticate all repositories on the same host:

# Secret URL (repository type)
url: https://github.com/myorg/charts.git

# Also matches (same hostname, no exact/pattern match)
- https://github.com/myorg/other-repo.git
- https://github.com/another-org/repo.git

URL Normalization

URLs are normalized before matching:

  • Case-insensitive comparison
  • .git suffix is optional
  • SSH shorthand converted to full URL:
    • git@github.com:org/repossh://git@github.com/org/repo
  • Trailing slashes removed

This means these URLs are considered equivalent:

  • https://GitHub.com/MyOrg/Repo.git
  • https://github.com/myorg/repo
  • git@github.com:myorg/repo (when matching SSH patterns)

Examples

Private Helm Chart

apiVersion: nyl.niklasrosenstein.github.com/v1
kind: HelmChart
metadata:
  name: my-app
  namespace: default
spec:
  chart:
    repository: git+git@github.com:myorg/private-charts.git
    version: main
    name: charts/my-app

Nyl will:

  1. Look for ArgoCD secret with URL git@github.com:myorg/private-charts.git
  2. If not found, look for secret with hostname github.com
  3. If not found, try SSH agent
  4. Clone repository and extract chart from charts/my-app

Private ApplicationGenerator

apiVersion: argocd.nyl.niklasrosenstein.github.com/v1
kind: ApplicationGenerator
metadata:
  name: microservices
spec:
  destination:
    server: https://kubernetes.default.svc
    namespace: argocd
  source:
    repoURL: https://github.com/myorg/gitops-repo.git
    targetRevision: main
    path: apps

Nyl will:

  1. Look for ArgoCD secret with URL https://github.com/myorg/gitops-repo.git
  2. If found, use credentials to clone repository
  3. Scan apps/ directory for application definitions
  4. Generate ArgoCD Application resources

Troubleshooting

Authentication Failures

Error: Authentication failed for git@github.com:myorg/repo.git: Failed to use SSH agent

Solutions:

  1. Create an ArgoCD repository secret with SSH key
  2. Start SSH agent and add your key: ssh-add ~/.ssh/id_rsa
  3. Verify SSH key has access to the repository

Error: No credentials found for repository: https://github.com/myorg/repo.git

Solutions:

  1. Create an ArgoCD repository secret for this URL
  2. Verify secret has label argocd.argoproj.io/secret-type=repository
  3. Check secret is in argocd namespace

Permission Issues

Error: Failed to query ArgoCD secrets: Forbidden

Solutions:

  1. Verify Nyl has RBAC permissions to read secrets in argocd namespace
  2. Add ClusterRole/Role with get, list permissions on secrets
  3. Bind role to Nyl service account

URL Matching Issues

If credentials aren’t being discovered:

  1. Check URL format matches between secret and chart
  2. Verify secret URL is normalized (lowercase, no trailing slash)
  3. Test with exact URL match before relying on hostname fallback
  4. Check secret has the correct label

Debugging

Enable debug logging to see credential discovery:

RUST_LOG=debug nyl render my-chart.yaml

Look for log messages:

  • Discovering credentials from ArgoCD secrets
  • Found credential for URL: ...
  • No credential found, falling back to SSH agent

Best Practices

  1. Use SSH keys: Preferred over HTTPS tokens for better security
  2. Choose the right secret type:
    • Use repo-creds for organization-wide access (scalable)
    • Use repository for specific repos needing different credentials
    • Combine both types for flexibility with proper precedence
  3. Pattern-based credentials: Use repo-creds with patterns like https://github.com/myorg/* to centrally manage credentials for multiple repositories
  4. Leverage precedence: Use exact match repository secrets to override broader repo-creds patterns for specific repos
  5. Rotate credentials: Update secrets regularly and test after rotation
  6. Least privilege: Ensure SSH keys/tokens have minimal required permissions
  7. Monitor access: Review ArgoCD secret access logs periodically
  8. Document secrets: Maintain inventory of repository secrets, their patterns, and their purpose

Example Setup:

# 1. Organization-wide access (repo-creds)
apiVersion: v1
kind: Secret
metadata:
  name: github-myorg-default
  labels:
    argocd.argoproj.io/secret-type: repo-creds
stringData:
  type: git
  url: https://github.com/myorg/*
  username: ci-bot
  password: ghp_org_token

---
# 2. Specific repo with different credentials (repository)
apiVersion: v1
kind: Secret
metadata:
  name: sensitive-repo-override
  labels:
    argocd.argoproj.io/secret-type: repository
stringData:
  type: git
  url: https://github.com/myorg/sensitive-repo.git
  sshPrivateKey: |
    -----BEGIN OPENSSH PRIVATE KEY-----
    ...dedicated SSH key for sensitive repo...

This setup provides:

  • Default credentials for all myorg repositories via pattern match
  • Override with dedicated SSH key for sensitive-repo via exact match
  • Separation of concerns and least privilege access

Kubernetes RBAC

Nyl requires these permissions to discover repository secrets:

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: nyl-secret-reader
rules:
- apiGroups: [""]
  resources: ["secrets"]
  verbs: ["get", "list"]
  # Optionally scope to argocd namespace
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: nyl-secret-reader
subjects:
- kind: ServiceAccount
  name: nyl
  namespace: argocd
roleRef:
  kind: ClusterRole
  name: nyl-secret-reader
  apiGroup: rbac.authorization.k8s.io

Security Considerations

  • Credential storage: Secrets are stored in Kubernetes, encrypted at rest
  • Access control: RBAC limits who can read repository secrets
  • Credential scope: Each secret applies only to specific repositories
  • No plaintext: Credentials never appear in logs or configuration files
  • SSH keys: Use dedicated deploy keys with read-only access
  • Token rotation: Update tokens/keys regularly and monitor for leaks

Best Practices

This guide covers recommended patterns and best practices for using Nyl with ArgoCD in production environments.

Directory Structure

Organize your GitOps repository with clear separation of concerns:

gitops-repo/
├── bootstrap/
│   └── bootstrap.yaml          # Initial bootstrap manifest
├── argocd/
│   └── argocd.yaml            # ArgoCD installation
├── apps.yaml                   # ApplicationGenerator for apps
├── clusters/
│   ├── production/
│   │   ├── app1.yaml
│   │   ├── app2.yaml
│   │   └── app3.yaml
│   ├── staging/
│   │   ├── app1.yaml
│   │   └── app2.yaml
│   └── development/
│       └── app1.yaml
├── base/                       # Shared base configurations
│   ├── postgres/
│   └── redis/
├── nyl.toml
└── nyl-secrets.yaml

Directory Organization Strategies

By Environment:

clusters/
├── production/
├── staging/
└── development/
  • Clear separation of environments
  • Easy to apply different policies per environment
  • Suitable for ApplicationGenerator scanning

By Team:

teams/
├── platform/
├── data/
└── ml/
  • Delegation to team-specific directories
  • Team-level RBAC with ArgoCD Projects
  • Enables team autonomy

By Application Type:

├── core-services/
├── data-services/
├── monitoring/
└── security/
  • Logical grouping by function
  • Useful for dependency management
  • Clear categorization

Hybrid Approach (Recommended):

├── core/                       # Critical platform services
│   ├── argocd/
│   ├── cert-manager/
│   └── ingress-nginx/
├── clusters/
│   ├── production/
│   │   ├── team-a/
│   │   └── team-b/
│   └── staging/
│       ├── team-a/
│       └── team-b/
└── shared/                     # Shared configurations
    ├── postgres/
    └── redis/

ApplicationGenerator Patterns

One Generator Per Environment

Create separate ApplicationGenerators for each environment:

# apps-production.yaml
apiVersion: argocd.nyl.niklasrosenstein.github.com/v1
kind: ApplicationGenerator
metadata:
  name: production-apps
spec:
  destination:
    server: https://kubernetes.default.svc
    namespace: argocd
  source:
    repoURL: https://github.com/myorg/gitops.git
    targetRevision: main
    path: clusters/production
  project: production
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
  labels:
    environment: production
---
# apps-staging.yaml
apiVersion: argocd.nyl.niklasrosenstein.github.com/v1
kind: ApplicationGenerator
metadata:
  name: staging-apps
spec:
  destination:
    server: https://kubernetes.default.svc
    namespace: argocd
  source:
    repoURL: https://github.com/myorg/gitops.git
    targetRevision: main
    path: clusters/staging
  project: staging
  syncPolicy:
    automated:
      selfHeal: true  # Note: No prune in staging
  labels:
    environment: staging

One App Per File

Each application should have its own YAML file:

clusters/production/
├── nginx.yaml          # One app
├── postgres.yaml       # One app
└── redis.yaml          # One app

Benefits:

  • Clear ownership and Git history per app
  • Easy to move apps between environments
  • Better for code review
  • Simpler conflict resolution

Avoid putting multiple apps in one file (unless they’re tightly coupled).

Exclude Patterns

Use exclude patterns to prevent accidental Application generation:

spec:
  source:
    exclude:
      - ".*"              # Hidden files
      - "_*"              # Underscore prefix (templates, WIP)
      - "*.backup"        # Backup files
      - "test-*"          # Test files
      - "apps.yaml"       # The generator itself
      - "README.md"       # Documentation

Sync Policies

Production Environments

For production, be conservative:

syncPolicy:
  automated:
    prune: true         # Delete removed resources
    selfHeal: true      # Force Git state
  syncOptions:
    - CreateNamespace=true
    - PruneLast=true    # Prune after other operations

Why?

  • prune: true: Ensures removed manifests are deleted
  • selfHeal: true: Prevents manual changes (drift detection)
  • PruneLast: Safer deletion order

Staging/Development Environments

For non-production, allow more flexibility:

syncPolicy:
  automated:
    prune: false        # Don't auto-delete (allow manual testing)
    selfHeal: true      # Still enforce Git state

Why?

  • prune: false: Allows temporary manual resources for testing
  • selfHeal: true: Still prevents accidental drift

Manual Sync for Critical Services

For very critical services (databases, auth), consider manual sync:

# No automated syncPolicy
# Sync manually via ArgoCD UI or CLI

Secret Management

Option 1: Sealed Secrets

Use Bitnami Sealed Secrets for encrypted secrets in Git:

apiVersion: nyl.niklasrosenstein.github.com/v1
kind: NylRelease
metadata:
  name: app
  namespace: default
---
apiVersion: v1
kind: SealedSecret
metadata:
  name: app-secrets
  namespace: default
spec:
  encryptedData:
    password: AgBjW8X... # Encrypted data safe for Git

Option 2: External Secrets Operator

Sync secrets from external providers (AWS Secrets Manager, Vault, etc.):

apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: app-secrets
  namespace: default
spec:
  secretStoreRef:
    name: aws-secrets-manager
  target:
    name: app-secrets
  data:
    - secretKey: password
      remoteRef:
        key: /app/password

Option 3: Nyl Secrets (SOPS)

Use Nyl’s built-in SOPS integration:

# nyl-secrets.yaml
type: sops
path: ./.secrets.yaml

# .secrets.yaml (encrypted with SOPS)
database:
  password: ENC[AES256_GCM,data:xxx,iv:yyy,tag:zzz]

Never commit plaintext secrets to Git!

Multi-Cluster Setup

Hub-and-Spoke Model

Manage multiple clusters from a central ArgoCD instance:

# apps-prod-cluster1.yaml
apiVersion: argocd.nyl.niklasrosenstein.github.com/v1
kind: ApplicationGenerator
metadata:
  name: prod-cluster1-apps
spec:
  destination:
    server: https://cluster1.example.com:6443
    namespace: argocd
  source:
    repoURL: https://github.com/myorg/gitops.git
    path: clusters/cluster1
  labels:
    cluster: cluster1
---
# apps-prod-cluster2.yaml
apiVersion: argocd.nyl.niklasrosenstein.github.com/v1
kind: ApplicationGenerator
metadata:
  name: prod-cluster2-apps
spec:
  destination:
    server: https://cluster2.example.com:6443
    namespace: argocd
  source:
    repoURL: https://github.com/myorg/gitops.git
    path: clusters/cluster2
  labels:
    cluster: cluster2

Register clusters in ArgoCD:

argocd cluster add cluster1-context
argocd cluster add cluster2-context

Per-Cluster ArgoCD

Each cluster has its own ArgoCD instance:

gitops-repo/
├── clusters/
│   ├── prod-us-east/
│   │   ├── argocd/
│   │   ├── apps.yaml          # ApplicationGenerator for this cluster
│   │   └── apps/
│   └── prod-eu-west/
│       ├── argocd/
│       ├── apps.yaml
│       └── apps/

Each cluster bootstraps independently.

Monitoring and Observability

Prometheus Metrics

ArgoCD exports metrics; monitor these key indicators:

  • argocd_app_info: Application status
  • argocd_app_sync_total: Sync operations
  • argocd_app_health_status: Health status

Alerts

Set up alerts for:

# OutOfSync alert
- alert: ArgoCDAppOutOfSync
  expr: argocd_app_sync_status{sync_status="OutOfSync"} == 1
  for: 15m
  labels:
    severity: warning

# Degraded health
- alert: ArgoCDAppUnhealthy
  expr: argocd_app_health_status{health_status!="Healthy"} == 1
  for: 10m
  labels:
    severity: critical

Notifications

Configure ArgoCD notifications for Slack, email, or PagerDuty:

apiVersion: v1
kind: ConfigMap
metadata:
  name: argocd-notifications-cm
  namespace: argocd
data:
  service.slack: |
    token: $slack-token
  template.app-sync-status: |
    message: Application {{.app.metadata.name}} sync is {{.app.status.sync.status}}
  trigger.on-sync-failed: |
    - when: app.status.sync.status == 'Failed'
      send: [app-sync-status]

CI/CD Integration

Validation Pipeline

Add CI checks to validate Nyl manifests before merge:

# .github/workflows/validate.yml
name: Validate Manifests
on: [pull_request]
jobs:
  validate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Install Nyl
        run: |
          curl -L https://github.com/NiklasRosenstein/nyl/releases/latest/download/nyl-linux-amd64 \
            -o /usr/local/bin/nyl
          chmod +x /usr/local/bin/nyl
      - name: Validate
        run: nyl validate .
      - name: Render
        run: nyl render apps.yaml > /dev/null

Dry-Run Rendering

Test ApplicationGenerator output in CI:

# Render and check output
nyl render apps.yaml > output.yaml

# Verify Applications were generated
cat output.yaml | grep "kind: Application" | wc -l

# Check specific apps exist
grep "name: nginx" output.yaml
grep "name: postgres" output.yaml

Preview Environments

Create preview environments for PRs:

# .github/workflows/preview.yml
name: Preview Environment
on: [pull_request]
jobs:
  preview:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Deploy Preview
        run: |
          # Create preview namespace
          kubectl create namespace pr-${{ github.event.pull_request.number }}

          # Render manifests with preview profile
          nyl render -e preview apps.yaml | kubectl apply -n pr-${{ github.event.pull_request.number }} -f -

Performance Considerations

Repository Size

Keep repositories focused:

  • Separate infrastructure repo from application repos
  • Use Git submodules for shared configurations if needed
  • Archive old applications to separate branches

Sync Frequency

Adjust sync frequency based on needs:

# Faster sync for development
metadata:
  annotations:
    argocd.argoproj.io/sync-options: "Timeout=120"

# Slower sync for large apps
metadata:
  annotations:
    argocd.argoproj.io/sync-options: "Timeout=600"

Resource Limits

Set appropriate resource limits for ArgoCD components:

spec:
  values:
    repoServer:
      resources:
        limits:
          cpu: "1000m"
          memory: "1Gi"
        requests:
          cpu: "500m"
          memory: "512Mi"

Disaster Recovery

Backup Strategy

  1. Git is Source of Truth: All manifests in Git
  2. Backup ArgoCD Configuration:
    kubectl get applications -n argocd -o yaml > argocd-apps-backup.yaml
    kubectl get appprojects -n argocd -o yaml > argocd-projects-backup.yaml
    
  3. Cluster Backups: Use Velero or similar for cluster state

Recovery Procedure

  1. Restore cluster from backup (if needed)
  2. Re-apply bootstrap manifest:
    kubectl apply -f bootstrap.yaml
    
  3. ArgoCD recreates all Applications from Git
  4. Applications sync and restore workloads

Why this works: Git is the source of truth; ArgoCD recreates everything from Git automatically.

Security

RBAC with Projects

Use ArgoCD Projects for RBAC:

apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
  name: team-a
  namespace: argocd
spec:
  description: Team A applications
  sourceRepos:
    - https://github.com/myorg/gitops.git
  destinations:
    - namespace: team-a-*
      server: https://kubernetes.default.svc
  clusterResourceWhitelist:
    - group: ''
      kind: Namespace
  namespaceResourceWhitelist:
    - group: '*'
      kind: '*'

Signed Commits

Require GPG-signed commits for production:

spec:
  source:
    repoURL: https://github.com/myorg/gitops.git
    targetRevision: main
  # In ArgoCD settings
  metadata:
    annotations:
      argocd.argoproj.io/verify-signature: "true"

Least Privilege

  • ArgoCD service account should have minimal permissions
  • Use separate service accounts per ApplicationGenerator/Project
  • Audit ArgoCD RBAC regularly

Troubleshooting

Common Issues

Issue: Applications not syncing

  • Check: ArgoCD repo-server logs
  • Fix: Ensure Nyl plugin is installed and accessible

Issue: Wrong Applications generated

  • Check: Render manifests locally: nyl render apps.yaml
  • Fix: Adjust include/exclude patterns

Issue: Sync timeouts

  • Check: Large Helm charts or slow cluster
  • Fix: Increase timeout annotation

Debug Commands

# View ApplicationGenerator output
nyl render apps.yaml

# Check ArgoCD Application
argocd app get <app-name>

# View rendered manifests
argocd app manifests <app-name>

# Force refresh
argocd app get <app-name> --refresh

# View repo-server logs
kubectl logs deployment/argocd-repo-server -n argocd -f

Summary

Key takeaways:

  • ✅ Use clear directory structure
  • ✅ One ApplicationGenerator per environment
  • ✅ One app per file
  • ✅ Configure appropriate sync policies
  • ✅ Never commit plaintext secrets
  • ✅ Monitor and alert on sync status
  • ✅ Validate in CI before merge
  • ✅ Git is your source of truth

Following these practices ensures reliable, secure, and scalable GitOps with Nyl and ArgoCD.

Resources

Nyl provides Kubernetes-style custom resources for declarative configuration and deployment.

Resource Types

Core Resources

  • NylRelease: Defines release metadata (name, namespace) for deployments
  • Component: Compact chart-backed resource using dynamic kind lookup
  • HelmChart: Declarative Helm chart deployment with templating support
  • RemoteManifest: Fetch and include manifests from a remote HTTPS URL

ArgoCD Resources

Policy Resources

  • Kyverno Policies: Apply Kyverno mutation and validation policies at render time

Resource Format

All Nyl resources follow Kubernetes resource conventions:

apiVersion: <api-version>
kind: <resource-kind>
metadata:
  name: <name>
  namespace: <namespace>  # Optional
spec:
  # Resource-specific fields

API Versions

  • nyl.niklasrosenstein.github.com/v1: Core Nyl resources (NylRelease, HelmChart)
    • Includes: NylRelease, HelmChart, RemoteManifest
  • argocd.nyl.niklasrosenstein.github.com/v1: ArgoCD integration resources (ApplicationGenerator)
  • components.nyl.niklasrosenstein.github.com/v1: Component resources (dynamic kind path/shortcut)

Processing Behavior

Regular Kubernetes Resources

Regular Kubernetes resources (ConfigMap, Deployment, etc.) are passed through unchanged during nyl render.

Nyl Resources

Nyl resources are processed based on their kind:

  • NylRelease: Extracted and removed from output (provides metadata only)
  • Component: Resolved to a chart reference and rendered via Helm, replaced with rendered manifests
  • HelmChart: Rendered using Helm templating, replaced with rendered manifests
  • RemoteManifest: Fetched via HTTPS and parsed into documents, then processed recursively
  • ApplicationGenerator: Processed to generate ArgoCD Applications, removed from output

Multi-Document Files

Nyl supports YAML multi-document files with --- separators:

apiVersion: nyl.niklasrosenstein.github.com/v1
kind: NylRelease
metadata:
  name: myapp
  namespace: default
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: myapp-config
data:
  key: value
---
apiVersion: v1
kind: Service
metadata:
  name: myapp-svc
spec:
  ports:
    - port: 80

Processing:

  1. NylRelease is extracted (provides name and namespace)
  2. ConfigMap and Service are output as-is

See Also

Component

Component is a chart-backed resource format identified by:

  • apiVersion: components.nyl.niklasrosenstein.github.com/v1
  • dynamic kind (not a fixed literal)

Unlike fixed resource kinds, kind encodes either:

  • a local component path (<apiVersion>/<kind>)
  • a remote shortcut (<base>[#<name>][@<version>])

Resource Shape

apiVersion: components.nyl.niklasrosenstein.github.com/v1
kind: example/v1/Nginx
metadata:
  name: web
  namespace: default
spec:
  replicaCount: 2

Semantics

  • kind: chart target (local or remote shortcut)
  • metadata.name: Helm release name
  • metadata.namespace: Helm release namespace (defaults to default)
  • spec: Helm values payload

Local Resolution

With:

[project]
components_search_paths = ["components"]

kind: example/v1/Nginx resolves to:

components/example/v1/Nginx/Chart.yaml

Nyl checks components_search_paths in order and uses the first match.

Alias Mapping

project.aliases can map regular resource types to component targets:

[project.aliases]
"myapi.io/v1/MyKind" = "oci://registry-1.docker.io/bitnamicharts/nginx@18.2.4"

When an alias matches, Nyl resolves directly to the alias target.

Remote Shortcut Parsing

Shortcut format:

<base>[#<name>][@<version>]

Remote bases:

  • http:// or https://
  • oci://
  • git+

For complete shortcut examples and guidance, see Remote Shortcuts & Aliases.

HelmChart

The HelmChart resource enables declarative Helm chart deployment with templating support. Charts can be referenced from local paths, chart names, or Git repositories.

Use HelmChart when you want explicit chart fields in spec.chart.*.
Use Component when you want compact chart-backed resources with dynamic kind and optional alias indirection.

Note: Git chart references are fully supported. Repositories are cloned automatically to a local cache. See the Git Integration guide for details.

Component resources provide a compact chart-backed format and support local component paths, remote shortcut syntax, and alias-based indirection.

For component syntax, examples, and pattern selection, see:

Resource Definition

apiVersion: nyl.niklasrosenstein.github.com/v1
kind: HelmChart
metadata:
  name: string              # Helm release name
  namespace: string         # Target namespace (optional, defaults to "default")
spec:
  chart:                    # Chart reference (choose one method)
    # Universal fields:
    repository: string      # Repository URL (Git, OCI, or Helm)
    name: string            # Universal name field (context-dependent)
    version: string         # Chart version or Git reference

    # Repository types (indicated by protocol prefix):
    # - Git: repository starts with "git+" (e.g., "git+https://...")
    # - OCI: repository starts with "oci://" (e.g., "oci://ghcr.io/...")
    # - Helm: plain HTTPS URL (e.g., "https://charts.example.com")
    # - Local: no repository, name is filesystem path

  values: object            # Chart values (merged with profile values)

Chart Reference Methods

Local Path

Reference a chart by filesystem path (absolute or relative) using the name field:

apiVersion: nyl.niklasrosenstein.github.com/v1
kind: HelmChart
metadata:
  name: nginx
  namespace: default
spec:
  chart:
    name: ./charts/nginx

Chart Name

Reference a chart by name (without path separators), searched in configured search paths:

apiVersion: nyl.niklasrosenstein.github.com/v1
kind: HelmChart
metadata:
  name: nginx
  namespace: default
spec:
  chart:
    name: nginx

Configure search paths in nyl.toml:

[project]
helm_chart_search_paths = ["./charts", "/opt/helm-charts"]

Git Repository

Reference a chart from a Git repository using the git+ protocol prefix:

apiVersion: nyl.niklasrosenstein.github.com/v1
kind: HelmChart
metadata:
  name: nginx
  namespace: default
spec:
  chart:
    repository: git+https://github.com/bitnami/charts.git
    version: main
    name: bitnami/nginx

Git Parameters:

  • repository (required): Git repository URL with git+ prefix (HTTPS or SSH)
  • version (optional): Branch, tag, or commit SHA (default: HEAD)
  • name (optional): Subdirectory within the repository containing the chart

Helm Dependencies:

Charts from Git repositories with dependencies are automatically handled. If your chart has a Chart.yaml with dependencies or a Chart.lock file, Nyl will automatically run helm dependency build to fetch and build the dependencies before rendering the chart.

Examples:

# Latest from main branch
chart:
  repository: git+https://github.com/example/charts.git
  version: main
  name: charts/myapp

# Specific version tag
chart:
  repository: git+https://github.com/example/charts.git
  version: v2.1.0
  name: charts/myapp

# Specific commit
chart:
  repository: git+https://github.com/example/charts.git
  version: abc123def456
  name: charts/myapp

# Root of repository (no subpath)
chart:
  repository: git+https://github.com/example/simple-chart.git
  version: main

# SSH URL
chart:
  repository: git+git@github.com:example/charts.git
  version: main
  name: charts/myapp

See Git Integration for more details on Git support.

Release Configuration

The Helm release is configured via the metadata fields:

metadata:
  name: myapp           # Helm release name
  namespace: production # Target namespace

Defaults:

  • namespace: Uses default if not specified

Creating Namespaces

If you need to create the namespace before deploying the chart, add a Namespace resource:

apiVersion: v1
kind: Namespace
metadata:
  name: production
---
apiVersion: nyl.niklasrosenstein.github.com/v1
kind: HelmChart
metadata:
  name: myapp
  namespace: production
spec:
  chart:
    name: ./charts/myapp
  values:
    replicas: 3

When using ArgoCD, you can alternatively enable automatic namespace creation:

apiVersion: argoproj.io/v1alpha1
kind: Application
spec:
  syncPolicy:
    syncOptions:
      - CreateNamespace=true

Values

Chart values can be provided in multiple ways:

Inline Values

spec:
  values:
    image:
      repository: nginx
      tag: "1.25"
    replicas: 3
    service:
      type: LoadBalancer

Profile Values

Values from the active profile are automatically merged:

[profile.values.production]
replicas = 5

[profile.values.production.resources.requests]
cpu = "500m"
memory = "512Mi"

Inline values take precedence over profile values.

Templating in Values

Values support Jinja2 templating:

spec:
  values:
    image:
      tag: "{{ env.NYL_IMAGE_TAG }}"
    environment: "{{ profile }}"

Complete Example

apiVersion: nyl.niklasrosenstein.github.com/v1
kind: NylRelease
metadata:
  name: myapp
  namespace: production
---
apiVersion: nyl.niklasrosenstein.github.com/v1
kind: HelmChart
metadata:
  name: myapp
  namespace: production
spec:
  chart:
    repository: git+https://github.com/company/charts.git
    version: v2.1.0
    name: applications/myapp
  values:
    replicas: 3
    image:
      repository: company/myapp
      tag: "{{ env.VERSION }}"
    ingress:
      enabled: true
      host: myapp.example.com
    resources:
      requests:
        cpu: 500m
        memory: 512Mi
      limits:
        cpu: 1000m
        memory: 1Gi

Multi-Environment Deployments

Use the same chart with different values per environment:

# base manifest
apiVersion: nyl.niklasrosenstein.github.com/v1
kind: HelmChart
metadata:
  name: myapp
spec:
  chart:
    repository: git+https://github.com/company/charts.git
    version: stable
    name: myapp
  values:
    # Base values here
[profile.values.development]
replicas = 1
environment = "development"

[profile.values.development.image]
tag = "latest"

[profile.values.production]
replicas = 5
environment = "production"

[profile.values.production.image]
tag = "v2.1.0"

[profile.values.production.resources.requests]
cpu = "1000m"

Render for specific environment:

nyl render --profile production app.yaml

See Also

RemoteManifest

RemoteManifest fetches YAML/JSON documents from a remote HTTPS URL and feeds them into Nyl’s normal render pipeline.

API Version

  • nyl.niklasrosenstein.github.com/v1

Schema

apiVersion: nyl.niklasrosenstein.github.com/v1
kind: RemoteManifest
metadata:
  name: <name>
spec:
  url: https://example.com/path/manifests.yaml
  overrideNamespace: false

Fields

  • spec.url (required): HTTPS URL containing one or more YAML/JSON documents.
  • spec.overrideNamespace (optional, default false): when true, fetched resources that already have metadata.namespace will have that value replaced with RemoteManifest.metadata.namespace.

Behavior

  • URL must use https://.
  • Fetching uses Nyl’s native HTTPS client (no shell-out), with HTTPS-only redirect policy.
  • Request timeouts are enforced (connect: 5s, total: 30s).
  • Response size is limited to 30 MiB; larger payloads fail fast.
  • Content is parsed as YAML multi-document stream.
  • Parsed resources are processed recursively like local resources.
  • Remote content is not rendered as a Jinja template.
  • When spec.overrideNamespace: true, remote manifests with metadata.namespace are rewritten to RemoteManifest.metadata.namespace.
  • Special case: for RoleBinding and ClusterRoleBinding (rbac.authorization.k8s.io/*), subjects[*].namespace is also rewritten (ServiceAccount subjects are forced to the override namespace).
  • Potential future rewrite targets (currently not handled): webhook service namespaces (MutatingWebhookConfiguration, ValidatingWebhookConfiguration, CRD conversion webhook), and APIService.spec.service.namespace.
  • Fetch or parse failures stop the command (render, diff, apply).

Example

apiVersion: nyl.niklasrosenstein.github.com/v1
kind: RemoteManifest
metadata:
  name: shared-crds
spec:
  url: https://example.com/platform/crds.yaml

NylRelease

The NylRelease resource specifies release metadata (name and namespace) for a deployment. It’s an optional resource that provides context for rendering manifests.

Resource Definition

apiVersion: nyl.niklasrosenstein.github.com/v1
kind: NylRelease
metadata:
  name: string        # Release name
  namespace: string   # Target namespace
spec: {}              # Reserved for future use
  argocd:             # Optional ArgoCD customization
    applicationOverride: {}   # Partial Application override (object)

Field Reference

metadata.name

  • Type: string (required)
  • Description: The release name
  • Usage: Used as the Helm release name when rendering HelmCharts

metadata.namespace

  • Type: string (required)
  • Description: The target namespace for this release
  • Usage: Resources are deployed to this namespace

spec.argocd.applicationOverride

  • Type: object (optional)
  • Description: Partial ArgoCD Application override for use with ApplicationGenerator release customization policy
  • Behavior:
    • Override fields are applied only if allowed by ApplicationGenerator.spec.releaseCustomization
    • Plain keys replace the generated Application value at that field path
    • Keys prefixed with + append to list-valued Application fields instead of replacing them
    • +<field> uses the canonical field path without + for allow/deny checks
    • Unsupported or disallowed fields are ignored
    • Invalid append operations (for example using + on a non-list field or with a non-list value) are ignored
    • Ignored fields are reported as a warning in generated Application.spec.info

Behavior

During Rendering

When nyl render encounters a NylRelease:

  1. The resource is extracted from the file
  2. Metadata is used for rendering other resources in the file
  3. The NylRelease itself is not included in the output

With ApplicationGenerator

When ApplicationGenerator scans files:

  1. Files with a NylRelease are discovered
  2. An ArgoCD Application is generated per NylRelease
  3. Application metadata comes from the NylRelease

Singleton Constraint

Only one NylRelease is allowed per file. Multiple NylReleases in the same file will cause an error:

Error: Multiple NylRelease resources found in file

Examples

Minimal NylRelease

apiVersion: nyl.niklasrosenstein.github.com/v1
kind: NylRelease
metadata:
  name: myapp
  namespace: default

With Other Resources

apiVersion: nyl.niklasrosenstein.github.com/v1
kind: NylRelease
metadata:
  name: myapp
  namespace: production
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: myapp-config
  namespace: production
data:
  environment: production
  version: "1.0.0"
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp
  namespace: production
spec:
  replicas: 3
  selector:
    matchLabels:
      app: myapp
  template:
    metadata:
      labels:
        app: myapp
    spec:
      containers:
        - name: myapp
          image: myapp:1.0.0

With HelmChart

apiVersion: nyl.niklasrosenstein.github.com/v1
kind: NylRelease
metadata:
  name: nginx
  namespace: web
---
apiVersion: nyl.niklasrosenstein.github.com/v1
kind: HelmChart
metadata:
  name: nginx
  namespace: web
spec:
  chart:
    repository: https://charts.bitnami.com/bitnami
    name: nginx
    version: "15.4.4"
  values:
    replicaCount: 2

Use Cases

ArgoCD Integration

NylRelease is primarily used for ArgoCD integration:

# app.yaml in Git repository
apiVersion: nyl.niklasrosenstein.github.com/v1
kind: NylRelease
metadata:
  name: myapp
  namespace: production
---
# ... other resources ...

ArgoCD Application:

apiVersion: argoproj.io/v1alpha1
kind: Application
spec:
  source:
    plugin:
      name: nyl-v2
      env:
        - name: NYL_RELEASE_NAME
          value: myapp          # From NylRelease.metadata.name
        - name: NYL_RELEASE_NAMESPACE
          value: production     # From NylRelease.metadata.namespace

Appending Sync Options

applicationOverride can append to list-valued Application fields by prefixing the field name with +.

apiVersion: nyl.niklasrosenstein.github.com/v1
kind: NylRelease
metadata:
  name: service-proxies
  namespace: home-proxy
spec:
  argocd:
    applicationOverride:
      spec:
        syncPolicy:
          +syncOptions:
            - RespectIgnoreDifferences=false

When combined with an ApplicationGenerator default such as:

syncPolicy:
  syncOptions:
    - ServerSideApply=true
    - ApplyOutOfSyncOnly=true

the generated ArgoCD Application contains all three entries in order:

spec:
  syncPolicy:
    syncOptions:
      - ServerSideApply=true
      - ApplyOutOfSyncOnly=true
      - RespectIgnoreDifferences=false

ApplicationGenerator Discovery

ApplicationGenerator scans for NylRelease resources:

# apps.yaml
apiVersion: argocd.nyl.niklasrosenstein.github.com/v1
kind: ApplicationGenerator
spec:
  source:
    path: clusters/default  # Scans this directory

For each file in clusters/default/ with a NylRelease, an Application is generated.

Metadata Propagation

NylRelease metadata can be used in templates (future enhancement):

apiVersion: v1
kind: ConfigMap
metadata:
  name: {{ .release.name }}-config
  namespace: {{ .release.namespace }}
data:
  release_name: {{ .release.name }}

Validation

Required Fields

  • metadata.name must be specified
  • metadata.namespace must be specified

Missing fields cause validation errors:

Error: Invalid NylRelease resource: missing field `metadata.name`

Valid Names

Names must follow Kubernetes naming conventions:

  • Lowercase alphanumeric characters, -, .
  • Start and end with alphanumeric
  • Max 253 characters

When Not to Use

You don’t need NylRelease if:

  • Deploying plain Kubernetes manifests without Helm
  • Not using ArgoCD integration
  • Rendering manifests locally without release context

In these cases, just use regular Kubernetes resources directly.

See Also

ApplicationGenerator Resource Reference

For detailed ApplicationGenerator documentation, see:

ArgoCD ApplicationGenerator Guide

Quick Reference

apiVersion: argocd.nyl.niklasrosenstein.github.com/v1
kind: ApplicationGenerator
metadata:
  name: cluster-apps
  namespace: argocd
spec:
  destination:
    server: https://kubernetes.default.svc
    namespace: argocd
  source:
    repoURL: https://github.com/org/repo.git
    targetRevision: HEAD
    path: clusters/default       # Exactly one of path or paths is required
    # paths: ["clusters/*/apps", "shared/apps/*.yaml"]
  project: default
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
  releaseCustomization:
    allowedPaths:
      - metadata.annotations."pref.argocd.argoproj.io/*"
      - spec.info.**
      - spec.ignoreDifferences.**
      - spec.syncPolicy.**
    deniedPaths:
      - spec.syncPolicy.automated.prune

The ApplicationGenerator resource enables automatic generation of ArgoCD Applications from NylRelease files in a directory.

Key behavior:

  • source.path and source.paths are mutually exclusive.
  • A directory selector is scanned non-recursively by default.
  • Use glob selectors in path/paths when you want recursive discovery.
  • include/exclude patterns are matched against file paths relative to the repository root.
  • Source resolution can reuse a matching current local Git checkout before falling back to ArgoCD checkout reuse or Nyl’s Git cache/worktree flow.
  • NylRelease.spec.argocd.applicationOverride is always evaluated against allowedPaths/deniedPaths.
  • allowedPaths/deniedPaths use dotted globs where * matches one segment and ** matches multiple segments.
  • If both allow and deny match, deny wins. Ignored fields are reported in generated Application.spec.info.

See the full guide for complete field reference, examples, and usage patterns.

Kyverno Policies

Nyl supports applying Kyverno policies to Kubernetes manifests at render time. This allows you to mutate and validate resources before they are applied to the cluster.

Overview

Kyverno policies in Nyl are standard Kyverno CRDs annotated with a scope that determines when and where they are applied during rendering. Policies without the scope annotation are treated as normal Kubernetes resources and passed through to the output.

Quick Start

apiVersion: policies.kyverno.io/v1
kind: MutatingPolicy
metadata:
  name: add-managed-by-label
  annotations:
    nyl.niklasrosenstein.github.com/apply-policy-scope: Global
spec:
  matchConstraints:
    resourceRules:
      - apiGroups: ['']
        apiVersions: ['v1']
        operations: ['CREATE']
        resources: ['configmaps', 'services']
  mutations:
    - patchType: ApplyConfiguration
      applyConfiguration:
        expression: >-
          Object{metadata: Object.metadata{labels: Object.metadata.labels{managedBy: "nyl"}}}

Supported Policy Types

Nyl supports standard Kyverno policy CRDs:

Working Policy Types

These policy types work with kyverno apply in offline mode:

  • ClusterPolicy (kyverno.io/v1) - Cluster-wide policies using traditional Kyverno format
  • MutatingPolicy (policies.kyverno.io/v1) - Namespace or cluster-scoped mutation policies
  • ValidatingPolicy (policies.kyverno.io/v1) - Namespace or cluster-scoped validation policies

Detected but Not Evaluable

These policy types are detected but require a live Kubernetes cluster:

  • GeneratingPolicy (policies.kyverno.io/v1) - Generate new resources
  • DeletingPolicy (policies.kyverno.io/v1) - Delete resources
  • ImageValidatingPolicy (policies.kyverno.io/v1) - Validate container images

Scope Annotation

Policies must include the nyl.niklasrosenstein.github.com/apply-policy-scope annotation to be processed by Nyl.

Available Scopes

Global (Currently Supported)

Applies to all resources in the rendered file. Since Nyl processes single files, this means all resources from the manifest being rendered.

metadata:
  annotations:
    nyl.niklasrosenstein.github.com/apply-policy-scope: Global

Use cases:

  • Organization-wide labeling standards
  • Security policies applied to all resources
  • Compliance requirements

Future Scopes (Not Yet Implemented)

  • Subtree: Applies to siblings and all descendant resources
  • Immediate: Applies to sibling resources only

Policies with these scopes will trigger a warning but won’t cause errors.

Policy Examples

Mutation Policy

Add labels to all ConfigMaps and Secrets:

apiVersion: policies.kyverno.io/v1
kind: MutatingPolicy
metadata:
  name: add-labels
  annotations:
    nyl.niklasrosenstein.github.com/apply-policy-scope: Global
spec:
  matchConstraints:
    resourceRules:
      - apiGroups: ['']
        apiVersions: ['v1']
        operations: ['CREATE']
        resources: ['configmaps', 'secrets']
  mutations:
    - patchType: ApplyConfiguration
      applyConfiguration:
        expression: >-
          Object{
            metadata: Object.metadata{
              labels: Object.metadata.labels{
                environment: "production",
                managedBy: "nyl"
              }
            }
          }

Validation Policy

Require specific labels on all resources:

apiVersion: policies.kyverno.io/v1
kind: ValidatingPolicy
metadata:
  name: require-labels
  annotations:
    nyl.niklasrosenstein.github.com/apply-policy-scope: Global
spec:
  matchConstraints:
    resourceRules:
      - apiGroups: ['*']
        apiVersions: ['*']
        operations: ['CREATE']
        resources: ['*']
  validations:
    - expression: >-
        has(object.metadata.labels) &&
        'environment' in object.metadata.labels &&
        'team' in object.metadata.labels
      message: "All resources must have 'environment' and 'team' labels"

ClusterPolicy (Traditional Format)

Using the older kyverno.io/v1 ClusterPolicy format:

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: add-default-network-policy
  annotations:
    nyl.niklasrosenstein.github.com/apply-policy-scope: Global
spec:
  rules:
    - name: add-network-policy-annotation
      match:
        any:
          - resources:
              kinds:
                - Namespace
      mutate:
        patchStrategicMerge:
          metadata:
            annotations:
              networking.policy: "default-deny"

Service Load Balancer Configuration

Automatically configure LoadBalancer services:

apiVersion: policies.kyverno.io/v1
kind: MutatingPolicy
metadata:
  name: configure-loadbalancer
  annotations:
    nyl.niklasrosenstein.github.com/apply-policy-scope: Global
spec:
  matchConstraints:
    resourceRules:
      - apiGroups: ['']
        apiVersions: ['v1']
        operations: ['CREATE']
        resources: ['services']
  mutations:
    - patchType: ApplyConfiguration
      applyConfiguration:
        expression: >-
          object.spec.type == 'LoadBalancer'
            ? Object{
                spec: Object.spec{
                  loadBalancerClass: "ngrok",
                  allocateLoadBalancerNodePorts: false
                }
              }
            : object

Unannotated Policies

Policies without the scope annotation are treated as normal Kubernetes resources and included in the output. This allows you to:

  1. Deploy policies to the cluster along with your applications
  2. Manage policies as part of your GitOps workflow
  3. Use Nyl for some policies (with annotations) and deploy others normally
# This policy will be included in output but NOT applied by Nyl
apiVersion: policies.kyverno.io/v1
kind: MutatingPolicy
metadata:
  name: cluster-policy  # No scope annotation
spec:
  matchConstraints:
    resourceRules:
      - apiGroups: ['apps']
        apiVersions: ['v1']
        operations: ['CREATE']
        resources: ['deployments']
  mutations:
    - patchType: ApplyConfiguration
      applyConfiguration:
        expression: "object"

Policy Processing

Processing Order

  1. Policy Detection: Nyl scans all manifests for Kyverno policy CRDs
  2. Scope Extraction: Policies with the scope annotation are extracted and grouped by scope
  3. Policy Application: Global policies are applied to all non-policy resources
  4. Output: Mutated resources are output; annotated policy resources are excluded

Policy Exclusion

Policy resources themselves are automatically excluded from mutation by other policies. This prevents circular mutations and ensures policies remain unchanged.

Validation Failures

If a validation policy fails, the nyl render command will fail with an error:

$ nyl render
ERROR: Configuration error: Kyverno apply failed with exit code Some(1):
...
Validation error: ConfigMap must have 'environment' label

CEL Expressions

Kyverno policies use Common Expression Language (CEL) for mutations and validations.

Common Patterns

Check if field exists:

has(object.metadata.labels)

Check if key exists in map:

'environment' in object.metadata.labels

Conditional mutation:

object.spec.type == 'LoadBalancer'
  ? Object{spec: Object.spec{loadBalancerClass: "ngrok"}}
  : object

Add or merge labels:

Object{
  metadata: Object.metadata{
    labels: Object.metadata.labels{
      newLabel: "value"
    }
  }
}

Set deeply nested field:

Object{
  spec: Object.spec{
    template: Object.spec.template{
      spec: Object.spec.template.spec{
        securityContext: Object{runAsNonRoot: true}
      }
    }
  }
}

Requirements

  • Kyverno CLI: Must be installed and available in PATH
    # Install on Linux
    curl -LO https://github.com/kyverno/kyverno/releases/download/v1.17.0/kyverno-cli_v1.17.0_linux_x86_64.tar.gz
    tar -xzf kyverno-cli_v1.17.0_linux_x86_64.tar.gz
    sudo mv kyverno /usr/local/bin/
    
    # Install on macOS
    brew install kyverno
    
    # Verify installation
    kyverno version
    

Best Practices

  1. Use Global scope sparingly: Global policies affect all resources and can have wide-reaching effects
  2. Test policies locally: Use nyl render to verify policy behavior before deployment
  3. Provide clear error messages: Include descriptive messages in validation policies
  4. Document policy intent: Use metadata annotations to document why policies exist
  5. Prefer immutable labels: Add labels rather than modifying existing ones
  6. Version your policies: Include version information in policy names or labels

Troubleshooting

Policy Not Applied

Issue: Policy appears in output but isn’t being applied

Solution: Check that the policy has the scope annotation:

metadata:
  annotations:
    nyl.niklasrosenstein.github.com/apply-policy-scope: Global

Kyverno CLI Not Found

Issue: Kyverno CLI is not installed but Kyverno policies were found

Solution: Install the Kyverno CLI (see Requirements section above)

Validation Policy Passes But Shouldn’t

Issue: Resources that should fail validation are passing

Solution: Verify the CEL expression logic and matchConstraints are correct. Test with kyverno apply directly:

kyverno apply policy.yaml --resource resource.yaml

Multiple Policies Conflict

Issue: Multiple policies are mutating the same field differently

Solution: Ensure policies are orthogonal (affect different fields) or use proper scoping to control application order

See Also

Schemas

nyl.toml JSON Schema

Generate from CLI:

nyl generate schema config

Committed schema used in published docs:

Renovate Integration

Nyl ships a reusable Renovate preset for Helm chart version bumps in HelmChart, Component shortcuts, and project.aliases.

Use The Shared Preset

In your project’s renovate.json:

{
  "extends": [
    "config:recommended",
    "github>NiklasRosenstein/nyl//.github/renovate/nyl-helm-components.json5"
  ]
}

What It Matches

The preset adds regex managers for:

  • HelmChart resources (apiVersion: nyl.niklasrosenstein.github.com/v1)
    • spec.chart.repository + spec.chart.name + spec.chart.version
  • Component shortcut resources (apiVersion: components.nyl.niklasrosenstein.github.com/v1)
    • kind: https://...#<chart>@<version>
    • kind: oci://...@<version>
    • kind: git+...#<path>@<ref>
  • nyl.toml alias targets in [project.aliases]
    • "myapi/v1/Kind" = "https://...#<chart>@<version>"
    • "myapi/v1/Kind" = "oci://...@<version>"
    • "myapi/v1/Kind" = "git+...#<path>@<ref>"

Datasource Mapping

  • https://... shortcuts and Helm repositories -> Renovate helm datasource
  • oci://... shortcuts and repositories -> Renovate docker datasource
  • git+... shortcuts and repositories -> Renovate git-tags datasource

Notes And Limitations

  • Local component kinds (for example example/v1/Nginx) are intentionally ignored.
  • Matches expect canonical field names (repository, name, version) in HelmChart specs.
  • git+...@<ref> updates depend on available Git tags for the referenced repository.

Troubleshooting

  • If a dependency is not detected, first verify it includes an explicit @<version> (or spec.chart.version for HelmChart).
  • Run renovate-config-validator against your final config to verify schema and templates.

Validate Matcher Coverage

Nyl includes fixture-based matcher tests for the shared preset:

mise run test-renovate-matchers

The test runs the official renovate/renovate Docker image and validates extracted dependencies from Renovate’s own logs. Docker daemon access is required.

Fixtures are in integrationtests/fixtures/renovate-matchers/:

  • positive/: inputs that must match
  • negative/: inputs that must not match
  • expected.json: expected extracted dependencies (datasource, depName, currentValue) per fixture file

The validation is extraction-focused: it asserts matching behavior and extracted dependency identity, and does not require successful network datasource lookups.

When changing .github/renovate/nyl-helm-components.json5, update fixtures and expected.json in the same change to keep matcher behavior explicit.