# Cloud Provider Development Guide\n\n**Version**: 2.0\n**Status**: Production Ready\n**Based On**: Hetzner, UpCloud, AWS (3 completed providers)\n\n---\n\n## Overview: 4-Task Completion Framework\n\nA cloud provider is **production-ready** when it completes all 4 tasks:\n\n| Task | Requirements | Reference |\n| ------ | --- | --- |\n| **1. Nushell Compliance** | 0 deprecated patterns, full implementations | `provisioning/extensions/providers/hetzner/` |\n| **2. Test Infrastructure** | 51 tests (14 unit + 37 integration, mock-based) | `provisioning/extensions/providers/upcloud/tests/` |\n| **3. Runtime Templates** | 3+ Jinja2/Bash templates for core resources | `provisioning/extensions/providers/aws/templates/` |\n| **4. Nickel Validation** | Schemas pass `nickel typecheck` | `provisioning/extensions/providers/hetzner/nickel/` |\n\n### Execution Sequence\n\n```\nTarea 4 (5 min) ──────┐\nTarea 1 (main) ───┐   ├──> Tarea 2 (tests)\nTarea 3 (parallel)┘   │\n                      └──> Production Ready ✅\n```\n\n---\n\n## Nushell 0.109.0+ Core Rules\n\nThese rules are **mandatory** for all provider Nushell code:\n\n### Rule 1: Module System & Imports\n```\nuse mod.nu\nuse api.nu\nuse servers.nu\n```\n\n### Rule 2: Function Signatures\n```\ndef function_name [param: type, optional: type = default] { }\n```\n\n### Rule 3: Return Early, Fail Fast\n```\ndef operation [resource: record] {\n    if ($resource | get -o id | is-empty) {\n        error make {msg: "Resource ID required"}\n    }\n}\n```\n\n### Rule 4: Modern Error Handling (CRITICAL)\n\n**❌ FORBIDDEN** - Deprecated try-catch:\n```\ntry {\n    ^external_command\n} catch {|err|\n    print $"Error: ($err.msg)"\n}\n```\n\n**✅ REQUIRED** - Modern do/complete pattern:\n```\nlet result = (do { ^external_command } | complete)\n\nif $result.exit_code != 0 {\n    error make {msg: $"Command failed: ($result.stderr)"}\n}\n\n$result.stdout\n```\n\n### Rule 5: Atomic Operations\nAll operations must fully succeed or fully fail. No partial state changes.\n\n### Rule 12: Structured Error Returns\n```\nerror make {\n    msg: "Human-readable message",\n    label: {text: "Error context", span: (metadata error).span}\n}\n```\n\n### Critical Violations (INSTANT FAIL)\n\n❌ **FORBIDDEN**:\n- `try { } catch { }` blocks\n- `let mut variable = value` (mutable state)\n- `error make {msg: "Not implemented"}` (stubs)\n- Empty function bodies returning ok\n- Deprecated error patterns\n\n---\n\n## Nickel IaC: Three-File Pattern\n\nAll Nickel schemas follow this pattern:\n\n### contracts.ncl: Type Definitions\n\n```\n{\n  Server = {\n    id | String,\n    name | String,\n    instance_type | String,\n    zone | String,\n  },\n\n  Volume = {\n    id | String,\n    name | String,\n    size | Number,\n    type | String,\n  }\n}\n```\n\n### defaults.ncl: Default Values\n\n```\n{\n  Server = {\n    instance_type = "t3.micro",\n    zone = "us-east-1a",\n  },\n\n  Volume = {\n    size = 20,\n    type = "gp3",\n  }\n}\n```\n\n### main.ncl: Public API\n\n```\nlet contracts = import "contracts.ncl" in\nlet defaults = import "defaults.ncl" in\n\n{\n  make_server = fun config => defaults.Server & config,\n  make_volume = fun config => defaults.Volume & config,\n}\n```\n\n### version.ncl: Version Tracking\n\n```\n{\n  provider_version = "1.0.0",\n  cli_tools = {\n    hcloud = "1.47.0+",\n  },\n  nickel_version = "1.7.0+",\n}\n```\n\n**Validation**:\n```\nnickel typecheck nickel/contracts.ncl\nnickel typecheck nickel/defaults.ncl\nnickel typecheck nickel/main.ncl\nnickel typecheck nickel/version.ncl\nnickel export nickel/main.ncl\n```\n\n---\n\n## Tarea 1: Nushell Compliance\n\n### Identify Violations\n\n```\ncd provisioning/extensions/providers/{PROVIDER}\n\ngrep -r "try {" nulib/ --include="*.nu" | wc -l\ngrep -r "let mut " nulib/ --include="*.nu" | wc -l\ngrep -r "not implemented" nulib/ --include="*.nu" | wc -l\n```\n\nAll three commands should return `0`.\n\n### Fix Mutable Loops: Accumulation Pattern\n\n```\ndef retry_with_backoff [\n    closure: closure,\n    max_attempts: int\n]: nothing -> any {\n    let result = (\n        0..$max_attempts | reduce --fold {\n            success: false,\n            value: null,\n            delay: 100 ms\n        } {|attempt, acc|\n            if $acc.success {\n                $acc\n            } else {\n                let op_result = (do { $closure | call } | complete)\n\n                if $op_result.exit_code == 0 {\n                    {success: true, value: $op_result.stdout, delay: $acc.delay}\n                } else if $attempt >= ($max_attempts - 1) {\n                    $acc\n                } else {\n                    sleep $acc.delay\n                    {success: false, value: null, delay: ($acc.delay * 2)}\n                }\n            }\n        }\n    )\n\n    if $result.success {\n        $result.value\n    } else {\n        error make {msg: $"Failed after ($max_attempts) attempts"}\n    }\n}\n```\n\n### Fix Mutable Loops: Recursive Pattern\n\n```\ndef _wait_for_state [\n    resource_id: string,\n    target_state: string,\n    timeout_sec: int,\n    elapsed: int = 0,\n    interval: int = 2\n]: nothing -> bool {\n    let current = (^aws ec2 describe-volumes \\n        --volume-ids $resource_id \\n        --query "Volumes[0].State" \\n        --output text)\n\n    if ($current | str contains $target_state) {\n        true\n    } else if $elapsed > $timeout_sec {\n        false\n    } else {\n        sleep ($"($interval)sec" | into duration)\n        _wait_for_state $resource_id $target_state $timeout_sec ($elapsed + $interval) $interval\n    }\n}\n```\n\n### Fix Error Handling\n\n```\ndef create_server [config: record] {\n    if ($config | get -o name | is-empty) {\n        error make {msg: "Server name required"}\n    }\n\n    let api_result = (do {\n        ^hcloud server create \\n            --name $config.name \\n            --type $config.instance_type \\n            --format json\n    } | complete)\n\n    if $api_result.exit_code != 0 {\n        error make {msg: $"Server creation failed: ($api_result.stderr)"}\n    }\n\n    let response = ($api_result.stdout | from json)\n    {\n        id: $response.server.id,\n        name: $response.server.name,\n        status: "created"\n    }\n}\n```\n\n### Validation\n\n```\ncd provisioning/extensions/providers/{PROVIDER}\n\nfor file in nulib/*/\*.nu; do\n    nu --ide-check 100 "$file" 2>&1 | grep -i error && exit 1\ndone\n\nnu -c "use nulib/{provider}/mod.nu; print 'OK'"\n\necho "✅ Nushell compliance complete"\n```\n\n---\n\n## Tarea 2: Test Infrastructure\n\n### Directory Structure\n\n```\ntests/\n├── mocks/\n│   └── mock_api_responses.json\n├── unit/\n│   └── test_utils.nu\n├── integration/\n│   ├── test_api_client.nu\n│   ├── test_server_lifecycle.nu\n│   └── test_pricing_cache.nu\n└── run_{provider}_tests.nu\n```\n\n### Mock API Responses\n\n```\n{\n  "list_servers": {\n    "servers": [\n      {\n        "id": "srv-123",\n        "name": "test-server",\n        "status": "running"\n      }\n    ]\n  },\n  "error_401": {\n    "error": {"message": "Unauthorized", "code": 401}\n  },\n  "error_429": {\n    "error": {"message": "Rate limited", "code": 429}\n  }\n}\n```\n\n### Unit Tests: 14 Tests\n\n```\ndef test-result [name: string, result: bool] {\n    if $result {\n        print $"✓ ($name)"\n    } else {\n        print $"✗ ($name)"\n    }\n    $result\n}\n\ndef test-validate-instance-id [] {\n    let valid = "i-1234567890abcdef0"\n    let invalid = "invalid-id"\n\n    let test1 = (test-result "Instance ID valid" ($valid | str contains "i-"))\n    let test2 = (test-result "Instance ID invalid" (($invalid | str contains "i-") == false))\n\n    $test1 and $test2\n}\n\ndef test-validate-ipv4 [] {\n    let valid = "10.0.1.100"\n    let parts = ($valid | split row ".")\n    test-result "IPv4 four octets" (($parts | length) == 4)\n}\n\ndef test-validate-instance-type [] {\n    let valid_types = ["t3.micro" "t3.small" "m5.large"]\n    let invalid = "invalid_type"\n\n    let test1 = (test-result "Instance type valid" (($valid_types | contains ["t3.micro"])))\n    let test2 = (test-result "Instance type invalid" (($valid_types | contains [$invalid]) == false))\n\n    $test1 and $test2\n}\n\ndef test-validate-zone [] {\n    let valid_zones = ["us-east-1a" "us-east-1b" "eu-west-1a"]\n    let invalid = "invalid-zone"\n\n    let test1 = (test-result "Zone valid" (($valid_zones | contains ["us-east-1a"])))\n    let test2 = (test-result "Zone invalid" (($valid_zones | contains [$invalid]) == false))\n\n    $test1 and $test2\n}\n\ndef test-validate-volume-id [] {\n    let valid = "vol-12345678"\n    let invalid = "invalid-vol"\n\n    let test1 = (test-result "Volume ID valid" ($valid | str contains "vol-"))\n    let test2 = (test-result "Volume ID invalid" (($invalid | str contains "vol-") == false))\n\n    $test1 and $test2\n}\n\ndef test-validate-volume-state [] {\n    let valid_states = ["available" "in-use" "creating"]\n    let invalid = "pending"\n\n    let test1 = (test-result "Volume state valid" (($valid_states | contains ["available"])))\n    let test2 = (test-result "Volume state invalid" (($valid_states | contains [$invalid]) == false))\n\n    $test1 and $test2\n}\n\ndef test-validate-cidr [] {\n    let valid = "10.0.0.0/16"\n    let invalid = "10.0.0.1"\n\n    let test1 = (test-result "CIDR valid" ($valid | str contains "/"))\n    let test2 = (test-result "CIDR invalid" (($invalid | str contains "/") == false))\n\n    $test1 and $test2\n}\n\ndef test-validate-volume-type [] {\n    let valid_types = ["gp2" "gp3" "io1" "io2"]\n    let invalid = "invalid-type"\n\n    let test1 = (test-result "Volume type valid" (($valid_types | contains ["gp3"])))\n    let test2 = (test-result "Volume type invalid" (($valid_types | contains [$invalid]) == false))\n\n    $test1 and $test2\n}\n\ndef test-validate-timestamp [] {\n    let valid = "2025-01-07T10:00:00.000Z"\n    let invalid = "not-a-timestamp"\n\n    let test1 = (test-result "Timestamp valid" ($valid | str contains "T" and $valid | str contains "Z"))\n    let test2 = (test-result "Timestamp invalid" (($invalid | str contains "T") == false))\n\n    $test1 and $test2\n}\n\ndef test-validate-server-state [] {\n    let valid_states = ["running" "stopped" "pending"]\n    let invalid = "hibernating"\n\n    let test1 = (test-result "Server state valid" (($valid_states | contains ["running"])))\n    let test2 = (test-result "Server state invalid" (($valid_states | contains [$invalid]) == false))\n\n    $test1 and $test2\n}\n\ndef test-validate-security-group [] {\n    let valid = "sg-12345678"\n    let invalid = "invalid-sg"\n\n    let test1 = (test-result "Security group valid" ($valid | str contains "sg-"))\n    let test2 = (test-result "Security group invalid" (($invalid | str contains "sg-") == false))\n\n    $test1 and $test2\n}\n\ndef test-validate-memory [] {\n    let valid_mems = ["512 MB" "1 GB" "2 GB" "4 GB"]\n    let invalid = "0 GB"\n\n    let test1 = (test-result "Memory valid" (($valid_mems | contains ["1 GB"])))\n    let test2 = (test-result "Memory invalid" (($valid_mems | contains [$invalid]) == false))\n\n    $test1 and $test2\n}\n\ndef test-validate-vcpu [] {\n    let valid_cpus = [1, 2, 4, 8, 16]\n    let invalid = 0\n\n    let test1 = (test-result "vCPU valid" (($valid_cpus | contains [1])))\n    let test2 = (test-result "vCPU invalid" (($valid_cpus | contains [$invalid]) == false))\n\n    $test1 and $test2\n}\n\ndef main [] {\n    print "=== Unit Tests ==="\n    print ""\n\n    let results = [\n        (test-validate-instance-id),\n        (test-validate-ipv4),\n        (test-validate-instance-type),\n        (test-validate-zone),\n        (test-validate-volume-id),\n        (test-validate-volume-state),\n        (test-validate-cidr),\n        (test-validate-volume-type),\n        (test-validate-timestamp),\n        (test-validate-server-state),\n        (test-validate-security-group),\n        (test-validate-memory),\n        (test-validate-vcpu)\n    ]\n\n    let passed = ($results | where {|it| $it == true} | length)\n    let failed = ($results | where {|it| $it == false} | length)\n\n    print ""\n    print $"Results: ($passed) passed, ($failed) failed"\n\n    {\n        passed: $passed,\n        failed: $failed,\n        total: ($passed + $failed)\n    }\n}\n\nmain\n```\n\n### Integration Tests: 37 Tests across 3 Modules\n\n**Module 1: test_api_client.nu** (13 tests)\n- Response structure validation\n- Error handling for 401, 404, 429\n- Resource listing operations\n- Pricing data validation\n\n**Module 2: test_server_lifecycle.nu** (12 tests)\n- Server creation, listing, state\n- Instance type and zone info\n- Storage and security attachment\n- Server state transitions\n\n**Module 3: test_pricing_cache.nu** (12 tests)\n- Pricing data structure validation\n- On-demand vs reserved pricing\n- Cost calculations\n- Volume pricing operations\n\n### Test Orchestrator\n\n```\ndef main [] {\n    print "=== Provider Test Suite ==="\n\n    let unit_result = (nu tests/unit/test_utils.nu)\n    let api_result = (nu tests/integration/test_api_client.nu)\n    let lifecycle_result = (nu tests/integration/test_server_lifecycle.nu)\n    let pricing_result = (nu tests/integration/test_pricing_cache.nu)\n\n    let total_passed = (\n        $unit_result.passed +\n        $api_result.passed +\n        $lifecycle_result.passed +\n        $pricing_result.passed\n    )\n\n    let total_failed = (\n        $unit_result.failed +\n        $api_result.failed +\n        $lifecycle_result.failed +\n        $pricing_result.failed\n    )\n\n    print $"Results: ($total_passed) passed, ($total_failed) failed"\n\n    {\n        passed: $total_passed,\n        failed: $total_failed,\n        success: ($total_failed == 0)\n    }\n}\n\nlet result = (main)\nexit (if $result.success {0} else {1})\n```\n\n### Validation\n\n```\ncd provisioning/extensions/providers/{PROVIDER}\nnu tests/run_{provider}_tests.nu\n```\n\nExpected: 51 tests passing, exit code 0\n\n---\n\n## Tarea 3: Runtime Templates\n\n### Directory Structure\n\n```\ntemplates/\n├── {provider}_servers.j2\n├── {provider}_networks.j2\n└── {provider}_volumes.j2\n```\n\n### Template Example\n\n```jinja2\n#!/bin/bash\n# {{ provider_name }} Server Provisioning\nset -e\n{% if debug %}set -x{% endif %}\n\n{%- for server in servers %}\n  {%- if server.name %}\n\necho "Creating server: {{ server.name }}"\n\n{%- if server.instance_type %}\nINSTANCE_TYPE="{{ server.instance_type }}"\n{%- else %}\nINSTANCE_TYPE="t3.micro"\n{%- endif %}\n\nSERVER_ID=$(^hcloud server create \\n  --name "{{ server.name }}" \\n  --type $INSTANCE_TYPE \\n  --query 'id' \\n  --output text 2>/dev/null)\n\nif [ -z "$SERVER_ID" ]; then\n  echo "Failed to create server {{ server.name }}"\n  exit 1\nfi\n\necho "✓ Server {{ server.name }} created: $SERVER_ID"\n\n  {%- endif %}\n{%- endfor %}\n\necho "Server provisioning complete"\n```\n\n### Validation\n\n```\ncd provisioning/extensions/providers/{PROVIDER}\n\nfor template in templates/*.j2; do\n    bash -n <(sed 's/{%.*%}//' "$template" | sed 's/{{.*}}/x/g')\ndone\n\necho "✅ Templates valid"\n```\n\n---\n\n## Tarea 4: Nickel Schema Validation\n\n```\ncd provisioning/extensions/providers/{PROVIDER}\n\nnickel typecheck nickel/contracts.ncl || exit 1\nnickel typecheck nickel/defaults.ncl || exit 1\nnickel typecheck nickel/main.ncl || exit 1\nnickel typecheck nickel/version.ncl || exit 1\n\nnickel export nickel/main.ncl || exit 1\n\necho "✅ Nickel schemas validated"\n```\n\n---\n\n## Complete Validation Script\n\n```\n#!/bin/bash\nset -e\n\nPROVIDER="hetzner"\nPROV="provisioning/extensions/providers/$PROVIDER"\n\necho "=== Provider Completeness Check: $PROVIDER ==="\n\necho ""\necho "✓ Tarea 4: Validating Nickel..."\nnickel typecheck "$PROV/nickel/main.ncl"\n\necho "✓ Tarea 1: Checking Nushell..."\n[ $(grep -r "try {" "$PROV/nulib" 2>/dev/null | wc -l) -eq 0 ]\n[ $(grep -r "let mut " "$PROV/nulib" 2>/dev/null | wc -l) -eq 0 ]\necho "  - No deprecated patterns ✓"\n\necho "✓ Tarea 3: Validating templates..."\nfor f in "$PROV"/templates/*.j2; do\n    bash -n <(sed 's/{%.*%}//' "$f" | sed 's/{{.*}}/x/g')\ndone\n\necho "✓ Tarea 2: Running tests..."\nnu "$PROV/tests/run_${PROVIDER}_tests.nu"\n\necho ""\necho "╔════════════════════════════════════════╗"\necho "║  ✅ ALL TASKS COMPLETE                 ║"\necho "║     PRODUCTION READY                   ║"\necho "╚════════════════════════════════════════╝"\n```\n\n---\n\n## Reference Implementations\n\n- **Hetzner**: `provisioning/extensions/providers/hetzner/`\n- **UpCloud**: `provisioning/extensions/providers/upcloud/`\n- **AWS**: `provisioning/extensions/providers/aws/`\n\nUse these as templates for new providers.\n\n---\n\n## Quick Start\n\n```\ncd provisioning/extensions/providers/{PROVIDER}\n\n# Validate completeness\nnickel typecheck nickel/main.ncl && \\n[ $(grep -r "try {" nulib/ 2>/dev/null | wc -l) -eq 0 ] && \\nnu tests/run_{provider}_tests.nu && \\nfor f in templates/*.j2; do bash -n <(sed 's/{%.*%}//' "$f"); done && \\necho "✅ PRODUCTION READY"\n```
