# Nickel Guide Comprehensive guide to using Nickel as the infrastructure-as-code language for the Provisioning platform. ## Critical Principle: Nickel is Source of Truth **TYPE-SAFETY ALWAYS REQUIRED**: ALL configurations MUST be type-safe and validated via Nickel. TOML is NOT acceptable as source of truth. Validation is NOT optional, NOT "progressive", NOT "production-only". This applies to ALL profiles (developer, production, cicd). Nickel is the PRIMARY IaC language. TOML files are GENERATED OUTPUT ONLY, never the source. ## Why Nickel Nickel provides: - **Type Safety**: Static type checking catches errors before deployment - **Lazy Evaluation**: Efficient configuration composition and merging - **Contract System**: Schema validation with gradual typing - **Record Merging**: Powerful composition without duplication - **LSP Support**: IDE integration for autocomplete and validation - **Human-Readable**: Clear syntax for infrastructure definition ## Installation ```bash # macOS (Homebrew) brew install nickel # Linux (Cargo) cargo install nickel-lang-cli # Verify installation nickel --version # 1.15.1+ ``` ## Core Concepts ### Records and Fields Records are the fundamental data structure in Nickel: ```nickel { name = "my-server" plan = "medium" zone = "de-fra1" } ``` ### Type Annotations Add type safety with contracts: ```nickel { name : String = "my-server" plan : String = "medium" cpu_count : Number = 4 enabled : Bool = true } ``` ### Record Merging Compose configurations by merging records: ```nickel let base_config = { provider = "upcloud" region = "de-fra1" } in let server_config = base_config & { name = "web-01" plan = "medium" } in server_config ``` Result: ```nickel { provider = "upcloud" region = "de-fra1" name = "web-01" plan = "medium" } ``` ### Contracts (Schema Validation) Define contracts to validate structure: ```nickel let ServerContract = { name | String plan | String | default = "small" zone | String | default = "de-fra1" cpu | Number | optional } in { name = "my-server" plan = "large" } | ServerContract ``` ## Three-File Pattern (Provisioning Standard) The platform uses a standardized three-file pattern for all schemas: ### 1. `contracts.ncl` - Type Definitions Defines the schema contracts: ```nickel # contracts.ncl { Server = { name | String plan | String | default = "small" zone | String | default = "de-fra1" disk_size_gb | Number | default = 25 backup_enabled | Bool | default = false role | | [ 'control, 'worker, 'standalone | ] | optional } Infrastructure = { servers | Array Server provider | String environment | | [ 'development, 'staging, 'production | ] } } ``` ### 2. `defaults.ncl` - Default Values Provides sensible defaults: ```nickel # defaults.ncl { server = { name = "unnamed-server" plan = "small" zone = "de-fra1" disk_size_gb = 25 backup_enabled = false } infrastructure = { servers = [] provider = "local" environment = 'development } } ``` ### 3. `main.ncl` - Entry Point Combines contracts and defaults, provides makers: ```nickel # main.ncl let contracts_lib = import "./contracts.ncl" in let defaults_lib = import "./defaults.ncl" in { # Direct access to defaults (for inspection) defaults = defaults_lib # Convenience makers (90% of use cases) make_server | not_exported = fun overrides => defaults_lib.server & overrides make_infrastructure | not_exported = fun overrides => defaults_lib.infrastructure & overrides # Default instances (bare defaults) DefaultServer = defaults_lib.server DefaultInfrastructure = defaults_lib.infrastructure } ``` ### Usage Example ```nickel # user-infra.ncl let infra_lib = import "provisioning/schemas/infrastructure/main.ncl" in infra_lib.make_infrastructure { provider = "upcloud" environment = 'production servers = [ infra_lib.make_server { name = "web-01" plan = "medium" backup_enabled = true } infra_lib.make_server { name = "web-02" plan = "medium" backup_enabled = true } ] } ``` ## Hybrid Interface Pattern Records can be used both as functions (makers) and as plain data: ```nickel let config_lib = import "./config.ncl" in # Use as function (with overrides) let custom_config = config_lib.make_server { name = "custom" } in # Use as plain data (defaults) let default_config = config_lib.DefaultServer in { custom = custom_config default = default_config } ``` ## Record Merging Strategies ### Priority Merging (Default) ```nickel let base = { a = 1, b = 2 } in let override = { b = 3, c = 4 } in base & override # Result: { a = 1, b = 3, c = 4 } ``` ### Recursive Merging ```nickel let base = { server = { cpu = 2, ram = 4 } } in let override = { server = { ram = 8, disk = 100 } } in std.record.merge_all [base, override] # Result: { server = { cpu = 2, ram = 8, disk = 100 } } ``` ## Lazy Evaluation Nickel evaluates expressions lazily, only when needed: ```nickel let expensive_computation = std.string.join " " ["a", "b", "c"] in { # Only evaluated when accessed computed_field = expensive_computation # Conditional evaluation conditional = if environment == 'production then expensive_computation else "dev-value" } ``` ## Schema Organization The platform organizes Nickel schemas by domain: ```text provisioning/schemas/ ├── main.ncl # Top-level entry point ├── config/ # Configuration schemas │ ├── settings/ │ │ ├── main.ncl │ │ ├── contracts.ncl │ │ └── defaults.ncl │ └── defaults/ │ ├── main.ncl │ ├── contracts.ncl │ └── defaults.ncl ├── infrastructure/ # Infrastructure definitions │ ├── servers/ │ ├── networks/ │ └── storage/ ├── deployment/ # Deployment schemas ├── services/ # Service configurations ├── operations/ # Operational schemas └── generator/ # Runtime schema generation ``` ## Type System ### Primitive Types ```nickel { string_field : String = "text" number_field : Number = 42 bool_field : Bool = true } ``` ### Array Types ```nickel { names : Array String = ["alice", "bob", "charlie"] ports : Array Number = [80, 443, 8080] } ``` ### Enum Types ```nickel { environment : | [ 'development, 'staging, 'production | ] = 'production role : | [ 'control, 'worker, 'standalone | ] = 'worker } ``` ### Optional Fields ```nickel { required_field : String = "value" optional_field : String | optional } ``` ### Default Values ```nickel { with_default : String | default = "default-value" } ``` ## Validation Patterns ### Runtime Validation ```nickel let validate_plan = fun plan => if plan == "small" | | plan == "medium" | | plan == "large" then plan else std.fail "Invalid plan: must be small, medium, or large" in { plan = validate_plan "medium" } ``` ### Contract-Based Validation ```nickel let PlanContract = | [ 'small, 'medium, 'large | ] in { plan | PlanContract = 'medium } ``` ## Real-World Examples ### Simple Server Configuration ```nickel { metadata = { name = "demo-server" provider = "upcloud" environment = 'development } infrastructure = { servers = [ { name = "web-01" plan = "medium" zone = "de-fra1" disk_size_gb = 50 backup_enabled = true role = 'standalone } ] } services = { taskservs = ["containerd", "docker"] } } ``` ### Kubernetes Cluster Configuration ```nickel { metadata = { name = "k8s-prod" provider = "upcloud" environment = 'production } infrastructure = { servers = [ { name = "k8s-control-01" plan = "medium" role = 'control zone = "de-fra1" disk_size_gb = 50 backup_enabled = true } { name = "k8s-worker-01" plan = "large" role = 'worker zone = "de-fra1" disk_size_gb = 100 backup_enabled = true } { name = "k8s-worker-02" plan = "large" role = 'worker zone = "de-fra1" disk_size_gb = 100 backup_enabled = true } ] } services = { taskservs = ["containerd", "etcd", "kubernetes", "cilium", "rook-ceph"] } kubernetes = { version = "1.28.0" pod_cidr = "10.244.0.0/16" service_cidr = "10.96.0.0/12" container_runtime = "containerd" cri_socket = "/run/containerd/containerd.sock" } } ``` ### Multi-Provider Batch Workflow ```nickel { batch_workflow = { operations = [ { id = "aws-cluster" provider = "aws" region = "us-east-1" servers = [ { name = "aws-web-01", plan = "t3.medium" } ] } { id = "upcloud-cluster" provider = "upcloud" region = "de-fra1" servers = [ { name = "upcloud-web-01", plan = "medium" } ] dependencies = ["aws-cluster"] } ] parallel_limit = 2 } } ``` ## Validation Workflow ### Type-Check Schema ```bash # Check syntax and types nickel typecheck infra/my-cluster.ncl # Export to JSON (validates during export) nickel export infra/my-cluster.ncl # Export to TOML (generated output only) nickel export --format toml infra/my-cluster.ncl > config.toml ``` ### Platform Validation ```bash # Validate against platform contracts provisioning validate config --infra my-cluster # Verbose validation provisioning validate config --verbose ``` ## IDE Integration ### Language Server (nickel-lang-lsp) Install LSP for IDE support: ```bash # Install LSP server cargo install nickel-lang-lsp # Configure your editor (VS Code example) # Install "Nickel" extension from marketplace ``` Features: - Syntax highlighting - Type checking on save - Autocomplete - Hover documentation - Go to definition ### VS Code Configuration ```json { "nickel.lsp.command": "nickel-lang-lsp", "nickel.lsp.args": ["--stdio"], "nickel.format.onSave": true } ``` ## Common Patterns ### Environment-Specific Configuration ```nickel let env_configs = { development = { plan = "small" backup_enabled = false } production = { plan = "large" backup_enabled = true } } in let environment = 'production in { servers = [ env_configs.%{std.string.from_enum environment} & { name = "server-01" } ] } ``` ### Configuration Composition ```nickel let base_server = { zone = "de-fra1" backup_enabled = false } in let prod_overrides = { backup_enabled = true disk_size_gb = 100 } in { servers = [ base_server & { name = "dev-01" } base_server & prod_overrides & { name = "prod-01" } ] } ``` ## Migration from TOML TOML is ONLY for generated output. Source is always Nickel. ```bash # Generate TOML from Nickel (if needed for external tools) nickel export --format toml infra/cluster.ncl > cluster.toml # NEVER edit cluster.toml directly - edit cluster.ncl instead ``` ## Best Practices 1. **Use Three-File Pattern**: Separate contracts, defaults, and main entry 2. **Type Everything**: Add type annotations for all fields 3. **Validate Early**: Run `nickel typecheck` before deployment 4. **Use Makers**: Leverage maker functions for composition 5. **Document Contracts**: Add comments explaining schema requirements 6. **Avoid Duplication**: Use record merging and defaults 7. **Test Locally**: Export and verify before deploying 8. **Version Schemas**: Track schema changes in version control ## Debugging ### Type Errors ```bash # Detailed type error messages nickel typecheck --color always infra/cluster.ncl ``` ### Schema Inspection ```bash # Export to JSON for inspection nickel export infra/cluster.ncl | jq '.' # Check specific field nickel export infra/cluster.ncl | jq '.metadata' ``` ### Format Code ```bash # Auto-format Nickel files nickel fmt infra/cluster.ncl # Check formatting without modifying nickel fmt --check infra/cluster.ncl ``` ## Next Steps - [Schemas Reference](schemas-reference.md) - Platform schema organization - [Configuration System](configuration-system.md) - Hierarchical configuration - [Providers](providers.md) - Cloud provider schemas - [Batch Workflows](batch-workflows.md) - Multi-cloud orchestration with Nickel