src/Invoke-CciGetBootstrap.ps1
|
function Invoke-CciGetBootstrap { <# .SYNOPSIS Prepares a freshly built machine in one command. .DESCRIPTION Connects to the tenant, installs the module that tenant nominates for a machine build, imports it, and runs whatever entry point that module declares - so a technician at OOBE types two lines total: Install-Module cciget -Scope CurrentUser Invoke-CciGetBootstrap Nothing here knows what a customization is. The module to install comes from the tenant registry (bootstrapModule) and the final command comes from the installed module's own manifest (PrivateData.CciGet.BootstrapCommand), so cciget stays tenant-generic and a different tenant can nominate something else entirely. The individual steps remain available and unchanged; this only removes the need to remember their order and their arguments. .PARAMETER Tenant Tenant to bootstrap from. Defaults to the configured default feed. .PARAMETER Scope Install scope for the bootstrap module. 'Auto' (the default) installs machine-wide and requires an elevated session - see the note below on why a per-user install cannot survive a machine build. Pass 'CurrentUser' explicitly to use cciget as an ordinary module installer. .PARAMETER Arguments Values for the declared entry point's parameters, e.g. @{ Team = 'platform' }. Anything not supplied is resolved interactively. .PARAMETER Source Force the module source. By default an existing connection is reused, and otherwise the source is resolved the same way Connect-CciGet resolves it. .PARAMETER SkipFinalStep Install and import, but do not run the module's declared entry point. .EXAMPLE Invoke-CciGetBootstrap .EXAMPLE Invoke-CciGetBootstrap -Arguments @{ Team = 'platform' } .EXAMPLE Invoke-CciGetBootstrap -SkipFinalStep #> [CmdletBinding()] param( [string]$Tenant, [ValidateSet('Auto','CurrentUser','AllUsers')] [string]$Scope = 'Auto', [hashtable]$Arguments = @{}, [ValidateSet('Auto','Feed','Store')] [string]$Source = 'Auto', [Alias('SkipPublish')] [switch]$SkipFinalStep ) # A machine build hands the machine between users: the technician runs this # as a temporary local account, the sequence reboots into an autologon as # the administrator account, and the second pass DELETES the technician # account and its profile. A CurrentUser install puts the modules inside # that profile, so the second pass either cannot find them (RequiredModules # resolves against the new user's PSModulePath) or deletes them from under # itself mid-run. Machine-wide is the only placement that survives, which is # why the legacy delivery used a machine-wide path too. $requestedScope = $Scope if ($Scope -eq 'Auto') { $Scope = 'AllUsers' } if ($Scope -eq 'AllUsers' -and -not (_Test-CciElevated)) { $reason = if ($requestedScope -eq 'Auto') { "Bootstrapping a machine installs modules machine-wide, because a build reboots into a different user and removes the account you are using now." } else { "-Scope AllUsers writes to the machine-wide module path." } throw "cciget: this session is not elevated. $reason Close this window, start Windows PowerShell with 'Run as administrator', and run Invoke-CciGetBootstrap again. To install for yourself instead - which is NOT enough for a machine build - pass -Scope CurrentUser explicitly." } if (-not $Tenant) { $Tenant = (Get-CciGetConfig).defaultFeed } $feed = (_Resolve-CciGetFeed -Tenant $Tenant)[0] $bootstrapProp = $feed.PSObject.Properties['bootstrapModule'] $bootstrapModule = if ($bootstrapProp) { $bootstrapProp.Value } if (-not $bootstrapModule) { throw "cciget: tenant '$($feed.name)' nominates no bootstrap module. Use Connect-CciGet and Install-CciModule directly." } Write-Host "cciget: preparing this machine from the '$($feed.name)' tenant." # Reuse an established connection rather than reconnecting: a second # Connect-CciGet re-resolves the source, so a session deliberately pointed # at the store would silently switch to the feed wherever an Azure CLI # session happens to exist. if ($PSBoundParameters.ContainsKey('Source')) { Connect-CciGet -Tenant $feed.name -Source $Source | Out-Null } elseif ($Script:CciGetSource) { Write-Host "cciget: using the existing '$Script:CciGetSource' connection." } else { Connect-CciGet -Tenant $feed.name | Out-Null } Write-Host "cciget: installing $bootstrapModule for $Scope." Install-CciModule -Name $bootstrapModule -Tenant $feed.name -Scope $Scope -NoGuidance $resolved = _Resolve-CciModuleName -Name $bootstrapModule -Feed $feed # -Global matters: without it the module loads into cciget's session state # and the caller's session sees none of its commands. Import-Module $resolved -Global -Force $module = Get-Module -Name $resolved | Select-Object -First 1 Write-Host "cciget: imported $resolved $($module.Version)." $command = $null $required = @() $data = $module.PrivateData if ($data -is [hashtable] -and $data.ContainsKey('CciGet')) { $cciget = $data['CciGet'] if ($cciget -is [hashtable]) { if ($cciget.ContainsKey('BootstrapCommand')) { $command = [string]$cciget['BootstrapCommand'] } if ($cciget.ContainsKey('BootstrapParameters')) { $required = @($cciget['BootstrapParameters']) } } } if (-not $command) { Write-Host "cciget: $resolved declares no entry point; nothing further to run." return } if ($SkipFinalStep) { Write-Host "cciget: skipping '$command' as requested. Run it yourself when ready." return } if (-not (Get-Command $command -ErrorAction SilentlyContinue)) { Write-Warning "cciget: $resolved declares '$command' but it is not available. Run it manually once the module is loaded." return } $splat = _Resolve-CciBootstrapArgument -Command $command -Required $required -Supplied $Arguments Write-Host '' Write-Host "cciget: running $command $(($splat.GetEnumerator() | ForEach-Object { "-$($_.Key) $($_.Value)" }) -join ' ')" Write-Host '' & $command @splat } |