provisioning-core/nulib/domain/version/loader.nu

432 lines
15 KiB
Text
Raw Permalink Normal View History

#!/usr/bin/env nu
# Dynamic configuration loader for version management
# Discovers and loads version configurations from the filesystem
# version/core star-import was dead — dropped (ADR-025 Phase 3 Layer 2).
use tools/nickel/process.nu [ncl-eval ncl-eval-soft]
# Discover version configurations
export def discover-configurations [
--base-path: string = ""
--types: list = [] # Filter by types
] {
let base = if ($base_path | is-empty) {
($env.PROVISIONING? | default $env.PWD)
} else { $base_path }
mut configurations = []
# Load from known version files directly - try Nickel first, then YAML
let version_files_nickel = [
($base | path join "core" | path join "versions.ncl")
]
for file in $version_files_nickel {
if ($file | path exists) {
let configs = (load-nickel-version-file $file)
if ($configs | is-not-empty) {
$configurations = ($configurations | append $configs)
}
}
}
# Fallback to YAML for backward compatibility
let version_files_yaml = [
($base | path join "versions.yaml")
($base | path join "core" | path join "versions.yaml")
]
for file in $version_files_yaml {
if ($file | path exists) {
let configs = (load-configuration-file $file)
if ($configs | is-not-empty) {
$configurations = ($configurations | append $configs)
}
}
}
# Also check providers directory
let providers_path = ($base | path join "catalog" | path join "providers")
let providers_path_fallback = ($base | path join "providers")
# Try catalog/providers first, then fall back to providers
let active_providers_path = if ($providers_path | path exists) {
$providers_path
} else if ($providers_path_fallback | path exists) {
$providers_path_fallback
} else {
""
}
if ($active_providers_path | is-not-empty) {
for provider_item in (ls $active_providers_path) {
let provider_dir = ($active_providers_path | path join $provider_item.name)
# Try Nickel version file first (single source of truth)
let nickel_version_file = ($provider_dir | path join "nickel" | path join "version.ncl")
if ($nickel_version_file | path exists) {
let configs = (load-nickel-version-file $nickel_version_file)
if ($configs | is-not-empty) {
$configurations = ($configurations | append $configs)
}
}
# Fallback to YAML for backward compatibility
let version_file = ($provider_dir | path join "versions.yaml")
if ($version_file | path exists) {
let configs = (load-configuration-file $version_file)
if ($configs | is-not-empty) {
$configurations = ($configurations | append $configs)
}
}
}
}
# Filter by types if specified
if ($types | length) > 0 {
$configurations | where type in $types
} else {
$configurations
}
}
# Load configuration from file
export def load-configuration-file [
file_path: string
] {
if not ($file_path | path exists) { return [] }
let ext = ($file_path | path parse | get extension)
let parent_dir = ($file_path | path dirname)
let context = (extract-context $parent_dir)
mut configs = []
match $ext {
"yaml" | "yml" => {
let data = (open $file_path)
if ($data | describe | str contains "record") {
# Convert record entries to configurations
# Only process items that look like tool/component configs (have version, source, tags, detector, or check_cmd)
for item in ($data | transpose key value) {
let value_desc = ($item.value | describe)
# Check if this is a valid configuration (record with tool-like fields)
# Must have version AND at least one of: source, tags, detector, check_cmd
let is_config = if ($value_desc | str contains "record") {
let has_version = (($item.value | get version? | default null) != null)
let has_source_or_tags = (
(($item.value | get source? | default null) != null) or
(($item.value | get tags? | default null) != null) or
(($item.value | get detector? | default null) != null) or
(($item.value | get check_cmd? | default null) != null)
)
$has_version and $has_source_or_tags
} else {
false
}
# Only append if it's a valid configuration, skip metadata-only entries
if $is_config {
let config = (create-configuration $item.key $item.value $context $file_path)
if ($config | is-not-empty) {
$configs = ($configs | append $config)
}
}
}
} else if ($data | describe | str contains "list") {
# Already a list of configurations
$configs = $data
}
}
"k" => {
# Parse Nickel files for version information
let content = (open $file_path)
let version_data = (extract-nickel-versions $content)
for item in $version_data {
let config = (create-configuration $item.name $item $context $file_path)
$configs = ($configs | append $config)
}
}
"toml" => {
let data = (open $file_path)
for section in ($data | transpose key value) {
if (($section.value | get version? | default null) != null) {
let config = (create-configuration $section.key $section.value $context $file_path)
$configs = ($configs | append $config)
}
}
}
"json" => {
let data = (open $file_path)
if (($data | get components? | default null) != null) {
$configs = $data.components
} else {
# Treat as single configuration
$configs = [$data]
}
}
_ => []
}
$configs
}
# Load Nickel version file by compiling it to JSON
export def load-nickel-version-file [
file_path: string
] {
if not ($file_path | path exists) { return [] }
# Determine parent context - could be provider or core
# provider: catalog/providers/{name}/nickel/version.ncl -> catalog/providers/{name}
# core: core/versions.ncl -> core (no nickel dir)
let parent_dir = if ($file_path | str contains "/nickel/version.ncl") {
$file_path | path dirname | path dirname # nickel/version.ncl -> provider_dir
} else {
$file_path | path dirname # versions.ncl -> core
}
let context = (extract-context $parent_dir)
mut configs = []
# Compile Nickel to JSON (null on failure — return empty configs)
let json_data = (ncl-eval-soft $file_path [] null)
if ($json_data | is-empty) { return $configs }
# Handle different Nickel output formats:
# 1. Provider files: Single object with {name, version, dependencies}
# 2. Core files: Object {core_versions: [{}]} or plain array [{}]
let is_array = ($json_data | describe | str contains "^list")
let items = (
if ($json_data | get core_versions? | default null) != null {
# Wrapped in core_versions key (core files)
$json_data.core_versions
} else if $is_array {
# It's an array (plain array format)
$json_data
} else if ($json_data | get name? | default null) != null {
# It's a single object (provider nickel/version.ncl)
[$json_data]
} else {
[]
}
)
# Process each tool in items
for item in $items {
let tool_name = ($item | get name?)
if ($tool_name | is-empty) { continue }
let version_obj = ($item | get version? | default {})
let current_version = ($version_obj | get current? | default "")
let source = ($version_obj | get source? | default "")
let tags = ($version_obj | get tags? | default "")
let site = ($version_obj | get site? | default "")
let detector_obj = ($item | get detector? | default {})
# Transform to our configuration format
let source_config = (if ($source | is-not-empty) {
if ($source | str contains "github") {
let repo_parse = ($source | parse -r 'github\.com/(?<repo>.+?)(/releases)?$')
let repo = (if ($repo_parse | is-empty) { "" } else { $repo_parse | get 0.repo | default "" })
{ type: "github", repo: $repo }
} else {
{ type: "url", url: $source }
}
} else if ($tags | is-not-empty) {
if ($tags | str contains "github") {
let repo_parse = ($tags | parse -r 'github\.com/(?<repo>.+?)(/tags)?$')
let repo = (if ($repo_parse | is-empty) { "" } else { $repo_parse | get 0.repo | default "" })
{ type: "github", repo: $repo }
} else {
{ type: "url", url: $tags }
}
} else {
{}
})
let config = {
id: $tool_name
type: ($context.type? | default "unknown")
category: ($context.category | default "")
version: $current_version
fixed: false
source: $source_config
detector: $detector_obj
comparison: "semantic"
metadata: {
source_file: $file_path
site: $site
description: ""
install_cmd: ""
lib: ""
}
}
if ($config | is-not-empty) {
$configs = ($configs | append $config)
}
}
$configs
}
# Extract context from path
export def extract-context [
dir_path: string
] {
let parts = ($dir_path | split row "/")
# Determine type based on path structure
let type = if ($parts | any { |p| $p == "providers" }) {
"provider"
} else if ($parts | any { |p| $p == "taskservs" }) {
"taskserv"
} else if ($parts | any { |p| $p == "clusters" }) {
"cluster"
} else if ($parts | any { |p| $p == "tools" }) {
"tool"
} else {
"generic"
}
# Extract category/subcategory
let category = if $type == "provider" {
$parts | skip while { |p| $p != "providers" } | skip 1 | first
} else if $type == "taskserv" {
$parts | skip while { |p| $p != "taskservs" } | skip 1 | first
} else {
""
}
{
type: $type
category: $category
path: $dir_path
}
}
# Create configuration object
export def create-configuration [
id: string
data: record
context: record
source_file: string
] {
# Build detector configuration
let detector = if (($data | get check_cmd? | default null) != null) {
{
method: "command"
command: $data.check_cmd
pattern: ($data | get parse_pattern? | default "")
capture: ($data | get capture_group? | default "version")
}
} else if (($data | get detector? | default null) != null) {
$data.detector
} else {
{}
}
# Build source configuration
let source = if (($data | get source? | default null) != null) {
if ($data.source | str contains "github.com") {
let parsed = ($data.source | parse -r 'github\.com/(?<repo>.+)')
let repo_str = if ($parsed | is-empty) { "" } else { $parsed | get 0 | get repo? | default "" }
let repo_final = ($repo_str | str replace -r '/(releases|tags).*$' '')
{
type: "github"
repo: $repo_final
}
} else if ($data.source | str starts-with "docker") {
{
type: "docker"
image: ($data.source | str replace "docker://" "")
}
} else if ($data.source | str starts-with "http") {
{
type: "url"
url: $data.source
field: ($data | get version_field? | default "")
}
} else {
{ type: "custom", config: $data.source }
}
} else if (($data | get tags? | default null) != null) {
# Infer from tags URL
if ($data.tags | str contains "github") {
let parsed = ($data.tags | parse -r 'github\.com/(?<repo>[^/]+/[^/]+)')
let repo_str = if ($parsed | is-empty) { "" } else { $parsed | get 0 | get repo? | default "" }
{
type: "github"
repo: $repo_str
}
} else {
{ type: "url", url: $data.tags }
}
} else {
{}
}
# Build complete configuration
{
id: $id
type: $context.type
category: ($context.category | default "")
version: ($data | get version? | default "")
fixed: ($data | get fixed? | default false)
source: $source
detector: $detector
comparison: ($data | get comparison? | default "semantic")
metadata: {
source_file: $source_file
site: ($data | get site? | default "")
description: ($data | get description? | default "")
install_cmd: ($data | get install_cmd? | default "")
lib: ($data | get lib? | default "")
}
}
}
# Extract version info from Nickel content
export def extract-nickel-versions [
content: string
] {
mut versions = []
# Look for schema definitions with version fields
let lines = ($content | lines)
mut current_schema = ""
mut current_data = {}
for line in $lines {
if ($line | str contains "schema ") {
# New schema found
if ($current_schema | is-not-empty) and (($current_data | get version? | default null) != null) {
$versions = ($versions | append {
name: $current_schema
...$current_data
})
}
let parsed_schema = ($line | parse -r 'schema\s+(\w+)')
$current_schema = if ($parsed_schema | is-empty) { "" } else { ($parsed_schema | get 0 | get capture0? | default "") }
$current_data = {}
} else if ($line | str contains "version:") or ($line | str contains "version =") {
# Extract version
let parsed_version = ($line | parse -r 'version[:\s=]+"?([^"]+)"?')
let version = if ($parsed_version | is-empty) { "" } else { ($parsed_version | get 0 | get capture0? | default "") }
if ($version | is-not-empty) {
$current_data.version = $version
}
}
}
# Add last schema if valid
if ($current_schema | is-not-empty) and (($current_data | get version? | default null) != null) {
$versions = ($versions | append {
name: $current_schema
...$current_data
})
}
$versions
}