# 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 { 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 { 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 { 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 = [] ] -> 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 { # TODO: Implement conflict detection # - Check for version conflicts # - Check for conflicting dependencies [] }