Skip to content

Commit

Permalink
Run manifest generators inside a container
Browse files Browse the repository at this point in the history
  • Loading branch information
MatejKafka committed Apr 27, 2024
1 parent 21dc08f commit 059f00d
Show file tree
Hide file tree
Showing 3 changed files with 184 additions and 138 deletions.
130 changes: 2 additions & 128 deletions app/Pog/Pog.psm1
Original file line number Diff line number Diff line change
Expand Up @@ -118,35 +118,8 @@ Export function New-PogImportedPackage {
}
}

<# Retrieve all existing versions of a package by calling the package version generator script. #>
function RetrievePackageVersions([Pog.PackageGenerator]$Generator, $ExistingVersionSet) {
foreach ($Obj in & $Generator.Manifest.ListVersionsSb $ExistingVersionSet) {
# the returned object should either be the version string directly, or a map object
# (hashtable/pscustomobject/psobject/ordered) that has the Version property
# why not use -is? that's why: https://github.com/PowerShell/PowerShell/issues/16361
$IsMap = $Obj.PSTypeNames[0] -in @("System.Collections.Hashtable", "System.Management.Automation.PSCustomObject", "System.Collections.Specialized.OrderedDictionary")
$VersionStr = if (-not $IsMap) {$Obj} else {
try {$Obj.Version}
catch {
throw "Version generator for package '$($Generator.PackageName)' returned a custom object without a Version property: $Obj" +`
" (Version generators must return either a version string, or a" +`
" map container (hashtable, psobject, pscustomobject) with a Version property.)"
}
}

if ([string]::IsNullOrEmpty($VersionStr)) {
throw "Empty package version generated by the version generator for package '$($Generator.PackageName)' (either `$null or empty string)."
}

[pscustomobject]@{
Version = [Pog.PackageVersion]$VersionStr
# store the original value, so that we can pass it unchanged to the manifest generator
OriginalValue = $Obj
}
}
}

function UpdateSinglePackage([string]$PackageName, [string[]]$Version, [switch]$Force, [switch]$ListOnly) {
function UpdateSinglePackage([string]$PackageName, [string[]]$Version, [switch]$Force, [switch]$ListOnly) {
Write-Information "Checking updates for '$PackageName'..."

$g = try {$GENERATOR_REPOSITORY.GetPackage($PackageName, $true, $true)}
Expand All @@ -162,108 +135,9 @@ function UpdateSinglePackage([string]$PackageName, [string[]]$Version, [switch]
"manifest generators are only supported for existing templated packages."
}

# list available versions without existing manifest (unless -Force is set, then all versions are listed)
# only generate manifests for versions that don't already exist, unless -Force is passed
$ExistingVersions = [System.Collections.Generic.HashSet[string]]::new($c.EnumerateVersionStrings())
$GeneratedVersions = RetrievePackageVersions $g $ExistingVersions `
<# if -Force was not passed, filter out versions with already existing manifest #> `
| ? {$Force -or -not $ExistingVersions.Contains($_.Version)} `
<# if $Version was passed, filter out the versions; as the versions generated by the script
may have other metadata, we cannot use the versions passed in $Version directly #> `
| ? {-not $Version -or $_.Version -in $Version}

if ($Version -and @($Version).Count -ne @($GeneratedVersions).Count) {
$FoundVersions = $GeneratedVersions | % {$_.Version}
$MissingVersionsStr = ($Version | ? {$_ -notin $FoundVersions}) -join ", "
throw "Some of the package versions passed in -Version were not found for package '$($c.PackageName)': $MissingVersionsStr " +`
"(Are you sure these versions exist?)"
return
}

if ($ListOnly) {
# useful for testing if all expected versions are retrieved
return $GeneratedVersions | % {$c.GetVersionPackage($_.Version, $false)}
}

# generate manifest for each version
foreach ($v in $GeneratedVersions) {
$p = $c.GetVersionPackage($v.Version, $false)

$TemplateData = if ($g.Manifest.GenerateSb) {
# pass the value both as $_ and as a parameter, the scriptblock can accept whichever one is more convenient
Invoke-DollarUnder $g.Manifest.GenerateSb $v.OriginalValue $v.OriginalValue
} else {
$v.OriginalValue # if no Generate block exists, forward the value emitted by ListVersions
}

$Count = @($TemplateData).Count
if ($Count -ne 1) {
throw "Manifest generator for package '$($p.PackageName)' generated " +`
"$(if ($Count -eq 0) {"no output"} else {"multiple values"}) for version '$($p.Version)', expected a single [Hashtable]."
}

# unwrap the collection
$TemplateData = @($TemplateData)[0]

if ($TemplateData -isnot [Hashtable] -and $TemplateData -isnot [System.Collections.Specialized.OrderedDictionary]) {
$Type = if ($TemplateData) {$TemplateData.GetType().ToString()} else {"null"}
throw "Manifest generator for package '$($p.PackageName)' did not generate a [Hashtable] for version '$($p.Version)', got '$Type'."
}

# write out the manifest
[Pog.ManifestTemplateFile]::SerializeSubstitutionFile($p.ManifestPath, $TemplateData)

# manifest is not loaded yet, no need to reload
echo $p
}
}

# used from inside the manifest generators
function Get-GitHubRelease {
[CmdletBinding()]
param(
[Parameter(Mandatory, ValueFromPipeline)]
[ValidatePattern("^[^/\s]+/[^/\s]+$")]
[string[]]
$Repository,
### Retrieves tags instead of releases.
[switch]
$Tags
)

process {
foreach ($r in $Repository) {
$Endpoint = if ($Tags) {"tags"} else {"releases"}
$Url = "https://api.github.com/repos/$r/$Endpoint"
Write-Verbose "Listing GitHub releases for '$r'... (URL: $Url)"

try {
# piping through Write-Output enumerates the array returned by irm into individual values
# (see https://github.com/PowerShell/PowerShell/issues/15280)
# -FollowRelLink automatically goes through all pages to get older releases
Invoke-RestMethod -UseBasicParsing -FollowRelLink $Url | Write-Output
} catch [Microsoft.PowerShell.Commands.HttpResponseException] {
$e = $_.Exception
if ($e.StatusCode -eq 404) {
throw "Cannot list $Endpoint for '$r', GitHub repository does not exist."
} elseif ($e.StatusCode -eq 403 -and $e.Response.ReasonPhrase -eq "rate limit exceeded") {
$Limit = try {$e.Response.Headers.GetValues("X-RateLimit-Limit")} catch {}
$LimitMsg = if ($Limit) {" (at most $Limit requests/hour are allowed)"}
throw "Cannot list $Endpoint for '$r', GitHub API rate limit exceeded$LimitMsg."
} else {
throw
}
}
}
}
Invoke-Container -Modules $PSScriptRoot\container\Env_ManifestGenerator.psm1 -ArgumentList @($g, $c, $Version, $Force, $ListOnly)
}

# TODO: run this inside a container
# run both scriptblocks in the same container instance
# think what utility cmdlets can Pog reasonably provide and which is best left to existing tools like iwr/import
# likely provide cmdlets to work with github (listing releases, using regex to select asset URL) and calculate hash (Show-PogManifestHash)
# add Get-UrlHash and Get-GitHubRelease in the container env; maybe also set $PSDefaultParameterValues to pass -UseBasicParsing to iwr and irm
# FIXME: if -Force is passed, track if there are any leftover manifests (for removed versions) and delete them
Export function Update-PogManifest {
# .SYNOPSIS
# Generate new manifests in the package repository for the given package manifest generator.
Expand Down
171 changes: 171 additions & 0 deletions app/Pog/container/Env_ManifestGenerator.psm1
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
using module .\..\lib\Utils.psm1
. $PSScriptRoot\..\lib\header.ps1

# always use basic parsing inside the generators, to ease compatibility with PowerShell 5
$PSDefaultParameterValues = @{
"Invoke-WebRequest:UseBasicParsing" = $true
"Invoke-RestMethod:UseBasicParsing" = $true
}

<# Retrieve all existing versions of a package by calling the package version generator script. #>
function RetrievePackageVersions([Pog.PackageGenerator]$Generator, $ExistingVersionSet) {
foreach ($Obj in & $Generator.Manifest.ListVersionsSb $ExistingVersionSet) {
# the returned object should either be the version string directly, or a map object
# (hashtable/pscustomobject/psobject/ordered) that has the Version property
# why not use -is? that's why: https://github.com/PowerShell/PowerShell/issues/16361
$IsMap = $Obj.PSTypeNames[0] -in @("System.Collections.Hashtable", "System.Management.Automation.PSCustomObject", "System.Collections.Specialized.OrderedDictionary")
$VersionStr = if (-not $IsMap) {$Obj} else {
try {$Obj.Version}
catch {
throw "Version generator for package '$($Generator.PackageName)' returned a custom object without a Version property: $Obj" +`
" (Version generators must return either a version string, or a" +`
" map container (hashtable, psobject, pscustomobject) with a Version property.)"
}
}

if ([string]::IsNullOrEmpty($VersionStr)) {
throw "Empty package version generated by the version generator for package '$($Generator.PackageName)' (either `$null or empty string)."
}

[pscustomobject]@{
Version = [Pog.PackageVersion]$VersionStr
# store the original value, so that we can pass it unchanged to the manifest generator
OriginalValue = $Obj
}
}
}

# TODO: think what utility cmdlets can Pog reasonably provide and which is best left to existing tools like iwr/import
# FIXME: if -Force is passed, track if there are any leftover manifests (for removed versions) and delete them
Export function __main {
param(
[Pog.PackageGenerator]$Generator,
[Pog.LocalRepositoryVersionedPackage]$Package,
[string[]]$Version,
[bool]$Force,
[bool]$ListOnly
)

# list available versions without existing manifest (unless -Force is set, then all versions are listed)
# only generate manifests for versions that don't already exist, unless -Force is passed
$ExistingVersions = [System.Collections.Generic.HashSet[string]]::new($Package.EnumerateVersionStrings())
$GeneratedVersions = RetrievePackageVersions $Generator $ExistingVersions `
<# if -Force was not passed, filter out versions with already existing manifest #> `
| ? {$Force -or -not $ExistingVersions.Contains($_.Version)} `
<# if $Version was passed, filter out the versions; as the versions generated by the script
may have other metadata, we cannot use the versions passed in $Version directly #> `
| ? {-not $Version -or $_.Version -in $Version}

if ($Version -and @($Version).Count -ne @($GeneratedVersions).Count) {
$FoundVersions = $GeneratedVersions | % {$_.Version}
$MissingVersionsStr = ($Version | ? {$_ -notin $FoundVersions}) -join ", "
throw "Some of the package versions passed in -Version were not found for package '$($Package.PackageName)': $MissingVersionsStr " +`
"(Are you sure these versions exist?)"
return
}

if ($ListOnly) {
# useful for testing if all expected versions are retrieved
return $GeneratedVersions | % {$Package.GetVersionPackage($_.Version, $false)}
}

# generate manifest for each version
foreach ($v in $GeneratedVersions) {
$p = $Package.GetVersionPackage($v.Version, $false)

$TemplateData = if ($Generator.Manifest.GenerateSb) {
# pass the value both as $_ and as a parameter, the scriptblock can accept whichever one is more convenient
Invoke-DollarUnder $Generator.Manifest.GenerateSb $v.OriginalValue $v.OriginalValue
} else {
$v.OriginalValue # if no Generate block exists, forward the value emitted by ListVersions
}

$Count = @($TemplateData).Count
if ($Count -ne 1) {
throw "Manifest generator for package '$($p.PackageName)' generated " +`
"$(if ($Count -eq 0) {"no output"} else {"multiple values"}) for version '$($p.Version)', expected a single [Hashtable]."
}

# unwrap the collection
$TemplateData = @($TemplateData)[0]

if ($TemplateData -isnot [Hashtable] -and $TemplateData -isnot [System.Collections.Specialized.OrderedDictionary]) {
$Type = if ($TemplateData) {$TemplateData.GetType().ToString()} else {"null"}
throw "Manifest generator for package '$($p.PackageName)' did not generate a [Hashtable] for version '$($p.Version)', got '$Type'."
}

# write out the manifest
[Pog.ManifestTemplateFile]::SerializeSubstitutionFile($p.ManifestPath, $TemplateData)

# manifest is not loaded yet, no need to reload
echo $p
}
}


Export function Get-GitHubRelease {
[CmdletBinding()]
param(
[Parameter(Mandatory, ValueFromPipeline)]
[ValidatePattern("^[^/\s]+/[^/\s]+$")]
[string[]]
$Repository,
### Retrieves tags instead of releases.
[switch]
$Tags
)

process {
foreach ($r in $Repository) {
$Endpoint = if ($Tags) {"tags"} else {"releases"}
$Url = "https://api.github.com/repos/$r/$Endpoint"
Write-Verbose "Listing GitHub releases for '$r'... (URL: $Url)"

try {
# piping through Write-Output enumerates the array returned by irm into individual values
# (see https://github.com/PowerShell/PowerShell/issues/15280)
# -FollowRelLink automatically goes through all pages to get older releases
Invoke-RestMethod -FollowRelLink $Url | Write-Output
} catch [Microsoft.PowerShell.Commands.HttpResponseException] {
$e = $_.Exception
if ($e.StatusCode -eq 404) {
throw "Cannot list $Endpoint for '$r', GitHub repository does not exist."
} elseif ($e.StatusCode -eq 403 -and $e.Response.ReasonPhrase -eq "rate limit exceeded") {
$Limit = try {$e.Response.Headers.GetValues("X-RateLimit-Limit")} catch {}
$LimitMsg = if ($Limit) {" (at most $Limit requests/hour are allowed)"}
throw "Cannot list $Endpoint for '$r', GitHub API rate limit exceeded$LimitMsg."
} else {
throw
}
}
}
}
}

Export function Get-HashFromChecksumFile {
[CmdletBinding(DefaultParameterSetName="FileName", PositionalBinding=$false)]
param(
[Parameter(Mandatory, Position=0)]
[uri]
$Uri,
[Parameter(Mandatory, Position=1, ParameterSetName="FileName")]
[string]
$FileName,
[Parameter(Mandatory, ParameterSetName="Pattern")]
[string]
$Pattern
)

begin {
if ($FileName) {
$Pattern = [regex]::Escape($FileName)
}

# regex matches the almost-standard format generated by md5sum and sha*sum
if ((Invoke-WebRequest $Uri) -notmatch ("(?:^|\n)([a-z0-9]+) +$Pattern(?:$|\n)")) {
throw "Could not find the hash for file '$FileName' in the checksum file at '$Uri'."
} else {
return $Matches[1].ToUpperInvariant()
}
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using System.Collections;
using System.Management.Automation;
using System.Management.Automation;
using JetBrains.Annotations;
using Pog.InnerCommands;
using Pog.InnerCommands.Common;
Expand All @@ -10,19 +9,21 @@ namespace Pog.Commands.InternalCommands;
[Cmdlet(VerbsLifecycle.Invoke, "Container")]
[OutputType(typeof(object))]
public class InvokeContainerCommand : PogCmdlet {
[Parameter(Mandatory = true, Position = 0)] public Container.ContainerType ContainerType;
[Parameter(Mandatory = true, Position = 1)] public Package Package = null!;
[Parameter] public Hashtable? InternalArguments;
[Parameter] public Hashtable? PackageArguments;
[Parameter] public string? WorkingDirectory = null;
[Parameter] public object? Context = null;

[Parameter] public string[] Modules = [];
[Parameter(Mandatory = true)] [AllowNull] public object[] ArgumentList = null!;

protected override void BeginProcessing() {
base.BeginProcessing();

var it = InvokePogCommand(new InvokeContainer(this) {
ContainerType = ContainerType,
Package = Package,
InternalArguments = InternalArguments,
PackageArguments = PackageArguments,
WorkingDirectory = WorkingDirectory,
Context = Context,

Modules = Modules,
Run = ps => ps.AddCommand("__main").AddParameters(ArgumentList),
});

foreach (var o in it) {
Expand Down

0 comments on commit 059f00d

Please sign in to comment.