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 Type | Purpose | Examples |
|---|---|---|
| Providers | Cloud platform integrations | Custom cloud, on-premises |
| Task Services | Software components | Custom databases, monitoring |
| Clusters | Service orchestration | Application stacks, platforms |
| Templates | Reusable configurations | Standard 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:
- Study existing extensions in the
providers/andtaskservs/directories - Practice with simple extensions before building complex ones
- Join the community to share and collaborate on extensions
- Contribute to the core system by improving extension APIs
- Build a library of reusable templates and patterns
You’re now equipped to extend provisioning for any custom requirements!