diff --git a/powershell-runtime/source/bootstrap b/powershell-runtime/source/bootstrap index 81bcad7..15934cd 100755 --- a/powershell-runtime/source/bootstrap +++ b/powershell-runtime/source/bootstrap @@ -18,6 +18,22 @@ Import-Module '/opt/modules/pwsh-runtime.psd1' if ($env:POWERSHELL_RUNTIME_VERBOSE -eq 'TRUE') { Write-Host '[RUNTIME-bootstrap]Importing .NET class from .cs file to support script properties and method' } Add-Type -TypeDefinition ([System.IO.File]::ReadAllText('/opt/PowerShellLambdaContext.cs')) +# Unpack compressed modules, if present + +# Combined +If (Test-RuntimePackedModule -Combined) { + if ($env:POWERSHELL_RUNTIME_VERBOSE -eq 'TRUE') {Write-Host '[RUNTIME-bootstrap]Unpacking combined module archives'} + Import-ModuleArchive + if ($env:POWERSHELL_RUNTIME_VERBOSE -eq 'TRUE') {Write-Host '[RUNTIME-bootstrap]Finished unpacking archives'} + +} +# NuPkg +If (Test-RuntimePackedModule -NuPkg) { + if ($env:POWERSHELL_RUNTIME_VERBOSE -eq 'TRUE') {Write-Host '[RUNTIME-bootstrap]Unpacking module NuGet packages'} + Import-ModulePackage + if ($env:POWERSHELL_RUNTIME_VERBOSE -eq 'TRUE') {Write-Host '[RUNTIME-bootstrap]Finished unpacking NuGet packages'} +} + # Modify $env:PSModulePath to support Lambda paths if ($env:POWERSHELL_RUNTIME_VERBOSE -eq 'TRUE') {Write-Host '[RUNTIME-bootstrap]Modify PSModulePath to support Lambda paths'} Set-PSModulePath diff --git a/powershell-runtime/source/modules/Private/Import-ModuleArchive.ps1 b/powershell-runtime/source/modules/Private/Import-ModuleArchive.ps1 new file mode 100644 index 0000000..ed4347d --- /dev/null +++ b/powershell-runtime/source/modules/Private/Import-ModuleArchive.ps1 @@ -0,0 +1,40 @@ +function private:Import-ModuleArchive { + <# + .SYNOPSIS + Unpacks compressed PowerShell modules from .zip archives (modules.zip) + .DESCRIPTION + Unpacks compressed PowerShell modules from .zip archives (modules.zip) into a subdirectory of /tmp. + + This folder is later added to $env:PSModulePath, before user code runs, if module archives existed. + .NOTES + The contents of this archive should match the format of a folder in $Env:PSModulePath. More specifically: + * Module names should be top-level directories. + * One or more versions of the same module may be hosted in their own subdirectories, with respective version numbers. + * The module root (.psd1/.psm1 files, etc.) is contained within either the module-named or module-versioned directory. + + Module packages are imported from two locations, from lowest to highest precedence: + * /opt/ (Combined Lambda layer directory) + * $Env:LAMBDA_TASK_ROOT (Lambda Function Package deployment directory) + + If archives are detected at both locations, they will be extracted over the top of each-other. + #> + + $SearchPaths = @( + "/opt/modules.zip" + $(Join-Path $env:LAMBDA_TASK_ROOT -ChildPath "modules.zip") + ) + + If ($SearchPaths | ? { Test-Path $_ }) { + $UnpackDirectory = '/tmp/powershell-custom-runtime-unpacked-modules/combined/' + if ($env:POWERSHELL_RUNTIME_VERBOSE -eq 'TRUE') { Write-Host '[RUNTIME-bootstrap]Creating unpack directory for combined module archives' } + New-Item -ItemType Directory -Path $UnpackDirectory -Force + $SearchPaths | ? { Test-Path $_ } | ForEach-Object { + if ($env:POWERSHELL_RUNTIME_VERBOSE -eq 'TRUE') { Write-Host "[RUNTIME-bootstrap]Unpacking $_ to $UnpackDirectory" } + Expand-Archive -LiteralPath $_ -DestinationPath $UnpackDirectory -Force + } + if ($env:POWERSHELL_RUNTIME_VERBOSE -eq 'TRUE') { Write-Host '[RUNTIME-bootstrap]Archive unpack complete' } + } + else { + if ($env:POWERSHELL_RUNTIME_VERBOSE -eq 'TRUE') { Write-Host '[RUNTIME-bootstrap]No module archives detected; nothing to do.' } + } +} \ No newline at end of file diff --git a/powershell-runtime/source/modules/Private/Import-ModulePackage.ps1 b/powershell-runtime/source/modules/Private/Import-ModulePackage.ps1 new file mode 100644 index 0000000..3aa26ef --- /dev/null +++ b/powershell-runtime/source/modules/Private/Import-ModulePackage.ps1 @@ -0,0 +1,49 @@ +function private:Import-ModulePackage { + <# + .SYNOPSIS + Installs compressed PowerShell modules from NuGet packages (*.nupkg) + .DESCRIPTION + Installs compressed PowerShell modules from NuGet packages (*.nupkg) into a subdirectory of /tmp. + + This folder is later added to $env:PSModulePath, before user code runs, if module packages existed. + .NOTES + These packages should match the NuPkg format used by PSResourceGet or PowerShellGet. + + Packages can be exported either by: + * Downloading the .nupkg files directly from an upstream source (e.g. PowerShell Gallery) + * Using the -AsNuPkg parameter on Save-PSResource in the Microsoft.PowerShell.PSResourceGet module. + + Module packages are imported from two locations, from lowest to highest precedence: + * /opt/module-nupkgs/ (Combined Lambda layer directory) + * $Env:LAMBDA_TASK_ROOT/module-nupkgs/ (Lambda Function Package deployment directory) + #> + $SearchPaths = @{ + Layer = "/opt/module-nupkgs/*.nupkg" + Root = (Join-Path $env:LAMBDA_TASK_ROOT -ChildPath "module-nupkgs" -AdditionalChildPath "*.nupkg") + } + + If ($SearchPaths.Values | ? { Test-Path $_ }) { + $UnpackDirectory = '/tmp/powershell-custom-runtime-unpacked-modules/nupkgs/' + if ($env:POWERSHELL_RUNTIME_VERBOSE -eq 'TRUE') { Write-Host '[RUNTIME-bootstrap]Creating unpack directory for individual module packages' } + New-Item -ItemType Directory -Path $UnpackDirectory -Force + $SearchPaths.GetEnumerator() | ? { Test-Path $_.Value } | ForEach-Object { + $PackageDirectory = Split-Path $_.Value -Parent + if ($env:POWERSHELL_RUNTIME_VERBOSE -eq 'TRUE') { Write-Host "[RUNTIME-bootstrap]Importing module packages from $PackageDirectory" } + $RepositoryName = "Lambda-Local-$($_.Key)" + if ($env:POWERSHELL_RUNTIME_VERBOSE -eq 'TRUE') { Write-Host "[RUNTIME-bootstrap]Registering local package repository $RepositoryName" } + Register-PSResourceRepository -Name $RepositoryName -Uri $PackageDirectory -Trusted -Priority 1 + if ($env:POWERSHELL_RUNTIME_VERBOSE -eq 'TRUE') { Write-Host "[RUNTIME-bootstrap]Enumerating packages in $PackageDirectory (PSResource repository $RepositoryName)" } + Find-PSResource -Name * -Repository $RepositoryName | ForEach-Object -Parallel { + if ($env:POWERSHELL_RUNTIME_VERBOSE -eq 'TRUE') { Write-Host "[RUNTIME-bootstrap]Saving package $($_.Name) version $($_.Version) (PSResource repository $($using:RepositoryName))" } + $_ | Save-PSResource -SkipDependencyCheck -Path $using:PackageDirectory -Quiet -AcceptLicense -Confirm:$false + } + if ($env:POWERSHELL_RUNTIME_VERBOSE -eq 'TRUE') { Write-Host "[RUNTIME-bootstrap]Registering local package repository $RepositoryName" } + Unregister-PSResourceRepository -Name $RepositoryName -Confirm:$false + } + if ($env:POWERSHELL_RUNTIME_VERBOSE -eq 'TRUE') { Write-Host '[RUNTIME-bootstrap]Archive unpack complete' } + } + else { + if ($env:POWERSHELL_RUNTIME_VERBOSE -eq 'TRUE') { Write-Host '[RUNTIME-bootstrap]No module archives detected; nothing to do.' } + } + +} \ No newline at end of file diff --git a/powershell-runtime/source/modules/Private/Set-PSModulePath.ps1 b/powershell-runtime/source/modules/Private/Set-PSModulePath.ps1 index d79b113..1c15c72 100644 --- a/powershell-runtime/source/modules/Private/Set-PSModulePath.ps1 +++ b/powershell-runtime/source/modules/Private/Set-PSModulePath.ps1 @@ -14,6 +14,7 @@ function private:Set-PSModulePath { 1: Modules supplied with pwsh 2: User supplied modules as part of Lambda Layers 3: User supplied modules as part of function package + 4: Compressed modules #> if ($env:POWERSHELL_RUNTIME_VERBOSE -eq 'TRUE') { Write-Host '[RUNTIME-Set-PSModulePath]Start: Set-PSModulePath' } if ($env:POWERSHELL_RUNTIME_VERBOSE -eq 'TRUE') { Write-Host '[RUNTIME-Set-PSModulePath]Setting PSModulePath environment variable' } @@ -22,5 +23,13 @@ function private:Set-PSModulePath { '/opt/modules', # User supplied modules as part of Lambda Layers [System.IO.Path]::Combine($env:LAMBDA_TASK_ROOT, 'modules') # User supplied modules as part of function package ) -join ':' + If (Test-RuntimePackedModule -Combined) { + if ($env:POWERSHELL_RUNTIME_VERBOSE -eq 'TRUE') { Write-Host "[RUNTIME-Set-PSModulePath]Combined module package detected, adding unpack directory to PSModulePath" } + $env:PSModulePath += (':' + '/tmp/powershell-custom-runtime-unpacked-modules/combined') # Modules unpacked via Import-ModuleArchive + } + If (Test-RuntimePackedModule -NuPkg) { + if ($env:POWERSHELL_RUNTIME_VERBOSE -eq 'TRUE') { Write-Host "[RUNTIME-Set-PSModulePath]Nupkg module package(s) detected, adding unpack directory to PSModulePath" } + $env:PSModulePath += (':' + '/tmp/powershell-custom-runtime-unpacked-modules/nupkg') # Modules unpacked via Import-ModulePackage + } if ($env:POWERSHELL_RUNTIME_VERBOSE -eq 'TRUE') { Write-Host "[RUNTIME-Set-PSModulePath]PSModulePath environment variable set to: $($env:PSModulePath)" } } diff --git a/powershell-runtime/source/modules/Private/Test-RuntimePackedModule.ps1 b/powershell-runtime/source/modules/Private/Test-RuntimePackedModule.ps1 new file mode 100644 index 0000000..b2704ed --- /dev/null +++ b/powershell-runtime/source/modules/Private/Test-RuntimePackedModule.ps1 @@ -0,0 +1,79 @@ +function private:Test-RuntimePackedModule { + <# + .SYNOPSIS + Tests whether the current runtime environment contains compressed module packages (combined .zip or per-module .nupkg) + .DESCRIPTION + Tests whether the current runtime environment contains compressed module packages (combined .zip or per-module .nupkg) + .NOTES + Looks for module packages in two locations: + * /opt/ (Combined Lambda layer directory) + * $Env:LAMBDA_TASK_ROOT (Lambda Function package directory) + + Module packages can take two forms: + * A single, combined module archive, named "modules.zip". + The contents of this archive should match the format of a folder in $Env:PSModulePath. + (Module names as top-level directories, optional version subdirectory, corresponding module root) + * Individual module archives, as .nupkg files, inside a subdirectory named "module-nupkgs" + These files should match: + * The naming convention used by PSResourceGet. (e.g. ..nupkg) + * The Nupkg archive spec (module root at archive root, NuGet [Content_Types].xml/_rels, etc.) + + The following file locations should all be detected (assume $Env:LAMBDA_TASK_ROOT = /var/lambda/) + * /opt/modules.zip + * /var/lambda/modules.zip + * /opt/module-nupkgs/AWS.Tools.Common.4.1.833.nupkg + * /var/lambda/module-nupkgs/AWS.Tools.Common.4.1.833.nupkg + .EXAMPLE + Test-MyTestFunction -Verbose + Explanation of the function or its result. You can include multiple examples with additional .EXAMPLE lines + #> + + [CmdletBinding()] + param( + # Looks for combined module archives (modules.zip). + [Parameter( + Mandatory, + ParameterSetName="Combined" + )] + [Switch] + $Combined, + + # Looks for individual module packages (*.nupkg). + [Parameter( + Mandatory, + ParameterSetName="NuPkg" + )] + [Switch] + $NuPkg + ) + + $BaseDirectories = @( + "/opt", + $Env:LAMBDA_TASK_ROOT + ) + + switch ($PSCmdlet.ParameterSetName) { + "Combined" { + + if ($env:POWERSHELL_RUNTIME_VERBOSE -eq 'TRUE') { Write-Host "[RUNTIME-Test-RuntimePackedModule]Searching for combined module archives" } + + $BaseDirectories | Join-Path -ChildPath "modules.zip" | Get-Item -ErrorAction SilentlyContinue | Set-Variable FoundItems + + } + "NuPkg" { + + if ($env:POWERSHELL_RUNTIME_VERBOSE -eq 'TRUE') { Write-Host "[RUNTIME-Test-RuntimePackedModule]Searching for individual module packages" } + + $BaseDirectories | Join-Path -ChildPath "module-nupkgs" -AdditionalChildPath "*.nupkg" | Get-Item -ErrorAction SilentlyContinue | Set-Variable FoundItems + + } + } + + If ($FoundItems) { + if ($env:POWERSHELL_RUNTIME_VERBOSE -eq 'TRUE') { Write-Host "[RUNTIME-Test-RuntimePackedModule]Found $($FoundItems | Measure-Object | % Count) match(es)" } + return $true + } else { + if ($env:POWERSHELL_RUNTIME_VERBOSE -eq 'TRUE') { Write-Host "[RUNTIME-Test-RuntimePackedModule]No matches found" } + return $false + } +} \ No newline at end of file