winget-cli

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README | LICENSE

Microsoft.WinGet.DSC.psm1 (17746B)


      1 # Copyright (c) Microsoft Corporation. All rights reserved.
      2 # Licensed under the MIT License.
      3 
      4 using namespace System.Collections.Generic
      5 
      6 # Check that we are running as an administrator
      7 function Assert-IsAdministrator
      8 {
      9     $windowsIdentity = [System.Security.Principal.WindowsIdentity]::GetCurrent()
     10     $windowsPrincipal = New-Object -TypeName 'System.Security.Principal.WindowsPrincipal' -ArgumentList @( $windowsIdentity )
     11 
     12     $adminRole = [System.Security.Principal.WindowsBuiltInRole]::Administrator
     13 
     14     if (-not $windowsPrincipal.IsInRole($adminRole))
     15     {
     16         New-InvalidOperationException -Message "This resource must run as an Administrator."
     17     }
     18 }
     19 
     20 #region enums
     21 enum WinGetAction
     22 {
     23     Partial
     24     Full
     25 }
     26 
     27 enum WinGetEnsure
     28 {
     29     Absent
     30     Present
     31 }
     32 
     33 enum WinGetMatchOption
     34 {
     35     Equals
     36     EqualsCaseInsensitive
     37     StartsWithCaseInsensitive
     38     ContainsCaseInsensitive
     39 }
     40 
     41 enum WinGetInstallMode
     42 {
     43     Default
     44     Silent
     45     Interactive
     46 }
     47 
     48 enum WinGetTrustLevel
     49 {
     50     Undefined
     51     None
     52     Trusted
     53 }
     54 
     55 #endregion enums
     56 
     57 #region DscResources
     58 # Author here all DSC Resources.
     59 # DSC Powershell doesn't support binary DSC resources without the MOF schema.
     60 # DSC Powershell classes aren't discoverable if placed outside of the psm1.
     61 
     62 # This resource is in charge of managing the settings.json file of winget.
     63 [DSCResource()]
     64 class WinGetUserSettings
     65 {
     66     # We need a key. Do not set.
     67     [DscProperty(Key)]
     68     [string]$SID
     69 
     70     # A hash table with the desired settings.
     71     [DscProperty(Mandatory)]
     72     [Hashtable]$Settings
     73 
     74     [DscProperty()]
     75     [WinGetAction]$Action = [WinGetAction]::Full
     76 
     77     # Gets the current UserSettings by looking at the settings.json file for the current user.
     78     [WinGetUserSettings] Get()
     79     {
     80         $userSettings = Get-WinGetUserSetting
     81         $result = @{
     82             SID = ''
     83             Settings = $userSettings
     84         }
     85         return $result
     86     }
     87 
     88     # Tests if desired properties match.
     89     [bool] Test()
     90     {
     91         $hashArgs = @{
     92             UserSettings = $this.Settings
     93         }
     94 
     95         if ($this.Action -eq [WinGetAction]::Partial)
     96         {
     97             $hashArgs.Add('IgnoreNotSet', $true)
     98         }
     99 
    100         return Test-WinGetUserSetting @hashArgs
    101     }
    102 
    103     # Sets the desired properties.
    104     [void] Set()
    105     {
    106         $hashArgs = @{
    107             UserSettings = $this.Settings
    108         }
    109 
    110         if ($this.Action -eq [WinGetAction]::Partial)
    111         {
    112             $hashArgs.Add('Merge', $true)
    113         }
    114 
    115         Set-WinGetUserSetting @hashArgs
    116     }
    117 }
    118 
    119 # Handles configuration of administrator settings.
    120 [DSCResource()]
    121 class WinGetAdminSettings
    122 {
    123     # We need a key. Do not set.
    124     [DscProperty(Key)]
    125     [string]$SID
    126 
    127     # A hash table with the desired admin settings.
    128     [DscProperty(Mandatory)]
    129     [Hashtable]$Settings
    130 
    131     # Gets the administrator settings.
    132     [WinGetAdminSettings] Get()
    133     {
    134         $settingsJson = Get-WinGetSetting
    135         # Get admin setting values.
    136 
    137         $result = @{
    138             SID = ''
    139             Settings = $settingsJson.adminSettings
    140         }
    141         return $result
    142     }
    143 
    144     # Tests if administrator settings given are set as expected.
    145     # This doesn't do a full comparison to allow users to don't have to update
    146     # their resource every time a new admin setting is added on winget.
    147     [bool] Test()
    148     {
    149         $adminSettings = $this.Get().Settings
    150         foreach ($adminSetting in $adminSettings.GetEnumerator())
    151         {
    152             if ($this.Settings.ContainsKey($adminSetting.Name))
    153             {
    154                 if ($this.Settings[$adminSetting.Name] -ne $adminSetting.Value)
    155                 {
    156                     return $false
    157                 }
    158             }
    159         }
    160 
    161         return $true
    162     }
    163 
    164     # Sets the desired properties.
    165     [void] Set()
    166     {
    167         Assert-IsAdministrator
    168 
    169         # It might be better to implement an internal Test with one value, or
    170         # create a new instances with only one setting than calling Enable/Disable
    171         # for all of them even if only one is different.
    172         if (-not $this.Test())
    173         {
    174             foreach ($adminSetting in $this.Settings.GetEnumerator())
    175             {
    176                 if ($adminSetting.Value)
    177                 {
    178                     Enable-WinGetSetting -Name $adminSetting.Name
    179                 }
    180                 else
    181                 {
    182                     Disable-WinGetSetting -Name $adminSetting.Name
    183                 }
    184             }
    185         }
    186     }
    187 }
    188 
    189 [DSCResource()]
    190 class WinGetSource
    191 {
    192     [DscProperty(Key, Mandatory)]
    193     [string]$Name
    194     
    195     [DscProperty(Mandatory)]
    196     [string]$Argument
    197     
    198     [DscProperty()]
    199     [string]$Type
    200     
    201     [DscProperty()]
    202     [WinGetTrustLevel]$TrustLevel = [WinGetTrustLevel]::Undefined
    203     
    204     [DscProperty()]
    205     [nullable[bool]]$Explicit = $null
    206 
    207     [DscProperty()]
    208     [WinGetEnsure]$Ensure = [WinGetEnsure]::Present
    209 
    210     [WinGetSource] Get()
    211     {
    212         if ([String]::IsNullOrWhiteSpace($this.Name))
    213         {
    214             throw "A value must be provided for WinGetSource::Name"
    215         }
    216 
    217         $currentSource = $null
    218 
    219         try {
    220             $currentSource = Get-WinGetSource -Name $this.Name
    221         }
    222         catch {
    223         }
    224 
    225         $result = [WinGetSource]::new()
    226 
    227         if ($currentSource)
    228         {
    229             $result.Ensure = [WinGetEnsure]::Present
    230             $result.Name = $currentSource.Name
    231             $result.Argument = $currentSource.Argument
    232             $result.Type = $currentSource.Type
    233             $result.TrustLevel = $currentSource.TrustLevel
    234             $result.Explicit = $currentSource.Explicit
    235         }
    236         else
    237         {
    238             $result.Ensure = [WinGetEnsure]::Absent
    239             $result.Name = $this.Name
    240         }
    241 
    242         return $result
    243     }
    244 
    245     [bool] Test()
    246     {
    247         return $this.TestAgainstCurrent($this.Get())
    248     }
    249 
    250     [void] Set()
    251     {
    252         Assert-IsAdministrator
    253 
    254         $currentSource = $this.Get()
    255 
    256         $removeSource = $false
    257         $resetSource = $false
    258         $addSource = $false
    259 
    260         if ($this.Ensure -eq [WinGetEnsure]::Present)
    261         {
    262             if ($currentSource.Ensure -eq [WinGetEnsure]::Present)
    263             {
    264                 if (-not $this.TestAgainstCurrent($currentSource))
    265                 {
    266                     $resetSource = $true
    267                     $addSource = $true
    268                 }
    269                 # else in desired state
    270             }
    271             else
    272             {
    273                 $addSource = $true
    274             }
    275         }
    276         else
    277         {
    278             if ($currentSource.Ensure -eq [WinGetEnsure]::Present)
    279             {
    280                 $removeSource = $true
    281             }
    282             # else in desired state (Absent)
    283         }
    284 
    285         if ($removeSource)
    286         {
    287             Remove-WinGetSource -Name $this.Name
    288         }
    289         # Only remove OR reset should be true, not both
    290         elseif ($resetSource)
    291         {
    292             Reset-WinGetSource -Name $this.Name
    293         }
    294 
    295         if ($addSource)
    296         {
    297             $hashArgs = @{
    298                 Name = $this.Name
    299                 Argument = $this.Argument
    300             }
    301 
    302             if (-not [string]::IsNullOrWhiteSpace($this.Type))
    303             {
    304                 $hashArgs.Add("Type", $this.Type)
    305             }
    306 
    307             if ($this.TrustLevel -ne [WinGetTrustLevel]::Undefined)
    308             {
    309                 $hashArgs.Add("TrustLevel", $this.TrustLevel)
    310             }
    311 
    312             if ($null -ne $this.Explicit)
    313             {
    314                 $hashArgs.Add("Explicit", $this.Explicit)
    315             }
    316 
    317             Add-WinGetSource @hashArgs
    318         }
    319     }
    320     
    321     # Test $this against a value retrieved from Get
    322     # We don't need to check Name because it is the Key for Get
    323     [bool] hidden TestAgainstCurrent([WinGetSource]$currentSource)
    324     {
    325         if ($this.Ensure -eq [WinGetEnsure]::Absent -and
    326             $currentSource.Ensure -eq [WinGetEnsure]::Absent)
    327         {
    328             return $true
    329         }
    330 
    331         if ($this.Ensure -ne $currentSource.Ensure -or
    332             $this.Argument -ne $currentSource.Argument)
    333         {
    334             return $false
    335         }
    336 
    337         if (-not([string]::IsNullOrWhiteSpace($this.Type)) -and
    338             $this.Type -ne $currentSource.Type)
    339         {
    340             return $false
    341         }
    342 
    343         if ($this.TrustLevel -ne [WinGetTrustLevel]::Undefined -and
    344             $this.TrustLevel -ne $currentSource.TrustLevel)
    345         {
    346             return $false
    347         }
    348 
    349         if ($null -ne $this.Explicit -and
    350             $this.Explicit -ne $currentSource.Explicit)
    351         {
    352             return $false
    353         }
    354 
    355         return $true
    356     }
    357 }
    358 
    359 # TODO: It would be nice if these resource has a non configurable property that has extra information that comes from
    360 # GitHub. We could implement it here or add more cmdlets in Microsoft.WinGet.Client.
    361 [DSCResource()]
    362 class WinGetPackageManager
    363 {
    364     # We need a key. Do not set.
    365     [DscProperty(Key)]
    366     [string]$SID
    367 
    368     [DscProperty()]
    369     [string]$Version = ""
    370 
    371     [DscProperty()]
    372     [bool]$UseLatest
    373 
    374     [DscProperty()]
    375     [bool]$UseLatestPreRelease
    376 
    377     # If winget is not installed the version will be empty.
    378     [WinGetPackageManager] Get()
    379     {
    380         $integrityResource = [WinGetPackageManager]::new()
    381         if ($integrityResource.Test())
    382         {
    383             $integrityResource.Version = Get-WinGetVersion
    384         }
    385 
    386         return $integrityResource
    387     }
    388 
    389     # Tests winget is installed.
    390     [bool] Test()
    391     {
    392         try
    393         {
    394             $hashArgs = @{}
    395 
    396             if ($this.UseLatest)
    397             {
    398                 $hashArgs.Add("Latest", $true)
    399             } elseif ($this.UseLatestPreRelease)
    400             {
    401                 $hashArgs.Add("Latest", $true)
    402                 $hashArgs.Add("IncludePrerelease", $true)
    403             } elseif (-not [string]::IsNullOrWhiteSpace($this.Version))
    404             {
    405                 $hashArgs.Add("Version", $this.Version)
    406             }
    407 
    408             Assert-WinGetPackageManager @hashArgs
    409         }
    410         catch
    411         {
    412             return $false
    413         }
    414 
    415         return $true
    416     }
    417 
    418     # Repairs Winget.
    419     [void] Set()
    420     {
    421         if (-not $this.Test())
    422         {
    423             $result = -1
    424             $hashArgs = @{}
    425 
    426             if ($this.UseLatest)
    427             {
    428                 $hashArgs.Add("Latest", $true)
    429             } elseif ($this.UseLatestPreRelease)
    430             {
    431                 $hashArgs.Add("Latest", $true)
    432                 $hashArgs.Add("IncludePrerelease", $true)
    433             } elseif (-not [string]::IsNullOrWhiteSpace($this.Version))
    434             {
    435                 $hashArgs.Add("Version", $this.Version)
    436             }
    437 
    438             $result = Repair-WinGetPackageManager @hashArgs
    439 
    440             if ($result -ne 0)
    441             {
    442                 # TODO: Localize.
    443                 throw "Failed to repair winget. Result $result"
    444             }
    445         }
    446     }
    447 }
    448 
    449 [DSCResource()]
    450 class WinGetPackage
    451 {
    452     [DscProperty(Key, Mandatory)]
    453     [string]$Id
    454 
    455     [DscProperty(Key)]
    456     [string]$Source
    457 
    458     [DscProperty()]
    459     [string]$Version
    460 
    461     [DscProperty()]
    462     [WinGetEnsure]$Ensure = [WinGetEnsure]::Present
    463 
    464     [DscProperty()]
    465     [WinGetMatchOption]$MatchOption = [WinGetMatchOption]::EqualsCaseInsensitive
    466 
    467     [DscProperty()]
    468     [bool]$UseLatest = $false
    469 
    470     [DSCProperty()]
    471     [WinGetInstallMode]$InstallMode = [WinGetInstallMode]::Silent
    472 
    473     [PSObject] hidden $CatalogPackage = $null
    474 
    475     [WinGetPackage] Get()
    476     {
    477         if ([String]::IsNullOrWhiteSpace($this.Id))
    478         {
    479             throw "A value must be provided for WinGetPackage::Id"
    480         }
    481 
    482         $result = [WinGetPackage]::new()
    483 
    484         $hashArgs = @{
    485             Id = $this.Id
    486             MatchOption = $this.MatchOption
    487         }
    488 
    489         if (-not([string]::IsNullOrWhiteSpace($this.Source)))
    490         {
    491             $hashArgs.Add("Source", $this.Source)
    492         }
    493 
    494         $result.CatalogPackage = Get-WinGetPackage @hashArgs
    495         if ($null -ne $result.CatalogPackage)
    496         {
    497             $result.Ensure = [WinGetEnsure]::Present
    498             $result.Id = $result.CatalogPackage.Id
    499             $result.Source = $result.CatalogPackage.Source
    500             $result.Version = $result.CatalogPackage.InstalledVersion
    501             $result.UseLatest = -not $result.CatalogPackage.IsUpdateAvailable
    502         }
    503         else
    504         {
    505             $result.Ensure = [WinGetEnsure]::Absent
    506             $result.Id = $this.Id
    507             $result.MatchOption = $this.MatchOption
    508             $result.Source = $this.Source
    509         }
    510 
    511         return $result
    512     }
    513 
    514     [bool] Test()
    515     {
    516         return $this.TestAgainstCurrent($this.Get())
    517     }
    518 
    519     [void] Set()
    520     {
    521         $currentPackage = $this.Get()
    522 
    523         if (-not $this.TestAgainstCurrent($currentPackage))
    524         {
    525             $hashArgs = @{
    526                 Id = $this.Id
    527                 MatchOption = $this.MatchOption
    528                 Mode = $this.InstallMode
    529             }
    530             
    531             if ($this.Ensure -eq [WinGetEnsure]::Present)
    532             {
    533                 if (-not([string]::IsNullOrWhiteSpace($this.Source)))
    534                 {
    535                     $hashArgs.Add("Source", $this.Source)
    536                 }
    537 
    538                 if ($currentPackage.Ensure -eq [WinGetEnsure]::Present)
    539                 {
    540                     if ($this.UseLatest)
    541                     {
    542                         $this.TryUpdate($hashArgs)
    543                     }
    544                     elseif (-not([string]::IsNullOrWhiteSpace($this.Version)))
    545                     {
    546                         $hashArgs.Add("Version", $this.Version)
    547 
    548                         $compareResult = $currentPackage.CatalogPackage.CompareToVersion($this.Version)
    549                         switch ($compareResult)
    550                         {
    551                             'Lesser'
    552                             {
    553                                 $this.TryUpdate($hashArgs)
    554                                 break
    555                             }
    556                             {'Greater' -or 'Unknown'}
    557                             {
    558                                 # The installed package has a greater version or unknown. Uninstall and install.
    559                                 $this.Uninstall()
    560                                 $this.Install($hashArgs)
    561                                 break
    562                             }
    563                         }
    564                     }
    565                 }
    566                 else
    567                 {
    568                     if (-not([string]::IsNullOrWhiteSpace($this.Version)))
    569                     {
    570                         $hashArgs.Add("Version", $this.Version)
    571                     }
    572 
    573                     $this.Install($hashArgs)
    574                 }
    575             }
    576             else
    577             {
    578                 $this.Uninstall()
    579             }
    580         }
    581     }
    582     
    583     [bool] hidden TestAgainstCurrent([WinGetPackage]$currentPackage)
    584     {
    585         if ($this.Ensure -eq [WinGetEnsure]::Absent -and
    586             $currentPackage.Ensure -eq [WinGetEnsure]::Absent)
    587         {
    588             return $true
    589         }
    590 
    591         $this.CatalogPackage = $currentPackage.CatalogPackage
    592 
    593         if ($this.Ensure -ne $currentPackage.Ensure)
    594         {
    595             return $false
    596         }
    597 
    598         # At this point we know is installed.
    599         # If asked for latest, but there are updates available.
    600         if ($this.UseLatest)
    601         {
    602             if (-not $currentPackage.UseLatest)
    603             {
    604                 return $false
    605             }
    606         }
    607         # If there is an specific version, compare with the current installed version.
    608         elseif (-not ([string]::IsNullOrWhiteSpace($this.Version)))
    609         {
    610             $compareResult = $currentPackage.CatalogPackage.CompareToVersion($this.Version)
    611             if ($compareResult -ne 'Equal')
    612             {
    613                 return $false
    614             }
    615         }
    616 
    617         return $true
    618     }
    619 
    620     hidden Install([Hashtable]$hashArgs)
    621     {
    622         $installResult = Install-WinGetPackage @hashArgs
    623         if (-not $installResult.Succeeded())
    624         {
    625             # TODO: Localize.
    626             throw "WinGetPackage Failed installing $($this.Id). $($installResult.ErrorMessage())"
    627         }
    628     }
    629 
    630     hidden Uninstall()
    631     {
    632         $uninstallResult = Uninstall-WinGetPackage -PSCatalogPackage $this.CatalogPackage
    633         if (-not $uninstallResult.Succeeded())
    634         {
    635             # TODO: Localize.
    636             throw "WinGetPackage Failed uninstalling $($this.Id). $($uninstallResult.ErrorMessage())"
    637         }
    638     }
    639 
    640     hidden Update([Hashtable]$hashArgs)
    641     {
    642         $updateResult = Update-WinGetPackage @hashArgs
    643         if (-not $updateResult.Succeeded())
    644         {
    645             # TODO: Localize.
    646             throw "WinGetPackage Failed updating $($this.Id). $($updateResult.ErrorMessage())"
    647         }
    648     }
    649 
    650     # Tries to update, if not, uninstall and install.
    651     hidden TryUpdate([Hashtable]$hashArgs)
    652     {
    653         try
    654         {
    655             $this.Update($hashArgs)
    656         }
    657         catch
    658         {
    659             $this.Uninstall()
    660             $this.Install($hashArgs)
    661         }
    662     }
    663 }
    664 
    665 #endregion DscResources