Private/func_CdfRegistry.ps1

# Registry provider abstraction for CDF package management
# Supports pluggable backends (ACR, GitHub Packages, etc.)

class CdfRegistryProvider {
    [string]$Type
    [string]$Endpoint

    CdfRegistryProvider([string]$Type, [string]$Endpoint) {
        $this.Type = $Type
        $this.Endpoint = $Endpoint
    }

    # List available tags/releases for a package in the registry
    [string[]] ListReleases([string]$PackagePath) {
        throw "ListReleases must be implemented by subclass"
    }

    # Pull a package from the registry to a local directory
    [void] Pull([string]$PackagePath, [string]$Tag, [string]$OutputDir) {
        throw "Pull must be implemented by subclass"
    }

    # Push a local directory as a package to the registry
    [void] Push([string]$PackagePath, [string]$Tag, [string]$SourceDir) {
        throw "Push must be implemented by subclass"
    }

    [void] Push([string]$PackagePath, [string]$Tag, [string]$SourceDir, [hashtable]$Annotations) {
        throw "Push must be implemented by subclass"
    }
}

class CdfAcrRegistryProvider : CdfRegistryProvider {

    CdfAcrRegistryProvider([string]$Endpoint) : base('acr', $Endpoint) {}

    [string] GetOrasRef([string]$PackagePath, [string]$Tag) {
        return "$($this.Endpoint)/${PackagePath}:${Tag}"
    }

    # Ensure oras CLI is available and user is logged in
    [void] EnsureOras() {
        if (-not (Get-Command 'oras' -ErrorAction SilentlyContinue)) {
            throw "The 'oras' CLI is required for ACR registry operations. Install from https://oras.land/docs/installation"
        }
    }

    # Login to ACR using Azure CLI token
    [void] Login() {
        $this.EnsureOras()
        $token = (Get-AzAccessToken -ResourceUrl "https://$($this.Endpoint)" -ErrorAction Stop).Token
        oras login $this.Endpoint --username '00000000-0000-0000-0000-000000000000' --password $token 2>&1 | Out-Null
        if ($LASTEXITCODE -ne 0) {
            throw "Failed to login to ACR '$($this.Endpoint)'"
        }
    }

    [string[]] ListReleases([string]$PackagePath) {
        $this.EnsureOras()
        $ref = "$($this.Endpoint)/${PackagePath}"
        $output = oras repo tags $ref 2>&1
        if ($LASTEXITCODE -ne 0) {
            Write-Warning "Failed to list tags for '$ref': $output"
            return @()
        }
        return ($output -split "`n" | Where-Object { $_ -match '^\d+\.\d+\.\d+' })
    }

    [void] Pull([string]$PackagePath, [string]$Tag, [string]$OutputDir) {
        $this.EnsureOras()
        $ref = $this.GetOrasRef($PackagePath, $Tag)
        if (!(Test-Path $OutputDir)) {
            New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null
        }
        Push-Location $OutputDir
        try {
            $output = oras pull $ref 2>&1
            if ($LASTEXITCODE -ne 0) {
                throw "Failed to pull '$ref': $output"
            }
        }
        finally {
            Pop-Location
        }
    }

    [void] Push([string]$PackagePath, [string]$Tag, [string]$SourceDir) {
        $this.Push($PackagePath, $Tag, $SourceDir, @{})
    }

    [void] Push([string]$PackagePath, [string]$Tag, [string]$SourceDir, [hashtable]$Annotations) {
        $this.EnsureOras()
        $ref = $this.GetOrasRef($PackagePath, $Tag)
        Push-Location $SourceDir
        try {
            $files = Get-ChildItem -Recurse -File | ForEach-Object {
                $_.FullName.Substring($SourceDir.Length + 1)
            }
            $annotationArgs = @()
            foreach ($key in $Annotations.Keys) {
                $annotationArgs += '--annotation'
                $annotationArgs += "${key}=$($Annotations[$key])"
            }
            $output = oras push $ref @files @annotationArgs 2>&1
            if ($LASTEXITCODE -ne 0) {
                throw "Failed to push '$ref': $output"
            }
        }
        finally {
            Pop-Location
        }
    }
}

class CdfOciRegistryProvider : CdfRegistryProvider {
    [string]$Username
    [string]$PasswordEnvVar

    CdfOciRegistryProvider([string]$Endpoint, [string]$Username, [string]$PasswordEnvVar) : base('oci', $Endpoint) {
        $this.Username = $Username
        $this.PasswordEnvVar = $PasswordEnvVar
    }

    [string] GetOrasRef([string]$PackagePath, [string]$Tag) {
        return "$($this.Endpoint)/${PackagePath}:${Tag}"
    }

    [void] EnsureOras() {
        if (-not (Get-Command 'oras' -ErrorAction SilentlyContinue)) {
            throw "The 'oras' CLI is required for OCI registry operations. Install from https://oras.land/docs/installation"
        }
    }

    # Login using username + password from environment variable
    [void] Login() {
        $this.EnsureOras()
        $password = [System.Environment]::GetEnvironmentVariable($this.PasswordEnvVar)
        if ([string]::IsNullOrEmpty($password)) {
            throw "Environment variable '$($this.PasswordEnvVar)' is not set. Set it to your registry token/password."
        }
        # oras login requires just the registry host, not the full path
        $registryHost = ($this.Endpoint -split '/')[0]
        $output = oras login $registryHost --username $this.Username --password $password 2>&1
        if ($LASTEXITCODE -ne 0) {
            throw "Failed to login to OCI registry '$registryHost': $output"
        }
    }

    [string[]] ListReleases([string]$PackagePath) {
        $this.EnsureOras()
        $ref = "$($this.Endpoint)/${PackagePath}"
        $output = oras repo tags $ref 2>&1
        if ($LASTEXITCODE -ne 0) {
            Write-Warning "Failed to list tags for '$ref': $output"
            return @()
        }
        return ($output -split "`n" | Where-Object { $_ -match '^\d+\.\d+\.\d+' })
    }

    [void] Pull([string]$PackagePath, [string]$Tag, [string]$OutputDir) {
        $this.EnsureOras()
        $ref = $this.GetOrasRef($PackagePath, $Tag)
        if (!(Test-Path $OutputDir)) {
            New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null
        }
        Push-Location $OutputDir
        try {
            $output = oras pull $ref 2>&1
            if ($LASTEXITCODE -ne 0) {
                throw "Failed to pull '$ref': $output"
            }
        }
        finally {
            Pop-Location
        }
    }

    [void] Push([string]$PackagePath, [string]$Tag, [string]$SourceDir) {
        $this.Push($PackagePath, $Tag, $SourceDir, @{})
    }

    [void] Push([string]$PackagePath, [string]$Tag, [string]$SourceDir, [hashtable]$Annotations) {
        $this.EnsureOras()
        $ref = $this.GetOrasRef($PackagePath, $Tag)
        Push-Location $SourceDir
        try {
            $files = Get-ChildItem -Recurse -File | ForEach-Object {
                $_.FullName.Substring($SourceDir.Length + 1)
            }
            $annotationArgs = @()
            foreach ($key in $Annotations.Keys) {
                $annotationArgs += '--annotation'
                $annotationArgs += "${key}=$($Annotations[$key])"
            }
            $output = oras push $ref @files @annotationArgs 2>&1
            if ($LASTEXITCODE -ne 0) {
                throw "Failed to push '$ref': $output"
            }
        }
        finally {
            Pop-Location
        }
    }
}

# Factory function to create a registry provider from a registry config entry
Function New-CdfRegistryProvider {
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory = $true)]
        [hashtable]$RegistryConfig
    )

    switch ($RegistryConfig.type) {
        'acr' {
            return [CdfAcrRegistryProvider]::new($RegistryConfig.endpoint)
        }
        'oci' {
            $username = $RegistryConfig.username ?? 'cdf'
            $passwordEnvVar = $RegistryConfig.passwordEnvVar ?? 'CDF_REGISTRY_TOKEN'
            return [CdfOciRegistryProvider]::new($RegistryConfig.endpoint, $username, $passwordEnvVar)
        }
        default {
            throw "Unsupported registry type: '$($RegistryConfig.type)'. Supported types: acr, oci"
        }
    }
}

# Resolve a named registry config using layered lookup:
# 1. <project>/.cdf/registries/<name>.json
# 2. $HOME/.cdf/registries/<name>.json
# 3. Inline registries from cdf-packages.json manifest (optional hashtable)
Function Resolve-CdfRegistryConfig {
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory = $false)]
        [string]$Name = 'default',
        [Parameter(Mandatory = $false)]
        [hashtable]$InlineRegistries,
        [Parameter(Mandatory = $false)]
        [string]$ProjectDir = (Get-Location).Path
    )

    # 1. Project-level
    $projectFile = Join-Path $ProjectDir ".cdf/registries/$Name.json"
    if (Test-Path $projectFile) {
        Write-Verbose "Resolved registry '$Name' from project: $projectFile"
        return Get-Content -Raw $projectFile | ConvertFrom-Json -AsHashtable
    }

    # 2. User-level
    $userFile = Join-Path $HOME ".cdf/registries/$Name.json"
    if (Test-Path $userFile) {
        Write-Verbose "Resolved registry '$Name' from user: $userFile"
        return Get-Content -Raw $userFile | ConvertFrom-Json -AsHashtable
    }

    # 3. Inline from manifest
    if ($InlineRegistries -and $InlineRegistries.ContainsKey($Name)) {
        Write-Verbose "Resolved registry '$Name' from inline manifest"
        return $InlineRegistries[$Name]
    }

    throw "Registry '$Name' not found. Create '$userFile' or define it in cdf-packages.json registries."
}