Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions powershell-runtime/source/bootstrap
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For performance reasons (reading from disk is relatively slow), it would be great to merge these into a single call, that looks for both .zip and .nupkg files, and passes the files found into appropriate import functions.

As it stands, this will traverse the file system twice (in each of the Test-RuntimePackedModule calls, and then if something is found, again in the relative Import-* functions).

Perhaps the Test-RuntimePackedModule could return the specific paths to the files found, and the Import-* functions could accept a string of path values as a parameters reducing the disk reads.

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
Expand Down
40 changes: 40 additions & 0 deletions powershell-runtime/source/modules/Private/Import-ModuleArchive.ps1
Original file line number Diff line number Diff line change
@@ -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/'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might be best to define in pwsh-runtime.psm1 as a script (/module) scoped variable. That will mean it can also be used to configure $env:PSModulePath in Set-PSModulePath, allowing the unpacked modules to be imported as normal.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

db51cda hoisted these variables into the script scope and d2fb0e9 updated the Import-Module* cmdlets to use the script-scope dirs. Let me know what you think.

if ($env:POWERSHELL_RUNTIME_VERBOSE -eq 'TRUE') { Write-Host '[RUNTIME-Import-ModuleArchive]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-Import-ModuleArchive]Unpacking $_ to $UnpackDirectory" }
Expand-Archive -LiteralPath $_ -DestinationPath $UnpackDirectory -Force
}
if ($env:POWERSHELL_RUNTIME_VERBOSE -eq 'TRUE') { Write-Host '[RUNTIME-Import-ModuleArchive]Archive unpack complete' }
}
else {
if ($env:POWERSHELL_RUNTIME_VERBOSE -eq 'TRUE') { Write-Host '[RUNTIME-Import-ModuleArchive]No module archives detected; nothing to do.' }
}
}
49 changes: 49 additions & 0 deletions powershell-runtime/source/modules/Private/Import-ModulePackage.ps1
Original file line number Diff line number Diff line change
@@ -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-Import-ModulePackage]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-Import-ModulePackage]Importing module packages from $PackageDirectory" }
$RepositoryName = "Lambda-Local-$($_.Key)"
if ($env:POWERSHELL_RUNTIME_VERBOSE -eq 'TRUE') { Write-Host "[RUNTIME-Import-ModulePackage]Registering local package repository $RepositoryName" }
Register-PSResourceRepository -Name $RepositoryName -Uri $PackageDirectory -Trusted -Priority 1
if ($env:POWERSHELL_RUNTIME_VERBOSE -eq 'TRUE') { Write-Host "[RUNTIME-Import-ModulePackage]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-Import-ModulePackage]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-Import-ModulePackage]Registering local package repository $RepositoryName" }
Unregister-PSResourceRepository -Name $RepositoryName -Confirm:$false
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do these need to be invoked for every item from $SearchPaths.GetEnumerator()?

Instead, perhaps each file should be stored appropriately, and then a single repository is registered and used to save the modules.

$using:PackageDirectory - from another comment, this could use a module scoped variable for where these compressed/packed modules will be extracted into.

if ($env:POWERSHELL_RUNTIME_VERBOSE -eq 'TRUE') { Write-Host '[RUNTIME-Import-ModulePackage]Archive unpack complete' }
}
else {
if ($env:POWERSHELL_RUNTIME_VERBOSE -eq 'TRUE') { Write-Host '[RUNTIME-Import-ModulePackage]No module archives detected; nothing to do.' }
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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' }
Expand All @@ -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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the paths to the extracted modules was the module scoped variable, we could just add that to the $env:ModulePath directly (perhaps if it exists, or just add it). rather than traversing the file system again.

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)" }
}
Original file line number Diff line number Diff line change
@@ -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. <module-name>.<version>.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
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this was call in bootstrap after the Set-PSModulePath call, perhaps this could use the paths configured in $env:PSModulePath for the evaluation?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because this is called before Set-PSModulePath (and dirs Set-PSModulePath looks for are only populated because this cmdlet says so), I don't think relying on contents of $env:PSModulePath would work as expected.

I could potentially put these paths in a separate $Script: scope variable, but given the very specific use case I'm not sure they'd be of any use.


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
}
}