provisioning-code/templates/workspace/nickel/README.md

8.2 KiB

Nickel Workspace Configuration Templates

This directory contains reusable templates and factories for generating NixOS flakes from Nickel workspace configurations.

Structure

nickel/
├── main.ncl                    # Workspace factory: compose defaults → mode → overrides
├── servers.ncl                 # Server definitions template
├── lib/
│   ├── helpers.ncl            # Composition helpers (deep merge, validation, filtering)
│   ├── provider_defaults.ncl   # Generic provider defaults
│   ├── hetzner_defaults.ncl    # Hetzner Cloud pre-configured defaults
│   └── taskserv_defaults.ncl   # Taskserv configuration patterns
└── taskservs/
    └── (provider-specific overrides)

Quick Start

Create Workspace Config

In your workspace (workspaces/librecloud_hetzner/config/config.ncl):

let provisioning = import "schemas/main.ncl" in

{
  workspace_name = "librecloud_hetzner",
  deployment_mode = "solo",
  provider = "hetzner",
  region = "eu-central",
  custom_setting = "value",
}

Define Servers

In your workspace (workspaces/librecloud_hetzner/infra/wuji/servers.ncl):

let provisioning = import "schemas/main.ncl" in
let lib = provisioning.lib in
let workspace_hetzner_defaults = import "../lib/hetzner_defaults.ncl" in

{
  servers = [
    (workspace_hetzner_defaults & {
      hostname = "cp-0",
      title = "Control Plane 0",
      plan = "CX21",
      nixos_anywhere_enabled = true,
      taskservs = [
        {name = "os", profile = "control-plane"} | lib.TaskServDef,
        {name = "kubernetes", role = "master"} | lib.TaskServDef,
        {name = "etcd"} | lib.TaskServDef,
      ],
    }),
  ],
}

Generate NixOS Flakes

nu provisioning/scripts/generate-flakes.nu workspaces/librecloud_hetzner

This generates directories for each server:

workspaces/librecloud_hetzner/nixos/cp-0/
├── flake.nix                    # Flake inputs + outputs
├── configuration.nix            # NixOS base config
└── hardware-configuration.nix   # Hardware setup (customize)

Composition Pattern

The factory uses three-layer composition:

  1. Provider Defaults: Cloud-specific settings (Hetzner, Upcloud, OCI)
  2. Mode Defaults: Deployment mode settings (solo, multiuser, cicd)
  3. User Overrides: Workspace-specific and server-specific settings

Deep merge preserves nested fields (unlike shallow & operator):

# Shallow merge (❌ loses settings):
{server = {port = 9000}} & {server = {workers = 4}}
# Result: {server = {workers = 4}}  port lost!

# Deep merge (✅ preserves nested):
helpers.compose_config
  {server = {port = 9000}}
  {server = {workers = 4}}
  {}
# Result: {server = {port = 9000, workers = 4}}

Core Templates

main.ncl - Workspace Factory

Generates complete workspace config by composing defaults:

  • Loads mode-specific defaults (solo/multiuser/cicd)
  • Loads provider-specific defaults (Hetzner/Upcloud/OCI)
  • Merges with user overrides
  • Returns fully composed configuration

servers.ncl - Server Definitions

Template showing the pattern for server definitions:

  • Import provider defaults (e.g., hetzner_defaults.ncl)
  • Define servers as merge of defaults + overrides
  • Include taskservs array with enabled services
  • Use lib.TaskServDef for validation

lib/helpers.ncl - Composition Utilities

Core functions for configuration management:

  • compose_config: Deep recursive merge of three records
  • validate_required: Ensure required fields exist
  • filter_active_servers: Remove disabled servers
  • get_provider: Extract provider name from config
  • to_flake_path: Generate NixOS flake directory path
  • collect_taskservs: Extract unique taskserv names
  • minimal_server: Create minimal test server record

lib/provider_defaults.ncl - Generic Defaults

Base provider template with fields for all providers:

  • Network configuration (IPv4, IPv6)
  • Storage defaults (root filesystem, additional volumes)
  • Liveness probing (health checks)
  • Instance plan/size
  • Labels and metadata
  • Taskservs list
  • Server lifecycle (enabled/disabled)
  • Cloud API settings

lib/hetzner_defaults.ncl - Hetzner Cloud

Pre-configured defaults for Hetzner deployments:

  • Region: eu-central (customizable)
  • Plans: CX11, CX21, CX31, CX41, CX51, CX61, CX81
  • Network: IPv6 enabled, private networking
  • Storage: Ext4 root filesystem with data partition
  • Labels: provisioning=true, environment=production
  • Liveness: SSH port 22 probe

lib/taskserv_defaults.ncl - Taskserv Patterns

Configuration templates for common taskservs:

  • os: Operating system (profiles: worker, control-plane)
  • containerd: Container runtime daemon
  • kubernetes: Container orchestration (roles: master, worker)
  • etcd: Distributed key-value store
  • coredns: DNS resolution
  • cilium: Network policy engine
  • rook_ceph: Ceph storage operator
  • registry: Container image registry
  • nfs_client: NFS client mounts
  • postgres: PostgreSQL database
  • redis: Cache/queue system
  • resolv: DNS configuration
  • proxy: HTTP gateway (nginx/envoy)
  • webhook: Event hook service

Each taskserv includes version, enablement flag, and profile-specific settings.

Integration with generate-flakes.nu

The script orchestrates three execution phases:

Phase 1: Export

  • Executes nickel export on servers.ncl
  • Converts Nickel config to JSON
  • Parses JSON into servers array

Phase 2: Generate

  • For each server with nixos_anywhere_enabled = true
  • Build flake.nix with taskserv inputs and outputs
  • Generate configuration.nix with base NixOS settings
  • Create hardware-configuration.nix template

Phase 3: Write

  • Create workspace directories if needed
  • Save three files per server
  • Report generation status

Customization Examples

Add New Provider

  1. Create lib/myprovider_defaults.ncl:
{
  provider = "myprovider",
  region = "my-region-1",
  plan = "standard-plan",
  cloud_api_enabled = true,
}
  1. Import in workspace servers:
let workspace_defaults = import "../lib/myprovider_defaults.ncl" in

Add Taskserv

  1. Define in taskserv_defaults.ncl:
my_service = {
  name = "my_service",
  version = "1.0.0",
  enable = false,
}
  1. Use in server definition:
taskservs = [
  {name = "my_service", enable = true} | lib.TaskServDef,
]

Override Server Settings

Customize any field in server definition:

(workspace_defaults & {
  hostname = "custom-host",
  plan = "CX51",
  taskservs = [
    {name = "os", profile = "control-plane"} | lib.TaskServDef,
    {name = "kubernetes", role = "master"} | lib.TaskServDef,
  ],
  labels = "custom=true,role=master",
})

Validation

Nickel provides automatic validation via contract types. Invalid configurations are rejected at export time:

  • Empty strings rejected by String contract
  • Negative numbers rejected by range constraints
  • Missing required fields fail during validation
  • Type mismatches caught before JSON generation

Dependencies

  • Nickel (0.7+): Configuration language and export
  • Nushell (0.110.0+): Script orchestration
  • nickel export: JSON/YAML output generation

Workflow

User servers.ncl (Nickel)
        ↓
  nickel export → JSON
        ↓
  generate-flakes.nu (Phase 1: Export)
        ↓
  Parse servers array
        ↓
  For each server (Phase 2: Generate)
        ↓
  Generate flake.nix
  Generate configuration.nix
  Generate hardware-configuration.nix
        ↓
  Phase 3: Write to filesystem
        ↓
  workspaces/{ws}/nixos/{hostname}/
        ↓
  Ready for: nix flake check
  Ready for: nixos-anywhere deploy

Files

  • main.ncl - Workspace composition factory (262 lines)
  • servers.ncl - Server definition template (180 lines)
  • lib/helpers.ncl - Composition utilities (130 lines)
  • lib/provider_defaults.ncl - Generic provider (75 lines)
  • lib/hetzner_defaults.ncl - Hetzner specific (90 lines)
  • lib/taskserv_defaults.ncl - Taskserv patterns (210 lines)
  • provisioning/scripts/generate-flakes.nu - NixOS flake generator
  • provisioning/schemas/lib.ncl - Base library (TaskServDef, Storage, etc.)
  • docs/architecture/adr/ADR-011-nickel-migration.md - Nickel adoption details