2026-01-08 09:55:37 +00:00
|
|
|
# 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 |
|
2026-01-12 04:42:18 +00:00
|
|
|
| ------ | --- | --- |
|
2026-01-08 09:55:37 +00:00
|
|
|
| **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"
|
|
|
|
|
```
|