src/Connect-CciGet.ps1
|
function Connect-CciGet { <# .SYNOPSIS Prepare access to CCI module sources for this session. .DESCRIPTION A tenant publishes the same modules to two places: * the Azure Artifacts FEED - canonical publish target, and the natural source for developers and CI. Reading it requires an Azure DevOps org entitlement and a paid Basic licence (Stakeholder has no Artifacts access). * the tenant DISTRIBUTION STORE - an Entra/Azure-RBAC gated mirror in the tenant's storage account. Machine builders already hold RBAC there for customization payloads and need no Azure DevOps identity at all. Connect-CciGet picks ONE source and signs in once: Auto (default): 1. Live Azure CLI session -> FEED, seeded from that session (no prompt). 2. Otherwise -> STORE (one device code). This is the machine-builder path: no Azure DevOps entitlement required, and no credential provider download. 3. Store unavailable/unconfigured -> FEED via an interactive device-code sign-in. Feed / Store: force that source. Earlier versions always tried the feed first and fell back to the store on failure, which made a machine builder complete TWO device codes - one for a resource they cannot use. Choosing up front avoids that. .PARAMETER Tenant Optional. Limit to the named tenant. .PARAMETER Source Auto (default), Feed, or Store. .PARAMETER SkipCredentialProvider Skip the Azure Artifacts credential-provider bootstrap. Implied when the resolved source is the store. #> [CmdletBinding()] param( [string]$Tenant, [ValidateSet('Auto','Feed','Store')] [string]$Source = 'Auto', [switch]$SkipCredentialProvider ) if (-not (Get-Module -ListAvailable -Name Microsoft.PowerShell.PSResourceGet)) { throw "cciget: Microsoft.PowerShell.PSResourceGet is required but not installed. Run: Install-Module Microsoft.PowerShell.PSResourceGet -Scope CurrentUser" } Import-Module Microsoft.PowerShell.PSResourceGet -ErrorAction Stop # Remote registry refresh (designed-in hook, unbound by default): when the # config carries a registryUrl, fetch a replacement registry and persist it. # The baked registry remains the fallback on any failure. $cfg = Get-CciGetConfig $registryUrlProp = $cfg.PSObject.Properties['registryUrl'] $registryUrl = if ($registryUrlProp) { $registryUrlProp.Value } if ($registryUrl) { try { $remote = Invoke-RestMethod -Uri $registryUrl -TimeoutSec 15 if ($remote.feeds -and $remote.defaultFeed) { $path = _Get-CciGetConfigPath $dir = Split-Path $path -Parent if (-not (Test-Path $dir)) { New-Item -ItemType Directory -Path $dir -Force | Out-Null } [System.IO.File]::WriteAllText($path, ($remote | ConvertTo-Json -Depth 6), (New-Object System.Text.UTF8Encoding $false)) Write-Host "cciget: tenant registry refreshed from $registryUrl." } else { Write-Warning "cciget: registry at $registryUrl is malformed (needs feeds + defaultFeed); using local config." } } catch { Write-Warning "cciget: registry refresh from $registryUrl failed ($_); using local config." } } $feeds = _Resolve-CciGetFeed -Tenant $Tenant if (-not $Tenant -and @($feeds).Count -eq 1) { Write-Host "cciget: single configured tenant '$($feeds[0].name)' auto-selected." } # --- decide the source BEFORE prompting for anything ------------------- $azdoToken = $null if ($Source -ne 'Store' -and (Get-Command az -ErrorAction SilentlyContinue)) { try { $azdoToken = az account get-access-token --resource '499b84ac-1321-427f-aa17-267ca6975798' --query accessToken -o tsv 2>$null } catch { } } $storeConfigured = @($feeds | Where-Object { $p = $_.PSObject.Properties['blobAccount']; $p -and $p.Value }).Count -gt 0 $useStore = switch ($Source) { 'Store' { $true } 'Feed' { $false } default { (-not $azdoToken) -and $storeConfigured } } # --- store path: one Entra sign-in, no Azure DevOps identity needed ----- if ($useStore) { $connected = $false foreach ($feed in $feeds) { $blobCtx = _Connect-CciBlobStore -Feed $feed if (-not $blobCtx) { continue } $index = _Get-CciBlobModuleIndex -Feed $feed -Context $blobCtx if (-not $index) { Write-Warning "cciget: the '$($feed.name)' distribution store has no module index yet." continue } $names = @($index.modules.PSObject.Properties.Name) Write-Host "cciget: using the '$($feed.name)' tenant distribution store ($($names.Count) modules available)." $connected = $true } if ($connected) { $Script:CciGetSource = 'store' $Script:CciGetModuleCache = $null _Write-CciGetNextStep return [pscustomobject]@{ Source = 'store'; Tenant = ($feeds | ForEach-Object { $_.name }) -join ',' } } if ($Source -eq 'Store') { throw "cciget: the tenant distribution store is not reachable and -Source Store was requested." } Write-Warning "cciget: distribution store unavailable; falling back to the Azure Artifacts feed." } # --- feed path --------------------------------------------------------- if (-not $SkipCredentialProvider) { $pluginRoot = Join-Path $env:USERPROFILE '.nuget\plugins\netcore' if (-not (Test-Path $pluginRoot) -or -not (Get-ChildItem $pluginRoot -Filter 'CredentialProvider.Microsoft.dll' -Recurse -ErrorAction SilentlyContinue)) { Write-Host "cciget: installing Azure Artifacts Credential Provider..." try { $script = (New-Object System.Net.WebClient).DownloadString('https://aka.ms/install-artifacts-credprovider.ps1') Invoke-Expression $script } catch { Write-Warning "cciget: credential provider install failed: $_. You may need to install manually from https://github.com/microsoft/artifacts-credprovider." } } } # Credential provider configuration for this session. These persist after # Connect-CciGet returns so Find-/Install-CciModule benefit too. # # FORCE_CANSHOWDIALOG_TO=false: PSResourceGet invokes the provider in plugin # mode with CanShowDialog=true, which attempts the WAM broker dialog and # fails on freshly built machines with 0x800703f0 (ERROR_NO_TOKEN) instead # of falling back. Forcing it off selects the device-code flow, which works. # DEVICEFLOWTIMEOUTSECONDS: the provider default is 90s - not enough time for # a human to open a browser and complete the sign-in, producing # "A task was canceled". Give it 10 minutes. # Both preferred (ARTIFACTS_*) and legacy (NUGET_*/VSS_*) names are set; # older provider builds only understand the legacy ones. $env:ARTIFACTS_CREDENTIALPROVIDER_FORCE_CANSHOWDIALOG_TO = 'false' $env:NUGET_CREDENTIALPROVIDER_FORCE_CANSHOWDIALOG_TO = 'false' $env:ARTIFACTS_CREDENTIALPROVIDER_DEVICEFLOWTIMEOUTSECONDS = '600' $env:NUGET_CREDENTIALPROVIDER_VSTS_DEVICEFLOWTIMEOUTSECONDS = '600' if (-not $azdoToken) { # No Azure CLI: PSResourceGet runs the provider in PLUGIN mode, which # cannot prompt (it fails with exit code 2). Drive it directly. $azdoToken = _Invoke-CciCredentialProvider -FeedUrl $feeds[0].url } if ($azdoToken) { $endpointJson = @{ endpointCredentials = @($feeds | ForEach-Object { @{ endpoint = $_.url; username = 'VssSessionToken'; password = $azdoToken } }) } | ConvertTo-Json -Compress -Depth 3 $env:VSS_NUGET_EXTERNAL_FEED_ENDPOINTS = $endpointJson $env:ARTIFACTS_CREDENTIALPROVIDER_EXTERNAL_FEED_ENDPOINTS = $endpointJson Write-Verbose "cciget: seeded feed credentials." } $authFailed = @() foreach ($feed in $feeds) { $repoName = _Get-CciGetRepositoryName -FeedName $feed.name if ($feed.tenantId) { $env:ARTIFACTS_CREDENTIALPROVIDER_MSAL_AUTHORITY = "https://login.microsoftonline.com/$($feed.tenantId)" $env:NUGET_CREDENTIALPROVIDER_MSAL_AUTHORITY = "https://login.microsoftonline.com/$($feed.tenantId)" } $existing = Get-PSResourceRepository -Name $repoName -ErrorAction SilentlyContinue if ($existing) { if ($existing.Uri -ne $feed.url) { Set-PSResourceRepository -Name $repoName -Uri $feed.url -Trusted Write-Verbose "cciget: updated $repoName URL." } } else { Register-PSResourceRepository -Name $repoName -Uri $feed.url -Trusted Write-Host "cciget: registered repository '$repoName' -> $($feed.url)" } Write-Host "cciget: authenticating to '$repoName'..." $probeOk = $false try { # Wildcard probe: hits the V2 search endpoint, which genuinely # requires auth (specific-name lookups can return empty 200s). $null = Find-PSResource -Name '*' -Repository $repoName -ErrorAction Stop $probeOk = $true } catch { $msg = $_.Exception.Message if ($msg -match 'No match was found|could not be found in repository') { $probeOk = $true # authenticated; feed is simply empty } elseif ($msg -match '\[Warning\].*CredentialProvider' -and $msg -notmatch '401|Unauthorized|forbidden') { Write-Verbose "cciget: credential provider warning (non-fatal): $msg" $probeOk = $true } else { Write-Warning "cciget: authentication failed for '$repoName'." Write-Warning " Error: $msg" $authFailed += $repoName } } if ($probeOk) { Write-Host "cciget: '$repoName' authenticated and ready." } } if ($authFailed.Count -gt 0) { Write-Warning "cciget: $($authFailed.Count) feed(s) failed authentication: $($authFailed -join ', ')" if ($storeConfigured -and $Source -eq 'Auto') { Write-Warning "cciget: retry with -Source Store to use the tenant distribution store instead (no Azure DevOps entitlement required)." } $Script:CciGetSource = $null } else { $Script:CciGetSource = 'feed' } Get-PSResourceRepository -Name (_Get-CciGetRepositoryName -FeedName '*') | Select-Object Name, Uri, Trusted } |