# Image state management — read/write role image state from ~/.config/provisioning/images/ use ../lib_provisioning/utils/nickel_processor.nu [ncl-eval-soft] export def image-state-path [provider: string, role: string]: nothing -> string { let dir = ($env.HOME | path join ".config" | path join "provisioning" | path join "images") $dir | path join $"($provider)-($role).ncl" } export def image-state-dir []: nothing -> string { $env.HOME | path join ".config" | path join "provisioning" | path join "images" } # Read state file. Returns a record with ImageRoleState fields. # If the file does not exist, returns a pending-state record. export def image-state-read [provider: string, role: string]: nothing -> record { let path = (image-state-path $provider $role) if not ($path | path exists) { return { provider: $provider, role: $role, snapshot_id: "SNAPSHOT_PENDING", built_at: null, last_used: null, os_base: "unknown", labels: {}, } } let result = (ncl-eval-soft $path [] (error make { msg: $"Failed to parse image state ($path)" })) $result } # Write state file as a Nickel record literal. export def image-state-write [provider: string, role: string, state: record]: nothing -> nothing { let dir = (image-state-dir) let path = (image-state-path $provider $role) if not ($dir | path exists) { ^mkdir -p $dir } let built_at_val = if ($state.built_at? | is-empty) { "null" } else { $"\"($state.built_at)\"" } let last_used_val = if ($state.last_used? | is-empty) { "null" } else { $"\"($state.last_used)\"" } let labels_str = ( $state.labels? | default {} | items {|k, v| $" ($k) = \"($v)\"," } | str join "\n" ) let content = $" \{ provider = \"($state.provider)\", role = \"($state.role)\", snapshot_id = \"($state.snapshot_id)\", built_at = ($built_at_val), last_used = ($last_used_val), os_base = \"($state.os_base | default "unknown")\", labels = \{ ($labels_str) \}, \} " | str trim $content | save --force $path } # List state files. Optionally filter by provider. export def image-state-list [--provider: string = ""]: nothing -> list { let dir = (image-state-dir) if not ($dir | path exists) { return [] } let files = (ls $dir | where name =~ '\.ncl$' | get name) let states = ($files | each {|f| ncl-eval-soft $f [] null } | where { $in != null }) if ($provider | is-empty) { $states } else { $states | where provider == $provider } } # Returns true if the snapshot exists and is within freshness_days of built_at. export def image-state-is-fresh [provider: string, role: string]: nothing -> bool { let state = (image-state-read $provider $role) if $state.snapshot_id == "SNAPSHOT_PENDING" { return false } if ($state.built_at | is-empty) { return false } let freshness_days = 30 let built = ($state.built_at | into datetime) let age_days = ((date now) - $built | into duration | $in / 1day) $age_days <= $freshness_days } # Update only the snapshot_id and built_at fields in an existing state file. export def image-state-set-snapshot [provider: string, role: string, snapshot_id: string]: nothing -> nothing { let existing = (image-state-read $provider $role) let updated = ($existing | merge { snapshot_id: $snapshot_id, built_at: ((date now) | format date "%Y-%m-%dT%H:%M:%SZ"), }) image-state-write $provider $role $updated } # Touch last_used timestamp for the given role state. export def image-state-touch-used [provider: string, role: string]: nothing -> nothing { let existing = (image-state-read $provider $role) let updated = ($existing | merge { last_used: ((date now) | format date "%Y-%m-%dT%H:%M:%SZ"), }) image-state-write $provider $role $updated }