2025-10-07 10:32:04 +01:00

536 lines
14 KiB
Plaintext

# Multi-Repository Dependency Resolution System
# Handles dependency resolution across multiple repositories with OCI support
# Version: 1.0.0
use ../config/loader.nu get-config
use ../oci/client.nu *
use std log
# Dependency resolution cache
let $cache_dir = ($env.HOME | path join ".provisioning" "dep-cache")
# Initialize dependency cache
export def init-cache [] -> nothing {
mkdir $cache_dir
}
# Load repository configuration
export def load-repositories [] -> list<record> {
let config = (get-config)
# Check if dependencies configuration exists
if ($config.dependencies? | is-empty) {
return []
}
# Build repository list from configuration
let repos = []
# Core repository
if ($config.dependencies.core? | is-not-empty) {
$repos = ($repos | append {
name: "core"
type: "core"
source: ($config.dependencies.core.source)
priority: 1000
enabled: true
})
}
# Extensions repository
if ($config.dependencies.extensions? | is-not-empty) {
$repos = ($repos | append {
name: "extensions"
type: "extensions"
source_type: ($config.dependencies.extensions.source_type)
source: ($config.dependencies.extensions)
priority: 500
enabled: true
})
}
# Platform repository
if ($config.dependencies.platform? | is-not-empty) {
$repos = ($repos | append {
name: "platform"
type: "platform"
source_type: ($config.dependencies.platform.source_type)
source: ($config.dependencies.platform)
priority: 300
enabled: true
})
}
$repos
}
# Resolve dependency from repository
export def resolve-dependency [
dep_name: string # Dependency name (e.g., "kubernetes")
dep_type: string # Dependency type (provider, taskserv, cluster)
version?: string # Version constraint (e.g., "1.28.0", ">=1.25.0")
--source-type: string = "oci" # Source type override
] -> record {
let config = (get-config)
let repos = (load-repositories)
log info $"Resolving dependency: ($dep_type)/($dep_name):($version)"
# Find repository for this dependency type
let repo = ($repos | where type == "extensions" | first)
if ($repo | is-empty) {
error make {
msg: $"No repository found for ($dep_type)"
}
}
# Resolve based on source type
match ($repo.source_type? | default "oci") {
"oci" => { resolve-oci-dependency $dep_name $dep_type $version $repo }
"gitea" => { resolve-gitea-dependency $dep_name $dep_type $version $repo }
"local" => { resolve-local-dependency $dep_name $dep_type $version $repo }
_ => {
error make {
msg: $"Unsupported source type: ($repo.source_type)"
}
}
}
}
# Resolve OCI-based dependency
def resolve-oci-dependency [
dep_name: string
dep_type: string
version?: string
repo: record
] -> record {
let oci_config = ($repo.source.oci)
let registry = $oci_config.registry
let namespace = $oci_config.namespace
# Get available versions
let auth_token = if ($oci_config.auth_token_path? | is-not-empty) {
open ($oci_config.auth_token_path | path expand)
} else {
""
}
let insecure = ($oci_config.tls_enabled == false)
let available_versions = (get-artifact-tags $registry $namespace $dep_name
--auth-token $auth_token
--insecure=$insecure)
if ($available_versions | is-empty) {
error make {
msg: $"Dependency not found: ($dep_name)"
}
}
# Select version based on constraint
let selected_version = if ($version | is-not-empty) {
# For now, exact match. TODO: Implement semver constraint matching
if ($version in $available_versions) {
$version
} else {
error make {
msg: $"Version ($version) not found for ($dep_name)"
}
}
} else {
# Select latest version (assumes sorted)
$available_versions | first
}
{
name: $dep_name
type: $dep_type
version: $selected_version
source: "oci"
registry: $registry
namespace: $namespace
reference: $"($registry)/($namespace)/($dep_name):($selected_version)"
available_versions: $available_versions
}
}
# Resolve Gitea-based dependency
def resolve-gitea-dependency [
dep_name: string
dep_type: string
version?: string
repo: record
] -> record {
let gitea_config = ($repo.source.gitea)
# For Gitea, we'll use git tags as versions
# This requires cloning or using Gitea API
{
name: $dep_name
type: $dep_type
version: ($version | default "main")
source: "gitea"
url: $"($gitea_config.url)/($gitea_config.organization)/($dep_name)"
branch: ($gitea_config.branch? | default "main")
}
}
# Resolve local-based dependency
def resolve-local-dependency [
dep_name: string
dep_type: string
version?: string
repo: record
] -> record {
let local_config = ($repo.source.local)
let dep_path = ($local_config.path | path join $dep_type | path join $dep_name)
if not ($dep_path | path exists) {
error make {
msg: $"Local dependency not found: ($dep_path)"
}
}
{
name: $dep_name
type: $dep_type
version: ($version | default "local")
source: "local"
path: $dep_path
}
}
# Install resolved dependency
export def install-dependency [
dep: record # Resolved dependency
--destination: string # Override destination path
] -> string {
let config = (get-config)
# Determine installation path
let install_path = if ($destination | is-not-empty) {
$destination
} else {
match $dep.type {
"taskserv" => ($config.paths.extensions | path join "taskservs" $dep.name)
"provider" => ($config.paths.extensions | path join "providers" $dep.name)
"cluster" => ($config.paths.extensions | path join "clusters" $dep.name)
_ => {
error make {
msg: $"Unknown dependency type: ($dep.type)"
}
}
}
}
log info $"Installing ($dep.name):($dep.version) to ($install_path)"
# Install based on source
match $dep.source {
"oci" => { install-oci-dependency $dep $install_path }
"gitea" => { install-gitea-dependency $dep $install_path }
"local" => { install-local-dependency $dep $install_path }
_ => {
error make {
msg: $"Unsupported source: ($dep.source)"
}
}
}
}
# Install OCI-based dependency
def install-oci-dependency [
dep: record
install_path: string
] -> string {
let config = (get-config)
# Get OCI configuration
let repos = (load-repositories)
let repo = ($repos | where type == "extensions" | first)
let oci_config = ($repo.source.oci)
let auth_token = if ($oci_config.auth_token_path? | is-not-empty) {
open ($oci_config.auth_token_path | path expand)
} else {
""
}
let insecure = ($oci_config.tls_enabled == false)
# Pull artifact
let result = (pull-artifact $dep.registry $dep.namespace $dep.name $dep.version $install_path
--auth-token $auth_token
--insecure=$insecure)
if not $result {
error make {
msg: $"Failed to install ($dep.name):($dep.version)"
}
}
$install_path
}
# Install Gitea-based dependency
def install-gitea-dependency [
dep: record
install_path: string
] -> string {
# Clone repository
log info $"Cloning from ($dep.url)"
try {
git clone --branch $dep.branch --depth 1 $dep.url $install_path
$install_path
} catch {
error make {
msg: $"Failed to clone ($dep.url)"
}
}
}
# Install local-based dependency
def install-local-dependency [
dep: record
install_path: string
] -> string {
# Copy local dependency
log info $"Copying from ($dep.path)"
try {
cp -r $dep.path $install_path
$install_path
} catch {
error make {
msg: $"Failed to copy ($dep.path)"
}
}
}
# Resolve and install all dependencies for an extension
export def resolve-extension-deps [
extension_name: string
extension_type: string
--recursive # Recursively resolve dependencies
] -> list<record> {
log info $"Resolving dependencies for ($extension_type)/($extension_name)"
# Load extension manifest
let manifest = (load-extension-manifest $extension_name $extension_type)
if ($manifest | is-empty) {
log warning $"No manifest found for ($extension_name)"
return []
}
if ($manifest.dependencies? | is-empty) {
log info $"No dependencies for ($extension_name)"
return []
}
# Resolve each dependency
let resolved = ($manifest.dependencies | items | each { |dep|
let dep_name = ($dep | get 0)
let dep_version = ($dep | get 1)
log info $" Resolving ($dep_name):($dep_version)"
try {
let resolved_dep = (resolve-dependency $dep_name "taskserv" $dep_version)
# Install dependency
let install_path = (install-dependency $resolved_dep)
# Recursive resolution if enabled
if $recursive {
let sub_deps = (resolve-extension-deps $dep_name "taskserv" --recursive)
[$resolved_dep] | append $sub_deps
} else {
[$resolved_dep]
}
} catch { |err|
log error $" Failed to resolve ($dep_name): ($err.msg)"
[]
}
} | flatten)
$resolved
}
# Load extension manifest
def load-extension-manifest [
extension_name: string
extension_type: string
] -> record {
let config = (get-config)
# Try to find manifest.yaml in extension directory
let ext_path = match $extension_type {
"taskserv" => ($config.paths.extensions | path join "taskservs" $extension_name)
"provider" => ($config.paths.extensions | path join "providers" $extension_name)
"cluster" => ($config.paths.extensions | path join "clusters" $extension_name)
_ => ""
}
if ($ext_path | is-empty) or not ($ext_path | path exists) {
return {}
}
let manifest_path = ($ext_path | path join "manifest.yaml")
if not ($manifest_path | path exists) {
return {}
}
open $manifest_path | from yaml
}
# Check for dependency updates
export def check-dependency-updates [
extension_name: string
extension_type: string
] -> list<record> {
log info $"Checking updates for ($extension_type)/($extension_name)"
# Load current manifest
let manifest = (load-extension-manifest $extension_name $extension_type)
if ($manifest | is-empty) or ($manifest.dependencies? | is-empty) {
return []
}
# Check each dependency for updates
let updates = ($manifest.dependencies | items | each { |dep|
let dep_name = ($dep | get 0)
let current_version = ($dep | get 1)
try {
let resolved = (resolve-dependency $dep_name "taskserv")
let latest_version = $resolved.version
if $current_version != $latest_version {
{
name: $dep_name
current: $current_version
latest: $latest_version
update_available: true
}
} else {
{
name: $dep_name
current: $current_version
latest: $latest_version
update_available: false
}
}
} catch {
{
name: $dep_name
current: $current_version
latest: "unknown"
update_available: false
}
}
})
$updates
}
# Validate dependency graph (detect cycles, conflicts)
export def validate-dependency-graph [
extension_name: string
extension_type: string
] -> record {
log info $"Validating dependency graph for ($extension_type)/($extension_name)"
# Build dependency graph
let graph = (build-dependency-graph $extension_name $extension_type)
# Check for cycles
let has_cycles = (detect-cycles $graph)
# Check for conflicts
let conflicts = (detect-conflicts $graph)
{
valid: (not $has_cycles and ($conflicts | is-empty))
has_cycles: $has_cycles
conflicts: $conflicts
graph: $graph
}
}
# Build dependency graph recursively
def build-dependency-graph [
extension_name: string
extension_type: string
--visited: list<string> = []
] -> record {
# Prevent infinite recursion
if ($extension_name in $visited) {
return {
name: $extension_name
type: $extension_type
dependencies: []
circular: true
}
}
let new_visited = ($visited | append $extension_name)
# Load manifest
let manifest = (load-extension-manifest $extension_name $extension_type)
if ($manifest | is-empty) or ($manifest.dependencies? | is-empty) {
return {
name: $extension_name
type: $extension_type
dependencies: []
circular: false
}
}
# Build graph for each dependency
let deps = ($manifest.dependencies | items | each { |dep|
let dep_name = ($dep | get 0)
let dep_version = ($dep | get 1)
build-dependency-graph $dep_name "taskserv" --visited $new_visited
})
{
name: $extension_name
type: $extension_type
dependencies: $deps
circular: false
}
}
# Detect cycles in dependency graph
def detect-cycles [
graph: record
] -> bool {
if $graph.circular {
return true
}
if ($graph.dependencies | is-empty) {
return false
}
($graph.dependencies | any { |dep| detect-cycles $dep })
}
# Detect conflicts in dependency graph
def detect-conflicts [
graph: record
] -> list<record> {
# TODO: Implement conflict detection
# - Check for version conflicts
# - Check for conflicting dependencies
[]
}