Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Extension Development Guide

This guide will help you create custom providers, task services, and cluster configurations to extend provisioning for your specific needs.

What You’ll Learn

  • Extension architecture and concepts
  • Creating custom cloud providers
  • Developing task services
  • Building cluster configurations
  • Publishing and sharing extensions
  • Best practices and patterns
  • Testing and validation

Extension Architecture

Extension Types

Extension TypePurposeExamples
ProvidersCloud platform integrationsCustom cloud, on-premises
Task ServicesSoftware componentsCustom databases, monitoring
ClustersService orchestrationApplication stacks, platforms
TemplatesReusable configurationsStandard deployments

Extension Structure

my-extension/
├── kcl/                    # KCL schemas and models
│   ├── models/            # Data models
│   ├── providers/         # Provider definitions
│   ├── taskservs/         # Task service definitions
│   └── clusters/          # Cluster definitions
├── nulib/                 # Nushell implementation
│   ├── providers/         # Provider logic
│   ├── taskservs/         # Task service logic
│   └── utils/             # Utility functions
├── templates/             # Configuration templates
├── tests/                 # Test files
├── docs/                  # Documentation
├── extension.toml         # Extension metadata
└── README.md              # Extension documentation

Extension Metadata

extension.toml:

[extension]
name = "my-custom-provider"
version = "1.0.0"
description = "Custom cloud provider integration"
author = "Your Name <you@example.com>"
license = "MIT"

[compatibility]
provisioning_version = ">=1.0.0"
kcl_version = ">=0.11.2"

[provides]
providers = ["custom-cloud"]
taskservs = ["custom-database"]
clusters = ["custom-stack"]

[dependencies]
extensions = []
system_packages = ["curl", "jq"]

[configuration]
required_env = ["CUSTOM_CLOUD_API_KEY"]
optional_env = ["CUSTOM_CLOUD_REGION"]

Creating Custom Providers

Provider Architecture

A provider handles:

  • Authentication with cloud APIs
  • Resource lifecycle management (create, read, update, delete)
  • Provider-specific configurations
  • Cost estimation and billing integration

Step 1: Define Provider Schema

kcl/providers/custom_cloud.k:

# Custom cloud provider schema
import models.base

schema CustomCloudConfig(base.ProviderConfig):
    """Configuration for Custom Cloud provider"""

    # Authentication
    api_key: str
    api_secret?: str
    region?: str = "us-west-1"

    # Provider-specific settings
    project_id?: str
    organization?: str

    # API configuration
    api_url?: str = "https://api.custom-cloud.com/v1"
    timeout?: int = 30

    # Cost configuration
    billing_account?: str
    cost_center?: str

schema CustomCloudServer(base.ServerConfig):
    """Server configuration for Custom Cloud"""

    # Instance configuration
    machine_type: str
    zone: str
    disk_size?: int = 20
    disk_type?: str = "ssd"

    # Network configuration
    vpc?: str
    subnet?: str
    external_ip?: bool = true

    # Custom Cloud specific
    preemptible?: bool = false
    labels?: {str: str} = {}

    # Validation rules
    check:
        len(machine_type) > 0, "machine_type cannot be empty"
        disk_size >= 10, "disk_size must be at least 10GB"

# Provider capabilities
provider_capabilities = {
    "name": "custom-cloud"
    "supports_auto_scaling": True
    "supports_load_balancing": True
    "supports_managed_databases": True
    "regions": [
        "us-west-1", "us-west-2", "us-east-1", "eu-west-1"
    ]
    "machine_types": [
        "micro", "small", "medium", "large", "xlarge"
    ]
}

Step 2: Implement Provider Logic

nulib/providers/custom_cloud.nu:

# Custom Cloud provider implementation

# Provider initialization
export def custom_cloud_init [] {
    # Validate environment variables
    if ($env.CUSTOM_CLOUD_API_KEY | is-empty) {
        error make {
            msg: "CUSTOM_CLOUD_API_KEY environment variable is required"
        }
    }

    # Set up provider context
    $env.CUSTOM_CLOUD_INITIALIZED = true
}

# Create server instance
export def custom_cloud_create_server [
    server_config: record
    --check: bool = false    # Dry run mode
] -> record {
    custom_cloud_init

    print $"Creating server: ($server_config.name)"

    if $check {
        return {
            action: "create"
            resource: "server"
            name: $server_config.name
            status: "planned"
            estimated_cost: (calculate_server_cost $server_config)
        }
    }

    # Make API call to create server
    let api_response = (custom_cloud_api_call "POST" "instances" $server_config)

    if ($api_response.status | str contains "error") {
        error make {
            msg: $"Failed to create server: ($api_response.message)"
        }
    }

    # Wait for server to be ready
    let server_id = $api_response.instance_id
    custom_cloud_wait_for_server $server_id "running"

    return {
        id: $server_id
        name: $server_config.name
        status: "running"
        ip_address: $api_response.ip_address
        created_at: (date now | format date "%Y-%m-%d %H:%M:%S")
    }
}

# Delete server instance
export def custom_cloud_delete_server [
    server_name: string
    --keep_storage: bool = false
] -> record {
    custom_cloud_init

    let server = (custom_cloud_get_server $server_name)

    if ($server | is-empty) {
        error make {
            msg: $"Server not found: ($server_name)"
        }
    }

    print $"Deleting server: ($server_name)"

    # Delete the instance
    let delete_response = (custom_cloud_api_call "DELETE" $"instances/($server.id)" {
        keep_storage: $keep_storage
    })

    return {
        action: "delete"
        resource: "server"
        name: $server_name
        status: "deleted"
    }
}

# List servers
export def custom_cloud_list_servers [] -> list<record> {
    custom_cloud_init

    let response = (custom_cloud_api_call "GET" "instances" {})

    return ($response.instances | each {|instance|
        {
            id: $instance.id
            name: $instance.name
            status: $instance.status
            machine_type: $instance.machine_type
            zone: $instance.zone
            ip_address: $instance.ip_address
            created_at: $instance.created_at
        }
    })
}

# Get server details
export def custom_cloud_get_server [server_name: string] -> record {
    let servers = (custom_cloud_list_servers)
    return ($servers | where name == $server_name | first)
}

# Calculate estimated costs
export def calculate_server_cost [server_config: record] -> float {
    # Cost calculation logic based on machine type
    let base_costs = {
        micro: 0.01
        small: 0.05
        medium: 0.10
        large: 0.20
        xlarge: 0.40
    }

    let machine_cost = ($base_costs | get $server_config.machine_type)
    let storage_cost = ($server_config.disk_size | default 20) * 0.001

    return ($machine_cost + $storage_cost)
}

# Make API call to Custom Cloud
def custom_cloud_api_call [
    method: string
    endpoint: string
    data: record
] -> record {
    let api_url = ($env.CUSTOM_CLOUD_API_URL | default "https://api.custom-cloud.com/v1")
    let api_key = $env.CUSTOM_CLOUD_API_KEY

    let headers = {
        "Authorization": $"Bearer ($api_key)"
        "Content-Type": "application/json"
    }

    let url = $"($api_url)/($endpoint)"

    match $method {
        "GET" => {
            http get $url --headers $headers
        }
        "POST" => {
            http post $url --headers $headers ($data | to json)
        }
        "PUT" => {
            http put $url --headers $headers ($data | to json)
        }
        "DELETE" => {
            http delete $url --headers $headers
        }
        _ => {
            error make {
                msg: $"Unsupported HTTP method: ($method)"
            }
        }
    }
}

# Wait for server to reach desired state
def custom_cloud_wait_for_server [
    server_id: string
    target_status: string
    --timeout: int = 300
] {
    let start_time = (date now)

    loop {
        let response = (custom_cloud_api_call "GET" $"instances/($server_id)" {})
        let current_status = $response.status

        if $current_status == $target_status {
            print $"Server ($server_id) reached status: ($target_status)"
            break
        }

        let elapsed = ((date now) - $start_time) / 1000000000  # Convert to seconds
        if $elapsed > $timeout {
            error make {
                msg: $"Timeout waiting for server ($server_id) to reach ($target_status)"
            }
        }

        sleep 10sec
        print $"Waiting for server status: ($current_status) -> ($target_status)"
    }
}

Step 3: Provider Registration

nulib/providers/mod.nu:

# Provider module exports
export use custom_cloud.nu *

# Provider registry
export def get_provider_info [] -> record {
    {
        name: "custom-cloud"
        version: "1.0.0"
        capabilities: {
            servers: true
            load_balancers: true
            databases: false
            storage: true
        }
        regions: ["us-west-1", "us-west-2", "us-east-1", "eu-west-1"]
        auth_methods: ["api_key", "oauth"]
    }
}

Creating Custom Task Services

Task Service Architecture

Task services handle:

  • Software installation and configuration
  • Service lifecycle management
  • Health checking and monitoring
  • Version management and updates

Step 1: Define Service Schema

kcl/taskservs/custom_database.k:

# Custom database task service
import models.base

schema CustomDatabaseConfig(base.TaskServiceConfig):
    """Configuration for Custom Database service"""

    # Database configuration
    version?: str = "14.0"
    port?: int = 5432
    max_connections?: int = 100
    memory_limit?: str = "512MB"

    # Data configuration
    data_directory?: str = "/var/lib/customdb"
    log_directory?: str = "/var/log/customdb"

    # Replication
    replication?: {
        enabled?: bool = false
        mode?: str = "async"  # async, sync
        replicas?: int = 1
    }

    # Backup configuration
    backup?: {
        enabled?: bool = true
        schedule?: str = "0 2 * * *"  # Daily at 2 AM
        retention_days?: int = 7
        storage_location?: str = "local"
    }

    # Security
    ssl?: {
        enabled?: bool = true
        cert_file?: str = "/etc/ssl/certs/customdb.crt"
        key_file?: str = "/etc/ssl/private/customdb.key"
    }

    # Monitoring
    monitoring?: {
        enabled?: bool = true
        metrics_port?: int = 9187
        log_level?: str = "info"
    }

    check:
        port > 1024 and port < 65536, "port must be between 1024 and 65535"
        max_connections > 0, "max_connections must be positive"

# Service metadata
service_metadata = {
    "name": "custom-database"
    "description": "Custom Database Server"
    "version": "14.0"
    "category": "database"
    "dependencies": ["systemd"]
    "supported_os": ["ubuntu", "debian", "centos", "rhel"]
    "ports": [5432, 9187]
    "data_directories": ["/var/lib/customdb"]
}

Step 2: Implement Service Logic

nulib/taskservs/custom_database.nu:

# Custom Database task service implementation

# Install custom database
export def install_custom_database [
    config: record
    --check: bool = false
] -> record {
    print "Installing Custom Database..."

    if $check {
        return {
            action: "install"
            service: "custom-database"
            version: ($config.version | default "14.0")
            status: "planned"
            changes: [
                "Install Custom Database packages"
                "Configure database server"
                "Start database service"
                "Set up monitoring"
            ]
        }
    }

    # Check prerequisites
    validate_prerequisites $config

    # Install packages
    install_packages $config

    # Configure service
    configure_service $config

    # Initialize database
    initialize_database $config

    # Set up monitoring
    if ($config.monitoring?.enabled | default true) {
        setup_monitoring $config
    }

    # Set up backups
    if ($config.backup?.enabled | default true) {
        setup_backups $config
    }

    # Start service
    start_service

    # Verify installation
    let status = (verify_installation $config)

    return {
        action: "install"
        service: "custom-database"
        version: ($config.version | default "14.0")
        status: $status.status
        endpoint: $"localhost:($config.port | default 5432)"
        data_directory: ($config.data_directory | default "/var/lib/customdb")
    }
}

# Configure custom database
export def configure_custom_database [
    config: record
] {
    print "Configuring Custom Database..."

    # Generate configuration file
    let db_config = generate_config $config
    $db_config | save "/etc/customdb/customdb.conf"

    # Set up SSL if enabled
    if ($config.ssl?.enabled | default true) {
        setup_ssl $config
    }

    # Configure replication if enabled
    if ($config.replication?.enabled | default false) {
        setup_replication $config
    }

    # Restart service to apply configuration
    restart_service
}

# Start service
export def start_custom_database [] {
    print "Starting Custom Database service..."
    ^systemctl start customdb
    ^systemctl enable customdb
}

# Stop service
export def stop_custom_database [] {
    print "Stopping Custom Database service..."
    ^systemctl stop customdb
}

# Check service status
export def status_custom_database [] -> record {
    let systemd_status = (^systemctl is-active customdb | str trim)
    let port_check = (check_port 5432)
    let version = (get_database_version)

    return {
        service: "custom-database"
        status: $systemd_status
        port_accessible: $port_check
        version: $version
        uptime: (get_service_uptime)
        connections: (get_active_connections)
    }
}

# Health check
export def health_custom_database [] -> record {
    let status = (status_custom_database)
    let health_checks = [
        {
            name: "Service Running"
            status: ($status.status == "active")
            message: $"Systemd status: ($status.status)"
        }
        {
            name: "Port Accessible"
            status: $status.port_accessible
            message: "Database port 5432 is accessible"
        }
        {
            name: "Database Responsive"
            status: (test_database_connection)
            message: "Database responds to queries"
        }
    ]

    let healthy = ($health_checks | all {|check| $check.status})

    return {
        service: "custom-database"
        healthy: $healthy
        checks: $health_checks
        last_check: (date now | format date "%Y-%m-%d %H:%M:%S")
    }
}

# Update service
export def update_custom_database [
    target_version: string
] -> record {
    print $"Updating Custom Database to version ($target_version)..."

    # Create backup before update
    backup_database "pre-update"

    # Stop service
    stop_custom_database

    # Update packages
    update_packages $target_version

    # Migrate database if needed
    migrate_database $target_version

    # Start service
    start_custom_database

    # Verify update
    let new_version = (get_database_version)

    return {
        action: "update"
        service: "custom-database"
        old_version: (get_previous_version)
        new_version: $new_version
        status: "completed"
    }
}

# Remove service
export def remove_custom_database [
    --keep_data: bool = false
] -> record {
    print "Removing Custom Database..."

    # Stop service
    stop_custom_database

    # Remove packages
    ^apt remove --purge -y customdb-server customdb-client

    # Remove configuration
    rm -rf "/etc/customdb"

    # Remove data (optional)
    if not $keep_data {
        print "Removing database data..."
        rm -rf "/var/lib/customdb"
        rm -rf "/var/log/customdb"
    }

    return {
        action: "remove"
        service: "custom-database"
        data_preserved: $keep_data
        status: "completed"
    }
}

# Helper functions

def validate_prerequisites [config: record] {
    # Check operating system
    let os_info = (^lsb_release -is | str trim | str downcase)
    let supported_os = ["ubuntu", "debian"]

    if not ($os_info in $supported_os) {
        error make {
            msg: $"Unsupported OS: ($os_info). Supported: ($supported_os | str join ', ')"
        }
    }

    # Check system resources
    let memory_mb = (^free -m | lines | get 1 | split row ' ' | get 1 | into int)
    if $memory_mb < 512 {
        error make {
            msg: $"Insufficient memory: ($memory_mb)MB. Minimum 512MB required."
        }
    }
}

def install_packages [config: record] {
    let version = ($config.version | default "14.0")

    # Update package list
    ^apt update

    # Install packages
    ^apt install -y $"customdb-server-($version)" $"customdb-client-($version)"
}

def configure_service [config: record] {
    let config_content = generate_config $config
    $config_content | save "/etc/customdb/customdb.conf"

    # Set permissions
    ^chown -R customdb:customdb "/etc/customdb"
    ^chmod 600 "/etc/customdb/customdb.conf"
}

def generate_config [config: record] -> string {
    let port = ($config.port | default 5432)
    let max_connections = ($config.max_connections | default 100)
    let memory_limit = ($config.memory_limit | default "512MB")

    return $"
# Custom Database Configuration
port = ($port)
max_connections = ($max_connections)
shared_buffers = ($memory_limit)
data_directory = '($config.data_directory | default "/var/lib/customdb")'
log_directory = '($config.log_directory | default "/var/log/customdb")'

# Logging
log_level = '($config.monitoring?.log_level | default "info")'

# SSL Configuration
ssl = ($config.ssl?.enabled | default true)
ssl_cert_file = '($config.ssl?.cert_file | default "/etc/ssl/certs/customdb.crt")'
ssl_key_file = '($config.ssl?.key_file | default "/etc/ssl/private/customdb.key")'
"
}

def initialize_database [config: record] {
    print "Initializing database..."

    # Create data directory
    let data_dir = ($config.data_directory | default "/var/lib/customdb")
    mkdir $data_dir
    ^chown -R customdb:customdb $data_dir

    # Initialize database
    ^su - customdb -c $"customdb-initdb -D ($data_dir)"
}

def setup_monitoring [config: record] {
    if ($config.monitoring?.enabled | default true) {
        print "Setting up monitoring..."

        # Install monitoring exporter
        ^apt install -y customdb-exporter

        # Configure exporter
        let exporter_config = $"
port: ($config.monitoring?.metrics_port | default 9187)
database_url: postgresql://localhost:($config.port | default 5432)/postgres
"
        $exporter_config | save "/etc/customdb-exporter/config.yaml"

        # Start exporter
        ^systemctl enable customdb-exporter
        ^systemctl start customdb-exporter
    }
}

def setup_backups [config: record] {
    if ($config.backup?.enabled | default true) {
        print "Setting up backups..."

        let schedule = ($config.backup?.schedule | default "0 2 * * *")
        let retention = ($config.backup?.retention_days | default 7)

        # Create backup script
        let backup_script = $"#!/bin/bash
customdb-dump --all-databases > /var/backups/customdb-$(date +%Y%m%d_%H%M%S).sql
find /var/backups -name 'customdb-*.sql' -mtime +($retention) -delete
"

        $backup_script | save "/usr/local/bin/customdb-backup.sh"
        ^chmod +x "/usr/local/bin/customdb-backup.sh"

        # Add to crontab
        $"($schedule) /usr/local/bin/customdb-backup.sh" | ^crontab -u customdb -
    }
}

def test_database_connection [] -> bool {
    let result = (^customdb-cli -h localhost -c "SELECT 1;" | complete)
    return ($result.exit_code == 0)
}

def get_database_version [] -> string {
    let result = (^customdb-cli -h localhost -c "SELECT version();" | complete)
    if ($result.exit_code == 0) {
        return ($result.stdout | lines | first | parse "Custom Database {version}" | get version.0)
    } else {
        return "unknown"
    }
}

def check_port [port: int] -> bool {
    let result = (^nc -z localhost $port | complete)
    return ($result.exit_code == 0)
}

Creating Custom Clusters

Cluster Architecture

Clusters orchestrate multiple services to work together as a cohesive application stack.

Step 1: Define Cluster Schema

kcl/clusters/custom_web_stack.k:

# Custom web application stack
import models.base
import models.server
import models.taskserv

schema CustomWebStackConfig(base.ClusterConfig):
    """Configuration for Custom Web Application Stack"""

    # Application configuration
    app_name: str
    app_version?: str = "latest"
    environment?: str = "production"

    # Web tier configuration
    web_tier: {
        replicas?: int = 3
        instance_type?: str = "t3.medium"
        load_balancer?: {
            enabled?: bool = true
            ssl?: bool = true
            health_check_path?: str = "/health"
        }
    }

    # Application tier configuration
    app_tier: {
        replicas?: int = 5
        instance_type?: str = "t3.large"
        auto_scaling?: {
            enabled?: bool = true
            min_replicas?: int = 2
            max_replicas?: int = 10
            cpu_threshold?: int = 70
        }
    }

    # Database tier configuration
    database_tier: {
        type?: str = "postgresql"  # postgresql, mysql, custom-database
        instance_type?: str = "t3.xlarge"
        high_availability?: bool = true
        backup_enabled?: bool = true
    }

    # Monitoring configuration
    monitoring: {
        enabled?: bool = true
        metrics_retention?: str = "30d"
        alerting?: bool = true
    }

    # Networking
    network: {
        vpc_cidr?: str = "10.0.0.0/16"
        public_subnets?: [str] = ["10.0.1.0/24", "10.0.2.0/24"]
        private_subnets?: [str] = ["10.0.10.0/24", "10.0.20.0/24"]
        database_subnets?: [str] = ["10.0.100.0/24", "10.0.200.0/24"]
    }

    check:
        len(app_name) > 0, "app_name cannot be empty"
        web_tier.replicas >= 1, "web_tier replicas must be at least 1"
        app_tier.replicas >= 1, "app_tier replicas must be at least 1"

# Cluster blueprint
cluster_blueprint = {
    "name": "custom-web-stack"
    "description": "Custom web application stack with load balancer, app servers, and database"
    "version": "1.0.0"
    "components": [
        {
            "name": "load-balancer"
            "type": "taskserv"
            "service": "haproxy"
            "tier": "web"
        }
        {
            "name": "web-servers"
            "type": "server"
            "tier": "web"
            "scaling": "horizontal"
        }
        {
            "name": "app-servers"
            "type": "server"
            "tier": "app"
            "scaling": "horizontal"
        }
        {
            "name": "database"
            "type": "taskserv"
            "service": "postgresql"
            "tier": "database"
        }
        {
            "name": "monitoring"
            "type": "taskserv"
            "service": "prometheus"
            "tier": "monitoring"
        }
    ]
}

Step 2: Implement Cluster Logic

nulib/clusters/custom_web_stack.nu:

# Custom Web Stack cluster implementation

# Deploy web stack cluster
export def deploy_custom_web_stack [
    config: record
    --check: bool = false
] -> record {
    print $"Deploying Custom Web Stack: ($config.app_name)"

    if $check {
        return {
            action: "deploy"
            cluster: "custom-web-stack"
            app_name: $config.app_name
            status: "planned"
            components: [
                "Network infrastructure"
                "Load balancer"
                "Web servers"
                "Application servers"
                "Database"
                "Monitoring"
            ]
            estimated_cost: (calculate_cluster_cost $config)
        }
    }

    # Deploy in order
    let network = (deploy_network $config)
    let database = (deploy_database $config)
    let app_servers = (deploy_app_tier $config)
    let web_servers = (deploy_web_tier $config)
    let load_balancer = (deploy_load_balancer $config)
    let monitoring = (deploy_monitoring $config)

    # Configure service discovery
    configure_service_discovery $config

    # Set up health checks
    setup_health_checks $config

    return {
        action: "deploy"
        cluster: "custom-web-stack"
        app_name: $config.app_name
        status: "deployed"
        components: {
            network: $network
            database: $database
            app_servers: $app_servers
            web_servers: $web_servers
            load_balancer: $load_balancer
            monitoring: $monitoring
        }
        endpoints: {
            web: $load_balancer.public_ip
            monitoring: $monitoring.grafana_url
        }
    }
}

# Scale cluster
export def scale_custom_web_stack [
    app_name: string
    tier: string
    replicas: int
] -> record {
    print $"Scaling ($tier) tier to ($replicas) replicas for ($app_name)"

    match $tier {
        "web" => {
            scale_web_tier $app_name $replicas
        }
        "app" => {
            scale_app_tier $app_name $replicas
        }
        _ => {
            error make {
                msg: $"Invalid tier: ($tier). Valid options: web, app"
            }
        }
    }

    return {
        action: "scale"
        cluster: "custom-web-stack"
        app_name: $app_name
        tier: $tier
        new_replicas: $replicas
        status: "completed"
    }
}

# Update cluster
export def update_custom_web_stack [
    app_name: string
    config: record
] -> record {
    print $"Updating Custom Web Stack: ($app_name)"

    # Rolling update strategy
    update_app_tier $app_name $config
    update_web_tier $app_name $config
    update_load_balancer $app_name $config

    return {
        action: "update"
        cluster: "custom-web-stack"
        app_name: $app_name
        status: "completed"
    }
}

# Delete cluster
export def delete_custom_web_stack [
    app_name: string
    --keep_data: bool = false
] -> record {
    print $"Deleting Custom Web Stack: ($app_name)"

    # Delete in reverse order
    delete_load_balancer $app_name
    delete_web_tier $app_name
    delete_app_tier $app_name

    if not $keep_data {
        delete_database $app_name
    }

    delete_monitoring $app_name
    delete_network $app_name

    return {
        action: "delete"
        cluster: "custom-web-stack"
        app_name: $app_name
        data_preserved: $keep_data
        status: "completed"
    }
}

# Cluster status
export def status_custom_web_stack [
    app_name: string
] -> record {
    let web_status = (get_web_tier_status $app_name)
    let app_status = (get_app_tier_status $app_name)
    let db_status = (get_database_status $app_name)
    let lb_status = (get_load_balancer_status $app_name)
    let monitoring_status = (get_monitoring_status $app_name)

    let overall_healthy = (
        $web_status.healthy and
        $app_status.healthy and
        $db_status.healthy and
        $lb_status.healthy and
        $monitoring_status.healthy
    )

    return {
        cluster: "custom-web-stack"
        app_name: $app_name
        healthy: $overall_healthy
        components: {
            web_tier: $web_status
            app_tier: $app_status
            database: $db_status
            load_balancer: $lb_status
            monitoring: $monitoring_status
        }
        last_check: (date now | format date "%Y-%m-%d %H:%M:%S")
    }
}

# Helper functions for deployment

def deploy_network [config: record] -> record {
    print "Deploying network infrastructure..."

    # Create VPC
    let vpc_config = {
        cidr: ($config.network.vpc_cidr | default "10.0.0.0/16")
        name: $"($config.app_name)-vpc"
    }

    # Create subnets
    let subnets = [
        {name: "public-1", cidr: ($config.network.public_subnets | get 0)}
        {name: "public-2", cidr: ($config.network.public_subnets | get 1)}
        {name: "private-1", cidr: ($config.network.private_subnets | get 0)}
        {name: "private-2", cidr: ($config.network.private_subnets | get 1)}
        {name: "database-1", cidr: ($config.network.database_subnets | get 0)}
        {name: "database-2", cidr: ($config.network.database_subnets | get 1)}
    ]

    return {
        vpc: $vpc_config
        subnets: $subnets
        status: "deployed"
    }
}

def deploy_database [config: record] -> record {
    print "Deploying database tier..."

    let db_config = {
        name: $"($config.app_name)-db"
        type: ($config.database_tier.type | default "postgresql")
        instance_type: ($config.database_tier.instance_type | default "t3.xlarge")
        high_availability: ($config.database_tier.high_availability | default true)
        backup_enabled: ($config.database_tier.backup_enabled | default true)
    }

    # Deploy database servers
    if $db_config.high_availability {
        deploy_ha_database $db_config
    } else {
        deploy_single_database $db_config
    }

    return {
        name: $db_config.name
        type: $db_config.type
        high_availability: $db_config.high_availability
        status: "deployed"
        endpoint: $"($config.app_name)-db.local:5432"
    }
}

def deploy_app_tier [config: record] -> record {
    print "Deploying application tier..."

    let replicas = ($config.app_tier.replicas | default 5)

    # Deploy app servers
    mut servers = []
    for i in 1..$replicas {
        let server_config = {
            name: $"($config.app_name)-app-($i | fill --width 2 --char '0')"
            instance_type: ($config.app_tier.instance_type | default "t3.large")
            subnet: "private"
        }

        let server = (deploy_app_server $server_config)
        $servers = ($servers | append $server)
    }

    return {
        tier: "application"
        servers: $servers
        replicas: $replicas
        status: "deployed"
    }
}

def calculate_cluster_cost [config: record] -> float {
    let web_cost = ($config.web_tier.replicas | default 3) * 0.10
    let app_cost = ($config.app_tier.replicas | default 5) * 0.20
    let db_cost = if ($config.database_tier.high_availability | default true) { 0.80 } else { 0.40 }
    let lb_cost = 0.05

    return ($web_cost + $app_cost + $db_cost + $lb_cost)
}

Extension Testing

Test Structure

tests/
├── unit/                   # Unit tests
│   ├── provider_test.nu   # Provider unit tests
│   ├── taskserv_test.nu   # Task service unit tests
│   └── cluster_test.nu    # Cluster unit tests
├── integration/            # Integration tests
│   ├── provider_integration_test.nu
│   ├── taskserv_integration_test.nu
│   └── cluster_integration_test.nu
├── e2e/                   # End-to-end tests
│   └── full_stack_test.nu
└── fixtures/              # Test data
    ├── configs/
    └── mocks/

Example Unit Test

tests/unit/provider_test.nu:

# Unit tests for custom cloud provider

use std testing

export def test_provider_validation [] {
    # Test valid configuration
    let valid_config = {
        api_key: "test-key"
        region: "us-west-1"
        project_id: "test-project"
    }

    let result = (validate_custom_cloud_config $valid_config)
    assert equal $result.valid true

    # Test invalid configuration
    let invalid_config = {
        region: "us-west-1"
        # Missing api_key
    }

    let result2 = (validate_custom_cloud_config $invalid_config)
    assert equal $result2.valid false
    assert str contains $result2.error "api_key"
}

export def test_cost_calculation [] {
    let server_config = {
        machine_type: "medium"
        disk_size: 50
    }

    let cost = (calculate_server_cost $server_config)
    assert equal $cost 0.15  # 0.10 (medium) + 0.05 (50GB storage)
}

export def test_api_call_formatting [] {
    let config = {
        name: "test-server"
        machine_type: "small"
        zone: "us-west-1a"
    }

    let api_payload = (format_create_server_request $config)

    assert str contains ($api_payload | to json) "test-server"
    assert equal $api_payload.machine_type "small"
    assert equal $api_payload.zone "us-west-1a"
}

Integration Test

tests/integration/provider_integration_test.nu:

# Integration tests for custom cloud provider

use std testing

export def test_server_lifecycle [] {
    # Set up test environment
    $env.CUSTOM_CLOUD_API_KEY = "test-api-key"
    $env.CUSTOM_CLOUD_API_URL = "https://api.test.custom-cloud.com/v1"

    let server_config = {
        name: "test-integration-server"
        machine_type: "micro"
        zone: "us-west-1a"
    }

    # Test server creation
    let create_result = (custom_cloud_create_server $server_config --check true)
    assert equal $create_result.status "planned"

    # Note: Actual creation would require valid API credentials
    # In integration tests, you might use a test/sandbox environment
}

export def test_server_listing [] {
    # Mock API response for testing
    with-env [CUSTOM_CLOUD_API_KEY "test-key"] {
        # This would test against a real API in integration environment
        let servers = (custom_cloud_list_servers)
        assert ($servers | is-not-empty)
    }
}

Publishing Extensions

Extension Package Structure

my-extension-package/
├── extension.toml         # Extension metadata
├── README.md             # Documentation
├── LICENSE               # License file
├── CHANGELOG.md          # Version history
├── examples/             # Usage examples
├── src/                  # Source code
│   ├── kcl/
│   ├── nulib/
│   └── templates/
└── tests/               # Test files

Publishing Configuration

extension.toml:

[extension]
name = "my-custom-provider"
version = "1.0.0"
description = "Custom cloud provider integration"
author = "Your Name <you@example.com>"
license = "MIT"
homepage = "https://github.com/username/my-custom-provider"
repository = "https://github.com/username/my-custom-provider"
keywords = ["cloud", "provider", "infrastructure"]
categories = ["providers"]

[compatibility]
provisioning_version = ">=1.0.0"
kcl_version = ">=0.11.2"

[provides]
providers = ["custom-cloud"]
taskservs = []
clusters = []

[dependencies]
system_packages = ["curl", "jq"]
extensions = []

[build]
include = ["src/**", "examples/**", "README.md", "LICENSE"]
exclude = ["tests/**", ".git/**", "*.tmp"]

Publishing Process

# 1. Validate extension
provisioning extension validate .

# 2. Run tests
provisioning extension test .

# 3. Build package
provisioning extension build .

# 4. Publish to registry
provisioning extension publish ./dist/my-custom-provider-1.0.0.tar.gz

Best Practices

1. Code Organization

# Follow standard structure
extension/
├── kcl/          # Schemas and models
├── nulib/        # Implementation
├── templates/    # Configuration templates
├── tests/        # Comprehensive tests
└── docs/         # Documentation

2. Error Handling

# Always provide meaningful error messages
if ($api_response | get -o status | default "" | str contains "error") {
    error make {
        msg: $"API Error: ($api_response.message)"
        label: {
            text: "Custom Cloud API failure"
            span: (metadata $api_response | get span)
        }
        help: "Check your API key and network connectivity"
    }
}

3. Configuration Validation

# Use KCL's validation features
schema CustomConfig:
    name: str
    size: int

    check:
        len(name) > 0, "name cannot be empty"
        size > 0, "size must be positive"
        size <= 1000, "size cannot exceed 1000"

4. Testing

  • Write comprehensive unit tests
  • Include integration tests
  • Test error conditions
  • Use fixtures for consistent test data
  • Mock external dependencies

5. Documentation

  • Include README with examples
  • Document all configuration options
  • Provide troubleshooting guide
  • Include architecture diagrams
  • Write API documentation

Next Steps

Now that you understand extension development:

  1. Study existing extensions in the providers/ and taskservs/ directories
  2. Practice with simple extensions before building complex ones
  3. Join the community to share and collaborate on extensions
  4. Contribute to the core system by improving extension APIs
  5. Build a library of reusable templates and patterns

You’re now equipped to extend provisioning for any custom requirements!