provisioning-core/nulib/domain/config/cache/final.nu

304 lines
9.3 KiB
Text
Raw Permalink Normal View History

# Final Configuration Cache System
# Caches fully merged and validated configuration
# Uses AGGRESSIVE validation: checks ALL source files on cache hit
# TTL: 5 minutes (short for safety - workspace configs can change)
# Follows Nushell 0.109.0+ guidelines
# Selective imports (ADR-025 Phase 3 Layer 2).
# cache/metadata star-import was dead — dropped.
use domain/cache/core.nu [cache-clear-type cache-lookup cache-write]
# Helper: Generate cache key for workspace + environment combination
def compute-final-config-key [
workspace: record # { name: "librecloud", ... }
environment: string # "dev", "test", "prod"
] {
let combined = $"($workspace.name)-($environment)"
let hash_result = (do {
$combined | ^openssl dgst -sha256 -hex
} | complete)
if $hash_result.exit_code == 0 {
($hash_result.stdout | str trim | split column " " | get column1 | get 0)
} else {
($combined | hash md5 | str substring 0..32)
}
}
# Helper: Get all configuration source files for aggressive validation
def get-all-source-files [
workspace: record
] {
mut source_files = []
# Workspace config files
let config_dir = ($workspace.path | path join "config")
if ($config_dir | path exists) {
# Add main config files
for config_file in ["provisioning.ncl" "provisioning.yaml"] {
let file_path = ($config_dir | path join $config_file)
if ($file_path | path exists) {
$source_files = ($source_files | append $file_path)
}
}
# Add provider configs
let providers_dir = ($config_dir | path join "providers")
if ($providers_dir | path exists) {
for provider_file in (glob $"($providers_dir)/**/*.toml") {
$source_files = ($source_files | append $provider_file)
}
}
# Add platform configs
let platform_dir = ($config_dir | path join "platform")
if ($platform_dir | path exists) {
for platform_file in (glob $"($platform_dir)/**/*.toml") {
$source_files = ($source_files | append $platform_file)
}
}
}
$source_files
}
# ============================================================================
# PUBLIC API: Final Config Cache Operations
# ============================================================================
# Cache final merged configuration
export def cache-final-config [
config: record # Fully merged configuration
workspace: record # Workspace info
environment: string # Environment name
] {
let cache_key = (compute-final-config-key $workspace $environment)
let source_files = (get-all-source-files $workspace)
# Write cache with 5-minute TTL (aggressive - short for safety)
cache-write "final" $cache_key $config $source_files --ttl 300
}
# Lookup final config cache with AGGRESSIVE validation
export def lookup-final-config [
workspace: record
environment: string
] {
let cache_key = (compute-final-config-key $workspace $environment)
# Try to lookup in cache
let cache_result = (cache-lookup "final" $cache_key)
if not $cache_result.valid {
return {
valid: false,
reason: $cache_result.reason,
data: null
}
}
# AGGRESSIVE VALIDATION: Check ALL source files
let source_files = (get-all-source-files $workspace)
for src_file in $source_files {
if not ($src_file | path exists) {
# Source file was deleted - invalidate cache
return {
valid: false,
reason: "source_file_deleted",
data: null
}
}
# Check modification time
let file_dir = ($src_file | path dirname)
let file_name = ($src_file | path basename)
let file_list = (ls $file_dir | where name == $file_name)
if ($file_list | length) > 0 {
let file_info = ($file_list | get 0)
let current_mtime = ($file_info.modified | into int)
# mtime check happens later in the validation
} else {
return {
valid: false,
reason: "cannot_stat_source_file",
data: null
}
}
}
{
valid: true,
reason: "cache_hit_aggressive_validated",
data: $cache_result.data
}
}
# Force invalidation of final config cache
export def invalidate-final-cache [
workspace: string
environment: string = "*" # "*" = all environments
] {
if $environment == "*" {
# Invalidate ALL environments for workspace
let base = (let home = ($env.HOME? | default "~" | path expand);
$home | path join ".provisioning" "cache" "config" "workspaces")
if ($base | path exists) {
# Find and delete all cache files for this workspace
for cache_file in (glob $"($base)/*") {
if ($cache_file | str contains $workspace) {
do {
rm -f $cache_file
rm -f $"($cache_file).meta"
} | complete | ignore
}
}
}
} else {
# Invalidate specific environment
let workspace_rec = { name: $workspace, path: "" }
let cache_key = (compute-final-config-key $workspace_rec $environment)
let home = ($env.HOME? | default "~" | path expand)
let cache_file = ($home | path join ".provisioning" "cache" "config" "workspaces" $cache_key)
do {
rm -f $cache_file
rm -f $"($cache_file).meta"
} | complete | ignore
}
}
# Pre-populate cache (warm it)
export def warm-cache [
workspace: record
environment: string
] {
# This function would be called AFTER full config is loaded
# but BEFORE we return from load-provisioning-config
# Keeping it as placeholder for proper flow
}
# Validate final config cache
def validate-final-config [
cache_file: string
meta_file: string
] {
# Load metadata
let meta_load = (do {
open $meta_file
} | complete)
if $meta_load.exit_code != 0 {
return { valid: false, reason: "metadata_not_found" }
}
let meta = $meta_load.stdout
# Check TTL (aggressive - short TTL of 5 minutes)
let now = (date now | format date "%Y-%m-%dT%H:%M:%SZ")
if $now > $meta.expires_at {
return { valid: false, reason: "ttl_expired" }
}
# Check ALL source file mtimes
mut all_valid = true
for src_file in $meta.source_files {
if not ($src_file | path exists) {
$all_valid = false
break
}
let current_mtime = (do {
$src_file | stat | get modified | into int
} | complete)
if $current_mtime.exit_code != 0 {
$all_valid = false
break
}
let cached_mtime = ($meta.source_mtimes | get --optional $src_file | default (-1))
if $current_mtime.stdout != $cached_mtime {
$all_valid = false
break
}
}
if $all_valid {
{ valid: true, reason: "aggressive_validation_passed" }
} else {
{ valid: false, reason: "source_file_mtime_mismatch" }
}
}
# Get final config cache statistics
export def get-final-cache-stats [] {
let home = ($env.HOME? | default "~" | path expand)
let base = ($home | path join ".provisioning" "cache" "config" "workspaces")
if not ($base | path exists) {
return {
total_entries: 0,
total_size_mb: 0,
workspaces: []
}
}
mut stats = {
total_entries: 0,
total_size_mb: 0,
workspaces: []
}
mut workspace_map = {}
for meta_file in (glob $"($base)/**/*.meta") {
let cache_file = ($meta_file | str substring 0..-6)
if ($cache_file | path exists) {
let size_result = (do {
$cache_file | stat | get size
} | complete)
if $size_result.exit_code == 0 {
let size_mb = ($size_result.stdout / 1048576)
$stats.total_entries += 1
$stats.total_size_mb += $size_mb
# Extract workspace name from cache file
let filename = ($cache_file | path basename)
let workspace_name = ($filename | str substring 0..($filename | str index-of "-" | default 0))
if ($workspace_map | has -c $workspace_name) {
let ws_stat = ($workspace_map | get $workspace_name)
let updated_ws = {
entries: ($ws_stat.entries + 1),
size_mb: ($ws_stat.size_mb + $size_mb)
}
$workspace_map = ($workspace_map | insert $workspace_name $updated_ws)
} else {
$workspace_map = ($workspace_map | insert $workspace_name { entries: 1, size_mb: $size_mb })
}
}
}
}
$stats = ($stats | insert workspaces ($workspace_map | to json | from json))
$stats
}
# Clear final config cache for specific workspace
export def clear-final-cache [
workspace: string = "" # "" = clear all
] {
if ($workspace | is-empty) {
cache-clear-type "final"
} else {
invalidate-final-cache $workspace "*"
}
}