diff --git a/.site/hugo.yaml b/.site/hugo.yaml index 05183d2..ff62813 100644 --- a/.site/hugo.yaml +++ b/.site/hugo.yaml @@ -36,7 +36,8 @@ markup: module: replacements: | - github.com/PowerShell/DSC-Samples/go/resources/first -> go/resources/first + github.com/PowerShell/DSC-Samples/go/resources/first -> go/resources/first, + github.com/PowerShell/DSC-Samples/pwsh/resources/first -> pwsh/resources/first imports: - path: github.com/platenio/platen/modules/platen - path: github.com/PowerShell/DSC-Samples/go/resources/first @@ -45,3 +46,9 @@ module: target: content/tutorials/first-resource/go - source: docs target: content/languages/go/first-resource + - path: github.com/PowerShell/DSC-Samples/pwsh/resources/first + mounts: + - source: docs + target: content/tutorials/first-resource/pwsh + - source: docs + target: content/languages/pwsh/first-resource diff --git a/docs/languages/pwsh/_index.md b/docs/languages/pwsh/_index.md new file mode 100644 index 0000000..c6d0711 --- /dev/null +++ b/docs/languages/pwsh/_index.md @@ -0,0 +1,8 @@ +--- +title: DSC and PowerShell tutorials +dscs: + languages_title: PowerShell +platen: + menu: + collapse_section: true +--- diff --git a/samples/pwsh/resources/first/.gitignore b/samples/pwsh/resources/first/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/samples/pwsh/resources/first/DscSamples.TailspinToys/DscSamples.TailspinToys.psd1 b/samples/pwsh/resources/first/DscSamples.TailspinToys/DscSamples.TailspinToys.psd1 new file mode 100644 index 0000000..f381405 --- /dev/null +++ b/samples/pwsh/resources/first/DscSamples.TailspinToys/DscSamples.TailspinToys.psd1 @@ -0,0 +1,19 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +@{ + RootModule = 'DscSamples.TailspinToys.psm1' + ModuleVersion = '0.0.1' + GUID = '1dc8e453-6197-4557-82fe-429736fdd358' + Author = 'mlombardi' + CompanyName = 'Unknown' + Copyright = '(c) mlombardi. All rights reserved.' + FunctionsToExport = '*' + CmdletsToExport = '*' + VariablesToExport = '*' + AliasesToExport = '*' + DscResourcesToExport = 'PSTailspinToys' + PrivateData = @{ + PSData = @{} + } +} diff --git a/samples/pwsh/resources/first/DscSamples.TailspinToys/DscSamples.TailspinToys.psm1 b/samples/pwsh/resources/first/DscSamples.TailspinToys/DscSamples.TailspinToys.psm1 new file mode 100644 index 0000000..94f02a9 --- /dev/null +++ b/samples/pwsh/resources/first/DscSamples.TailspinToys/DscSamples.TailspinToys.psm1 @@ -0,0 +1,238 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +[DscResource()] +class PSTailspinToys { + [DscProperty(Key)] [TailspinScope] + $ConfigurationScope + + [DscProperty()] [TailspinEnsure] + $Ensure = [TailspinEnsure]::Present + + [DscProperty()] [bool] + $UpdateAutomatically + + [DscProperty()] [int] [ValidateRange(1, 90)] + $UpdateFrequency + + hidden [PSTailspinToys] $CachedCurrentState + hidden [PSCustomObject] $CachedData + hidden [System.Management.Automation.ApplicationInfo] $CachedApplicationInfo + hidden [string] $CachedConfigurationFilePath + + [PSTailspinToys] Get() { + $CurrentState = [PSTailspinToys]::new() + + $CurrentState.ConfigurationScope = $this.ConfigurationScope + + $FilePath = $this.GetConfigurationFilePath() + + if (!(Test-Path -path $FilePath)) { + $CurrentState.Ensure = [TailspinEnsure]::Absent + $this.CachedCurrentState = $CurrentState + + return $CurrentState + } + + $Data = $this.GetConfigurationData() + + if ($null -ne $Data.Updates.Automatic) { + $CurrentState.UpdateAutomatically = $Data.Updates.Automatic + } + + if ($null -ne $Data.Updates.CheckFrequency) { + $CurrentState.UpdateFrequency = $Data.Updates.CheckFrequency + } + + $this.CachedCurrentState = $CurrentState + + return $CurrentState + } + + [bool] Test() { + $CurrentState = $this.Get() + + if ($CurrentState.Ensure -ne $this.Ensure) { + return $false + } + + if ($CurrentState.Ensure -eq [TailspinEnsure]::Absent) { + return $true + } + + if ($CurrentState.UpdateAutomatically -ne $this.UpdateAutomatically) { + return $false + } + + if ($this.UpdateFrequency -eq 0) { + return $true + } + + if ($CurrentState.UpdateFrequency -ne $this.UpdateFrequency) { + return $false + } + + return $true + } + + [void] Set() { + if ($this.Test()) { + return + } + + Write-Warning "Setting for scope $($this.ConfigurationScope)" + $CurrentState = $this.CachedCurrentState + $IsAbsent = $CurrentState.Ensure -eq [TailspinEnsure]::Absent + $ShouldBeAbsent = $this.Ensure -eq [TailspinEnsure]::Absent + + if ($IsAbsent) { + Write-Warning "Creating $($this.CachedConfigurationFilePath)" + $this.Create() + } elseif ($ShouldBeAbsent) { + Write-Warning "Removing $($this.CachedConfigurationFilePath)" + $this.Remove() + } else { + Write-Warning "Updating $($this.CachedConfigurationFilePath)" + $this.Update() + } + } + + [void] Create() { + $ErrorActionPreference = 'Stop' + + $Json = $this.ToConfigurationJson() + + $FilePath = $this.GetConfigurationFilePath() + $FolderPath = Split-Path -Path $FilePath + + if (!(Test-Path -Path $FolderPath)) { + New-Item -Path $FolderPath -ItemType Directory -Force + } + + Set-Content -Path $FilePath -Value $Json -Encoding utf8 -Force + } + + [void] Remove() { + Remove-Item -Path $this.GetConfigurationFilePath() -Force -ErrorAction Stop + } + + [void] Update() { + $ErrorActionPreference = 'Stop' + + $Json = $this.ToConfigurationJson() + $FilePath = $this.GetConfigurationFilePath() + + Set-Content -Path $FilePath -Value $Json -Encoding utf8 -Force + } + + [string] ToConfigurationJson() { + $config = @{ + updates = @{ + automatic = $this.UpdateAutomatically + } + } + + if ($this.CachedData) { + # Copy unmanaged settings without changing the cached values + $this.CachedData | + Get-Member -MemberType NoteProperty | + Where-Object -Property Name -NE -Value 'updates' | + ForEach-Object -Process { + $setting = $_.Name + $config.$setting = $this.CachedData.$setting + } + + # Add the checkFrequency to the hashtable if it is set in the cache + if ($frequency = $this.CachedData.updates.checkFrequency) { + $config.updates.checkFrequency = $frequency + } + } + + # If the user specified an UpdateFrequency, use that value + if ($this.UpdateFrequency -ne 0) { + $config.updates.checkFrequency = $this.UpdateFrequency + } + + return ($config | ConvertTo-Json -Depth 99) + } + + [System.Management.Automation.ApplicationInfo] GetApplicationInfo() { + if ($null -ne $this.CachedApplicationInfo) { + return $this.CachedApplicationInfo + } + + try { + $Parameters = @{ + Name = 'tstoy' + CommandType = 'Application' + ErrorAction = 'Stop' + } + $this.CachedApplicationInfo = Get-Command @Parameters + | Select-Object -First 1 + } catch [System.Management.Automation.CommandNotFoundException] { + throw [System.Management.Automation.CommandNotFoundException]::new( + "tstoy application not found, unable to retrieve path to configuration file", + $_ + ) + } + + return $this.CachedApplicationInfo + } + + [string] GetConfigurationFilePath() { + if (-not ([string]::IsNullOrEmpty($this.CachedConfigurationFilePath))) { + return $this.CachedConfigurationFilePath + } + + $this.GetApplicationInfo() + + $Arguments = @( + 'show' + 'path' + $this.ConfigurationScope.ToString().ToLower() + ) + + $this.CachedConfigurationFilePath = & $this.CachedApplicationInfo @Arguments + + return $this.CachedConfigurationFilePath + } + + [PSCustomObject] GetConfigurationData() { + if ($null -ne $this.CachedData) { + return $this.CachedData + } + + $ConfigurationFilePath = $this.GetConfigurationFilePath() + + if (-not (Test-Path -Path $ConfigurationFilePath)) { + return $null + } + + try { + $GetParameters = @{ + Path = $ConfigurationFilePath + Raw = $true + } + + $this.CachedData = Get-Content @GetParameters + | ConvertFrom-Json -ErrorAction Stop + } catch [Newtonsoft.Json.JsonReaderException] { + throw [Newtonsoft.Json.JsonReaderException]::new( + "Unable to load TSToy configuration data from $ConfigurationFilePath", + $_ + ) + } + + return $this.CachedData + } +} + +enum TailspinScope { + Machine + User +} + +enum TailspinEnsure { + Absent + Present +} diff --git a/samples/pwsh/resources/first/DscSamples.TailspinToys/Helpers.ps1 b/samples/pwsh/resources/first/DscSamples.TailspinToys/Helpers.ps1 new file mode 100644 index 0000000..b70bcaf --- /dev/null +++ b/samples/pwsh/resources/first/DscSamples.TailspinToys/Helpers.ps1 @@ -0,0 +1,5 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +$env:PSModulePath += [System.IO.Path]::PathSeparator + $pwd +$MachinePath,$UserPath = tstoy show path \ No newline at end of file diff --git a/samples/pwsh/resources/first/PSTailspinToys.dsc.config.yaml b/samples/pwsh/resources/first/PSTailspinToys.dsc.config.yaml new file mode 100644 index 0000000..2861b76 --- /dev/null +++ b/samples/pwsh/resources/first/PSTailspinToys.dsc.config.yaml @@ -0,0 +1,22 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +$schema: https://schemas.microsoft.com/dsc/2023/03/configuration.schema.json +resources: +- name: TSToy PowerShell resources + type: DSC/PowerShellGroup + properties: + resources: + - name: All Users Configuration + type: DscSamples.TailspinToys/PSTailspinToys + properties: + ConfigurationScope: Machine + Ensure: Present + UpdateAutomatically: false + - name: Current User Configuration + type: DscSamples.TailspinToys/PSTailspinToys + properties: + ConfigurationScope: User + Ensure: Present + UpdateAutomatically: true + UpdateFrequency: 30 diff --git a/samples/pwsh/resources/first/docs/1-scaffold-module.md b/samples/pwsh/resources/first/docs/1-scaffold-module.md new file mode 100644 index 0000000..569efe4 --- /dev/null +++ b/samples/pwsh/resources/first/docs/1-scaffold-module.md @@ -0,0 +1,108 @@ +--- +title: Step 1 - Scaffold a DSC Resource module +weight: 1 +dscs: + menu_title: 1. Scaffold a module +--- + +PowerShell resources are defined in PowerShell modules. + +## Create the module folder + +Create a new folder called `DscSamples.TailspinToys`. This folder is used as the root folder for +the module and all code in this tutorial. + +```powershell +New-Item -Path './DscSamples.TailspinToys' -ItemType Directory +``` + +```console + Directory: C:\code\dsc + +Mode LastWriteTime Length Name +---- ------------- ------ ---- +d---- 8/14/2023 3:56 PM DscSamples.TailspinToys +``` + +### Use VS Code to author the module + +Open the `DscSamples.TailspinToys` folder in VS Code. Open the integrated terminal in VS Code. Make +sure your terminal is running PowerShell or Windows PowerShell. + +```alert +--- +variant: primary +--- + +For the rest of this tutorial, run the specified commands in the integrated +terminal at the root of the module folder. This is the default working +directory in VS Code. +``` + +### Create the module files + +Create the module manifest with the `New-ModuleManifest` cmdlet. Use +`./DscSamples.TailspinToys.psd1` as the **Path**. Specify **RootModule** as +`DscSamples.TailspinToys.psm1` and **DscResourcesToExport** as `Tailspin`. + +```powershell +$ModuleSettings = @{ + RootModule = 'DscSamples.TailspinToys.psm1' + DscResourcesToExport = 'PSTailspinToys' +} + +New-ModuleManifest -Path ./DscSamples.TailspinToys.psd1 @ModuleSettings +Get-Module -ListAvailable -Name ./DscSamples.TailspinToys.psd1 | Format-List +``` + +```console +Name : DscSamples.TailspinToys +Path : C:\code\dsc\DscSamples.TailspinToys\DscSamples.TailspinToys.psd1 +Description : +ModuleType : Script +Version : 0.0.1 +PreRelease : +NestedModules : {} +ExportedFunctions : +ExportedCmdlets : +ExportedVariables : +ExportedAliases : +``` + +Create the root module file as `DscSamples.TailspinToys.psm1`. + +```powershell +New-Item -Path ./DscSamples.TailspinToys.psm1 +``` + +```console + Directory: C:\code\dsc\DscSamples.TailspinToys + +Mode LastWriteTime Length Name +---- ------------- ------ ---- +-a--- 9/8/2022 1:57 PM 0 DscSamples.TailspinToys.psm1 +``` + +Create a script file called `Helpers.ps1`. + +```powershell +New-Item -Path ./Helpers.ps1 +``` + +```console + Directory: C:\code\dsc\DscSamples.TailspinToys + +Mode LastWriteTime Length Name +---- ------------- ------ ---- +-a--- 9/8/2022 1:58 PM 0 Helpers.ps1 +``` + +Open `Helpers.ps1` in VS Code. Add the following lines. + +```powershell +$env:PSModulePath += [System.IO.Path]::PathSeparator + $pwd +$MachinePath, $UserPath = tstoy show path +``` + +Open `DscSamples.TailspinToys.psm1` in VS Code. The module is now scaffolded and ready for you to +author a PowerShell DSC Resource. diff --git a/samples/pwsh/resources/first/docs/2-add-class-based-resource/Empty-Class-Warnings.png b/samples/pwsh/resources/first/docs/2-add-class-based-resource/Empty-Class-Warnings.png new file mode 100644 index 0000000..961bbb4 Binary files /dev/null and b/samples/pwsh/resources/first/docs/2-add-class-based-resource/Empty-Class-Warnings.png differ diff --git a/samples/pwsh/resources/first/docs/2-add-class-based-resource/index.md b/samples/pwsh/resources/first/docs/2-add-class-based-resource/index.md new file mode 100644 index 0000000..75be54a --- /dev/null +++ b/samples/pwsh/resources/first/docs/2-add-class-based-resource/index.md @@ -0,0 +1,68 @@ +--- +title: Step 2 - Add a class-based DSC Resource +weight: 2 +dscs: + menu_title: 2. Add a resource class +--- + +To define a class-based DSC Resource, we write a PowerShell class in a module file and add the +**DscResource** attribute to it. + +## Define the class + +In `DscSamples.TailspinToys.psm1`, add the following code: + +```powershell +[DscResource()] +class PSTailspinToys { + +} +``` + +This code adds `PSTailspinToys` as a class-based DSC Resource to the **DscSamples.TailspinToys** +module. + +Hover on `[DscResource()]` and read the warnings. + +![Screenshot of the DSC Resource attribute's warnings in VS Code.](Empty-Class-Warnings.png) + + + + +These warnings list the requirements for the class to be valid resource. + +## Minimally implement required methods + +Add a minimal implementation of the `Get()`, `Test()`, and `Set()` methods to the class. + +```powershell +class Tailspin { + [Tailspin] Get() { + $CurrentState = [Tailspin]::new() + return $CurrentState + } + + [bool] Test() { + return $true + } + + [void] Set() {} +} +``` + +With the methods added, the **DscResource** attribute only warns about the class not having a +**Key** property. diff --git a/samples/pwsh/resources/first/docs/3-define-properties.md b/samples/pwsh/resources/first/docs/3-define-properties.md new file mode 100644 index 0000000..4924696 --- /dev/null +++ b/samples/pwsh/resources/first/docs/3-define-properties.md @@ -0,0 +1,201 @@ +--- +title: Step 3 - Define the configuration properties +weight: 3 +dscs: + menu_title: 3. Define properties +--- + +You should define the properties of the DSC Resource before the methods. The properties define the +manageable settings for the resource instances. They're used in the methods. + +The resource needs to manage four properties: `$ConfigurationScope`, `$Ensure`, +`$UpdateAutomatically`, and `$UpdateFrequency`. + +## Define `$ConfigurationScope` + +The `$ConfigurationScope` property defines which instance of the `tstoy` configuration file the +resource should manage. To define the `$ConfigurationScope` property in the resource, add the +following code in the class before the methods: + +```powershell +[DscProperty(Key)] [TailspinScope] +$ConfigurationScope +``` + +This code defines `$ConfigurationScope` as a **Key** property of the resource. A **Key** property +is used to uniquely identify an instance of the resource. Adding this property meets one of the +requirements the **DscResource** attribute warned about when you scaffolded the class. + +It also specifies that `$ConfigurationScope`'s type is **TailspinScope**. To define the +**TailspinScope** type, add the following **TailspinScope** enum after the class definition in +`DscSamples.TailspinToys.psm1`: + +```powershell +enum TailspinScope { + Machine + User +} +``` + +This enumeration makes `Machine` and `User` the only valid values for the `$ConfigurationScope` +property of the resource. + +## Define `$Ensure` + +It's best practice to define an `$Ensure` property to control whether an instance of a resource +exists. An `$Ensure` property usually has two valid values, `Absent` and `Present`. + +- If `$Ensure` is specified as `Present`, the resource creates the item if it doesn't exist. +- If `$Ensure` is `Absent`, the resource deletes the item if it exists. + +For the **PSTailspinToys** resource, the item to create or delete is the configuration file for the +specified `$ConfigurationScope`. + +Define **TailspinEnsure** as an enum after **TailspinScope**. It should have the values `Absent` +and `Present`. + +```powershell +enum TailspinEnsure { + Absent + Present +} +``` + +Next, add the `$Ensure` property in the class after the `$ConfigurationScope` property. It should +have an empty **DscProperty** attribute and its type should be **TailspinEnsure**. It should default +to `Present`. + +```powershell +[DscProperty()] [TailspinEnsure] +$Ensure = [TailspinEnsure]::Present +``` + +## Define `$UpdateAutomatically` + +To manage automatic updates, define the `$UpdateAutomatically` property in the class after the +`$Ensure` property. Its **DscProperty** attribute should indicate that it's mandatory and its type +should be **boolean**. + +```powershell +[DscProperty(Mandatory)] [bool] +$UpdateAutomatically +``` + +## Define `$UpdateFrequency` + +To manage how often `tstoy` should check for updates, add the `$UpdateFrequency` property in the +class after the `$UpdateAutomatically` property. It should have an empty **DscProperty** attribute +and its type should be **int**. Use the **ValidateRange** attribute to limit the valid values for +`$UpdateFrequency` to between 1 and 90. + +```powershell +[DscProperty()] [int] [ValidateRange(1, 90)] +$UpdateFrequency +``` + +## Add hidden cache properties + +Next, add three hidden properties for caching the current state of the resource: +`$CachedCurrentState`, `$CachedData`, and `$CachedApplicationInfo`. Set the type of +`$CachedCurrentState` to **PSTailspinToys**, the same as the class and the return type for the +`Get()` method. Set the type of `$CachedData` to **PSCustomObject**. Set the type of +`$CachedApplicationInfo` to **System.Management.Automation.ApplicationInfo**. Prefix the properties +with the `hidden` keyword. Don't specify the **DscProperty** attribute for them. + +```powershell +hidden [PSTailspinToys] $CachedCurrentState +hidden [PSCustomObject] $CachedData +hidden [System.Management.Automation.ApplicationInfo] $CachedApplicationInfo +``` + +These hidden properties will be used in the `Get()` and `Set()` methods that you define later. + +## Review the module file + +At this point, `DscSamples.TailspinToys.psm1` should define: + +- The **PSTailspinToys** class with the properties `$ConfigurationScope`, `$Ensure`, + `$UpdateAutomatically`, and `$UpdateFrequency` +- The **TailspinScope** enum with the values `Machine` and `User` +- The **TailspinEnsure** enum with the values `Present` and `Absent` +- The minimal implementations of the `Get()`, `Test()`, and `Set()` methods. + +```powershell +[DscResource()] +class PSTailspinToys { + [DscProperty(Key)] [TailspinScope] + $ConfigurationScope + + [DscProperty()] [TailspinEnsure] + $Ensure = [TailspinEnsure]::Present + + [DscProperty(Mandatory)] [bool] + $UpdateAutomatically + + [DscProperty()] [int] [ValidateRange(1,90)] + $UpdateFrequency + + hidden [PSTailspinToys] $CachedCurrentState + hidden [PSCustomObject] $CachedData + hidden [System.Management.Automation.ApplicationInfo] $CachedApplicationInfo + hidden [string] $CachedConfigFilePath + + [PSTailspinToys] Get() { + $CurrentState = [PSTailspinToys]::new() + return $CurrentState + } + + [bool] Test() { + $InDesiredState = $true + return $InDesiredState + } + + [void] Set() {} +} + +enum TailspinScope { + Machine + User +} + +enum TailspinEnsure { + Absent + Present +} +``` + +## Inspect the resource + +Now that the resource class meets the requirements, you can use `Get-DscResource` to see it. In VS +Code, open a new PowerShell terminal. + +```powershell +. ./Helpers.ps1 +Get-DscResource -Name PSTailspinToys -Module DscSamples.TailspinToys | Format-List +Get-DscResource -Name PSTailspinToys -Module DscSamples.TailspinToys -Syntax +``` + +```console +ImplementationDetail : ClassBased +ResourceType : PSTailspinToys +Name : PSTailspinToys +FriendlyName : +Module : DscSamples.TailspinToys +ModuleName : DscSamples.TailspinToys +Version : 0.0.1 +Path : C:\code\dsc\DscSamples.TailspinToys\DscSamples.TailspinToys.psd1 +ParentPath : C:\code\dsc\DscSamples.TailspinToys +ImplementedAs : PowerShell +CompanyName : Unknown +Properties : {ConfigurationScope, UpdateAutomatically, DependsOn, Ensure…} + +PSTailspinToys [String] #ResourceName +{ + ConfigurationScope = [string]{ Machine | User } + UpdateAutomatically = [bool] + [DependsOn = [string[]]] + [Ensure = [string]{ Absent | Present }] + [PsDscRunAsCredential = [PSCredential]] + [UpdateFrequency = [Int32]] +} +``` diff --git a/samples/pwsh/resources/first/docs/4-implement-get.md b/samples/pwsh/resources/first/docs/4-implement-get.md new file mode 100644 index 0000000..bf654d6 --- /dev/null +++ b/samples/pwsh/resources/first/docs/4-implement-get.md @@ -0,0 +1,336 @@ +--- +title: Step 4 - Implement the Get method +weight: 4 +dscs: + menu_title: 4. Implement `Get()` +--- + +The `Get()` method retrieves the current state of the DSC Resource. It's used to inspect a resource +instance manually and is called by the `Test()` method. + +The `Get()` method has no parameters and returns an instance of the class as its output. For the +`PSTailspinToys` resource, the minimal implementation looks like this: + +```powershell +[PSTailspinToys] Get() { + $CurrentState = [PSTailspinToys]::new() + return $CurrentState +} +``` + +The only thing this implementation does is create an instance of the **PSTailspinToys** class and +return it. You can call the method with `Invoke-DscResource` to see this behavior. + +```powershell +Invoke-DscResource -Name PSTailspinToys -Module DscSamples.TailspinToys -Method Get -Property @{ + ConfigurationScope = 'User' + UpdateAutomatically = $true +} +``` + +```console +ConfigurationScope Ensure UpdateAutomatically UpdateFrequency +------------------ ------ ------------------- --------------- + Machine Present False 0 +``` + +The returned object's properties are all set to their default value. + +## Define `Get` helpers + +To implement the get command, the resource needs to be able to find and load the settings from a +specific `tstoy` configuration file. + +Recall from [About the TSToy application][01] that you can use the `tstoy show path` command to get +the full path to the applications configuration files. The resource can use those commands instead +of trying to generate the paths itself. + +### Define `GetApplicationInfo()` { toc_md="`GetApplicationInfo()`" } + +The resource needs to retrieve the `tstoy` command to call it. Define a new method for the class +called `GetApplicationInfo()`. The method should have no parameters and return a +**System.Management.Automation.ApplicationInfo** object. + +```powershell +[System.Management.Automation.ApplicationInfo] GetApplicationInfo() { + if ($null -ne $this.CachedApplicationInfo) { + return $this.CachedApplicationInfo + } + + try { + $Parameters = @{ + Name = 'tstoy' + CommandType = 'Application' + ErrorAction = 'Stop' + } + $this.CachedApplicationInfo = Get-Command @Parameters + } catch [System.Management.Automation.CommandNotFoundException] { + throw [System.Management.Automation.CommandNotFoundException]::new( + "tstoy application not found, unable to retrieve path to configuration file", + $_ + ) + } + + return $this.CachedApplicationInfo +} +``` + +The method generates the parameters to search for the `tstoy` command, caching it in the +`$CachedApplicationInfo` property before returning it. If the command has already been cached, the +method returns that value. If the command can't be found, the method throws an error. + +### Define `GetConfigurationFilePath()` { toc_md="`GetConfigurationFilePath()`" } + +Now that the resource can get a handle for the `tstoy` application, it can call the application as +a command to return the path to the configuration file for the desired scope. + +Define the `GetConfigurationFilePath()` method. The method should have no parameters and return a +**String**. + +```powershell +[string] GetConfigurationFilePath() { + if (-not ([string]::IsNullOrEmpty($this.CachedConfigurationFilePath))) { + return $this.CachedConfigurationFilePath + } + + $this.GetApplicationInfo() + + $Arguments = @( + 'show' + 'path' + $this.ConfigurationScope.ToString().ToLower() + ) + + $this.CachedConfigurationFilePath = & $this.CachedApplicationInfo @Arguments + + return $this.CachedConfigurationFilePath +} +``` + +The method calls `tstoy show path ` and caches the result string as the value for +`$CachedConfigurationFilePath`. If the path has already been cached, it returns that value instead. + +### Define `GetConfigurationData()` { toc_md="`GetConfigurationData()`" } + +The last helper required for `Get` is to retrieve and cache the configuration data from the scope +configuration file. + +Define the `GetConfigurationData()` method. The method should have no parameters and return a +**PSCustomObject**. + +```powershell +[PSCustomObject] GetConfigurationData() { + if ($null -ne $this.CachedData) { + return $this.CachedData + } + + $ConfigurationFilePath = $this.GetConfigurationFilePath() + + if (-not (Test-Path -Path $ConfigurationFilePath)) { + return $null + } + + try { + $GetParameters = @{ + Path = $ConfigurationFilePath + Raw = $true + } + + $this.CachedData = Get-Content @GetParameters + | ConvertFrom-Json -ErrorAction Stop + } catch [Newtonsoft.Json.JsonReaderException] { + throw [Newtonsoft.Json.JsonReaderException]::new( + "Unable to load TSToy configuration data from $ConfigurationFilePath", + $_ + ) + } + + return $this.CachedData +} +``` + +### Verify helper methods + +With the three helper methods defined, you can test them. Execute the `using` statement to load the +**DscSamples.TailspinToys** module's classes and enums into your current session. + +```powershell +using module ./DscSamples.TailspinToys.psd1 + +$machine = [PSTailspinToys]@{ ConfigurationScope = 'Machine' } +$machine.GetApplicationInfo() +$machine.GetConfigurationFilePath() +$null -eq $machine.GetConfigurationData() + +$user = [PSTailspinToys]@{ ConfigurationScope = 'User' } +$user.GetConfigurationFilePath() +$null -eq $user.GetConfigurationData() +``` + +```console +CommandType Name Version Source +----------- ---- ------- ------ +Application tstoy.exe 0.0.0.0 C:\Users\mikey\go\bin\tstoy.exe + +C:\ProgramData\TailSpinToys\tstoy\tstoy.config.json + +True + +C:\Users\mikey\AppData\Local\TailSpinToys\tstoy\tstoy.config.json + +True +``` + +The `GetConfigurationData()` method returned `$null` because the configuration files doesn't exist +yet. + +Create the user scope configuration file. + +```powershell +New-Item -Path (tstoy show path user) -Force -Value @' +{ + "unmanaged_key": true, + "updates": { + "automatic": true, + "checkFrequency": 30 + } +} +'@ +``` + +```console + Directory: C:\Users\mikey\AppData\Local\TailSpinToys\tstoy + +Mode LastWriteTime Length Name +---- ------------- ------ ---- +-a--- 8/15/2023 11:47 AM 0 tstoy.config.json +``` + +Call the `GetConfigurationData()` method for the user-scope again. + +```powershell +$user.GetConfigurationData() +``` + +```console +unmanaged_key updates +------------- ------- + True @{automatic=True; checkFrequency=30} +``` + +Exit the terminal in VS Code and open a new terminal. Dot-source `Helpers.ps1`. + +```powershell +. ./Helpers.ps1 +``` + +Now you can write the rest of the `Get()` method. + +## Return the actual state + +The value of `$ConfigurationScope` should always be the value the user supplied. To make the +`Get()` method useful, it must return the actual state of the resource instance. + +```powershell +[PSTailspinToys] Get() { + $CurrentState = [PSTailspinToys]::new() + + $CurrentState.ConfigurationScope = $this.ConfigurationScope + + $this.CachedCurrentState = $CurrentState + + return $CurrentState +} +``` + +The `$this` variable references the working instance of the resource. Now, if you use +`Invoke-DscResource` again, `$ConfigurationScope` has the correct value. + +```powershell +Invoke-DscResource -Name PSTailspinToys -Module DscSamples.TailspinToys -Method Get -Property @{ + ConfigurationScope = 'User' + UpdateAutomatically = $true +} +``` + +```console +ConfigurationScope Ensure UpdateAutomatically UpdateFrequency +------------------ ------ ------------------- --------------- + User Present False 0 +``` + +Now you can write the rest of the `Get()` method. + +```powershell +[PSTailspinToys] Get() { + $CurrentState = [PSTailspinToys]::new() + + $CurrentState.ConfigurationScope = $this.ConfigurationScope + + $FilePath = $this.GetConfigurationFilePath() + + if (!(Test-Path -path $FilePath)) { + $CurrentState.Ensure = [TailspinEnsure]::Absent + return $CurrentState + } + + $Data = $this.GetConfigurationData() + + if ($null -ne $Data.Updates.Automatic) { + $CurrentState.UpdateAutomatically = $Data.Updates.Automatic + } + + if ($null -ne $Data.Updates.CheckFrequency) { + $CurrentState.UpdateFrequency = $Data.Updates.CheckFrequency + } + + $this.CachedCurrentState = $CurrentState + + return $CurrentState +} +``` + +After setting the `$ConfigurationScope` and determining the configuration file's path, the method +checks to see if the file exists. If it doesn't exist, setting `$Ensure` to `Absent` and returning +the result is all that's needed. + +If the file does exist, the method needs to retrieve the configuration data. The +`GetConfigurationData()` method returns the data and caches it. Caching the data allows you to +inspect the data during development and will be useful when implementing the `Set()` method. + +Next, the method checks to see if the managed keys have any value before +assigning them to the current state's properties. If they're not specified, the resource must +consider them unset and in their default state. + +You can verify this behavior locally. + +```powershell +$GetParameters = @{ + Name = 'PSTailspinToys' + Module = 'DscSamples.TailspinToys' + Method = 'Get' + Property = @{ + ConfigurationScope = 'Machine' + } +} + +Invoke-DscResource @GetParameters + +$GetParameters.Property.ConfigurationScope = 'User' +Invoke-DscResource @GetParameters +``` + +```console +ConfigurationScope Ensure UpdateAutomatically UpdateFrequency +------------------ ------ ------------------- --------------- + Machine Absent False 0 + +ConfigurationScope Ensure UpdateAutomatically UpdateFrequency +------------------ ------ ------------------- --------------- + User Present True 30 +``` + +The `Get()` method now returns accurate information about the current state of the resource +instance. + +[01]: /tstoy/about/ diff --git a/samples/pwsh/resources/first/docs/5-implement-test.md b/samples/pwsh/resources/first/docs/5-implement-test.md new file mode 100644 index 0000000..4670a2f --- /dev/null +++ b/samples/pwsh/resources/first/docs/5-implement-test.md @@ -0,0 +1,237 @@ +--- +title: Step 5 - Implement the Test method +weight: 5 +dscs: + menu_title: 5. Implement `Test()` +--- + +With the `Get()` method implemented, you can verify whether the current state is compliant with the +desired state. + +The `Test()` method minimal implementation always returns `$true`. + +```powershell +[bool] Test() { + return $true +} +``` + +You can verify that by running `Invoke-DscResource`. + +```powershell +$SharedParameters = @{ + Name = 'PSTailspinToys' + Module = 'DscSamples.TailspinToys' + Property = @{ + ConfigurationScope = 'User' + UpdateAutomatically = $false + } +} + +Invoke-DscResource -Method Get @SharedParameters +Invoke-DscResource -Method Test @SharedParameters +``` + +```console +ConfigurationScope Ensure UpdateAutomatically UpdateFrequency +------------------ ------ ------------------- --------------- + User Present True 30 + +InDesiredState +-------------- + True +``` + +## Test the `$Ensure` property { toc_md="Test `$Ensure`" } + +You need to make the `Test()` method accurately reflect whether the DSC Resource is in the desired +state. The `Test()` method should always call the `Get()` method to have the current state to +compare against the desired state. Then check whether the `$Ensure` property is correct. If it +isn't, return `$false` immediately. No further checks are required if the `$Ensure` property is out +of the desired state. + +```powershell +[bool] Test() { + $CurrentState = $this.Get() + + if ($CurrentState.Ensure -ne $this.Ensure) { + return $false + } + + return $true +} +``` + +Now you can verify the updated behavior. + +```powershell +$TestParameters = @{ + Name = 'PSTailspinToys' + Module = 'DscSamples.TailspinToys' + Property = @{ + ConfigurationScope = 'User' + UpdateAutomatically = $false + Ensure = 'Absent' + } +} + +Invoke-DscResource -Method Test @TestParameters + +$TestParameters.Property.Ensure = 'Present' + +Invoke-DscResource -Method Test @TestParameters +``` + +```console +InDesiredState +-------------- + False + +InDesiredState +-------------- + True +``` + +Next, check to see if the value of `$Ensure` is `Absent`. If the configuration file doesn't exist +and shouldn't exist, there's no reason to check the remaining properties. + +```powershell +[bool] Test() { + $CurrentState = $this.Get() + + if ($CurrentState.Ensure -ne $this.Ensure) { + return $false + } + + if ($CurrentState.Ensure -eq [TailspinEnsure]::Absent) { + return $true + } + + return $true +} +``` + +## Test the `$UpdateAutomatically` property { toc_md="Test `$UpdateAutomatically`" } + +Now that the method handles the `$Ensure` property, it should check if the `$UpdateAutomatically` +property is in the correct state. If it isn't, return `$false`. + +```powershell +[bool] Test() { + $CurrentState = $this.Get() + + if ($CurrentState.Ensure -ne $this.Ensure) { + return $false + } + + if ($CurrentState.Ensure -eq [TailspinEnsure]::Absent) { + return $true + } + + if ($CurrentState.UpdateAutomatically -ne $this.UpdateAutomatically) { + return $false + } + + return $true +} +``` + +## Test the `$UpdateFrequency` property { toc_md="Test `$UpdateFrequency`" } + +To compare `$UpdateFrequency`, we need to determine if the user specified the value. Because +`$UpdateFrequency` is initialized to `0` and the property's **ValidateRange** attribute specifies +that it must be between `1` and `90`, we know that a value of `0` indicates that the property wasn't +specified. + +With that information, the `Test()` method should: + +1. Return `$true` if the user didn't specify `$UpdateFrequency` +1. Return `$false` if the user did specify `$UpdateFrequency` and the value of the system doesn't + equal the user-specified value +1. Return `$true` if neither of the prior conditions were met + +```powershell +[bool] Test() { + $CurrentState = $this.Get() + + if ($CurrentState.Ensure -ne $this.Ensure) { + return $false + } + + if ($CurrentState.Ensure -eq [TailspinEnsure]::Absent) { + return $true + } + + if ($CurrentState.UpdateAutomatically -ne $this.UpdateAutomatically) { + return $false + } + + if ($this.UpdateFrequency -eq 0) { + return $true + } + + if ($CurrentState.UpdateFrequency -ne $this.UpdateFrequency) { + return $false + } + + return $true +} +``` + +## Review and validate the `Test()` method { toc_md="Review and Validate" } + +Now the `Test()` method uses the following order of operations: + +1. Retrieve the current state of TSToy's configuration. +1. Return `$false` if the configuration exists when it shouldn't or doesn't exist when it should. +1. Return `$true` if the configuration doesn't exist and shouldn't exist. +1. Return `$false` if the configuration's automatic update setting doesn't match the desired one. +1. Return `$true` if the user didn't specify a value for the update frequency setting. +1. Return `$false` if the user's specified value for the update frequency setting doesn't match the + configuration's setting. +1. Return `$true` if none of the prior conditions were met. + +You can verify the `Test()` method locally: + +```powershell +$SharedParameters = @{ + Name = 'PSTailspinToys' + Module = 'DscSamples.TailspinToys' + Property = @{ + ConfigurationScope = 'User' + Ensure = 'Present' + UpdateAutomatically = $false + } +} + +Invoke-DscResource -Method Get @SharedParameters + +Invoke-DscResource -Method Test @SharedParameters + +$SharedParameters.Property.UpdateAutomatically = $true +Invoke-DscResource -Method Test @SharedParameters + +$SharedParameters.Property.UpdateFrequency = 1 +Invoke-DscResource -Method Test @SharedParameters +``` + +```console +ConfigurationScope Ensure UpdateAutomatically UpdateFrequency +------------------ ------ ------------------- --------------- + User Present True 30 + +InDesiredState +-------------- + False + +InDesiredState +-------------- + True + +InDesiredState +-------------- + False +``` + +With this code, the `Test()` method is able to accurately determine whether the configuration file +is in the desired state. diff --git a/samples/pwsh/resources/first/docs/6-implement-set.md b/samples/pwsh/resources/first/docs/6-implement-set.md new file mode 100644 index 0000000..3e21644 --- /dev/null +++ b/samples/pwsh/resources/first/docs/6-implement-set.md @@ -0,0 +1,327 @@ +--- +title: Step 6 - Implement the Set method +weight: 6 +dscs: + menu_title: 6. Implement `Set()` +--- + +Now that the `Get()` and `Test()` methods reliably work, you can define the `Set()` method to +actually enforce the desired state. + +In the minimal implementation, the `Set()` method does nothing. + +```powershell +[void] Set() {} +``` + +## Minimally handle sub-operations + +The `Set()` method needs to handle three different sub-operations: + +- If the configuration file doesn't exist and should exist, the resource needs to _create_ it. +- If the configuration file exists and should exist, but has out-of-state properties, the resource + needs to _update_ the file. +- If the configuration file exists and shouldn't exist, the resource needs to _remove_ it. + +```powershell +[void] Set() { + if ($this.Test()) { + return + } + + $CurrentState = $this.CachedCurrentState + $IsAbsent = $CurrentState.Ensure -eq [TailspinEnsure]::Absent + $ShouldBeAbsent = $this.Ensure -eq [TailspinEnsure]::Absent + + if ($IsAbsent) { + # Create + } elseif ($ShouldBeAbsent) { + # Remove + } else { + # Update + } +} +``` + +`Set()` first calls the `Test()` method to determine if the resource actually needs to do anything. +Some tools, like DSCv3 and Azure Automanage's machine configuration feature, ensure that the +`Set()` method is only called after the `Test()` method. However, there's no such guarantee when +you use the `Invoke-DscResource` cmdlet directly. + +Since the `Test()` method calls `Get()`, which caches the current state, the resource can access +the cached current state without having to call the `Get()` method again. + +Next, the resource needs to distinguish between create, remove, and update behaviors for the +configuration file. + +Create three new methods to handle these operations and call them in the `Set()` method as needed. +The return type for all three should be **void**. + +```powershell +[void] Set() { + if ($this.Test()) { + return + } + + $CurrentState = $this.CachedCurrentState + $IsAbsent = $CurrentState.Ensure -eq [TailspinEnsure]::Absent + $ShouldBeAbsent = $this.Ensure -eq [TailspinEnsure]::Absent + + if ($IsAbsent) { + $this.Create() + } elseif ($ShouldBeAbsent) { + $this.Remove() + } else { + $this.Update() + } +} + +[void] Create() {} +[void] Remove() {} +[void] Update() {} +``` + +## Handle Converting Properties to JSON { toc_md="`ToConfigurationJson()`" } + +The resource needs to be able to handle representing the instance properties in the application's +actual JSON data model. It also needs to be able to update only the data the instance is enforcing. +It shouldn't ever alter unmanaged data except by deleting the file when `$Ensure` is `Absent`. + +### Implement `ToConfigurationJson()` helper method { toc_md="Implement the helper method" } + +Create a new method called `ToConfigurationJson()`. Its return type should be **string**. This +method converts the resource instance into the JSON that the configuration file expects. You can +start with the following minimal implementation: + +```powershell +[string] ToConfigurationJson() { + $config = @{} + + return ($config | ConvertTo-Json) +} +``` + +The minimal implementation returns an empty JSON object as a string. To make it useful, it needs to +return the actual JSON representation of the settings in TSToy's configuration file. + +First, prepopulate the `$config` hashtable with the mandatory automatic updates setting by +adding the `updates` key with its value as a **hashtable**. The hashtable should have the +`automatic` key. Assign the value of the class's `$UpdateAutomatically` property to the `automatic` +key. + +```powershell +[string] ToConfigurationJson() { + $config = @{ + updates = @{ + automatic = $this.UpdateAutomatically + } + } + + return ($config | ConvertTo-Json) +} +``` + +This code translates the resource instance representation of TSToy's settings to the structure that +TSToy's configuration file expects. + +Next, the method needs to check whether the class has cached the data from an existing +configuration file. The cached data allows the resource to manage the defined settings without +overwriting or removing unmanaged settings. + +```powershell +[string] ToConfigurationJson() { + $config = @{ + updates = @{ + automatic = $this.UpdateAutomatically + } + } + + if ($this.CachedData) { + # Copy unmanaged settings without changing the cached values + $this.CachedData | + Get-Member -MemberType NoteProperty | + Where-Object -Property Name -NE -Value 'updates' | + ForEach-Object -Process { + $setting = $_.Name + $config.$setting = $this.CachedData.$setting + } + + # Add the checkFrequency to the hashtable if it is set in the cache + if ($frequency = $this.CachedData.updates.checkFrequency) { + $config.updates.checkFrequency = $frequency + } + } + + # If the user specified an UpdateFrequency, use that value + if ($this.UpdateFrequency -ne 0) { + $config.updates.checkFrequency = $this.UpdateFrequency + } + + return ($config | ConvertTo-Json -Depth 99) +} +``` + +If the class has cached the settings from an existing configuration, it: + +1. Inspects the cached data's properties, looking for any properties the resource doesn't + manage. If it finds any, the method inserts those unmanaged properties into the `$config` + hashtable. + + Because the resource only manages the update settings, every setting except for `updates` is + inserted. +1. Checks to see if the `checkFrequency` setting in `updates` is set. If it's set, the method + inserts this value into the `$config` hashtable. + + This operation allows the resource to ignore the `$UpdateFrequency` property if the user + doesn't specify it. + +1. Finally, the method needs to check if the user specified the `$UpdateFrequency` property and + insert it into the `$config` hashtable if they did. + +With this code, the `ToConfigurationJson()` method: + +1. Returns an accurate JSON representation of the desired state that the TSToy application expects + in its configuration file +1. Respects any of TSToy's settings that the resource doesn't explicitly manage +1. Respects the existing value for TSToy's update frequency if the user didn't specify one, + including leaving it undefined in the configuration file + +### Validate `ToConfigurationJson()` { toc_md="Validate the method" } + +To test this new method, close your VS Code terminal and open a new one. Execute the `using` +statement to load the **DscSamples.TailspinToys** module's classes and enums into your current +session and dot-source the `Helpers.ps1` script. + +```powershell +using module ./DscSamples.TailspinToys.psd1 +. ./Helpers.ps1 +$Example = [PSTailspinToys]::new() +Get-Content -Path $(tstoy show path user) +$Example.ConfigurationScope = 'User' +$Example.ToConfigurationJson() +``` + +Before the `Get()` method is called, the only value in the output of the **ToJsonConfig** method is +the converted value for the `$UpdateAutomatically` property. + +```console +{ + "unmanaged_key": true, + "updates": { + "automatic": true, + "checkFrequency": 30 + } +} + +{ + "updates": { + "automatic": false + } +} +``` + +After you call `Get()`, the output includes an unmanaged top-level key, `unmanaged_key`. It also +includes the existing setting in the configuration file for `$UpdateFrequency` since it wasn't +explicitly set on the resource instance. + +```powershell +$Example.Get() +$Example.ToConfigurationJson() +``` + +```console + Ensure ConfigurationScope UpdateAutomatically UpdateFrequency + ------ ------------------ ------------------- --------------- +Present User True 30 + +{ + "unmanaged_key": true, + "updates": { + "checkFrequency": 30, + "automatic": false + } +} +``` + +After `$UpdateFrequency` is set, the output reflects the specified value. + +```powershell +$Example.UpdateFrequency = 7 +$Example.ToConfigurationJson() +``` + +```console +{ + "unmanaged_key": true, + "updates": { + "checkFrequency": 7, + "automatic": false + } +} +``` + +## Implement the `Create()` method { toc_md="Implement `Create()`" } + +To implement the `Create()` method, we need to convert the user-specified properties for the DSC +Resource into the JSON that TSToy expects in its configuration file and write it to that file. + +```powershell +[void] Create() { + $ErrorActionPreference = 'Stop' + + $Json = $this.ToConfigurationJson() + + $FilePath = $this.GetConfigurationFilePath() + $FolderPath = Split-Path -Path $FilePath + + if (!(Test-Path -Path $FolderPath)) { + New-Item -Path $FolderPath -ItemType Directory -Force + } + + Set-Content -Path $FilePath -Value $Json -Encoding utf8 -Force +} +``` + +The method uses the `ToConfigurationJson()` method to get the JSON for the configuration file. It +checks whether the configuration file's folder exists and creates it if necessary. Finally, it +creates the configuration file and writes the JSON to it. + +## Implement the `Remove()` method { toc_md="Implement `Remove()`" } + +The `Remove()` method has the simplest behavior. If the configuration file exists, delete it. + +```powershell +[void] Remove() { + Remove-Item -Path $this.GetConfigurationFilePath() -Force -ErrorAction Stop +} +``` + +## Implement the `Update()` method { toc_md="Implement `Update()`" } + +The `Update()` method implementation is like the implementation for the `Create()` method. It needs +to convert the user-specified properties for the resource instance into the JSON that TSToy expects +in its configuration file and replace the settings in that file. + +```powershell +[void] Update() { + $ErrorActionPreference = 'Stop' + + $Json = $this.ToConfigurationJson() + $FilePath = $this.GetConfigurationFilePath() + + Set-Content -Path $FilePath -Value $Json -Encoding utf8 -Force +} +``` + +## Review + +With the `ToConfigurationJson()`, `Create()`, `Remove()`, and `Update()` helper methods defined, +the `Set()` method can now idempotently manage resource instances. The method: + +- Returns immediately if the configuration file is in the desired state. +- Creates the configuration file with the correct settings when `$Ensure` is `Present` and the file + doesn't exist. +- Removes the configuration file when `$Ensure` is `Absent`. +- Updates the file contents when `$Ensure` is `Present`, the file exists, and + `$UpdateAutomatically` or `$UpdateFrequency` are out of the desired state. When it updates the + file, it doesn't alter the unmanaged settings. diff --git a/samples/pwsh/resources/first/docs/7-validate.md b/samples/pwsh/resources/first/docs/7-validate.md new file mode 100644 index 0000000..d73fd73 --- /dev/null +++ b/samples/pwsh/resources/first/docs/7-validate.md @@ -0,0 +1,589 @@ +--- +title: Step 7 - Validate the DSC Resource with DSC and PSDSC +weight: 7 +dscs: + menu_title: 7. Validate the resource +--- + +With the DSC Resource fully implemented, you can now test its behavior with PowerShell DSC and DSCv3. + +## Validate with PowerShell DSC + +PowerShell DSC Resources can be invoked directly with the `Invoke-DscResource` cmdlet from the +**PSDesiredStateConfiguration** module. This section describes a set of scenarios to test the +resource against. + +Before testing each scenario, close your VS Code terminal and open a new one. Dot-source the +`Helpers.ps1` script. For each test scenario, create the `$DesiredState` hashtable containing the +shared parameters and call the methods in the following order: + +1. `Get()`, to retrieve the initial state of the DSC Resource +1. `Test()`, to see whether the DSC Resource considers it to be in the desired state +1. `Set()`, to enforce the desired state +1. `Test()`, to see whether the DSC Resource considers it to be set correctly +1. `Get()`, to confirm the final state of the DSC Resource + +### Scenario: Disable automatic updating in user scope { toc_md="Disable updates in user scope" } + +In this scenario, the existing configuration in the user scope needs to be configured not to update +automatically. All other settings should be left untouched. + +```powershell +. ./Helpers.ps1 + +$DesiredState = @{ + Name = 'PSTailspinToys' + Module = 'DscSamples.TailspinToys' + Property = @{ + ConfigurationScope = 'User' + UpdateAutomatically = $false + Ensure = 'Present' + } +} + +Get-Content -Path $UserPath + +Invoke-DscResource @DesiredState -Method Get +Invoke-DscResource @DesiredState -Method Test +Invoke-DscResource @DesiredState -Method Set +Invoke-DscResource @DesiredState -Method Test +Invoke-DscResource @DesiredState -Method Get + +Get-Content -Path $UserPath +``` + +```console +{ + "unmanaged_key": true, + "updates": { + "automatic": true, + "checkFrequency": 30 + } +} + +ConfigurationScope Ensure UpdateAutomatically UpdateFrequency +------------------ ------ ------------------- --------------- + User Present True 30 + +InDesiredState +-------------- + False + +RebootRequired +-------------- + False + +InDesiredState +-------------- + True + +ConfigurationScope Ensure UpdateAutomatically UpdateFrequency +------------------ ------ ------------------- --------------- + User Present False 30 + +{ + "unmanaged_key": true, + "updates": { + "checkFrequency": 30, + "automatic": false + } +} +``` + +### Scenario: Enable automatic updating in the user scope { toc_md="Enable updates in user scope" } + +In this scenario, the existing configuration in the user scope needs to be configured to update +automatically. All other settings should be left untouched. + +```powershell +. ./Helpers.ps1 + +$DesiredState = @{ + Name = 'PSTailspinToys' + Module = 'DscSamples.TailspinToys' + Property = @{ + ConfigurationScope = 'User' + UpdateAutomatically = $true + Ensure = 'Present' + } +} + +Get-Content -Path $UserPath + +Invoke-DscResource @DesiredState -Method Get +Invoke-DscResource @DesiredState -Method Test +Invoke-DscResource @DesiredState -Method Set +Invoke-DscResource @DesiredState -Method Test +Invoke-DscResource @DesiredState -Method Get + +Get-Content -Path $UserPath +``` + +```console +{ + "unmanaged_key": true, + "updates": { + "checkFrequency": 30, + "automatic": false + } +} + +ConfigurationScope Ensure UpdateAutomatically UpdateFrequency +------------------ ------ ------------------- --------------- + User Present False 30 + +InDesiredState +-------------- + False + +RebootRequired +-------------- + False + +InDesiredState +-------------- + True + +ConfigurationScope Ensure UpdateAutomatically UpdateFrequency +------------------ ------ ------------------- --------------- + User Present True 30 + +{ + "unmanaged_key": true, + "updates": { + "checkFrequency": 30, + "automatic": true + } +} +``` + +### Scenario: Update daily in the user scope { toc_md="Update daily in user scope" } + +In this scenario, the existing configuration in the user scope needs to be configured to update +automatically and daily. All other settings should be left untouched. + +```powershell +. ./Helpers.ps1 + +$DesiredState = @{ + Name = 'PSTailspinToys' + Module = 'DscSamples.TailspinToys' + Property = @{ + ConfigurationScope = 'User' + UpdateAutomatically = $true + UpdateFrequency = 1 + Ensure = 'Present' + } +} + +Get-Content -Path $UserPath + +Invoke-DscResource @DesiredState -Method Get +Invoke-DscResource @DesiredState -Method Test +Invoke-DscResource @DesiredState -Method Set +Invoke-DscResource @DesiredState -Method Test +Invoke-DscResource @DesiredState -Method Get + +Get-Content -Path $UserPath +``` + +```console +{ + "unmanaged_key": true, + "updates": { + "checkFrequency": 30, + "automatic": true + } +} + +ConfigurationScope Ensure UpdateAutomatically UpdateFrequency +------------------ ------ ------------------- --------------- + User Present True 30 + +InDesiredState +-------------- + False + +RebootRequired +-------------- + False + +InDesiredState +-------------- + True + +ConfigurationScope Ensure UpdateAutomatically UpdateFrequency +------------------ ------ ------------------- --------------- + User Present True 1 + +{ + "unmanaged_key": true, + "updates": { + "automatic": true, + "checkFrequency": 1 + } +} +``` + +### Scenario: No user scope configuration { toc_md="No user scope configuration" } + +In this scenario, the configuration file for TSToy in the user scope shouldn't exist. If it does, +the DSC Resource should delete the file. + +```powershell +. ./Helpers.ps1 + +$DesiredState = @{ + Name = 'PSTailspinToys' + Module = 'DscSamples.TailspinToys' + Property = @{ + ConfigurationScope = 'User' + UpdateAutomatically = $true + Ensure = 'Absent' + } +} + +Get-Content -Path $UserPath + +Invoke-DscResource @DesiredState -Method Get +Invoke-DscResource @DesiredState -Method Test +Invoke-DscResource @DesiredState -Method Set +Invoke-DscResource @DesiredState -Method Test +Invoke-DscResource @DesiredState -Method Get + +Test-Path -Path $UserPath +``` + +```console +{ + "unmanaged_key": true, + "updates": { + "checkFrequency": 30, + "automatic": true + } +} + +ConfigurationScope Ensure UpdateAutomatically UpdateFrequency +------------------ ------ ------------------- --------------- + User Present True 30 + +InDesiredState +-------------- + False + +RebootRequired +-------------- + False + +InDesiredState +-------------- + True + +ConfigurationScope Ensure UpdateAutomatically UpdateFrequency +------------------ ------ ------------------- --------------- + User Absent False 0 + +False +``` + +### Scenario: Update weekly in the machine scope { toc_md="Update weekly in machine scope" } + +In this scenario, there's no defined configuration in the machine scope. The machine scope needs to +be configured to update automatically and daily. The DSC Resource should create the file and any +parent folders as required. + +```powershell +. ./Helpers.ps1 + +$DesiredState = @{ + Name = 'PSTailspinToys' + Module = 'DscSamples.TailspinToys' + Property = @{ + ConfigurationScope = 'Machine' + UpdateAutomatically = $true + Ensure = 'Present' + } +} + +Test-Path -Path $MachinePath, (Split-Path -Path $MachinePath) + +Invoke-DscResource @DesiredState -Method Get +Invoke-DscResource @DesiredState -Method Test +Invoke-DscResource @DesiredState -Method Set +Invoke-DscResource @DesiredState -Method Test +Invoke-DscResource @DesiredState -Method Get + +Get-Content -Path $MachinePath +``` + +```console +False +False + +ConfigurationScope Ensure UpdateAutomatically UpdateFrequency +------------------ ------ ------------------- --------------- + Machine Absent False 0 + +InDesiredState +-------------- + False + +RebootRequired +-------------- + False + +InDesiredState +-------------- + True + +ConfigurationScope Ensure UpdateAutomatically UpdateFrequency +------------------ ------ ------------------- --------------- + Machine Present True 0 + +{ + "updates": { + "automatic": true + } +} +``` + +## Validate with DSCv3 + +The `DSC/PowerShellGroup` resource provider enables invoking instances of PowerShell DSC Resources +and declaring them in DSC Configuration Documents. + +### List the resource with `dsc resource list` { toc_md="List the resource" } + +You can list the resource with the `dsc resource list` command. Specify the resource's module and +name as the argument, like `/`. + +```powershell +dsc --format yaml resource list DscSamples.TailspinToys/PSTailspinToys +``` + +```yaml +type: DscSamples.TailspinToys/PSTailspinToys +version: 0.0.1 +path: C:\code\dsc\DscSamples.TailspinToys\DscSamples.TailspinToys.psd1 +description: null +directory: C:\code\dsc\DscSamples.TailspinToys +implementedAs: ClassBased +author: '' +properties: +- ConfigurationScope +- DependsOn +- Ensure +- PsDscRunAsCredential +- UpdateAutomatically +- UpdateFrequency +requires: DSC/PowerShellGroup +manifest: null +``` + +### Manage state with `dsc resource` + +Once you've confirmed that DSCv3 can find the resource, you can invoke it directly. + +Define a new desired state for the machine-scope configuration: + +```powershell +$Desired = @{ + ConfigurationScope = 'Machine' + Ensure = 'Absent' +} | ConvertTo-Json +``` + +Get the current state of the machine-scope configuration file. + +```powershell +$Desired | dsc resource get --resource DscSamples.TailspinToys/PSTailspinToys +``` + +```yaml +actualState: + ConfigurationScope: 0 + Ensure: 1 + UpdateAutomatically: true + UpdateFrequency: 0 + CachedCurrentState: null + CachedData: null + CachedApplicationInfo: null + CachedConfigurationFilePath: null +``` + +Test whether the machine configuration is in the desired state: + +```powershell +$Desired | dsc resource test --resource DscSamples.TailspinToys/PSTailspinToys +``` + +```yaml +desiredState: + ConfigurationScope: Machine + Ensure: Absent + type: DscSamples.TailspinToys/PSTailspinToys +actualState: + InDesiredState: false +inDesiredState: false +differingProperties: +- ConfigurationScope +- Ensure +- type +``` + +Enforce the desired state: + +```powershell +$Desired | dsc resource set --resource DscSamples.TailspinToys/PSTailspinToys +``` + +```yaml +beforeState: + ConfigurationScope: 0 + Ensure: 1 + UpdateAutomatically: true + UpdateFrequency: 0 + CachedCurrentState: null + CachedData: null + CachedApplicationInfo: null + CachedConfigurationFilePath: null +afterState: + RebootRequired: false +changedProperties: +- RebootRequired +``` + +Because DSCv3's result output includes the before and after state for the resource, you don't need +to call `dsc resource get` again. + +### Manage state with `dsc config` + +Save the following configuration file as `PSTailspinToys.dsc.config.yaml`. It defines an instance +for both configuration scopes, disabling automatic updates in the machine scope and enabling it +with a 30-day frequency in the user scope. + +```yaml +$schema: https://schemas.microsoft.com/dsc/2023/03/configuration.schema.json +resources: +- name: TSToy PowerShell resources + type: DSC/PowerShellGroup + properties: + resources: + - name: All Users Configuration + type: DscSamples.TailspinToys/PSTailspinToys + properties: + ConfigurationScope: Machine + Ensure: Present + UpdateAutomatically: false + - name: Current User Configuration + type: DscSamples.TailspinToys/PSTailspinToys + properties: + ConfigurationScope: User + Ensure: Present + UpdateAutomatically: true + UpdateFrequency: 30 +``` + +Get the current state of the resource instances. + +```powershell +Get-Content -Path ./PSTailspinToys.dsc.config.yaml | dsc config get +``` + +```yaml +results: +- name: TSToy PowerShell resources + type: DSC/PowerShellGroup + result: + actualState: + - ConfigurationScope: 0 + Ensure: 0 + UpdateAutomatically: false + UpdateFrequency: 0 + CachedCurrentState: null + CachedData: null + CachedApplicationInfo: null + CachedConfigurationFilePath: null + - ConfigurationScope: 1 + Ensure: 0 + UpdateAutomatically: false + UpdateFrequency: 0 + CachedCurrentState: null + CachedData: null + CachedApplicationInfo: null + CachedConfigurationFilePath: null +messages: [] +hadErrors: false +``` + +Test whether the instances are in the desired state. + +```powershell +Get-Content -Path ./PSTailspinToys.dsc.config.yaml | dsc config test +``` + +```yaml +results: +- name: TSToy PowerShell resources + type: DSC/PowerShellGroup + result: + desiredState: + resources: + - name: All Users Configuration + type: DscSamples.TailspinToys/PSTailspinToys + properties: + ConfigurationScope: Machine + Ensure: Present + UpdateAutomatically: false + - name: Current User Configuration + type: DscSamples.TailspinToys/PSTailspinToys + properties: + ConfigurationScope: User + Ensure: Present + UpdateAutomatically: true + UpdateFrequency: 30 + actualState: + - InDesiredState: false + - InDesiredState: false + inDesiredState: false + differingProperties: + - resources +messages: [] +hadErrors: false +``` + +Set the instances to the desired state. + +```powershell +Get-Content -Path ./PSTailspinToys.dsc.config.yaml | dsc config set +``` + +```yaml +results: +- name: TSToy PowerShell resources + type: DSC/PowerShellGroup + result: + beforeState: + - ConfigurationScope: 0 + Ensure: 0 + UpdateAutomatically: false + UpdateFrequency: 0 + CachedCurrentState: null + CachedData: null + CachedApplicationInfo: null + CachedConfigurationFilePath: null + - ConfigurationScope: 1 + Ensure: 0 + UpdateAutomatically: false + UpdateFrequency: 0 + CachedCurrentState: null + CachedData: null + CachedApplicationInfo: null + CachedConfigurationFilePath: null + afterState: + - RebootRequired: false + - RebootRequired: false + changedProperties: [] +messages: [] +hadErrors: false +``` diff --git a/samples/pwsh/resources/first/docs/_index.md b/samples/pwsh/resources/first/docs/_index.md new file mode 100644 index 0000000..263bca1 --- /dev/null +++ b/samples/pwsh/resources/first/docs/_index.md @@ -0,0 +1,58 @@ +--- +title: Write your first DSC Resource in PowerShell +dscs: + tutorials_title: In PowerShell + languages_title: Write a DSC Resource +platen: + menu: + collapse_section: true +--- + +With DSC v3, you can author command-based DSC Resources in any language. This enables you to manage +applications in the programming language you and your team prefer, or in the same language as the +application you're managing. + +DSC v3 also supports authoring DSC Resources in PowerShell. PowerShell resources are made available +through the `DSC/PowerShellGroup` resource provider and the **PSDesiredStateConfiguration** +PowerShell module. + +This tutorial describes how you can implement a DSC Resource as a PowerShell class to manage an +application's configuration files. While this tutorial creates a resource to manage the fictional +[Tailspin Toys `tstoy` application][01], the principles apply when you author any class-based +PowerShell resource. + +In this tutorial, you learn how to: + +- Create a small Go application to use as a DSC Resource. +- Define the properties of the resource. +- Implement `get` and `set` commands for the resource. +- Write a manifest for the resource. +- Manually test the resource. + +## Prerequisites + +- Familiarize yourself with the structure of a command-based DSC Resource. +- Read [About the TSToys application][01], install `tstoy`, and add it to your `PATH`. +- PowerShell 7.2 or higher. +- VS Code with the PowerShell extension. + +## Steps + +1. [Scaffold a DSC Resource module][02] +1. [Add a class-based DSC Resource][03] +1. [Define the configuration properties][04] +1. [Implement the Get method][05] +1. [Implement the Test method][06] +1. [Implement the Set method][07] +1. [Validate the DSC Resource with DSC and PSDSC][08] +1. [Review and next steps][09] + +[01]: /tstoy/about/ +[02]: 1-scaffold-module.md +[03]: 2-add-class-based-resource.md +[04]: 3-define-properties.md +[05]: 4-implement-get.md +[06]: 5-implement-test.md +[07]: 6-implement-set.md +[08]: 7-validate.md +[09]: review.md diff --git a/samples/pwsh/resources/first/docs/review.md b/samples/pwsh/resources/first/docs/review.md new file mode 100644 index 0000000..4c1d2ad --- /dev/null +++ b/samples/pwsh/resources/first/docs/review.md @@ -0,0 +1,34 @@ +--- +title: Review and next steps +weight: 100 +--- + +In this tutorial, you: + +1. Scaffolded a new PowerShell module with a class-based DSC Resource. +1. Defined the configurable settings to manage the TSToy application's configuration files and + update behavior. +1. Implemented the `get` method to return the current state of a TSToy configuration file as an + instance of the resource. +1. Implemented the `test` method to return whether the configuration file for a specified scope is + in the desired state. +1. Implemented the `set` command to idempotently enforce the desired state for TSToy's + configuration files. +1. Tested using the resource in PSDSC with the `Invoke-DscResource` cmdlet. +1. Tested the resource in DSCv3 by invoking the resource with the `dsc resource` commands and + managing instances of the resource in a configuration document. + +At the end of this implementation, you have a functional class-based PowerShell DSC Resource. + +## Clean up + +If you're not going to continue to work with this resource, delete the `DscSamples.TailspinToys` +folder and the files in it. + +## Next steps + +1. Read about class-based DSC Resources, learn how they work, and consider why the resource + in this tutorial is implemented this way. +1. Consider how this resource can be improved. Are there any edge cases or features it doesn't + handle? Can you make the user experience in the terminal more delightful? Update the + implementation with your improvements. diff --git a/samples/pwsh/resources/first/go.mod b/samples/pwsh/resources/first/go.mod new file mode 100644 index 0000000..528e058 --- /dev/null +++ b/samples/pwsh/resources/first/go.mod @@ -0,0 +1,3 @@ +module github.com/PowerShell/DSC-Samples/pwsh/resources/first + +go 1.19