commands.ps1
<# .SYNOPSIS Retrieves or creates a browser context using Microsoft Playwright. .DESCRIPTION The Get-BrowserContext function retrieves the current browser context or creates a new one using Microsoft Playwright. It supports creating a new session by closing and disposing of the existing browser context. The function also uses a storage state file (cookies) if available to maintain session persistence. .PARAMETER NewSession A switch parameter. If specified, a new browser session is created by closing and disposing of the existing browser context. .OUTPUTS [Microsoft.Playwright.IBrowserContext] Returns an instance of the browser context. .EXAMPLE PS C:\> Get-BrowserContext Retrieves the current browser context or creates a new one if none exists. .EXAMPLE PS C:\> Get-BrowserContext -NewSession Creates a new browser session by closing and disposing of the existing browser context. .NOTES - This function depends on Microsoft Playwright for browser automation. - Ensure that Microsoft Playwright is properly installed and configured in your environment. - The function uses the global variables $Script:Playwright, $Script:BrowserContext, and $Script:CookiesPath. - Author: Oleksandr Nikolaiev (@onikolaiev) .LINK https://playwright.dev #> function Get-BrowserContext { [CmdletBinding()] [OutputType([Microsoft.Playwright.IBrowserContext])] param( [Parameter(Mandatory=$false)] [switch]$NewSession ) begin { Invoke-TimeSignal -Start if(-not $Script:Playwright) { $Script:Playwright = [Microsoft.Playwright.Playwright]::CreateAsync().GetAwaiter().GetResult() } $_browserContext = $Script:BrowserContext if($NewSession) { if($_browserContext) { $_browserContext.CloseAsync().GetAwaiter().GetResult() | Out-Null $_browserContext.DisposeAsync().GetAwaiter().GetResult() | Out-Null } $_browserContext = $null } $_headless = Get-PSFConfigValue -FullName "fscps.lcs.settings.all.headless" } process { if (-not $_browserContext) { $browser = $Script:Playwright.Chromium.LaunchAsync([Microsoft.Playwright.BrowserTypeLaunchOptions]@{ Headless = $_headless }).Result if(Test-Path "$Script:CookiesPath" -ErrorAction SilentlyContinue) { $_browserContext = $browser.NewContextAsync(@{ StorageStatePath = $Script:CookiesPath }).GetAwaiter().GetResult() } else { $_browserContext = $browser.NewContextAsync().GetAwaiter().GetResult() } } return $_browserContext } end { Invoke-TimeSignal -End $Script:BrowserContext = $_browserContext } } <# .SYNOPSIS Creates a new browser page using Microsoft Playwright. .DESCRIPTION The Get-NewPage function creates a new browser page within the current browser context using Microsoft Playwright. If the browser context is not initialized or the browser is disconnected, it creates a new browser context session. It also ensures that any previously opened page is closed before creating a new one. .PARAMETER None This function does not take any parameters. .OUTPUTS [Microsoft.Playwright.IPage] Returns a new browser page object. .EXAMPLE PS C:\> Get-NewPage Creates a new browser page in the current browser context. .NOTES - This function depends on the Get-BrowserContext function to manage browser contexts. - Ensure that Microsoft Playwright is properly installed and configured in your environment. - The function uses global variables $Script:BrowserContext and $Script:CurrentPage to manage browser state. .LINK https://playwright.dev #> function Get-NewPage { [CmdletBinding()] [OutputType([Microsoft.Playwright.IPage])] param( ) begin { Invoke-TimeSignal -Start if($Script:BrowserContext) { # Check if the browser context is null if(-not $Script:BrowserContext.Browser.IsConnected) { Get-BrowserContext -NewSession | Out-Null } if($Script:CurrentPage) { $Script:CurrentPage.CloseAsync().GetAwaiter().GetResult() | Out-Null $Script:CurrentPage = $null } } else { Get-BrowserContext -NewSession | Out-Null } } process { # Check if the browser context is null $_page = $Script:BrowserContext.NewPageAsync().Result return $_page } end { Invoke-TimeSignal -End $Script:CurrentPage = $_page } } <# .SYNOPSIS Automates the login process to the LCS (Lifecycle Services) portal using Microsoft Playwright. .DESCRIPTION The Invoke-Login function automates the login process to the LCS (Lifecycle Services) portal. It retrieves the username and password from the PSFramework configuration, navigates to the login page, fills in the credentials, and saves the session cookies for future use. If the login is successful, it updates the browser context with a new session. .PARAMETER None This function does not take any parameters. .OUTPUTS None .EXAMPLE PS C:\> Invoke-Login Logs into the LCS portal using the credentials stored in the PSFramework configuration. .NOTES - This function depends on the Get-NewPage and Get-BrowserContext functions for browser management. - Ensure that Microsoft Playwright is properly installed and configured in your environment. - The username and password must be stored in the PSFramework configuration under the keys: "fscps.lcs.settings.all.lcsUsername" and "fscps.lcs.settings.all.lcsPassword". - The function uses the global variable $Script:CookiesPath to store session cookies. - Author: Oleksandr Nikolaiev (@onikolaiev) .LINK https://playwright.dev #> function Invoke-Login { [CmdletBinding()] param( ) begin { Invoke-TimeSignal -Start $_page = Get-NewPage $lcsUsername = Get-PSFConfigValue -FullName "fscps.lcs.settings.all.lcsUsername" $lcsPassword = Get-PSFConfigValue -FullName "fscps.lcs.settings.all.lcsPassword" } process { # Navigate to the login page $_page.GotoAsync("https://lcs.dynamics.com/Logon/ADLogon").Wait() if($_page.Url -ne "https://lcs.dynamics.com/V2") { # Fill in the username/email field $_page.FillAsync("input[type='email']", "$lcsUsername").Wait() # Click the "Next" button $_page.ClickAsync("input[type='submit']").Wait() # Wait for the password field to appear $_page.WaitForSelectorAsync("input[type='password']").Wait() # Fill in the password field $_page.FillAsync("input[type='password']", "$lcsPassword").Wait() # Click the "Sign in" button $_page.ClickAsync("input[type='submit']").Wait() $_page.ClickAsync("input[type='submit']").Wait() # Wait for the login process to complete (adjust selector as needed) #$page.WaitForNavigationAsync().Wait() $_page.Context.StorageStateAsync(@{ Path = $Script:CookiesPath }).Wait() #$Script:BrowserContext = Get-BrowserContext -NewSession } } end { Invoke-TimeSignal -End } } <# .SYNOPSIS Invoke a process .DESCRIPTION Invoke a process and pass the needed parameters to it .PARAMETER Path Path to the program / executable that you want to start .PARAMETER Params Array of string parameters that you want to pass to the executable .PARAMETER LogPath The path where the log file(s) will be saved .PARAMETER ShowOriginalProgress Instruct the cmdlet to show the standard output in the console Default is $false which will silence the standard output .PARAMETER OutputCommandOnly Instruct the cmdlet to only output the command that you would have to execute by hand Will include full path to the executable and the needed parameters based on your selection .PARAMETER EnableException This parameters disables user-friendly warnings and enables the throwing of exceptions This is less user friendly, but allows catching exceptions in calling scripts .EXAMPLE PS C:\> Invoke-Process -Path "C:\AOSService\PackagesLocalDirectory\Bin\xppc.exe" -Params "-metadata=`"C:\AOSService\PackagesLocalDirectory\Bin`"", "-modelmodule=`"ApplicationSuite`"", "-output=`"C:\AOSService\PackagesLocalDirectory\Bin`"", "-referencefolder=`"C:\AOSService\PackagesLocalDirectory\Bin`"", "-log=`"C:\temp\d365fo.tools\ApplicationSuite\Dynamics.AX.$Module.xppc.log`"", "-xmlLog=`"C:\temp\d365fo.tools\ApplicationSuite\Dynamics.AX.ApplicationSuite.xppc.xml`"", "-verbose" This will invoke the "C:\AOSService\PackagesLocalDirectory\Bin\xppc.exe" executable. All parameters will be passed to it. The standard output will be redirected to a local variable. The error output will be redirected to a local variable. The standard output will be written to the verbose stream before exiting. If an error should occur, both the standard output and error output will be written to the console / host. .EXAMPLE PS C:\> Invoke-Process -ShowOriginalProgress -Path "C:\AOSService\PackagesLocalDirectory\Bin\xppc.exe" -Params "-metadata=`"C:\AOSService\PackagesLocalDirectory\Bin`"", "-modelmodule=`"ApplicationSuite`"", "-output=`"C:\AOSService\PackagesLocalDirectory\Bin`"", "-referencefolder=`"C:\AOSService\PackagesLocalDirectory\Bin`"", "-log=`"C:\temp\d365fo.tools\ApplicationSuite\Dynamics.AX.$Module.xppc.log`"", "-xmlLog=`"C:\temp\d365fo.tools\ApplicationSuite\Dynamics.AX.ApplicationSuite.xppc.xml`"", "-verbose" This will invoke the "C:\AOSService\PackagesLocalDirectory\Bin\xppc.exe" executable. All parameters will be passed to it. The standard output will be outputted directly to the console / host. The error output will be outputted directly to the console / host. .NOTES This is refactored function from d365fo.tools Original Author: Mötz Jensen (@Splaxi) Author: Oleksandr Nikolaiev (@onikolaiev) #> function Invoke-Process { [CmdletBinding()] [OutputType()] param ( [Parameter(Mandatory = $true)] [Alias('Executable')] [string] $Path, [Parameter(Mandatory = $true)] [string[]] $Params, [Parameter(Mandatory = $false)] [string] $LogPath, [switch] $ShowOriginalProgress, [switch] $OutputCommandOnly, [switch] $EnableException ) Invoke-TimeSignal -Start if (-not (Test-PathExists -Path $Path -Type Leaf)) { return } if (Test-PSFFunctionInterrupt) { return } $tool = Split-Path -Path $Path -Leaf $pinfo = New-Object System.Diagnostics.ProcessStartInfo $pinfo.FileName = "$Path" $pinfo.WorkingDirectory = Split-Path -Path $Path -Parent if (-not $ShowOriginalProgress) { Write-PSFMessage -Level Verbose "Output and Error streams will be redirected (silence mode)" $pinfo.RedirectStandardError = $true $pinfo.RedirectStandardOutput = $true } $pinfo.UseShellExecute = $false $pinfo.Arguments = "$($Params -join " ")" $p = New-Object System.Diagnostics.Process $p.StartInfo = $pinfo Write-PSFMessage -Level Verbose "Starting the $tool" -Target "$($params -join " ")" if ($OutputCommandOnly) { Write-PSFMessage -Level Host "$Path $($pinfo.Arguments)" return } $p.Start() | Out-Null if (-not $ShowOriginalProgress) { $outTask = $p.StandardOutput.ReadToEndAsync(); $errTask = $p.StandardError.ReadToEndAsync(); } Write-PSFMessage -Level Verbose "Waiting for the $tool to complete" $p.WaitForExit() if (-not $ShowOriginalProgress) { $stdout = $outTask.Result $stderr = $errTask.Result } if ($p.ExitCode -ne 0 -and (-not $ShowOriginalProgress)) { Write-PSFMessage -Level Host "Exit code from $tool indicated an error happened. Will output both standard stream and error stream." Write-PSFMessage -Level Host "Standard output was: \r\n $stdout" Write-PSFMessage -Level Host "Error output was: \r\n $stderr" $messageString = "Stopping because an Exit Code from $tool wasn't 0 (zero) like expected." Stop-PSFFunction -Message "Stopping because of Exit Code." -Exception $([System.Exception]::new($($messageString -replace '<[^>]+>', ''))) -StepsUpward 1 return } else { Write-PSFMessage -Level Verbose "Standard output was: \r\n $stdout" } if ((-not $ShowOriginalProgress) -and (-not ([string]::IsNullOrEmpty($LogPath)))) { if (-not (Test-PathExists -Path $LogPath -Type Container -Create)) { return } $stdOutputPath = Join-Path -Path $LogPath -ChildPath "$tool`_StdOutput.log" $errOutputPath = Join-Path -Path $LogPath -ChildPath "$tool`_ErrOutput.log" $stdout | Out-File -FilePath $stdOutputPath -Encoding utf8 -Force $stderr | Out-File -FilePath $errOutputPath -Encoding utf8 -Force } Invoke-TimeSignal -End } <# .SYNOPSIS Measures the execution time of a command or function by marking its start and end points. .DESCRIPTION The Invoke-TimeSignal function is used to measure the time spent executing a specific command or function. It works by marking the start and end points of the execution and calculating the time difference between them. The function uses a global hashtable `$Script:TimeSignals` to store the start time for each command or function. When the `-Start` parameter is used, the function records the current time for the specified command or function. If the command is already being tracked, the start time is updated. When the `-End` parameter is used, the function calculates the elapsed time since the start and logs the result. If the command was not started, a message is logged. .PARAMETER Start Marks the start of the time measurement for the current command or function. .PARAMETER End Marks the end of the time measurement for the current command or function and calculates the elapsed time. .EXAMPLE Invoke-TimeSignal -Start This example marks the start of the time measurement for the current command or function. .EXAMPLE Invoke-TimeSignal -End This example marks the end of the time measurement for the current command or function and logs the elapsed time. .NOTES This function uses the PSFramework module for logging and message handling. Ensure the PSFramework module is installed and imported before using this function. The function relies on a global hashtable `$Script:TimeSignals` to track the start times of commands or functions. Author: Oleksandr Nikolaiev (@onikolaiev) #> function Invoke-TimeSignal { [CmdletBinding(DefaultParameterSetName = 'Start')] param ( [Parameter(Mandatory = $true, ParameterSetName = 'Start', Position = 1 )] [switch] $Start, [Parameter(Mandatory = $True, ParameterSetName = 'End', Position = 2 )] [switch] $End ) $Time = (Get-Date) $Command = (Get-PSCallStack)[1].Command if ($Start) { if ($Script:TimeSignals.ContainsKey($Command)) { Write-PSFMessage -Level Verbose -Message "The command '$Command' was already taking part in time measurement. The entry has been update with current date and time." $Script:TimeSignals[$Command] = $Time } else { $Script:TimeSignals.Add($Command, $Time) } } else { if ($Script:TimeSignals.ContainsKey($Command)) { $TimeSpan = New-TimeSpan -End $Time -Start (($Script:TimeSignals)[$Command]) Write-PSFMessage -Level Verbose -Message "Total time spent inside the function was $TimeSpan" -Target $TimeSpan -FunctionName $Command -Tag "TimeSignal" $null = $Script:TimeSignals.Remove($Command) } else { Write-PSFMessage -Level Verbose -Message "The command '$Command' was never started to take part in time measurement." } } } <# .SYNOPSIS Modify the PATH environment variable. .DESCRIPTION Set-PathVariable allows you to add or remove paths to your PATH variable at the specified scope with logic that prevents duplicates. .PARAMETER AddPath A path that you wish to add. Can be specified with or without a trailing slash. .PARAMETER RemovePath A path that you wish to remove. Can be specified with or without a trailing slash. .PARAMETER Scope The scope of the variable to edit. Either Process, User, or Machine. If you specify Machine, you must be running as administrator. .EXAMPLE Set-PathVariable -AddPath C:\tmp\bin -RemovePath C:\path\java This will add the C:\tmp\bin path and remove the C:\path\java path. The Scope will be set to Process, which is the default. .INPUTS .OUTPUTS .NOTES Author: ThePoShWolf .LINK Author: Oleksandr Nikolaiev (@onikolaiev) #> Function Set-PathVariable { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] param ( [string]$AddPath, [string]$RemovePath, [ValidateSet('Process', 'User', 'Machine')] [string]$Scope = 'Process' ) $regexPaths = @() if ($PSBoundParameters.Keys -contains 'AddPath') { $regexPaths += [regex]::Escape($AddPath) } if ($PSBoundParameters.Keys -contains 'RemovePath') { $regexPaths += [regex]::Escape($RemovePath) } $arrPath = [System.Environment]::GetEnvironmentVariable('PATH', $Scope) -split ';' foreach ($path in $regexPaths) { $arrPath = $arrPath | Where-Object { $_ -notMatch "^$path\\?" } } $value = ($arrPath + $addPath) -join ';' [System.Environment]::SetEnvironmentVariable('PATH', $value, $Scope) } <# .SYNOPSIS Test multiple paths .DESCRIPTION Easy way to test multiple paths for public functions and have the same error handling .PARAMETER Path Array of paths you want to test They have to be the same type, either file/leaf or folder/container .PARAMETER Type Type of path you want to test Either 'Leaf' or 'Container' .PARAMETER Create Instruct the cmdlet to create the directory if it doesn't exist .PARAMETER ShouldNotExist Instruct the cmdlet to return true if the file doesn't exists .EXAMPLE PS C:\> Test-PathExists "c:\temp","c:\temp\dir" -Type Container This will test if the mentioned paths (folders) exists and the current context has enough permission. .NOTES This is refactored function from d365fo.tools Original Author: Mötz Jensen (@Splaxi) Author: Oleksandr Nikolaiev (@onikolaiev) #> function Test-PathExists { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "")] [CmdletBinding()] [OutputType([System.Boolean])] param ( [Parameter(Mandatory = $True)] [AllowEmptyString()] [string[]] $Path, [ValidateSet('Leaf', 'Container')] [Parameter(Mandatory = $True)] [string] $Type, [switch] $Create, [switch] $ShouldNotExist ) $res = $false $arrList = New-Object -TypeName "System.Collections.ArrayList" foreach ($item in $Path) { if ([string]::IsNullOrEmpty($item)) { Stop-PSFFunction -Message "Stopping because path was either null or empty string." -StepsUpward 1 return } Write-PSFMessage -Level Debug -Message "Testing the path: $item" -Target $item $temp = Test-Path -Path $item -Type $Type if ((-not $temp) -and ($Create) -and ($Type -eq "Container")) { Write-PSFMessage -Level Debug -Message "Creating the path: $item" -Target $item $null = New-Item -Path $item -ItemType Directory -Force -ErrorAction Stop $temp = $true } elseif ($ShouldNotExist) { Write-PSFMessage -Level Debug -Message "The should NOT exists: $item" -Target $item } elseif ((-not $temp) -and ($WarningPreference -ne [System.Management.Automation.ActionPreference]::SilentlyContinue)) { Write-PSFMessage -Level Host -Message "The <c='em'>$item</c> path wasn't found. Please ensure the path <c='em'>exists</c> and you have enough <c='em'>permission</c> to access the path." } $null = $arrList.Add($temp) } if ($arrList.Contains($false) -and (-not $ShouldNotExist)) { # The $ErrorActionPreference variable determines the behavior we are after, but the "Stop-PSFFunction -WarningAction" is where we need to put in the value. Stop-PSFFunction -Message "Stopping because of missing paths." -StepsUpward 1 -WarningAction $ErrorActionPreference } elseif ($arrList.Contains($true) -and $ShouldNotExist) { # The $ErrorActionPreference variable determines the behavior we are after, but the "Stop-PSFFunction -WarningAction" is where we need to put in the value. Stop-PSFFunction -Message "Stopping because file exists." -StepsUpward 1 -WarningAction $ErrorActionPreference } else { $res = $true } $res } <# .SYNOPSIS Test if a given registry key exists or not .DESCRIPTION Test if a given registry key exists in the path specified .PARAMETER Path Path to the registry hive and sub directories you want to work against .PARAMETER Name Name of the registry key that you want to test for .EXAMPLE PS C:\> Test-RegistryValue -Path "HKLM:\SOFTWARE\Microsoft\Dynamics\Deployment\" -Name "InstallationInfoDirectory" This will query the LocalMachine hive and the sub directories "HKLM:\SOFTWARE\Microsoft\Dynamics\Deployment\" for a registry key with the name of "InstallationInfoDirectory". .NOTES This is refactored function from d365fo.tools Original Author: Mötz Jensen (@Splaxi) Author: Oleksandr Nikolaiev (@onikolaiev) #> Function Test-RegistryValue { [OutputType('System.Boolean')] param( [Parameter(Mandatory = $true)] [string]$Path, [Parameter(Mandatory = $true)] [string]$Name ) if (Test-Path -Path $Path -PathType Any) { $null -ne (Get-ItemProperty $Path).$Name } else { $false } } <# .SYNOPSIS Retrieves details of a specific project from Microsoft Dynamics Lifecycle Services (LCS) using its project ID. .DESCRIPTION The Get-FSCPSLCSProject function uses Microsoft Playwright to interact with LCS and retrieves details of a specific project by its project ID. It constructs the API request URL dynamically, includes the required headers (such as __RequestVerificationToken), and sends a GET request to the LCS API endpoint. The function handles authentication, session management, and API requests to fetch the required project data. .PARAMETER ProjectId Specifies the ID of the project to retrieve. This parameter is mandatory. .EXAMPLE PS C:\> Get-FSCPSLCSProject -ProjectId "12345" Retrieves details of the project with ID "12345" from Microsoft Dynamics Lifecycle Services. .NOTES - This function uses Microsoft Playwright for browser automation. - Ensure that the Playwright environment is properly initialized before calling this function. - Author: [Your Name] #> function Get-FSCPSLCSProject { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$ProjectId ) begin { Invoke-TimeSignal -Start [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 Invoke-Login $_lcsUri = Get-PSFConfigValue -FullName "fscps.lcs.settings.all.lcsUrl" $_apiUri = "RainierProject/GetProject/$ProjectId"+"?_=$(Get-Date -UFormat %s)" $_requestUri = "$($_lcsUri.TrimEnd('/'))/$($_apiUri.TrimStart('/'))" } process { if (Test-PSFFunctionInterrupt) { return } try { # Create the request options $requestOptions = Get-PWRequestOptions # Send the GET request $response = $Script:CurrentPage.APIRequest.GetAsync($_requestUri, $requestOptions).GetAwaiter().GetResult() # Ensure the request was successful if ($response.Status -eq [System.Net.HttpStatusCode]::OK) { $result = $response.TextAsync().GetAwaiter().GetResult() | ConvertFrom-Json return $result.Data | Select-PSFObject * } else { throw "Failed to retrieve project. Status code: $($response.Status)" } } catch { Write-PSFMessage -Level Error -Message "An error occurred while retrieving the project: $_" } } end { Cleanup-Session Invoke-TimeSignal -End } } <# .SYNOPSIS Retrieves a list of all projects from D365 LCS with optional paging parameters. .DESCRIPTION The Get-FSCPSLCSProjectList function uses Microsoft Playwright to automate the login process to LCS (Lifecycle Services) and retrieves a list of all projects using a POST request. It handles authentication, session management, and API requests to fetch the required data. .PARAMETER StartPosition The starting position for paging. Defaults to 0. .PARAMETER ItemsRequested The number of items to retrieve. Defaults to 20. .EXAMPLE PS C:\> Get-FSCPSLCSProjectList -StartPosition 0 -ItemsRequested 20 Retrieves the first 20 projects from D365 LCS starting at position 0. .NOTES - This function uses Microsoft Playwright for browser automation. - Author: Oleksandr Nikolaiev (@onikolaiev) #> function Get-FSCPSLCSProjectList { [CmdletBinding()] [OutputType()] param( [Parameter(Mandatory=$false)] [int]$StartPosition = 0, [Parameter(Mandatory=$false)] [int]$ItemsRequested = 50 ) begin { Invoke-TimeSignal -Start [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 Invoke-Login $_lcsUri = Get-PSFConfigValue -FullName "fscps.lcs.settings.all.lcsUrl" $_apiUri = "RainierProject/AllProjectsList" $_requestUri = "$($_lcsUri.TrimEnd('/'))/$($_apiUri.TrimStart('/'))" } PROCESS { if (Test-PSFFunctionInterrupt) { return } try { $body = @{ DynamicPaging = @{ StartPosition = $StartPosition ItemsRequested = $ItemsRequested } Filtering = $null } $jsonBody = $body | ConvertTo-Json -Depth 10 # Create the request options $requestOptions = Get-PWRequestOptions $requestOptions.DataObject = $jsonBody $response = $Script:CurrentPage.APIRequest.PostAsync($_requestUri, $requestOptions).GetAwaiter().GetResult() # Ensure the request was successful if ($response.Status -eq [System.Net.HttpStatusCode]::OK) { $result = $response.TextAsync().GetAwaiter().GetResult() | ConvertFrom-Json return $result.Data | Select-PSFObject * } else { throw "Failed to retrieve project. Status code: $($response.Status)" } } catch { Write-PSFMessage -Level Error -Message "An error occurred while retrieving all projects: $_" } } END { Cleanup-Session Invoke-TimeSignal -End } } <# .SYNOPSIS Get the FSCPS configuration details .DESCRIPTION Get the FSCPS configuration details from the configuration store All settings retrieved from this cmdlets is to be considered the default parameter values across the different cmdlets .PARAMETER SettingsJsonString String contains settings JSON .PARAMETER SettingsJsonPath String contains path to the settings.json .PARAMETER OutputAsHashtable Instruct the cmdlet to return a hashtable object .EXAMPLE PS C:\> Get-FSCPSLCSSettings This will output the current FSCPS configuration. The object returned will be a PSCustomObject. .EXAMPLE PS C:\> Get-FSCPSLCSSettings -OutputAsHashtable This will output the current FSCPS configuration. The object returned will be a Hashtable. .LINK Set-FSCPSLCSSettings .NOTES Tags: Environment, Url, Config, Configuration, LCS, Upload, ClientId Author: Oleksandr Nikolaiev (@onikolaiev) #> function Get-FSCPSLCSSettings { [CmdletBinding()] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "")] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseOutputTypeCorrectly", "")] param ( [string] $SettingsJsonString, [string] $SettingsJsonPath, [switch] $OutputAsHashtable ) begin{ Invoke-TimeSignal -Start $helperPath = Join-Path -Path $($Script:ModuleRoot) -ChildPath "\internal\scripts\helpers.ps1" -Resolve . ($helperPath) $res = [Ordered]@{} if((-not ($SettingsJsonString -eq "")) -and (-not ($SettingsJsonPath -eq ""))) { throw "Both settings parameters should not be provided. Please provide only one of them." } if(-not ($SettingsJsonString -eq "")) { $tmpSettingsFilePath = "C:\temp\settings.json" $null = Test-PathExists -Path "C:\temp\" -Type Container -Create $null = Set-Content $tmpSettingsFilePath $SettingsJsonString -Force -PassThru $null = Set-FSCPSLCSSettings -SettingsFilePath $tmpSettingsFilePath } if(-not ($SettingsJsonPath -eq "")) { $null = Set-FSCPSLCSSettings -SettingsFilePath $SettingsJsonPath } } process{ foreach ($config in Get-PSFConfig -FullName "fscps.lcs.settings.all.*") { $propertyName = $config.FullName.ToString().Replace("fscps.lcs.settings.all.", "") $res.$propertyName = $config.Value } if($Script:IsOnGitHub)# If GitHub context { foreach ($config in Get-PSFConfig -FullName "fscps.lcs.settings.github.*") { $propertyName = $config.FullName.ToString().Replace("fscps.lcs.settings.github.", "") $res.$propertyName = $config.Value } } if($Script:IsOnAzureDevOps)# If ADO context { foreach ($config in Get-PSFConfig -FullName "fscps.lcs.settings.ado.*") { $propertyName = $config.FullName.ToString().Replace("fscps.lcs.settings.ado.", "") $res.$propertyName = $config.Value } } if($Script:IsOnLocalhost)# If localhost context { foreach ($config in Get-PSFConfig -FullName "fscps.lcs.settings.localhost.*") { $propertyName = $config.FullName.ToString().Replace("fscps.lcs.settings.localhost.", "") $res.$propertyName = $config.Value } } if($OutputAsHashtable) { $res } else { [PSCustomObject]$res } } end{ Invoke-TimeSignal -End } } <# .SYNOPSIS Retrieves a list of shared assets from D365 LCS. .DESCRIPTION The Get-FSCPSLCSSharedAssetList function uses Microsoft Playwright to automate the login process to LCS (Lifecycle Services) and retrieves a list of shared assets based on the specified asset file type. It handles authentication, session management, and API requests to fetch the required data. .PARAMETER AssetFileType The type of asset file to retrieve. This parameter is mandatory and defaults to "SoftwareDeployablePackage". .EXAMPLE PS C:\> Get-FSCPSLCSSharedAssetList -AssetFileType SoftwareDeployablePackage Retrieves a list of shared assets of type "SoftwareDeployablePackage" from D365 LCS. .NOTES - This function uses Microsoft Playwright for browser automation. - Author: Oleksandr Nikolaiev (@onikolaiev) #> function Get-FSCPSLCSSharedAssetList { [CmdletBinding()] [OutputType()] param( [Parameter(Mandatory=$false)] [AssetFileType]$AssetFileType = [AssetFileType]::SoftwareDeployablePackage ) begin { Invoke-TimeSignal -Start [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 Invoke-Login $_lcsUri = Get-PSFConfigValue -FullName "fscps.lcs.settings.all.lcsUrl" $_apiUri = "/FileAsset/GetSharedAssets/?assetKind=$([int]$AssetFileType)" $_requestUri = "$($_lcsUri.TrimEnd('/'))/$($_apiUri.TrimStart('/'))" } PROCESS { if (Test-PSFFunctionInterrupt) { return } $requestResult = $Script:CurrentPage.APIRequest.GetAsync($_requestUri).GetAwaiter().GetResult() if ($requestResult.Status -ne [System.Net.HttpStatusCode]::OK) { throw "Failed to retrieve shared assets. Status code: $($requestResult.StatusCode)" } $result = $requestResult.JsonAsync().GetAwaiter().GetResult() $assetList = $result.ToString() | ConvertFrom-Json return $assetList.Data.Assets } END { Cleanup-Session Invoke-TimeSignal -End } } <# .SYNOPSIS Set the FSCPS configuration details .DESCRIPTION Set the FSCPS configuration details from the configuration store All settings retrieved from this cmdlets is to be considered the default parameter values across the different cmdlets .PARAMETER SettingsJsonString String contains JSON with custom settings .PARAMETER SettingsFilePath Set path to the settings.json file .EXAMPLE PS C:\> Set-FSCPSSettings -SettingsFilePath "c:\temp\settings.json" This will output the current FSCPS configuration. The object returned will be a Hashtable. .LINK Get-FSCPSLCSSettings .NOTES Tags: Environment, Url, Config, Configuration, Upload, ClientId, Settings Author: Oleksandr Nikolaiev (@onikolaiev) #> function Set-FSCPSLCSSettings { [CmdletBinding()] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "")] [OutputType([System.Collections.Specialized.OrderedDictionary])] param ( [string] $SettingsFilePath, [string] $SettingsJsonString ) begin{ if((-not ($SettingsJsonString -eq "")) -and (-not ($SettingsFilePath -eq ""))) { throw "Both settings parameters cannot be provided. Please provide only one of them." } if(-not ($SettingsJsonString -eq "")) { $SettingsFilePath = "C:\temp\settings.json" $null = Test-PathExists -Path "C:\temp\" -Type Container -Create $null = Set-Content $SettingsFilePath $SettingsJsonString -Force -PassThru } $fscpsFolderName = Get-PSFConfigValue -FullName "fscps.lcs.settings.all.fscpsFolder" $fscmSettingsFile = Get-PSFConfigValue -FullName "fscps.lcs.settings.all.fscpsSettingsFile" $fscmRepoSettingsFile = Get-PSFConfigValue -FullName "fscps.lcs.settings.all.fscpsRepoSettingsFile" Write-PSFMessage -Level Verbose -Message "fscpsFolderName is: $fscpsFolderName" Write-PSFMessage -Level Verbose -Message "fscmSettingsFile is: $fscmSettingsFile" Write-PSFMessage -Level Verbose -Message "fscmRepoSettingsFile is: $fscmRepoSettingsFile" $settingsFiles = @() $res = [Ordered]@{} $reposytoryName = "" $reposytoryOwner = "" $currentBranchName = "" if($Script:IsOnGitHub)# If GitHub context { Write-PSFMessage -Level Important -Message "Running on GitHub" Set-PSFConfig -FullName 'fscps.lcs.settings.all.repoProvider' -Value 'GitHub' Set-PSFConfig -FullName 'fscps.lcs.settings.all.repositoryRootPath' -Value "$env:GITHUB_WORKSPACE" Set-PSFConfig -FullName 'fscps.lcs.settings.all.runId' -Value "$ENV:GITHUB_RUN_NUMBER" Set-PSFConfig -FullName 'fscps.lcs.settings.all.workflowName' -Value "$ENV:GITHUB_WORKFLOW" if($SettingsFilePath -eq "") { $RepositoryRootPath = "$env:GITHUB_WORKSPACE" Write-PSFMessage -Level Verbose -Message "GITHUB_WORKSPACE is: $RepositoryRootPath" $settingsFiles += (Join-Path $fscpsFolderName $fscmSettingsFile) } else{ $settingsFiles += $SettingsFilePath } $reposytoryOwner = "$env:GITHUB_REPOSITORY".Split("/")[0] $reposytoryName = "$env:GITHUB_REPOSITORY".Split("/")[1] Write-PSFMessage -Level Verbose -Message "GITHUB_REPOSITORY is: $reposytoryName" $branchName = "$env:GITHUB_REF" Write-PSFMessage -Level Verbose -Message "GITHUB_REF is: $branchName" $currentBranchName = [regex]::Replace($branchName.Replace("refs/heads/","").Replace("/","_"), '(?i)(?:^|-|_)(\p{L})', { $args[0].Groups[1].Value.ToUpper()}) $gitHubFolder = ".github" $workflowName = "$env:GITHUB_WORKFLOW" Write-PSFMessage -Level Verbose -Message "GITHUB_WORKFLOW is: $workflowName" $workflowName = ($workflowName.Split([System.IO.Path]::getInvalidFileNameChars()) -join "").Replace("(", "").Replace(")", "").Replace("/", "") $settingsFiles += (Join-Path $gitHubFolder $fscmRepoSettingsFile) $settingsFiles += (Join-Path $gitHubFolder "$workflowName.settings.json") } elseif($Script:IsOnAzureDevOps)# If Azure DevOps context { Write-PSFMessage -Level Verbose -Message "Running on Azure" Set-PSFConfig -FullName 'fscps.lcs.settings.all.repoProvider' -Value 'AzureDevOps' Set-PSFConfig -FullName 'fscps.lcs.settings.all.repositoryRootPath' -Value "$env:PIPELINE_WORKSPACE" Set-PSFConfig -FullName 'fscps.lcs.settings.all.runId' -Value "$ENV:Build_BuildNumber" Set-PSFConfig -FullName 'fscps.lcs.settings.all.workflowName' -Value "$ENV:Build_DefinitionName" if($SettingsFilePath -eq "") { $RepositoryRootPath = "$env:PIPELINE_WORKSPACE" Write-PSFMessage -Level Verbose -Message "RepositoryRootPath is: $RepositoryRootPath" } else{ $settingsFiles += $SettingsFilePath } $reposytoryOwner = $($env:SYSTEM_TEAMFOUNDATIONCOLLECTIONURI.replace('https://dev.azure.com/', '').replace('/', '').replace('https:','')) $reposytoryName = "$env:SYSTEM_TEAMPROJECT" $branchName = "$env:BUILD_SOURCEBRANCH" $currentBranchName = [regex]::Replace($branchName.Replace("/Metadata","").Replace("$/$($reposytoryName)/","").Replace("$/$($reposytoryName)","").Replace("Trunk/","").Replace("/","_"), '(?i)(?:^|-|_)(\p{L})', { $args[0].Groups[1].Value.ToUpper() }) #$settingsFiles += (Join-Path $fscpsFolderName $fscmSettingsFile) } else { # If Desktop or other Write-PSFMessage -Level Verbose -Message "Running on desktop" Set-PSFConfig -FullName 'fscps.lcs.settings.all.repoProvider' -Value 'Other' if($SettingsFilePath -eq "") { throw "SettingsFilePath variable should be passed if running on the cloud/personal computer" } $reposytoryName = "windows host" Set-PSFConfig -FullName 'fscps.lcs.settings.all.runId' -Value 1 $currentBranchName = 'DEV' $settingsFiles += $SettingsFilePath } Set-PSFConfig -FullName 'fscps.lcs.settings.all.currentBranch' -Value $currentBranchName Set-PSFConfig -FullName 'fscps.lcs.settings.all.repoOwner' -Value $reposytoryOwner Set-PSFConfig -FullName 'fscps.lcs.settings.all.repoName' -Value $reposytoryName function MergeCustomObjectIntoOrderedDictionary { Param( [System.Collections.Specialized.OrderedDictionary] $dst, [PSCustomObject] $src ) # Add missing properties in OrderedDictionary $src.PSObject.Properties.GetEnumerator() | ForEach-Object { $prop = $_.Name $srcProp = $src."$prop" $srcPropType = $srcProp.GetType().Name if (-not $dst.Contains($prop)) { if ($srcPropType -eq "PSCustomObject") { $dst.Add("$prop", [ordered]@{}) } elseif ($srcPropType -eq "Object[]") { $dst.Add("$prop", @()) } else { $dst.Add("$prop", $srcProp) } } } @($dst.Keys) | ForEach-Object { $prop = $_ if ($src.PSObject.Properties.Name -eq $prop) { $dstProp = $dst."$prop" $srcProp = $src."$prop" $dstPropType = $dstProp.GetType().Name $srcPropType = $srcProp.GetType().Name if($dstPropType -eq 'Int32' -and $srcPropType -eq 'Int64') { $dstPropType = 'Int64' } if ($srcPropType -eq "PSCustomObject" -and $dstPropType -eq "OrderedDictionary") { MergeCustomObjectIntoOrderedDictionary -dst $dst."$prop".Value -src $srcProp } elseif ($dstPropType -ne $srcPropType) { throw "property $prop should be of type $dstPropType, is $srcPropType." } else { if ($srcProp -is [Object[]]) { $srcProp | ForEach-Object { $srcElm = $_ $srcElmType = $srcElm.GetType().Name if ($srcElmType -eq "PSCustomObject") { $ht = [ordered]@{} $srcElm.PSObject.Properties | Sort-Object -Property Name -Culture "iv-iv" | ForEach-Object { $ht[$_.Name] = $_.Value } $dst."$prop" += @($ht) } else { $dst."$prop" += $srcElm } } } else { Write-PSFMessage -Level Verbose -Message "Searching fscps.lcs.settings.*.$prop" $setting = Get-PSFConfig -FullName "fscps.lcs.settings.*.$prop" Write-PSFMessage -Level Verbose -Message "Found $setting" if($setting) { Set-PSFConfig -FullName $setting.FullName -Value $srcProp } #$dst."$prop" = $srcProp } } } } } } process{ Invoke-TimeSignal -Start $res = Get-FSCPSSettings -OutputAsHashtable $settingsFiles | ForEach-Object { $settingsFile = $_ if($RepositoryRootPath) { $settingsPath = Join-Path $RepositoryRootPath $settingsFile } else { $settingsPath = $SettingsFilePath } Write-PSFMessage -Level Verbose -Message "Settings file '$settingsFile' - $(If (Test-Path $settingsPath) {"exists. Processing..."} Else {"not exists. Skip."})" if (Test-Path $settingsPath) { try { $settingsJson = Get-Content $settingsPath -Encoding UTF8 | ConvertFrom-Json # check settingsJson.version and do modifications if needed MergeCustomObjectIntoOrderedDictionary -dst $res -src $settingsJson } catch { Write-PSFMessage -Level Host -Message "Settings file $settingsPath, is wrongly formatted." -Exception $PSItem.Exception Stop-PSFFunction -Message "Stopping because of errors" return throw } } Write-PSFMessage -Level Verbose -Message "Settings file '$settingsFile' - processed" } Write-PSFMessage -Level Host -Message "Settings were updated succesfully." Invoke-TimeSignal -End } end{ } } |