From 2dcff278e51f25b24a093553c95fef53675cd1c3 Mon Sep 17 00:00:00 2001 From: Thomas Nieto <38873752+ThomasNieto@users.noreply.github.com> Date: Mon, 12 May 2025 23:27:53 -0500 Subject: [PATCH 1/3] Initial commit --- .cspell.jsonc | 10 ++ .github/workflows/ci.yml | 132 +++++++++++++++++++++++ .github/workflows/lint-powershell.yml | 43 ++++++++ .markdownlint.jsonc | 54 ++++++++++ PesterSettings.psd1 | 8 ++ SignSettings.psd1 | 5 + src/AnyPackage.Docker.psd1 | 24 +++++ src/AnyPackage.Docker.psm1 | 150 ++++++++++++++++++++++++++ test/Get-Package.Tests.ps1 | 14 +++ 9 files changed, 440 insertions(+) create mode 100644 .cspell.jsonc create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/lint-powershell.yml create mode 100644 .markdownlint.jsonc create mode 100644 PesterSettings.psd1 create mode 100644 SignSettings.psd1 create mode 100644 src/AnyPackage.Docker.psd1 create mode 100644 src/AnyPackage.Docker.psm1 create mode 100644 test/Get-Package.Tests.ps1 diff --git a/.cspell.jsonc b/.cspell.jsonc new file mode 100644 index 0000000..a381d53 --- /dev/null +++ b/.cspell.jsonc @@ -0,0 +1,10 @@ +{ + "words": [ + "anypackage", + "Authenticode", + "LASTEXITCODE", + "Nieto", + "SARIF", + "trunc" + ] +} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..4bebf4c --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,132 @@ +name: CI + +defaults: + run: + shell: pwsh + +on: + push: + branches: [ main ] + + pull_request: + branches: [ main ] + + release: + types: [ published ] + +jobs: + Build: + runs-on: ubuntu-latest + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Upload module + uses: actions/upload-artifact@v4 + with: + name: module + path: ./src/ + + Test: + needs: Build + strategy: + matrix: + os: [macos-latest, ubuntu-latest, windows-latest] + runs-on: ${{ matrix.os }} + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Install AnyPackage + run: Install-Module AnyPackage -Force -AllowClobber + + - name: Download module + uses: actions/download-artifact@v4 + with: + name: module + path: AnyPackage.Docker + + - name: Move module + run: | + if ($IsWindows) { + $path = "$HOME\Documents\PowerShell\Modules" + } else { + $path = "$HOME/.local/share/powershell/Modules" + } + + Move-Item AnyPackage.Docker $path + + - name: Test with Pester + run: | + $ht = Import-PowerShellDataFile PesterSettings.psd1 + $config = New-PesterConfiguration $ht + Invoke-Pester -Configuration $config + + Sign: + needs: Test + if: github.event_name == 'release' && github.event.action == 'published' + runs-on: windows-latest + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Download module + uses: actions/download-artifact@v4 + with: + name: module + path: module + + - name: Import certificate + env: + CERTIFICATE_BASE64: ${{ secrets.CERTIFICATE_BASE64 }} + CERTIFICATE_PASSWORD: ${{ secrets.CERTIFICATE_PASSWORD }} + CERTIFICATE_PASSWORD_KEY_BASE64: ${{ secrets.CERTIFICATE_PASSWORD_KEY_BASE64 }} + run: | + [convert]::FromBase64String($env:CERTIFICATE_BASE64) | Set-Content -Path cert.pfx -AsByteStream + $key = [convert]::FromBase64String($env:CERTIFICATE_PASSWORD_KEY_BASE64) + $password = ConvertTo-SecureString $env:CERTIFICATE_PASSWORD -Key $key + Import-PfxCertificate cert.pfx -Password $password -CertStoreLocation Cert:\CurrentUser\My + + - name: Sign files + run: | + $config = Import-PowerShellDataFile SignSettings.psd1 + $config['Certificate'] = Get-ChildItem Cert:\CurrentUser\My -CodeSigningCert + Set-Location .\module + Set-AuthenticodeSignature @config + + - name: Create and sign catalog file + run: | + $config = Import-PowerShellDataFile SignSettings.psd1 + $config['FilePath'] = 'AnyPackage.Docker.cat' + $config['Certificate'] = Get-ChildItem Cert:\CurrentUser\My -CodeSigningCert + Set-Location .\module + New-FileCatalog $config['FilePath'] -CatalogVersion 2 + Set-AuthenticodeSignature @config + + - name: Upload module + uses: actions/upload-artifact@v4 + with: + name: module-signed + path: ./module/ + + Publish: + needs: Sign + if: github.event_name == 'release' && github.event.action == 'published' + runs-on: ubuntu-latest + steps: + + - name: Download module + uses: actions/download-artifact@v4 + with: + name: module-signed + path: '~/.local/share/powershell/Modules/AnyPackage.Docker' + + - name: Install AnyPackage + run: Install-Module AnyPackage -Force -AllowClobber + + - name: Publish Module + env: + NUGET_KEY: ${{ secrets.NUGET_KEY }} + run: | + $module = Get-Module AnyPackage.Docker -ListAvailable + Publish-PSResource $module.ModuleBase -ApiKey $env:NUGET_KEY diff --git a/.github/workflows/lint-powershell.yml b/.github/workflows/lint-powershell.yml new file mode 100644 index 0000000..72a6155 --- /dev/null +++ b/.github/workflows/lint-powershell.yml @@ -0,0 +1,43 @@ +name: PSScriptAnalyzer + +defaults: + run: + shell: pwsh + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + schedule: + - cron: '22 1 * * 3' + +permissions: + contents: read + +jobs: + build: + permissions: + contents: read + security-events: write + name: PSScriptAnalyzer + runs-on: ubuntu-latest + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Install AnyPackage + run: Install-Module AnyPackage -Force -AllowClobber + + - name: Install ConvertToSARIF + run: Install-Module ConvertToSARIF -Force + + - name: Run PSScriptAnalyzer + run: | + Import-Module AnyPackage, ConvertToSARIF -PassThru + Invoke-ScriptAnalyzer -Path . -Recurse | ConvertTo-SARIF -FilePath results.sarif + + - name: Upload SARIF results file + uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: results.sarif diff --git a/.markdownlint.jsonc b/.markdownlint.jsonc new file mode 100644 index 0000000..173315f --- /dev/null +++ b/.markdownlint.jsonc @@ -0,0 +1,54 @@ +{ + "code-block-style": { + "style": "fenced" + }, + "code-fence-style": { + "style": "backtick" + }, + "emphasis-style": { + "style": "asterisk" + }, + "fenced-code-language": { + "allowed_languages": [ + "powershell", + "text" + ], + "language_only": true + }, + "heading-style": { + "style": "atx" + }, + "hr-style": { + "style": "---" + }, + "line-length": { + "strict": true, + "code_blocks": false + }, + "link-image-style": { + "collapsed": false, + "url_inline": false + }, + "no-duplicate-heading": { + "siblings_only": true + }, + "ol-prefix": { + "style": "ordered" + }, + "proper-names": { + "code_blocks": false, + "names": [ + "PowerShell", + "AnyPackage" + ] + }, + "reference-links-images": { + "shortcut_syntax": true + }, + "strong-style": { + "style": "asterisk" + }, + "ul-style": { + "style": "dash" + } +} diff --git a/PesterSettings.psd1 b/PesterSettings.psd1 new file mode 100644 index 0000000..c0c05f8 --- /dev/null +++ b/PesterSettings.psd1 @@ -0,0 +1,8 @@ +@{ + Run = @{ + Exit = $true + } + Output = @{ + Verbosity = 'Detailed' + } +} diff --git a/SignSettings.psd1 b/SignSettings.psd1 new file mode 100644 index 0000000..cae793c --- /dev/null +++ b/SignSettings.psd1 @@ -0,0 +1,5 @@ +@{ + FilePath = @('AnyPackage.Docker.psd1', 'AnyPackage.Docker.psm1') + TimeStampServer = 'http://timestamp.sectigo.com' + HashAlgorithm = 'SHA256' +} diff --git a/src/AnyPackage.Docker.psd1 b/src/AnyPackage.Docker.psd1 new file mode 100644 index 0000000..6324db2 --- /dev/null +++ b/src/AnyPackage.Docker.psd1 @@ -0,0 +1,24 @@ +@{ + RootModule = 'AnyPackage.Docker.psm1' + ModuleVersion = '0.1.0' + CompatiblePSEditions = @('Desktop', 'Core') + GUID = '4826c4eb-b455-434e-9d1f-253c9e0021fd' + Author = 'Thomas Nieto' + Copyright = '(c) 2025 Thomas Nieto. All rights reserved.' + Description = 'Docker provider for AnyPackage.' + PowerShellVersion = '5.1' + RequiredModules = @('AnyPackage') + FunctionsToExport = @() + CmdletsToExport = @() + AliasesToExport = @() + PrivateData = @{ + AnyPackage = @{ + Providers = 'Docker' + } + PSData = @{ + Tags = @('AnyPackage', 'Provider', 'Docker', 'Container', 'Windows', 'Linux', 'MacOS') + LicenseUri = 'https://github.com/anypackage/docker/blob/main/LICENSE' + ProjectUri = 'https://github.com/anypackage/docker' + } + } +} diff --git a/src/AnyPackage.Docker.psm1 b/src/AnyPackage.Docker.psm1 new file mode 100644 index 0000000..ab71d12 --- /dev/null +++ b/src/AnyPackage.Docker.psm1 @@ -0,0 +1,150 @@ +using module AnyPackage +using namespace AnyPackage.Provider +using namespace System.Management.Automation + +[PackageProvider('Docker')] +class DockerProvider : PackageProvider, IGetPackage, IInstallPackage, IUninstallPackage { + [void] GetPackage ([PackageRequest] $request) { + $images = docker image list --format json --no-trunc --digests | ConvertFrom-Json + + foreach ($image in $images) { + try { + $repo = $this.ParseRepository($image.Repository, $request) + } catch { + continue + } + + if ($request.IsMatch($repo.Name, $image.Tag)) { + $metadata = $image | ConvertTo-Metadata + $package = [PackageInfo]::new($repo.Name, $image.Tag, $repo.Source, '', $null, $metadata, $request.ProviderInfo) + $request.WritePackage($package) + } + } + } + + [void] InstallPackage ([PackageRequest] $request) { + $id = '' + + if ($request.Source) { + $id += "{0}/" -f $request.Source + } + + $id += $request.Name + + if ($request.Version -and $request.Version.MinVersion -ne $request.Version.MaxVersion) { + throw "Version ranges not supported." + } + elseif ($request.Version) { + $id += ":{0}" -f $request.Version.MinVersion + } + + $request.WriteVerbose("Full repository name: $id") + + docker pull $id 2>&1 | + ForEach-Object { + if ($_ -is [ErrorRecord]) { + $request.WriteError($_) + } else { + $request.WriteVerbose($_) + } + } + + if ($LASTEXITCODE -eq 0) { + $getPackageParameters = @{ + Name = $request.Name + Provider = $request.ProviderInfo.FullName + ErrorAction = 'SilentlyContinue' + } + + if ($request.Version) { + $getPackageParameters['Version'] = $request.Version + } + + $package = Get-Package @getPackageParameters + + $request.WritePackage($package) + } + } + + [void] UninstallPackage ([PackageRequest] $request) { + $getPackageParameters = @{ + Name = $request.Name + Provider = $request.ProviderInfo.FullName + ErrorAction = 'SilentlyContinue' + } + + if ($request.Version) { + $getPackageParameters['Version'] = $request.Version + } + + $package = Get-Package @getPackageParameters + + if (!$package) { + return + } + + $id = '{0}/{1}:{2}' -f $package.Source, $package.Name, $package.Version + docker image rm $id 2>&1 | + ForEach-Object { + if ($_ -is [ErrorRecord]) { + $request.WriteError($_) + } else { + $request.WriteVerbose($_) + } + } + + if ($LASTEXITCODE -eq 0) { + $request.WritePackage($package) + } + } + + hidden [hashtable] ParseRepository([string] $repository, [PackageRequest] $request) { + if ($repository -match '^(?:(?[^/]*\.[^/]+)/)?(?.+)$') { + if ($matches.ContainsKey('source')) { + $source = [PackageSourceInfo]::new($matches.source, $matches.source, $request.ProviderInfo) + } else { + $request.WriteVerbose('Host not found, defaulting to default docker.io') + $source = [PackageSourceInfo]::new('docker.io', 'docker.io', $request.ProviderInfo) + } + } else { + throw "Failed to parse $($repository)" + } + + return @{ + Name = $matches.Name + Source = $source + } + } +} + +[guid] $id = '9782932d-8d88-4f53-9ef9-4dbf1def9783' +[PackageProviderManager]::RegisterProvider($id, [DockerProvider], $MyInvocation.MyCommand.ScriptBlock.Module) + +$MyInvocation.MyCommand.ScriptBlock.Module.OnRemove = { + [PackageProviderManager]::UnregisterProvider($id) +} + +function ConvertTo-Metadata { + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '')] + [CmdletBinding()] + [OutputType([hashtable])] + param ( + [Parameter(Mandatory, Position = 0, ValueFromPipeline)] + [PSObject] + $InputObject + ) + + process { + $ht = @{ } + + $properties = $InputObject | + Get-Member -MemberType Properties | + Select-Object -ExpandProperty Name + + foreach ($prop in $properties) { + $ht[$prop] = $InputObject.$prop + } + + $ht + } +} diff --git a/test/Get-Package.Tests.ps1 b/test/Get-Package.Tests.ps1 new file mode 100644 index 0000000..e0f93e9 --- /dev/null +++ b/test/Get-Package.Tests.ps1 @@ -0,0 +1,14 @@ +#Requires -Modules AnyPackage.Docker + +Describe Get-Package { + Context 'with no parameters' { + It 'should return results' { + $packages = Get-Package + + Write-Verbose ($packages | Out-String) -Verbose + + Get-Package | + Should -Not -BeNullOrEmpty + } + } +} From c0eb742956732962ce68fe12ef7734596335a491 Mon Sep 17 00:00:00 2001 From: Thomas Nieto <38873752+ThomasNieto@users.noreply.github.com> Date: Mon, 12 May 2025 23:29:57 -0500 Subject: [PATCH 2/3] Add provider test --- ...et-Package.Tests.ps1 => Get-PackageProvider.Tests.ps1} | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) rename test/{Get-Package.Tests.ps1 => Get-PackageProvider.Tests.ps1} (51%) diff --git a/test/Get-Package.Tests.ps1 b/test/Get-PackageProvider.Tests.ps1 similarity index 51% rename from test/Get-Package.Tests.ps1 rename to test/Get-PackageProvider.Tests.ps1 index e0f93e9..eb5679e 100644 --- a/test/Get-Package.Tests.ps1 +++ b/test/Get-PackageProvider.Tests.ps1 @@ -1,13 +1,9 @@ #Requires -Modules AnyPackage.Docker -Describe Get-Package { +Describe Get-PackageProvider { Context 'with no parameters' { It 'should return results' { - $packages = Get-Package - - Write-Verbose ($packages | Out-String) -Verbose - - Get-Package | + Get-PackageProvider | Should -Not -BeNullOrEmpty } } From 0ce7a643e601cfb032a5e94cf49ae1640cdbd8ba Mon Sep 17 00:00:00 2001 From: Thomas Nieto <38873752+ThomasNieto@users.noreply.github.com> Date: Mon, 12 May 2025 23:31:29 -0500 Subject: [PATCH 3/3] Fix whitespace --- src/AnyPackage.Docker.psm1 | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/AnyPackage.Docker.psm1 b/src/AnyPackage.Docker.psm1 index ab71d12..05455c2 100644 --- a/src/AnyPackage.Docker.psm1 +++ b/src/AnyPackage.Docker.psm1 @@ -7,7 +7,7 @@ class DockerProvider : PackageProvider, IGetPackage, IInstallPackage, IUninstall [void] GetPackage ([PackageRequest] $request) { $images = docker image list --format json --no-trunc --digests | ConvertFrom-Json - foreach ($image in $images) { + foreach ($image in $images) { try { $repo = $this.ParseRepository($image.Repository, $request) } catch { @@ -24,7 +24,7 @@ class DockerProvider : PackageProvider, IGetPackage, IInstallPackage, IUninstall [void] InstallPackage ([PackageRequest] $request) { $id = '' - + if ($request.Source) { $id += "{0}/" -f $request.Source } @@ -55,13 +55,13 @@ class DockerProvider : PackageProvider, IGetPackage, IInstallPackage, IUninstall Provider = $request.ProviderInfo.FullName ErrorAction = 'SilentlyContinue' } - + if ($request.Version) { $getPackageParameters['Version'] = $request.Version } $package = Get-Package @getPackageParameters - + $request.WritePackage($package) } }