Wsl-ImageSource/Wsl-ImageSource.Cmdlets.ps1
|
# Copyright 2022 Antoine Martin # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. using namespace System.IO; [Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage()] $WslImageSources = @{ [WslImageType]::Incus = "https://raw.githubusercontent.com/antoinemartin/PowerShell-Wsl-Manager/refs/heads/rootfs/incus.rootfs.json" [WslImageType]::Builtin = "https://raw.githubusercontent.com/antoinemartin/PowerShell-Wsl-Manager/refs/heads/rootfs/builtins.rootfs.json" } function Update-WslBuiltinImageCache { <# .SYNOPSIS Updates the cache of builtin WSL root filesystems from the remote repository. .DESCRIPTION The Update-WslBuiltinImageCache cmdlet updates the local cache of available builtin WSL root filesystems from the official PowerShell-Wsl-Manager repository. This function handles the network operations and database updates for image metadata. The cmdlet implements intelligent caching with ETag support to reduce network requests and improve performance. Cached data is valid for 24 hours unless the -Sync parameter is used to force a refresh. .PARAMETER Type Specifies the source type for fetching root filesystems. Must be of type WslImageType. Defaults to [WslImageType]::Builtin. Valid values are Builtin and Incus which point to their respective official repositories. .PARAMETER Sync Forces a synchronization with the remote repository, bypassing the local cache validity check. When specified, the cmdlet will fetch the latest data from the remote repository using ETag headers if available. .PARAMETER Force Forces a complete refresh ignoring both cache validity and ETag headers. When specified, the cmdlet will always download fresh data from the remote repository. .EXAMPLE Update-WslBuiltinImageCache Updates the cache for builtin root filesystems from the default repository source. .EXAMPLE Update-WslBuiltinImageCache -Type Incus -Sync Forces a cache update for Incus root filesystems, using ETag validation. .EXAMPLE Update-WslBuiltinImageCache -Type Builtin -Force Forces a complete refresh of builtin root filesystems cache, ignoring both cache validity and ETag headers. .INPUTS None. You cannot pipe objects to Update-WslBuiltinImageCache. .OUTPUTS System.Boolean Returns $true if the cache was updated with new data, $false if no update was needed (cache still valid or 304 Not Modified response). .NOTES - This cmdlet requires an internet connection to fetch data from the remote repository - The source URL is determined by the WslImageSources hashtable using the Type parameter - Uses HTTP ETag headers for efficient caching and conditional requests (304 responses) - Cache is stored in the images.db SQLite database in the images directory - Cache validity period is 24 hours (86400 seconds) - If Type is not Builtin or Incus, the function returns false without performing an update - Supports ShouldProcess for -WhatIf and -Confirm scenarios .LINK https://github.com/antoinemartin/PowerShell-Wsl-Manager .COMPONENT Wsl-Manager .FUNCTIONALITY WSL Distribution Management #> [CmdletBinding(SupportsShouldProcess=$true)] param ( [Parameter(Mandatory = $false)] [WslImageType]$Type = [WslImageType]::Builtin, [switch]$Sync, [switch]$Force ) if (-not ($Type -in [WslImageType]::Builtin, [WslImageType]::Incus)) { Write-Verbose "No builtin image source defined for type $Type. Skipping cache update." return $false } $Uri = [System.Uri]$WslImageSources[$Type] $currentTime = [int][double]::Parse((Get-Date -UFormat %s)) $cacheValidDuration = 86400 # 24 hours in seconds [WslImageDatabase] $imageDb = Get-WslImageDatabase $dbCache = $imageDb.GetImageSourceCache($Type) if ($dbCache) { Write-Verbose "Cache lastUpdate: $($dbCache.LastUpdate) Current time $($currentTime), diff $($currentTime - $dbCache.LastUpdate)" if (-not $Sync) { if (($currentTime - $dbCache.LastUpdate) -lt $cacheValidDuration) { Write-Verbose "Cache is still valid, no update needed." return $false } } else { Write-Verbose "Forcing cache refresh for $Type images." } } try { $headers = @{} if ($dbCache) { if ($dbCache.Etag -and -not $Force) { Write-Verbose "Using cached ETag: $($dbCache.Etag)" $headers = @{ "If-None-Match" = $dbCache.Etag } } } Progress "Fetching $($Type) images from: $Uri" $prevProgressPreference = $global:ProgressPreference $global:ProgressPreference = 'SilentlyContinue' $response = try { Invoke-WebRequest -Uri $Uri -Headers $headers -UseBasicParsing } catch { $_.Exception.Response } finally { $global:ProgressPreference = $prevProgressPreference } if ($response.StatusCode -eq 304) { Write-Verbose "No updates found. Extending cache validity." if ($PSCmdlet.ShouldProcess($Type, "Updating cache timestamp.")) { $dbCache.LastUpdate = $currentTime $imageDb.UpdateImageSourceCache($Type, $dbCache) } return $false } if (-not $response.Content) { throw [WslManagerException]::new("The response content from $Uri is null. Please check the URL or network connection.") } $etag = $response.Headers["ETag"] # if etag is an array, take the first element if ($etag -is [array]) { # nocov $etag = $etag[0] } $imagesObjects = $response.Content | ConvertFrom-Json if ($PSCmdlet.ShouldProcess($Type, "Updating builtin images cache.")) { $imageDb.SaveImageBuiltins($Type, $imagesObjects, $etag) $cacheData = @{ Url = $Uri.AbsoluteUri LastUpdate = $currentTime Etag = $etag } $imageDb.UpdateImageSourceCache($Type, $cacheData) } Write-Verbose "Cache updated successfully." return $true } catch { if ($_.Exception -is [WslManagerException]) { throw $_.Exception } Write-Warning "Failed to update builtin root filesystems cache: $($_.Exception.Message)" throw } } function Get-WslImageSource { <# .SYNOPSIS Gets the list of WSL image sources from the local cache or remote repository. .DESCRIPTION The Get-WslImageSource cmdlet fetches WSL image sources based on various filtering criteria. It first updates the cache if needed using Update-WslBuiltinImageCache, then retrieves matching images from the local database. This provides an up-to-date list of supported images that can be used to create WSL instances. The cmdlet implements intelligent caching with ETag support to reduce network requests and improve performance. .PARAMETER Name Specifies the name(s) of image sources to retrieve. Supports wildcards for pattern matching. Can accept multiple values. .PARAMETER Distribution Filters image sources by distribution name (e.g., "ubuntu", "alpine"). .PARAMETER Source Specifies the source type filter for fetching root filesystems. Must be of type WslImageSourceType. Defaults to [WslImageSourceType]::Builtin. Valid values are: - Builtin: Official builtin images - Incus: Incus container images - All: All available sources .PARAMETER Type Specifies the exact image type to retrieve. Must be of type WslImageType. When specified, only images of this type will be returned and updated. .PARAMETER Configured When specified, filters to show only configured image sources (those that have been set up locally). .PARAMETER Id Filters image sources by their unique identifier(s). Can accept multiple GUIDs. .PARAMETER Sync Forces a synchronization with the remote repository for applicable source types, bypassing the local cache validity check. .EXAMPLE Get-WslImageSource Gets all available builtin root filesystems, updating cache if needed. .EXAMPLE Get-WslImageSource -Name "Ubuntu*" Gets all image sources with names starting with "Ubuntu". .EXAMPLE Get-WslImageSource -Source Incus -Sync Forces a fresh download of all Incus root filesystems, ignoring local cache. .EXAMPLE Get-WslImageSource -Distribution "alpine" -Configured Gets all configured Alpine Linux image sources. .EXAMPLE Get-WslImageSource -Type Builtin -Name "Debian*" Gets all builtin Debian image sources. .INPUTS None. You cannot pipe objects to Get-WslImageSource. .OUTPUTS WslImageSource[] Returns an array of WslImageSource objects representing the available images that match the specified criteria. .NOTES - This cmdlet may require an internet connection to update cache from the remote repository - The source URL is determined by the WslImageSources hashtable using the Type parameter - Returns null if the request fails or if no images are found - Uses HTTP ETag headers for efficient caching and conditional requests (304 responses) - Cache is stored in the images.db SQLite database in the images directory - Cache validity period is 24 hours (86400 seconds) - Supports complex filtering with multiple parameters that can be combined .LINK https://github.com/antoinemartin/PowerShell-Wsl-Manager .COMPONENT Wsl-Manager .FUNCTIONALITY WSL Distribution Management #> [CmdletBinding()] [OutputType([WslImageSource[]])] param ( [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [SupportsWildcards()] [string[]]$Name, [Parameter(Mandatory = $false)] [string]$Distribution, [Parameter(Mandatory = $false)] [WslImageSourceType]$Source = [WslImageSourceType]::Builtin, [Parameter(Mandatory = $false)] [WslImageType]$Type, [Parameter(Mandatory = $false)] [switch]$Configured, [Parameter(Mandatory = $false)] [guid[]]$Id, [switch]$Sync ) try { # Fetch from database # TODO: should update other types (Docker, Uri) as well if requested (Sync) [WslImageDatabase] $imageDb = Get-WslImageDatabase $operators = @() $parameters = @{} $typesInUse = @() $typesToUpdate = @() [WslImageDatabase] $imageDb = Get-WslImageDatabase if ($PSBoundParameters.ContainsKey("Type")) { $typesToUpdate += $Type $typesInUse = @($Type.ToString()) } else { if ($Source -ne [WslImageSourceType]::All) { foreach ($sourceType in [WslImageSourceType].GetEnumNames()) { if ('All' -eq $sourceType) { continue } if ($Source -band [WslImageSourceType]::$sourceType) { $typesInUse += $sourceType $typesToUpdate += $sourceType } } } else { $typesToUpdate = @([WslImageType]::Builtin, [WslImageType]::Incus) } } foreach ($typeToUpdate in $typesToUpdate) { Update-WslBuiltinImageCache -Type $typeToUpdate -Sync:$Sync | Out-Null } if ($typesInUse.Count -gt 0) { $operators += "Type IN (" + (($typesInUse | ForEach-Object { "'$_'" }) -join ", ") + ")" } if ($PSBoundParameters.ContainsKey("Id")) { $operators += "Id IN (" + (($Id | ForEach-Object { "'$($_)'" }) -join ", ") + ")" } if ($PSBoundParameters.ContainsKey("Distribution")) { $operators += "Distribution = @Distribution" $parameters["Distribution"] = $Distribution } if ($PSBoundParameters.ContainsKey("Configured")) { $operators += "Configured = @Configured" $parameters["Configured"] = if ($Configured.IsPresent) { 'TRUE' } else { 'FALSE' } } if ($Name.Length -gt 0) { $operators += ($Name | ForEach-Object { "(Name GLOB '$($_)')" }) -join " OR " } $whereClause = $operators -join " AND " Write-Verbose "Get-WslImageSource: WHERE $whereClause with parameters $($parameters | ConvertTo-Json -Compress)" $fileSystems = $imageDb.GetImageSources($whereClause, $parameters) return $fileSystems | ForEach-Object { [WslImageSource]::new($_) } } catch { if ($_.Exception -is [WslManagerException] -and -not ($_.Exception -is [WslImageSourceNotFoundException])) { throw $_.Exception } Write-Warning "Failed to retrieve image sources: $($_.Exception.Message)" # return $null } } function Remove-WslImageSource { <# .SYNOPSIS Removes one or more WSL image sources from the local cache. .DESCRIPTION The Remove-WslImageSource function removes WSL image sources from the local image database cache. It can remove sources by providing WslImageSource objects directly, by specifying source names with optional type filtering, or by GUID. The function only removes cached sources and will skip non-cached sources with a warning message. The function supports the ShouldProcess pattern, allowing -WhatIf and -Confirm parameters for safe operation. .PARAMETER ImageSource Specifies one or more WslImageSource objects to remove. This parameter accepts pipeline input and is used with the 'Source' parameter set. .PARAMETER Name Specifies the name(s) of the image source(s) to remove. Supports wildcards for pattern matching. This parameter is used with the 'Name' parameter set and is mandatory when using this parameter set. .PARAMETER Type Specifies the type of WSL image to filter by when using the Name parameter. This parameter is optional and only applies to the 'Name' parameter set. .PARAMETER Id Specifies the unique identifier (GUID) of the image source to remove. This parameter is mandatory when using the 'Id' parameter set. .INPUTS WslImageSource[] You can pipe WslImageSource objects to this function. .OUTPUTS WslImageSource Returns the WslImageSource object that was removed, with its Id set to Empty GUID. .EXAMPLE Remove-WslImageSource -Name "Ubuntu*" Removes all cached WSL image sources with names starting with "Ubuntu". .EXAMPLE Get-WslImageSource -Name "MyImage" | Remove-WslImageSource Gets a specific image source and pipes it to Remove-WslImageSource for removal. .EXAMPLE Remove-WslImageSource -Name "Alpine" -Type Builtin Removes the cached builtin WSL image source named "Alpine". .EXAMPLE Remove-WslImageSource -Id "12345678-1234-1234-1234-123456789012" Removes the image source with the specified GUID. .EXAMPLE Remove-WslImageSource -Name "Debian*" -WhatIf Shows what would happen if the command runs without actually removing anything. .NOTES - The function supports the ShouldProcess pattern for confirmation prompts - Only cached image sources will be removed; non-cached sources are skipped with a warning - Uses the WSL Image Database to perform the actual removal operation - When using the Id parameter, searches across all source types - Returns the removed image source objects with their Id property set to Empty GUID #> [CmdletBinding(SupportsShouldProcess=$true)] param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = 'Source', Position = 0)] [WslImageSource[]]$ImageSource, [Parameter(ParameterSetName = 'Name', Mandatory = $true)] [ValidateNotNullOrEmpty()] [SupportsWildcards()] [string[]]$Name, [Parameter(Mandatory = $false, ParameterSetName = 'Name')] [WslImageType]$Type, [Parameter(Mandatory = $true, ParameterSetName = 'Id')] [Guid]$Id ) process { [WslImageDatabase] $imageDb = Get-WslImageDatabase if ($PSCmdlet.ParameterSetName -eq 'Name') { $ImageSource = Get-WslImageSource -Name $Name -Type $Type } elseif ($PSCmdlet.ParameterSetName -eq 'Id') { $ImageSource = Get-WslImageSource -Id $Id -Source All } foreach ($source in $ImageSource) { if ($PSCmdlet.ShouldProcess("WslImageSource: $($source.Name)", "Removing image source")) { if (-not $source.IsCached) { Write-Warning "Image source $($source.Name) is not cached locally. Skipping removal." continue } $imageDb.RemoveImageSource($source.Id) $source.Id = [Guid]::Empty $source } } } } <# .SYNOPSIS Creates a new WSL image source from various input types. .DESCRIPTION Creates a WslImageSource object from a name, file path, or URI. The function automatically detects the input type and retrieves distribution information accordingly. It can handle local files, URLs, Docker images, and built-in distributions. .PARAMETER Name Specifies the name, file path, or URI of the WSL image source. The function will attempt to determine the type automatically. .PARAMETER File Specifies a FileInfo object representing a local WSL image file (typically a .tar.gz or .wsl file). .PARAMETER Uri Specifies a URI pointing to a WSL image. Supports http, https, docker, file, local, builtin, and incus schemes. .PARAMETER Sync Forces synchronization with remote sources to get the latest information, even if cached data exists. .INPUTS System.IO.FileInfo System.Uri .OUTPUTS WslImageSource Returns one or more WslImageSource objects containing distribution information. .EXAMPLE New-WslImageSource -Name "ubuntu-22.04-rootfs.tar.gz" Creates a WSL image source from a local file name. .EXAMPLE New-WslImageSource -Name "https://cloud-images.ubuntu.com/wsl/jammy/current/ubuntu-jammy-wsl-amd64-wsl.rootfs.tar.gz" Creates a WSL image source from a URL. .EXAMPLE Get-Item "C:\WSL\ubuntu.tar.gz" | New-WslImageSource Creates a WSL image source from a file object passed through the pipeline. .EXAMPLE New-WslImageSource -Uri "docker://ghcr.io/antoinemartin/powershell-wsl-manager/ubuntu#22.04" Creates a WSL image source from a Docker image URI. .EXAMPLE New-WslImageSource -Name "ubuntu" -Sync Creates a WSL image source for Ubuntu and forces synchronization with remote sources. .NOTES The function supports multiple input methods and automatically determines the appropriate handler based on the input type. It integrates with the WSL image database for caching and persistence. .LINK Save-WslImageSource Get-WslImageDatabase #> function New-WslImageSource { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')] [CmdletBinding()] [OutputType([WslImageSource])] param ( [Parameter(Position = 0, ParameterSetName = 'Name', Mandatory = $true)] [string]$Name, [Parameter(ParameterSetName = 'File', ValueFromPipeline = $true, Mandatory = $true)] [FileInfo]$File, [Parameter(ParameterSetName = 'Uri', ValueFromPipeline = $true, Mandatory = $true)] [Uri]$Uri, [Parameter(ValueFromPipeline = $false, Mandatory = $false)] [switch]$Sync ) process { if ($PSCmdlet.ParameterSetName -eq "Name") { try { $CandidateFile = [FileInfo]::new($Name) } catch { # nocov $CandidateFile = $null } if ($null -ne $CandidateFile -and $CandidateFile.Exists) { Write-Verbose "Interpreting Name parameter as existing file path: $($CandidateFile.FullName)" $File = $CandidateFile } else { $CandidateUri = [Uri]::new($Name, [UriKind]::RelativeOrAbsolute) if ($CandidateUri.IsAbsoluteUri) { Write-Verbose "Interpreting Name parameter as absolute URI: $($CandidateUri.AbsoluteUri)" $Uri = $CandidateUri } } } $result = $null if ($null -ne $Uri) { Write-Verbose "Creating WslImageSource by URI: $Uri ($($Uri.Scheme))" [WslImageDatabase] $db = Get-WslImageDatabase $result = $db.GetImageSources("Url Like @Url ORDER BY Type", @{ Url = $Uri.AbsoluteUri + '%' }) if (-not $result -or $Sync) { $existing = if ($result) { $result[0] } else { $null } $result = Get-DistributionInformationFromUri -Uri $Uri if ($existing) { # Copy all result properties to existing WslImageSource Write-Verbose "Updating existing WslImage (Id: $($existing.Id)) with new information from URI: $($Uri.AbsoluteUri)" foreach ($key in $result.Keys) { if ($existing.PSObject.Properties.Match($key).Count -eq 0) { if ($key -eq 'FileHash') { $existing.Digest = $result[$key] } else { Write-Verbose "Skipping unknown property $key with value $($result[$key])" } continue } $existing.$key = $result[$key] } # Save existing WslImage to database Write-Verbose "Saving updated WslImage (Id: $($existing.Id)) to database" $db.SaveImageSource($existing) Write-Verbose "Returning updated WslImage from database" $result = $existing $result.UpdateDate = [System.DateTime]::Now } } else { Write-Verbose "Found $($result.Count) matching images in database for URL: $($Uri.AbsoluteUri)" } } elseif ($null -ne $File) { Write-Verbose "Creating WslImage by file: $($File.FullName) (exists: $($File.Exists))" $result = Get-DistributionInformationFromFile -File $File } else { Write-Verbose "Creating WslImage by name: $Name" $result = Get-DistributionInformationFromName -Name $Name } if ($result) { $result = $result | ForEach-Object { [WslImageSource]::new($_) } } return $result } } <# .SYNOPSIS Saves a WSL image source to the database. .DESCRIPTION Saves an existing WslImageSource object to the WSL image database. If the ImageSource doesn't have an ID, a new GUID is generated. The function supports PowerShell's ShouldProcess pattern for safe execution. .PARAMETER ImageSource Specifies the WslImageSource object to save to the database. .INPUTS WslImageSource Accepts WslImageSource objects from the pipeline. .OUTPUTS WslImageSource Returns the saved WslImageSource object. .EXAMPLE $imageSource = New-WslImageSource -Name "ubuntu-22.04" $imageSource | Save-WslImageSource Saves the WSL image source to the database. .EXAMPLE Get-WslImageSource -Name "ubuntu" | Save-WslImageSource -WhatIf Shows what would happen when saving Ubuntu image sources without actually performing the save. .EXAMPLE $imageSource = New-WslImageSource -Name "alpine" $imageSource.Configured = $true $imageSource | Save-WslImageSource -Verbose Saves an Alpine image source with verbose output after modifying its properties. .NOTES This function is typically used after creating or modifying a WslImageSource object to persist changes to the database. It supports the -WhatIf and -Confirm parameters for safe execution. .LINK New-WslImageSource Get-WslImageDatabase #> function Save-WslImageSource { [CmdletBinding(SupportsShouldProcess = $true)] [OutputType([WslImageSource])] param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] [WslImageSource]$ImageSource ) process { Write-Verbose "Saving WslImageSource Id: $($ImageSource.Id), Name: $($ImageSource.Name)" if ([Guid]::Empty -eq $ImageSource.Id) { $ImageSource.Id = [Guid]::NewGuid() } if ($PSCmdlet.ShouldProcess("WslImageSource Id: $($ImageSource.Id)", "Save")) { [WslImageDatabase] $db = Get-WslImageDatabase $db.SaveImageSource($ImageSource.ToObject()) } return $ImageSource } } <# .SYNOPSIS Updates a WSL image source with the latest information from its URL. .DESCRIPTION This function takes a WslImageSource object and updates its properties by fetching the latest distribution information from the source URL. The function supports WhatIf and Confirm parameters for safe execution. .PARAMETER ImageSource The WslImageSource object to update. This parameter is mandatory and accepts pipeline input. .INPUTS WslImageSource - The WSL image source object to be updated. .OUTPUTS WslImageSource - Returns the updated WSL image source object. .EXAMPLE Update-WslImageSource -ImageSource $myImageSource Updates the specified WSL image source with latest information from its URL. .EXAMPLE $imageSource | Update-WslImageSource -WhatIf Shows what would happen if the image source was updated without actually performing the update. .NOTES This function uses Get-DistributionInformationFromUri internally to fetch the latest distribution information and supports PowerShell's ShouldProcess pattern for confirmation prompts. #> function Update-WslImageSource { [CmdletBinding(SupportsShouldProcess = $true)] [OutputType([WslImageSource])] param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] [WslImageSource]$ImageSource ) process { Write-Verbose "Updating WslImageSource Id: $($ImageSource.Id), Name: $($ImageSource.Name)" if ($PSCmdlet.ShouldProcess("WslImageSource Id: $($ImageSource.Id)", "Update")) { try { if (-not $ImageSource.Url) { Write-Warning "The WslImageSource $($ImageSource.Name) (Id: $($ImageSource.Id)) does not have a URL to update from." } else { $result = Get-DistributionInformationFromUri -Uri $ImageSource.Url if ($null -ne $result) { $result.Name = $ImageSource.Name $ImageSource.InitFromObject($result) if ($ImageSource.IsCached -and $PSCmdlet.ShouldProcess("WslImageSource Id: $($ImageSource.Id)", "Save updated image source to database")) { $ImageSource.UpdateDate = [System.DateTime]::Now $db = Get-WslImageDatabase $db.SaveImageSource($ImageSource.ToObject()) } } } } catch { if ($_.Exception -is [WslImageSourceNotFoundException]) { Write-Warning "Failed to update WslImageSource from URL $($ImageSource.Url): $($_.Exception.Message)" } else { throw $_.Exception } } } return $ImageSource } } |