# Cloud Provider Development Guide

**Version**: 2.0
**Status**: Production Ready
**Based On**: Hetzner, UpCloud, AWS (3 completed providers)

---

## Overview: 4-Task Completion Framework

A cloud provider is **production-ready** when it completes all 4 tasks:

| Task | Requirements | Reference |
|------|---|---|
| **1. Nushell Compliance** | 0 deprecated patterns, full implementations | `provisioning/extensions/providers/hetzner/` |
| **2. Test Infrastructure** | 51 tests (14 unit + 37 integration, mock-based) | `provisioning/extensions/providers/upcloud/tests/` |
| **3. Runtime Templates** | 3+ Jinja2/Bash templates for core resources | `provisioning/extensions/providers/aws/templates/` |
| **4. Nickel Validation** | Schemas pass `nickel typecheck` | `provisioning/extensions/providers/hetzner/nickel/` |

### Execution Sequence

```text
Tarea 4 (5 min) ──────┐
Tarea 1 (main) ───┐   ├──> Tarea 2 (tests)
Tarea 3 (parallel)┘   │
                      └──> Production Ready ✅
```

---

## Nushell 0.109.0+ Core Rules

These rules are **mandatory** for all provider Nushell code:

### Rule 1: Module System & Imports
```nushell
use mod.nu
use api.nu
use servers.nu
```

### Rule 2: Function Signatures
```nushell
def function_name [param: type, optional: type = default] { }
```

### Rule 3: Return Early, Fail Fast
```nushell
def operation [resource: record] {
    if ($resource | get -o id | is-empty) {
        error make {msg: "Resource ID required"}
    }
}
```

### Rule 4: Modern Error Handling (CRITICAL)

**❌ FORBIDDEN** - Deprecated try-catch:
```nushell
try {
    ^external_command
} catch {|err|
    print $"Error: ($err.msg)"
}
```

**✅ REQUIRED** - Modern do/complete pattern:
```nushell
let result = (do { ^external_command } | complete)

if $result.exit_code != 0 {
    error make {msg: $"Command failed: ($result.stderr)"}
}

$result.stdout
```

### Rule 5: Atomic Operations
All operations must fully succeed or fully fail. No partial state changes.

### Rule 12: Structured Error Returns
```nushell
error make {
    msg: "Human-readable message",
    label: {text: "Error context", span: (metadata error).span}
}
```

### Critical Violations (INSTANT FAIL)

❌ **FORBIDDEN**:
- `try { } catch { }` blocks
- `let mut variable = value` (mutable state)
- `error make {msg: "Not implemented"}` (stubs)
- Empty function bodies returning ok
- Deprecated error patterns

---

## Nickel IaC: Three-File Pattern

All Nickel schemas follow this pattern:

### contracts.ncl: Type Definitions

```nickel
{
  Server = {
    id | String,
    name | String,
    instance_type | String,
    zone | String,
  },

  Volume = {
    id | String,
    name | String,
    size | Number,
    type | String,
  }
}
```

### defaults.ncl: Default Values

```nickel
{
  Server = {
    instance_type = "t3.micro",
    zone = "us-east-1a",
  },

  Volume = {
    size = 20,
    type = "gp3",
  }
}
```

### main.ncl: Public API

```nickel
let contracts = import "contracts.ncl" in
let defaults = import "defaults.ncl" in

{
  make_server = fun config => defaults.Server & config,
  make_volume = fun config => defaults.Volume & config,
}
```

### version.ncl: Version Tracking

```nickel
{
  provider_version = "1.0.0",
  cli_tools = {
    hcloud = "1.47.0+",
  },
  nickel_version = "1.7.0+",
}
```

**Validation**:
```bash
nickel typecheck nickel/contracts.ncl
nickel typecheck nickel/defaults.ncl
nickel typecheck nickel/main.ncl
nickel typecheck nickel/version.ncl
nickel export nickel/main.ncl
```

---

## Tarea 1: Nushell Compliance

### Identify Violations

```bash
cd provisioning/extensions/providers/{PROVIDER}

grep -r "try {" nulib/ --include="*.nu" | wc -l
grep -r "let mut " nulib/ --include="*.nu" | wc -l
grep -r "not implemented" nulib/ --include="*.nu" | wc -l
```

All three commands should return `0`.

### Fix Mutable Loops: Accumulation Pattern

```nushell
def retry_with_backoff [
    closure: closure,
    max_attempts: int
]: nothing -> any {
    let result = (
        0..$max_attempts | reduce --fold {
            success: false,
            value: null,
            delay: 100 ms
        } {|attempt, acc|
            if $acc.success {
                $acc
            } else {
                let op_result = (do { $closure | call } | complete)

                if $op_result.exit_code == 0 {
                    {success: true, value: $op_result.stdout, delay: $acc.delay}
                } else if $attempt >= ($max_attempts - 1) {
                    $acc
                } else {
                    sleep $acc.delay
                    {success: false, value: null, delay: ($acc.delay * 2)}
                }
            }
        }
    )

    if $result.success {
        $result.value
    } else {
        error make {msg: $"Failed after ($max_attempts) attempts"}
    }
}
```

### Fix Mutable Loops: Recursive Pattern

```nushell
def _wait_for_state [
    resource_id: string,
    target_state: string,
    timeout_sec: int,
    elapsed: int = 0,
    interval: int = 2
]: nothing -> bool {
    let current = (^aws ec2 describe-volumes \
        --volume-ids $resource_id \
        --query "Volumes[0].State" \
        --output text)

    if ($current | str contains $target_state) {
        true
    } else if $elapsed > $timeout_sec {
        false
    } else {
        sleep ($"($interval)sec" | into duration)
        _wait_for_state $resource_id $target_state $timeout_sec ($elapsed + $interval) $interval
    }
}
```

### Fix Error Handling

```nushell
def create_server [config: record] {
    if ($config | get -o name | is-empty) {
        error make {msg: "Server name required"}
    }

    let api_result = (do {
        ^hcloud server create \
            --name $config.name \
            --type $config.instance_type \
            --format json
    } | complete)

    if $api_result.exit_code != 0 {
        error make {msg: $"Server creation failed: ($api_result.stderr)"}
    }

    let response = ($api_result.stdout | from json)
    {
        id: $response.server.id,
        name: $response.server.name,
        status: "created"
    }
}
```

### Validation

```bash
cd provisioning/extensions/providers/{PROVIDER}

for file in nulib/*/\*.nu; do
    nu --ide-check 100 "$file" 2>&1 | grep -i error && exit 1
done

nu -c "use nulib/{provider}/mod.nu; print 'OK'"

echo "✅ Nushell compliance complete"
```

---

## Tarea 2: Test Infrastructure

### Directory Structure

```text
tests/
├── mocks/
│   └── mock_api_responses.json
├── unit/
│   └── test_utils.nu
├── integration/
│   ├── test_api_client.nu
│   ├── test_server_lifecycle.nu
│   └── test_pricing_cache.nu
└── run_{provider}_tests.nu
```

### Mock API Responses

```json
{
  "list_servers": {
    "servers": [
      {
        "id": "srv-123",
        "name": "test-server",
        "status": "running"
      }
    ]
  },
  "error_401": {
    "error": {"message": "Unauthorized", "code": 401}
  },
  "error_429": {
    "error": {"message": "Rate limited", "code": 429}
  }
}
```

### Unit Tests: 14 Tests

```nushell
def test-result [name: string, result: bool] {
    if $result {
        print $"✓ ($name)"
    } else {
        print $"✗ ($name)"
    }
    $result
}

def test-validate-instance-id [] {
    let valid = "i-1234567890abcdef0"
    let invalid = "invalid-id"

    let test1 = (test-result "Instance ID valid" ($valid | str contains "i-"))
    let test2 = (test-result "Instance ID invalid" (($invalid | str contains "i-") == false))

    $test1 and $test2
}

def test-validate-ipv4 [] {
    let valid = "10.0.1.100"
    let parts = ($valid | split row ".")
    test-result "IPv4 four octets" (($parts | length) == 4)
}

def test-validate-instance-type [] {
    let valid_types = ["t3.micro" "t3.small" "m5.large"]
    let invalid = "invalid_type"

    let test1 = (test-result "Instance type valid" (($valid_types | contains ["t3.micro"])))
    let test2 = (test-result "Instance type invalid" (($valid_types | contains [$invalid]) == false))

    $test1 and $test2
}

def test-validate-zone [] {
    let valid_zones = ["us-east-1a" "us-east-1b" "eu-west-1a"]
    let invalid = "invalid-zone"

    let test1 = (test-result "Zone valid" (($valid_zones | contains ["us-east-1a"])))
    let test2 = (test-result "Zone invalid" (($valid_zones | contains [$invalid]) == false))

    $test1 and $test2
}

def test-validate-volume-id [] {
    let valid = "vol-12345678"
    let invalid = "invalid-vol"

    let test1 = (test-result "Volume ID valid" ($valid | str contains "vol-"))
    let test2 = (test-result "Volume ID invalid" (($invalid | str contains "vol-") == false))

    $test1 and $test2
}

def test-validate-volume-state [] {
    let valid_states = ["available" "in-use" "creating"]
    let invalid = "pending"

    let test1 = (test-result "Volume state valid" (($valid_states | contains ["available"])))
    let test2 = (test-result "Volume state invalid" (($valid_states | contains [$invalid]) == false))

    $test1 and $test2
}

def test-validate-cidr [] {
    let valid = "10.0.0.0/16"
    let invalid = "10.0.0.1"

    let test1 = (test-result "CIDR valid" ($valid | str contains "/"))
    let test2 = (test-result "CIDR invalid" (($invalid | str contains "/") == false))

    $test1 and $test2
}

def test-validate-volume-type [] {
    let valid_types = ["gp2" "gp3" "io1" "io2"]
    let invalid = "invalid-type"

    let test1 = (test-result "Volume type valid" (($valid_types | contains ["gp3"])))
    let test2 = (test-result "Volume type invalid" (($valid_types | contains [$invalid]) == false))

    $test1 and $test2
}

def test-validate-timestamp [] {
    let valid = "2025-01-07T10:00:00.000Z"
    let invalid = "not-a-timestamp"

    let test1 = (test-result "Timestamp valid" ($valid | str contains "T" and $valid | str contains "Z"))
    let test2 = (test-result "Timestamp invalid" (($invalid | str contains "T") == false))

    $test1 and $test2
}

def test-validate-server-state [] {
    let valid_states = ["running" "stopped" "pending"]
    let invalid = "hibernating"

    let test1 = (test-result "Server state valid" (($valid_states | contains ["running"])))
    let test2 = (test-result "Server state invalid" (($valid_states | contains [$invalid]) == false))

    $test1 and $test2
}

def test-validate-security-group [] {
    let valid = "sg-12345678"
    let invalid = "invalid-sg"

    let test1 = (test-result "Security group valid" ($valid | str contains "sg-"))
    let test2 = (test-result "Security group invalid" (($invalid | str contains "sg-") == false))

    $test1 and $test2
}

def test-validate-memory [] {
    let valid_mems = ["512 MB" "1 GB" "2 GB" "4 GB"]
    let invalid = "0 GB"

    let test1 = (test-result "Memory valid" (($valid_mems | contains ["1 GB"])))
    let test2 = (test-result "Memory invalid" (($valid_mems | contains [$invalid]) == false))

    $test1 and $test2
}

def test-validate-vcpu [] {
    let valid_cpus = [1, 2, 4, 8, 16]
    let invalid = 0

    let test1 = (test-result "vCPU valid" (($valid_cpus | contains [1])))
    let test2 = (test-result "vCPU invalid" (($valid_cpus | contains [$invalid]) == false))

    $test1 and $test2
}

def main [] {
    print "=== Unit Tests ==="
    print ""

    let results = [
        (test-validate-instance-id),
        (test-validate-ipv4),
        (test-validate-instance-type),
        (test-validate-zone),
        (test-validate-volume-id),
        (test-validate-volume-state),
        (test-validate-cidr),
        (test-validate-volume-type),
        (test-validate-timestamp),
        (test-validate-server-state),
        (test-validate-security-group),
        (test-validate-memory),
        (test-validate-vcpu)
    ]

    let passed = ($results | where {|it| $it == true} | length)
    let failed = ($results | where {|it| $it == false} | length)

    print ""
    print $"Results: ($passed) passed, ($failed) failed"

    {
        passed: $passed,
        failed: $failed,
        total: ($passed + $failed)
    }
}

main
```

### Integration Tests: 37 Tests across 3 Modules

**Module 1: test_api_client.nu** (13 tests)
- Response structure validation
- Error handling for 401, 404, 429
- Resource listing operations
- Pricing data validation

**Module 2: test_server_lifecycle.nu** (12 tests)
- Server creation, listing, state
- Instance type and zone info
- Storage and security attachment
- Server state transitions

**Module 3: test_pricing_cache.nu** (12 tests)
- Pricing data structure validation
- On-demand vs reserved pricing
- Cost calculations
- Volume pricing operations

### Test Orchestrator

```nushell
def main [] {
    print "=== Provider Test Suite ==="

    let unit_result = (nu tests/unit/test_utils.nu)
    let api_result = (nu tests/integration/test_api_client.nu)
    let lifecycle_result = (nu tests/integration/test_server_lifecycle.nu)
    let pricing_result = (nu tests/integration/test_pricing_cache.nu)

    let total_passed = (
        $unit_result.passed +
        $api_result.passed +
        $lifecycle_result.passed +
        $pricing_result.passed
    )

    let total_failed = (
        $unit_result.failed +
        $api_result.failed +
        $lifecycle_result.failed +
        $pricing_result.failed
    )

    print $"Results: ($total_passed) passed, ($total_failed) failed"

    {
        passed: $total_passed,
        failed: $total_failed,
        success: ($total_failed == 0)
    }
}

let result = (main)
exit (if $result.success {0} else {1})
```

### Validation

```bash
cd provisioning/extensions/providers/{PROVIDER}
nu tests/run_{provider}_tests.nu
```

Expected: 51 tests passing, exit code 0

---

## Tarea 3: Runtime Templates

### Directory Structure

```text
templates/
├── {provider}_servers.j2
├── {provider}_networks.j2
└── {provider}_volumes.j2
```

### Template Example

```jinja2
#!/bin/bash
# {{ provider_name }} Server Provisioning
set -e
{% if debug %}set -x{% endif %}

{%- for server in servers %}
  {%- if server.name %}

echo "Creating server: {{ server.name }}"

{%- if server.instance_type %}
INSTANCE_TYPE="{{ server.instance_type }}"
{%- else %}
INSTANCE_TYPE="t3.micro"
{%- endif %}

SERVER_ID=$(^hcloud server create \
  --name "{{ server.name }}" \
  --type $INSTANCE_TYPE \
  --query 'id' \
  --output text 2>/dev/null)

if [ -z "$SERVER_ID" ]; then
  echo "Failed to create server {{ server.name }}"
  exit 1
fi

echo "✓ Server {{ server.name }} created: $SERVER_ID"

  {%- endif %}
{%- endfor %}

echo "Server provisioning complete"
```

### Validation

```bash
cd provisioning/extensions/providers/{PROVIDER}

for template in templates/*.j2; do
    bash -n <(sed 's/{%.*%}//' "$template" | sed 's/{{.*}}/x/g')
done

echo "✅ Templates valid"
```

---

## Tarea 4: Nickel Schema Validation

```bash
cd provisioning/extensions/providers/{PROVIDER}

nickel typecheck nickel/contracts.ncl || exit 1
nickel typecheck nickel/defaults.ncl || exit 1
nickel typecheck nickel/main.ncl || exit 1
nickel typecheck nickel/version.ncl || exit 1

nickel export nickel/main.ncl || exit 1

echo "✅ Nickel schemas validated"
```

---

## Complete Validation Script

```bash
#!/bin/bash
set -e

PROVIDER="hetzner"
PROV="provisioning/extensions/providers/$PROVIDER"

echo "=== Provider Completeness Check: $PROVIDER ==="

echo ""
echo "✓ Tarea 4: Validating Nickel..."
nickel typecheck "$PROV/nickel/main.ncl"

echo "✓ Tarea 1: Checking Nushell..."
[ $(grep -r "try {" "$PROV/nulib" 2>/dev/null | wc -l) -eq 0 ]
[ $(grep -r "let mut " "$PROV/nulib" 2>/dev/null | wc -l) -eq 0 ]
echo "  - No deprecated patterns ✓"

echo "✓ Tarea 3: Validating templates..."
for f in "$PROV"/templates/*.j2; do
    bash -n <(sed 's/{%.*%}//' "$f" | sed 's/{{.*}}/x/g')
done

echo "✓ Tarea 2: Running tests..."
nu "$PROV/tests/run_${PROVIDER}_tests.nu"

echo ""
echo "╔════════════════════════════════════════╗"
echo "║  ✅ ALL TASKS COMPLETE                 ║"
echo "║     PRODUCTION READY                   ║"
echo "╚════════════════════════════════════════╝"
```

---

## Reference Implementations

- **Hetzner**: `provisioning/extensions/providers/hetzner/`
- **UpCloud**: `provisioning/extensions/providers/upcloud/`
- **AWS**: `provisioning/extensions/providers/aws/`

Use these as templates for new providers.

---

## Quick Start

```bash
cd provisioning/extensions/providers/{PROVIDER}

# Validate completeness
nickel typecheck nickel/main.ncl && \
[ $(grep -r "try {" nulib/ 2>/dev/null | wc -l) -eq 0 ] && \
nu tests/run_{provider}_tests.nu && \
for f in templates/*.j2; do bash -n <(sed 's/{%.*%}//' "$f"); done && \
echo "✅ PRODUCTION READY"
```
