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