Nixidy Architecture Guide for Contributors#
This guide provides a comprehensive overview of the nixidy codebase to help new contributors understand the project structure, key concepts, and development workflow.
Table of Contents#
- Project Structure
- Core Concepts
- Module System Deep Dive
- Library Functions
- Code Generators
- Testing Framework
- Development Workflow
- Adding New Features
- Common Tasks
- Code Style
Project Structure#
nixidy/
├── actions/ # GitHub Actions (build, switch)
├── cli/ # Python-based CLI tool
├── docs/ # Documentation (MkDocs)
│ ├── developer_guide/ # Contributor-facing documentation
│ └── user_guide/ # User-facing documentation
├── lib/ # Nix function library
│ ├── helpers/ # Helper scripts (e.g. update-chart.sh)
│ ├── default.nix # Library entry point
│ ├── helm.nix # Helm-related functions
│ ├── kube.nix # Kubernetes utility functions
│ ├── kustomize.nix # Kustomize-related functions
│ └── tests.nix # Library unit tests
├── modules/ # NixOS-style modules
│ ├── applications/ # Application submodule
│ │ ├── default.nix # Application options and config
│ │ ├── argocd.nix # ArgoCD options (syncPolicy, destination, ...)
│ │ ├── helm.nix # Helm release processing
│ │ ├── kustomize.nix # Kustomize processing
│ │ ├── lib.nix # Application helper functions
│ │ ├── objects.nix # Resource type registry and final object list
│ │ └── yamls.nix # Raw YAML processing
│ ├── generated/ # Auto-generated resource options
│ │ ├── argocd.nix # ArgoCD CRD options
│ │ └── k8s/ # Kubernetes resource options by version
│ ├── nixidy/ # Core nixidy configuration
│ │ ├── default.nix # Core options (env, target, charts, ...)
│ │ ├── app-of-apps.nix # App-of-apps pattern generation
│ │ ├── defaults.nix # Default settings (`nixidy.defaults`)
│ │ ├── extra-files.nix # Extra files configuration
│ │ └── transforms.nix # objectTransforms rule engine
│ ├── testing/ # Testing framework modules
│ │ ├── default.nix # Test suite configuration
│ │ └── eval.nix # Test evaluation logic
│ ├── build/ # Build output packages
│ │ ├── default.nix # Option surface, layout wiring, assertions
│ │ ├── layout.nix # Pure per-application [FileSpec] core
│ │ ├── render.nix # FileSpec -> shell fragment
│ │ ├── emit-environment.nix # environment/activation/bootstrap/extras
│ │ └── apply.nix # Apply path + activation post-process
│ ├── applications.nix # Main applications option
│ ├── default.nix # Module entry point
│ ├── modules.nix # Module list
│ └── templates.nix # Template system
├── pkgs/ # Nix packages and generators
│ └── generators/ # CRD and K8s schema generators
│ ├── compile/ # Schema → options compilation
│ │ ├── walk.nix # Shared schema traversal (backend-parameterized)
│ │ ├── backend-text.nix # Text backend: emits Nix source
│ │ ├── backend-value.nix # Value backend: live module values
│ │ ├── generator.nix # File assembler (text backend → .nix file)
│ │ ├── module.nix # Module assembler (value backend → module value)
│ │ └── runtime.nix # Runtime helpers as values (for module.nix)
│ ├── sources/ # Schema acquisition
│ │ ├── versions.nix # Kubernetes versions config
│ │ ├── k8s.nix # Kubernetes OpenAPI spec acquisition
│ │ ├── crd.nix # CRD file acquisition
│ │ └── chart.nix # Helm chart CRD acquisition
│ ├── crd2jsonschema.py # CRD to JSON schema converter
│ ├── default.nix # Generator entry point (accessors)
│ ├── equiv-test.nix # Accessor equivalence checks
│ └── test_crd2jsonschema.py # crd2jsonschema unit tests
├── tests/ # Module unit tests
│ ├── helm/ # Helm-specific tests
│ ├── kustomize/ # Kustomize-specific tests
│ └── *.nix # Individual test files
├── flake.nix # Nix flake definition
└── default.nix # Non-flake entry point
Core Concepts#
1. NixOS Module System#
Nixidy is built on top of the NixOS module system. If you're unfamiliar with it, read the NixOS Module System documentation first.
Key concepts:
- Options: Define the configuration interface with types, defaults, and descriptions
- Config: Contains the actual configuration values after module evaluation
- Imports: Modules can import other modules to extend functionality
- Special Args: Additional arguments passed to all modules (
lib,pkgs,config, etc.)
2. Application Lifecycle#
User Config → Module Evaluation → Resource Processing → Manifest Generation → Output
- User Configuration: Nix files defining applications and resources
- Module Evaluation: NixOS module system merges all configurations
- Resource Processing: Helm/Kustomize/YAML converted to typed resources
- Manifest Generation: Resources serialized to YAML files
- Output: Packages created for different deployment strategies
3. Resource Type System#
Resources are organized by Group/Version/Kind (GVK):
resources.<group>.<version>.<kind>.<name> = { ... };
# Examples:
resources.core.v1.ConfigMap.my-config = { ... };
resources.apps.v1.Deployment.nginx = { ... };
resources."networking.k8s.io".v1.Ingress.main = { ... };
Aliases provide convenient access:
resources.configMaps.my-config = { ... }; # → core.v1.ConfigMap
resources.deployments.nginx = { ... }; # → apps.v1.Deployment
resources.ingresses.main = { ... }; # → networking.k8s.io.v1.Ingress
4. Processing Pipeline#
All input sources are normalized to typed nix resources:
┌─────────────────┐
│ Helm Charts │──┐
├─────────────────┤ │
│ Kustomize │──┤──→ [GVK Classification] ──→ [Typed Nix Resources] ──→ [YAML Output]
├─────────────────┤ │
│ Raw YAML │──┘
└─────────────────┘
Module System Deep Dive#
Main Modules#
modules/applications.nix#
Defines the top-level applications option:
{
options.applications = mkOption {
type = attrsOf (submoduleWith {
modules = [ ./applications ] ++ config.nixidy.applicationImports;
specialArgs.nixidyDefaults = config.nixidy.defaults;
});
};
}
Key responsibilities:
- Creates application submodules
- Manages Kubernetes version selection (
nixidy.k8sVersion) - Imports generated resource options
modules/nixidy/#
Core nixidy configuration, split across several files. default.nix defines the
main options and imports the rest:
{
options.nixidy = {
env = mkOption { ... }; # Environment name
target.repository = mkOption { ... }; # Git repository URL
target.branch = mkOption { ... }; # Target branch
target.rootPath = mkOption { ... }; # Root path for manifests
build.revision = mkOption { ... }; # Revision written to `.revision`
objectTransforms = mkOption { ... }; # Env-wide transform rules
charts = mkOption { ... }; # Helm chart sources
chartsDir = mkOption { ... }; # Directory to build charts from
};
}
The directory also contains:
defaults.nix: default settings (nixidy.defaults, e.g. default transformers)app-of-apps.nix: bootstrap app-of-apps pattern generationextra-files.nix: extra files (nixidy.extraFiles) rendered into the outputtransforms.nix: theobjectTransformsrule type, matcher, and assertions
Also handled here:
- Chart attribute set building (from
nixidy.chartsDir) - Public apps list management
- Global assertions and warnings
modules/applications/default.nix#
Defines individual application options:
{
options = {
name = mkOption { ... };
namespace = mkOption { ... };
createNamespace = mkOption { ... };
project = mkOption { ... };
annotations = mkOption { ... };
labels = mkOption { ... };
objectTransforms = mkOption { ... }; # App-scoped transform rules
assertions = mkOption { ... }; # Build-time assertions
};
}
Related options live in sibling modules:
modules/applications/argocd.nix: ArgoCD-specific options (syncPolicy,destination,compareOptions,finalizer)modules/applications/objects.nix: the resource type registry (types), typedresources, and the internal finalobjectslist
Key responsibilities:
- Application metadata and settings
- Resource type registration
- Final object list generation
modules/applications/helm.nix#
Helm chart integration:
{
options.helm.releases = mkOption {
type = attrsOf (submodule {
options = {
chart = mkOption { ... };
values = mkOption { ... };
transformer = mkOption { ... };
};
});
};
}
Processing flow:
helm.buildHelmCharttemplates the chartbuiltins.readFilereads outputkube.fromYAMLparses to attribute setstransformerfunction appliedpartitionObjects(fromlib.nix) groups objects by GVK- Added to
resourcesorobjects
modules/applications/kustomize.nix#
Kustomize integration (similar pattern to Helm):
{
options.kustomize.applications = mkOption {
type = attrsOf (submodule {
options = {
kustomization.src = mkOption { ... };
kustomization.path = mkOption { ... };
transformer = mkOption { ... };
};
});
};
}
modules/applications/yamls.nix#
Raw YAML manifest support:
{
options.yamls = mkOption {
type = listOf str;
description = "List of YAML manifest strings";
};
# Also: extraRawYamls that are YAML files copied verbatim into
# the output directory (not parsed into Nix, so they can't be patched)
}
modules/build/#
Creates output packages:
environmentPackage: All application manifests combinedextrasPackage: Extra files fromnixidy.extraFilesactivationPackage: Fornixidy switchoperationsdeclarativePackage: Forkubectl apply --prune. Emits anapplyscript that consumesenvironmentPackageand runsobjectTransformspostProcessrules at apply time; it no longer renders standalone manifest files.bootstrapPackage: App-of-apps manifest
Everything is derived from one seam: layout.nix turns each application into a
list of FileSpec (one per output file), exposed as the internal
config.build.layout, keyed by the config.applications attribute name. The
emitters consume that list and never re-derive file paths:
| File | Responsibility |
|---|---|
layout.nix |
Pure core: eval-time rewrite transforms, object grouping, file classification, per-app [FileSpec] |
render.nix |
One FileSpec to the shell fragment that writes it |
emit-environment.nix |
environmentPackage, activationPackage, bootstrapPackage, extrasPackage |
apply.nix |
declarativePackage's apply script and the shared activation postProcess fragments |
default.nix |
Option surface, layout wiring, environment-scope assertions |
modules/templates.nix#
Template system for reusable patterns:
{
options.templates = mkOption {
type = attrsOf (submodule {
options = {
options = mkOption { ... }; # Template parameters
output = mkOption { ... }; # Resource generator function
};
});
};
}
Templates become application imports, allowing:
applications.myapp.templates.webApp.frontend = {
image = "nginx:latest";
replicas = 3;
};
Helper Functions (modules/applications/lib.nix)#
{
# Extract Group/Version/Kind from Kubernetes object
getGVK = object: {
group = ...; # "core" for v1, otherwise first part of apiVersion
version = ...; # Version string
kind = ...; # Kind string
};
# Flatten *List objects (e.g., ConfigMapList → [ConfigMap, ...])
flattenListObjects = ...;
# Split a flat object list into typed `resources` (registered GVKs) and
# untyped `objects`. This is the single intake path shared by the helm,
# kustomize, and yamls modules
partitionObjects = ...;
# Manifest filename stem for an object: `<Kind>-<dashed-name>`.
# Used by both `build.nix` and `yamls.nix` so they always agree
objectBaseName = ...;
}
Library Functions#
Entry Point (lib/default.nix)#
Extends nixpkgs.lib with nixidy functions:
lib.extend (self: old: {
kustomize = import ./kustomize.nix { ... };
helm = import ./helm.nix { ... };
kube = import ./kube.nix { ... };
})
Helm Functions (lib/helm.nix)#
| Function | Description |
|---|---|
downloadHelmChart |
Downloads chart from Helm registry |
buildHelmChart |
Templates chart with values |
getChartValues |
Parses chart's default values.yaml |
mkChartAttrs |
Creates chart attrset from directory structure |
mkChartsUpdateScript |
Builds a script that runs every chart's updateScript |
Kube Functions (lib/kube.nix)#
| Function | Description |
|---|---|
fromYAML |
Parses YAML string to attribute sets |
fromOctal |
Converts octal string to integer |
removeLabels |
Removes specified labels from manifests |
Kustomize Functions (lib/kustomize.nix)#
| Function | Description |
|---|---|
buildKustomization |
Builds kustomize application |
Code Generators#
Overview (pkgs/generators/)#
Nixidy generates typed Nix options from:
- Kubernetes OpenAPI schemas
- Custom Resource Definitions (CRDs)
Kubernetes Schema Generation#
pkgs/generators/default.nix wires everything together and acquisition lives in pkgs/generators/sources/:
sources/versions.nixlists the Kubernetes versions and their source hashessources/k8s.nixfetches the Kubernetes source for each version and extracts the OpenAPI swagger spec- Generates namespaced resource info
- Produces Nix options via
compile/generator.nix
Output: modules/generated/k8s/v1.XX.nix
Shared schema walk and backends#
The schema-to-options logic lives in one place, compile/walk.nix, parameterized over a backend so the same traversal can produce either Nix source text or live module values:
compile/walk.nix- the single traversal of the (JSON) schema. All type/coercion branch logic (int-or-string,additionalProperties→attrsOf, coerce-by-name lists, patch-merge-key,skipCoerceToList,specialMapKeys, nested submodules, etc.) lives here, expressed against an abstract backendb.compile/backend-text.nix- emits Nix source. Used by the file generators; it also inlines the runtime helpers (see below) as source so committed standalone files stay self-contained.compile/backend-value.nix- produces live module values usingruntime.nix.compile/generator.nix- thin assembler over the text backend (renders +nixfmt+ writes a.nixfile).compile/module.nix- thin assembler over the value backend (returns a module function).compile/runtime.nix- the per-generated-module runtime helpers (coercedTo,mergeValuesByKey,submoduleForDefinition, …) as values, imported by the value backend. The text backend inlines source-form equivalents of these same helpers. Only the walk is single-sourced; the helpers must exist in both forms because live values can't be serialized back to source and committed files can't reference nixidy internals.
CRD Generation#
CRD accessors form a matrix over output shape × source, all built on the shared walk:
| output | source files (src) |
Helm chart |
|---|---|---|
| generated file | fromCRD |
fromChartCRD |
| module value | fromCRDModule |
fromChartCRDModule |
| raw objects | crdObjects |
crdObjectsFromChart |
fromCRD {
name = "cilium";
src = pkgs.fetchFromGitHub { ... };
crdFiles = [ "path/to/crd.yaml" ]; # list of YAML files under `src`
kindFilter = [ ]; # Optional: only these CRD kinds (default: all)
namePrefix = ""; # Optional prefix for attribute names
attrNameOverrides = { }; # Manual name overrides
skipCoerceToList = { }; # Skip list coercion for specific fields
}
fromChartCRD {
name = "cert-manager";
chartAttrs = { repo = "..."; chart = "..."; version = "..."; };
kindFilter = [ "Certificate" ]; # Optional: only these CRD kinds (default: all)
kubeVersion = "v1.31.0"; # Optional: helm template --kube-version
}
The *Module variants take the same arguments and return a module value (no file); crdObjects/crdObjectsFromChart return the raw CustomResourceDefinition manifests as values.
Renamed arguments
The overloaded crds argument was split: source-based accessors now take crdFiles (the list of YAML files) and chart-based accessors take kindFilter (the kind filter). crds remains as a deprecated alias on every accessor (resolved by the shared renamedArg helper, which warns and points at the new name).
CRD Processing (crd2jsonschema.py)#
Python script that:
- Reads CRD YAML files
- Extracts OpenAPI v3 schemas
- Flattens
$refreferences - Outputs JSON schema for the shared walk (
compile/walk.nix)
Testing Framework#
Module Tests (tests/)#
Located in tests/, using nixidy's testing framework.
Test Structure#
# tests/my-feature.nix
{
lib,
config,
...
}:
let
apps = config.applications;
in
{
# Define test configuration
applications.test1 = {
namespace = "test";
resources.configMaps.cm.data.FOO = "bar";
};
# Define test assertions
test = {
name = "my feature test";
description = "Description of what's being tested";
assertions = [
{
description = "ConfigMap should have FOO key";
expression = (elemAt apps.test1.objects 0).data;
expected = { FOO = "bar"; };
}
{
description = "Custom assertion function";
expression = apps.test1.objects;
assertion = objs: length objs == 1;
}
];
};
}
Running Tests#
# Run module tests
nix run .#moduleTests
# Run library tests
nix run .#libTests
Test Registration (tests/default.nix)#
{
testing = {
name = "nixidy modules";
tests = [
./configmap.nix
./create-namespace.nix
./helm/with-values.nix
# ... more tests
];
};
}
Library Tests (lib/tests.nix)#
Uses lib.runTests pattern:
{
kube = {
fromYAML = {
testSingleObject = {
expr = lib.kube.fromYAML "...";
expected = [ { ... } ];
};
};
removeLabels = {
testLabelPresent = {
expr = lib.kube.removeLabels ["helm.sh/chart"] { ... };
expected = { ... };
};
};
};
}
Development Workflow#
Prerequisites#
- Nix with flakes enabled
- Basic understanding of NixOS module system
Common Commands#
# Format code
nix fmt
# Run static linter
nix run .#staticCheck
# Run library tests
nix run .#libTests
# Run module tests
nix run .#moduleTests
# Run crd2jsonschema unit tests
nix run .#crd2jsonschemaTest
# Assert CRD file/module/objects accessors agree with each other
nix run .#crdAccessorTest
# Generate Kubernetes modules
nix run .#generate
# Serve documentation locally
nix run .#docsServe
Development Cycle#
- Make changes to modules or library
- Write tests for new functionality
- Run tests to verify changes
- Format code with
nix fmt - Run linter with
nix run .#staticCheck - Test manually with a sample configuration
Manual Testing#
Create a test configuration:
# test-config.nix
{
nixidy.target = {
repository = "https://github.com/test/repo.git";
branch = "main";
};
applications.test = {
namespace = "test";
createNamespace = true;
resources.deployments.nginx.spec = {
selector.matchLabels.app = "nginx";
template = {
metadata.labels.app = "nginx";
spec.containers.nginx.image = "nginx:latest";
};
};
};
}
Build and inspect:
nix run .#cli -- build .#test
tree result/
cat result/test/Deployment-nginx.yaml
Adding New Features#
Adding a New Application Option#
-
Define the option in
modules/applications/default.nix:{ options = { myNewOption = mkOption { type = types.bool; default = false; description = "Description of the option."; }; }; } -
Use the option in config:
{ config = lib.mkIf config.myNewOption { # Configuration when option is enabled }; } -
Write tests in
tests/:# tests/my-new-option.nix { applications.test1 = { myNewOption = true; # ... }; test = { name = "my new option"; description = "Test the new option"; assertions = [ ... ]; }; } -
Register test in
tests/default.nix -
Document in
docs/user_guide/
Adding a New Library Function#
-
Add function to appropriate file in
lib/:# lib/kube.nix { myNewFunction = # Parameter description param: # Implementation ...; } -
Add documentation as comments:
/* Description of function. Type: myNewFunction :: ParamType -> ReturnType Example: myNewFunction "input" => "output" */ myNewFunction = ...; -
Write tests in
lib/tests.nix:{ kube = { myNewFunction = { testBasicCase = { expr = lib.kube.myNewFunction "input"; expected = "output"; }; }; }; }
Adding a New Resource Processor#
Similar to Helm/Kustomize, create a new module:
-
Create module
modules/applications/myprocessor.nix:{ nixidyDefaults, lib, config, ... }: let helpers = import ./lib.nix lib; in { options.myProcessor = mkOption { type = with types; attrsOf (submodule { ... }); }; config = { # Process inputs and add to resources/objects resources = mkMerge [ ... ]; objects = [ ... ]; }; } -
Import in
modules/applications/default.nix:{ imports = [ ./helm.nix ./kustomize.nix ./yamls.nix ./argocd.nix ./objects.nix ./myprocessor.nix # Add here ]; }
Common Tasks#
Updating Kubernetes Versions#
-
Edit
pkgs/generators/sources/versions.nix:{ "1.37.0" = { hash = "sha256-..."; spec = "api/openapi-spec/swagger.json"; discovery = { core = "api/discovery/api__v1.json"; aggregated = "api/discovery/aggregated_v2.json"; }; }; } -
Regenerate:
nix run .#generate -
Update default version in
modules/applications.nixif needed (thenixidy.k8sVersionenum is derived automatically from the generated files inmodules/generated/k8s/)
Adding a New Sync Option#
-
Add option in
modules/applications/argocd.nixundersyncPolicy.syncOptions:{ syncPolicy.syncOptions.myOption = mkOption { type = types.bool; default = false; apply = val: if val then "MyOption=true" else null; description = "Description"; }; } -
The
applyfunction converts to ArgoCD sync option format -
convertSyncOptionsListautomatically collects non-null options
Debugging Module Evaluation#
Use builtins.trace for debugging:
{
config = lib.mkMerge [
(builtins.trace "Processing ${config.name}" {
# ...
})
];
}
Or use lib.debug.traceValSeqN:
{
objects = lib.debug.traceValSeqN 2 config.resources;
}
Code Style#
Nix#
- Format: Use
nix fmt(nixfmt) - Sorting: Keep attribute sets alphabetically sorted
- Inherit: Use
inheritwhere possible to reduce verbosity - Imports: Group imports logically
- Types: Use specific types over
types.anythingwhen possible
# Good
{
lib,
config,
pkgs,
...
}:
let
inherit (config) namespace;
inherit (lib) mkOption types;
in
{
options.myOption = mkOption {
type = types.str;
default = "";
};
}
# Avoid
{lib, config, pkgs, ...}: let
namespace = config.namespace;
in {
options.myOption = lib.mkOption {
type = lib.types.str;
default = "";
};
}
Python (CLI)#
- Follow PEP 8 guidelines
- Use type hints for all function signatures
- Document public functions with docstrings
Documentation#
- Use MkDocs syntax
- Include code examples with syntax highlighting
- Cross-reference related documentation
- Keep language clear and concise
Getting Help#
- Issues: GitHub Issues
- Discussions: GitHub Discussions
- Documentation: nixidy.dev
Summary#
Key files for different tasks:
| Task | Files |
|---|---|
| Add application option | modules/applications/default.nix |
| Add nixidy option | modules/nixidy/ |
| Add library function | lib/*.nix |
| Add resource processor | modules/applications/ |
| Add template feature | modules/templates.nix |
| Modify build output | modules/build/ |
| Add K8s version | pkgs/generators/sources/versions.nix |
| Write module test | tests/*.nix, tests/default.nix |
| Write library test | lib/tests.nix |
Welcome to the nixidy project! We look forward to your contributions.