prvng_core/nulib/lib_provisioning/providers/loader.nu
Jesús Pérez 894046ef5a
feat(core): three-layer DAG, unified component arch, commands-registry cache, Nushell 0.112.2 migration
- DAG architecture: `dag show/validate/export` (nulib/main_provisioning/dag.nu),
    config loader (lib_provisioning/config/loader/dag.nu), taskserv dag-executor.
    Backed by schemas/lib/dag/*.ncl; orchestrator emits NATS events via
    WorkspaceComposition::into_workflow. See ADR-020, ADR-021.
  - Unified Component Architecture: components/mod.nu, main_provisioning/
    {components,workflow,extensions,ontoref-queries}.nu. Full workflow engine with
    topological sort and NATS subject emission. Blocks A-H complete (libre-daoshi).
  - Commands-registry: nulib/commands-registry.ncl (Nickel source, 314 lines) +
    JSON cache at ~/.cache/provisioning/commands-registry.json rebuilt on source
    change. cli/provisioning fast-path alias expansion avoids cold Nu startup.
    ADDING_COMMANDS.md documents new-command workflow.
  - Platform service manager: service-manager.nu (+573), startup.nu (+611),
    service-check.nu (+255); autostart/bootstrap/health/target refactored.
  - Nushell 0.112.2 migration: removed all try/catch and bash redirections;
    external commands prefixed with ^; type signatures enforced. Driven by
    scripts/refactor-try-catch{,-simplified}.nu.
  - TTY stack: removed shlib/*-tty.sh; replaced by cli/tty-dispatch.sh,
    tty-filter.sh, tty-commands.conf.
  - New domain modules: images/ (golden image lifecycle), workspace/{state,sync}.nu,
    main_provisioning/{bootstrap,cluster-deploy,fip,state}.nu, commands/{state,
    build,integrations/auth,utilities/alias}.nu, platform.nu expanded (+874).
  - Config loader overhaul: loader/core.nu slimmed (-759), cache/core.nu
    refactored (-454), removed legacy loaders/file_loader.nu (-330).
  - Thirteen new provisioning-<domain>.nu top-level modules for bash dispatcher.
  - Tests: test_workspace_state.nu (+351); updates to test_oci_registry,
    test_services.
  - README + CHANGELOG updated.
2026-04-17 04:27:33 +01:00

350 lines
12 KiB
Text

# Provider Loader System
# Dynamic provider loading and interface validation
use registry.nu *
use interface.nu *
use ../utils/logging.nu *
# Load provider dynamically with validation (cached)
export def load-provider [name: string] {
# Check cache first - provider loading happens multiple times due to wrapper scripts
let cache_key = $"PROVIDER_CACHE_($name)"
if ($cache_key in ($env | columns)) {
return ($env | get $cache_key)
}
# Silent loading - only log debug, not errors for repeated loads
if ($env.PROVISIONING_DEBUG? | default false) {
log-debug $"Loading provider: ($name)" "provider-loader"
}
# Check if provider is available
if not (is-provider-available $name) {
if ($env.PROVISIONING_DEBUG? | default false) {
log-debug $"Provider ($name) not found or not available" "provider-loader"
}
load-env { $cache_key: {} }
return {}
}
# Get provider registry entry
let provider_entry = (get-provider-entry $name)
# Load the provider module
let provider_instance = if ($provider_entry.type == "core") {
load-core-provider $provider_entry
} else {
load-extension-provider $provider_entry
}
if not ($provider_instance | is-empty) {
# IMPORTANT: Skip subprocess-based validation for extension providers.
# Child nu processes don't inherit NICKEL_IMPORT_PATH or the provisioning env,
# so validate-provider-interface always reports functions missing even when valid.
# (Same documented fix as registry.nu:132-146 and load-extension-provider above)
# Core providers are loaded from known paths where subprocess context is reliable.
let skip_validation = ($provider_entry.type == "extension")
let validation = if $skip_validation {
{ valid: true, missing_functions: [] }
} else {
validate-provider-interface $name $provider_instance
}
if $validation.valid {
load-env { $cache_key: $provider_instance }
$provider_instance
} else {
if ($env.PROVISIONING_DEBUG? | default false) {
log-error $"Provider ($name) failed interface validation" "provider-loader"
log-error $"Missing functions: ($validation.missing_functions | str join ', ')" "provider-loader"
}
load-env { $cache_key: {} }
{}
}
} else {
if ($env.PROVISIONING_DEBUG? | default false) {
log-error $"Failed to load provider module for ($name)" "provider-loader"
}
load-env { $cache_key: {} }
{}
}
}
# Load core provider
def load-core-provider [provider_entry: record] {
# For core providers, use direct module loading
# Core providers should be in the core library path
let module_path = $provider_entry.entry_point
# Create provider instance record
{
name: $provider_entry.name
type: "core"
loaded: true
entry_point: $module_path
load_time: (date now)
}
}
# Load extension provider
def load-extension-provider [provider_entry: record] {
# IMPORTANT: Do NOT spawn a child nu process to validate the provider.
# Child processes don't inherit NICKEL_IMPORT_PATH or the provisioning env,
# causing all providers to fail validation even though they are valid.
# (Same reason registry.nu skips subprocess validation — see registry.nu:132-146)
# Just verify the file exists and create the instance directly.
let module_path = $provider_entry.entry_point
if not ($module_path | path exists) {
log-error $"Provider module not found: ($module_path)" "provider-loader"
return {}
}
{
name: $provider_entry.name
type: "extension"
loaded: true
entry_point: $module_path
load_time: (date now)
metadata: {}
}
}
# Get provider instance (with caching)
export def get-provider [name: string] {
# Check if already loaded in this session
let cache_key = $"PROVIDER_LOADED_($name)"
let cached_value = if ($cache_key in ($env | columns)) { $env | get $cache_key } else { null }
if $cached_value != null {
return $cached_value
}
# Load and cache the provider
let provider = (load-provider $name)
if not ($provider | is-empty) {
load-env { $cache_key: $provider }
}
$provider
}
# Call a provider function dynamically
export def call-provider-function [
provider_name: string
function_name: string
...args
] {
# Get provider entry
let provider_entry = (get-provider-entry $provider_name)
if ($provider_entry | is-empty) {
log-error $"Provider ($provider_name) not found" "provider-loader"
return null
}
# Use direct import and call via a wrapper script
let temp_dir = ($env.TMPDIR? | default "/tmp")
# Save arguments as a list to a single file
let args_file = ($temp_dir | path join $"provider_args_(random chars).nuon")
$args | to nuon | save --force $args_file
# Create wrapper script that loads args and calls function
# Build individual arg references based on count
let wrapper_script = ($temp_dir | path join $"provider_wrapper_(random chars).nu")
let arg_count = ($args | length)
let call_line = if $arg_count == 0 {
$"($function_name) | to json"
} else if $arg_count == 1 {
$"($function_name) \(\$args | get 0\) | to json"
} else if $arg_count == 2 {
$"($function_name) \(\$args | get 0\) \(\$args | get 1\) | to json"
} else if $arg_count == 3 {
$"($function_name) \(\$args | get 0\) \(\$args | get 1\) \(\$args | get 2\) | to json"
} else if $arg_count == 4 {
$"($function_name) \(\$args | get 0\) \(\$args | get 1\) \(\$args | get 2\) \(\$args | get 3\) | to json"
} else if $arg_count == 5 {
$"($function_name) \(\$args | get 0\) \(\$args | get 1\) \(\$args | get 2\) \(\$args | get 3\) \(\$args | get 4\) | to json"
} else {
log-error $"Too many arguments \(($arg_count)\) for provider function" "provider-loader"
return null
}
let script_content = $"
use ($provider_entry.entry_point) *
let args = \(open ($args_file)\)
($call_line)
"
$script_content | save --force $wrapper_script
# Execute the wrapper script
let result = (do --ignore-errors { nu $wrapper_script } | complete)
# Clean up temp files
if ($args_file | path exists) { rm -f $args_file }
if ($wrapper_script | path exists) { rm -f $wrapper_script }
# Return result if successful, null otherwise
if $result.exit_code == 0 {
# Parse output: always try JSON first (handles strings, bools, records, lists)
# The wrapper script serializes all return values with | to json, so bare JSON
# strings like "91.98.28.202" must go through from json to strip the quotes.
let output = ($result.stdout | str trim)
if ($output | is-empty) {
null
} else {
let parsed = (do -i { $output | from json })
let value = if ($parsed | is-empty) { $output } else { $parsed }
log-debug $"($provider_name)::($function_name) → ($value)" "provider-loader"
$value
}
} else {
log-error $"Provider function call failed: ($result.stderr)" "provider-loader"
null
}
}
# Get required provider functions
def get-required-functions [] {
[
"get-provider-metadata"
"query_servers"
"server_exists"
"check_server_requirements"
]
}
# Validate provider interface compliance
def validate-provider-interface [provider_name: string, provider_instance: record] {
let required_functions = (get-required-functions)
mut missing_functions = []
mut valid = true
# Check if provider file exists
let provider_file = $provider_instance.entry_point
if not ($provider_file | path exists) {
return {
valid: false
missing_functions: ["provider file not found"]
provider: $provider_name
}
}
# Check each required function
for func in $required_functions {
let check_cmd = $"nu -c \"use ($provider_file) *; help commands | where name == '($func)' | length\""
let check_result = (nu -c $check_cmd | complete)
if $check_result.exit_code == 0 {
let func_count = ($check_result.stdout | str trim | into int)
if $func_count == 0 {
$missing_functions = ($missing_functions | append $func)
$valid = false
}
} else {
$missing_functions = ($missing_functions | append $func)
$valid = false
}
}
{
valid: $valid
missing_functions: $missing_functions
provider: $provider_name
checked_functions: ($required_functions | length)
validation_time: (date now)
}
}
# Load multiple providers
export def load-providers [provider_names: list<string>] {
mut results = {
successful: 0
failed: 0
total: ($provider_names | length)
details: []
}
for provider_name in $provider_names {
let result = (load-provider $provider_name)
if not ($result | is-empty) {
$results.successful = ($results.successful + 1)
$results.details = ($results.details | append {
provider: $provider_name
status: "success"
loaded: true
})
} else {
$results.failed = ($results.failed + 1)
$results.details = ($results.details | append {
provider: $provider_name
status: "failed"
loaded: false
})
}
}
$results
}
# Check provider health
export def check-provider-health [provider_name: string] {
let health_check = {
provider: $provider_name
available: false
loadable: false
interface_valid: false
metadata_accessible: false
timestamp: (date now)
}
# Check if provider is available
let available = (is-provider-available $provider_name)
let updated_health = ($health_check | merge { available: $available })
if not $available {
return $updated_health
}
# Try to load provider
let provider_instance = (load-provider $provider_name)
let loadable = not ($provider_instance | is-empty)
let updated_health = ($updated_health | merge { loadable: $loadable })
if not $loadable {
return $updated_health
}
# Check interface validation
let validation = (validate-provider-interface $provider_name $provider_instance)
let updated_health = ($updated_health | merge { interface_valid: $validation.valid })
# Check metadata access
let provider_entry = (get-provider-entry $provider_name)
let metadata_cmd = $"nu -c \"use ($provider_entry.entry_point) *; get-provider-metadata\""
let metadata_result = (nu -c $metadata_cmd | complete)
let metadata_accessible = ($metadata_result.exit_code == 0)
$updated_health | merge { metadata_accessible: $metadata_accessible }
}
# Check health of all providers
export def check-all-providers-health [] {
let providers = (list-providers --available-only)
$providers | each {|provider|
check-provider-health $provider.name
}
}
# Get loader statistics
export def get-loader-stats [] {
let provider_stats = (get-provider-stats)
let health_checks = (check-all-providers-health)
{
total_providers: $provider_stats.total_providers
available_providers: $provider_stats.available_providers
healthy_providers: ($health_checks | where interface_valid == true | length)
last_check: (date now)
}
}