536 lines
14 KiB
Plaintext
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
|
|
[]
|
|
}
|