PsLogicAppExtractor.psm1
$script:ModuleRoot = $PSScriptRoot $script:ModuleVersion = (Import-PowerShellDataFile -Path "$($script:ModuleRoot)\PsLogicAppExtractor.psd1").ModuleVersion # Detect whether at some level dotsourcing was enforced $script:doDotSource = Get-PSFConfigValue -FullName PsLogicAppExtractor.Import.DoDotSource -Fallback $false if ($PsLogicAppExtractor_dotsourcemodule) { $script:doDotSource = $true } <# Note on Resolve-Path: All paths are sent through Resolve-Path/Resolve-PSFPath in order to convert them to the correct path separator. This allows ignoring path separators throughout the import sequence, which could otherwise cause trouble depending on OS. Resolve-Path can only be used for paths that already exist, Resolve-PSFPath can accept that the last leaf my not exist. This is important when testing for paths. #> # Detect whether at some level loading individual module files, rather than the compiled module was enforced $importIndividualFiles = Get-PSFConfigValue -FullName PsLogicAppExtractor.Import.IndividualFiles -Fallback $false if ($PsLogicAppExtractor_importIndividualFiles) { $importIndividualFiles = $true } if (Test-Path (Resolve-PSFPath -Path "$($script:ModuleRoot)\..\.git" -SingleItem -NewChild)) { $importIndividualFiles = $true } if ("<was compiled>" -eq '<was not compiled>') { $importIndividualFiles = $true } function Import-ModuleFile { <# .SYNOPSIS Loads files into the module on module import. .DESCRIPTION This helper function is used during module initialization. It should always be dotsourced itself, in order to proper function. This provides a central location to react to files being imported, if later desired .PARAMETER Path The path to the file to load .EXAMPLE PS C:\> . Import-ModuleFile -File $function.FullName Imports the file stored in $function according to import policy #> [CmdletBinding()] Param ( [string] $Path ) $resolvedPath = $ExecutionContext.SessionState.Path.GetResolvedPSPathFromPSPath($Path).ProviderPath if ($doDotSource) { . $resolvedPath } else { $ExecutionContext.InvokeCommand.InvokeScript($false, ([scriptblock]::Create([io.file]::ReadAllText($resolvedPath))), $null, $null) } } #region Load individual files if ($importIndividualFiles) { # Execute Preimport actions foreach ($path in (& "$ModuleRoot\internal\scripts\preimport.ps1")) { . Import-ModuleFile -Path $path } # Import all internal functions foreach ($function in (Get-ChildItem "$ModuleRoot\internal\functions" -Filter "*.ps1" -Recurse -ErrorAction Ignore)) { . Import-ModuleFile -Path $function.FullName } # Import all public functions foreach ($function in (Get-ChildItem "$ModuleRoot\functions" -Filter "*.ps1" -Recurse -ErrorAction Ignore)) { . Import-ModuleFile -Path $function.FullName } # Execute Postimport actions foreach ($path in (& "$ModuleRoot\internal\scripts\postimport.ps1")) { . Import-ModuleFile -Path $path } # End it here, do not load compiled code below return } #endregion Load individual files #region Load compiled code <# This file loads the strings documents from the respective language folders. This allows localizing messages and errors. Load psd1 language files for each language you wish to support. Partial translations are acceptable - when missing a current language message, it will fallback to English or another available language. #> Import-PSFLocalizedString -Path "$($script:ModuleRoot)\en-us\*.psd1" -Module 'PsLogicAppExtractor' -Language 'en-US' class Helper { Helper([object] $values) { if ($values -is [System.Collections.IDictionary]) { foreach ($key in $values.Keys) { if ($this.PSObject.Properties.Item($key)) { $this.$key = $values[$key] } } } else { foreach ($property in $values.PSObject.Properties) { if ($this.PSObject.Properties.Item($property.Name)) { $this.($property.Name) = $property.Value } } } } } class Definition: Helper { [object] ${$schema} [string] $contentVersion [object] $parameters [object] $triggers [object] $actions [object] $outputs Definition([object] $values) : base($values) { } } class Properties : Helper { [string] $state [Definition] $definition [object] $parameters [object] $integrationAccount [object] $accessControl Properties([object] $values) : base($values) { } } class LogicApp : Helper { [string] $type [string] $apiVersion [string] $name [string] $location [object] $tags [object] $identity [Properties] $properties LogicApp([object] $values) : base($values) { } } class ArmTemplate: Helper { [object] ${$schema} [string] $contentVersion [object] $parameters [object] $variables [object] $resources [object] $outputs ArmTemplate([object] $values) : base($values) { } } <# .SYNOPSIS Get the header for a runbook file .DESCRIPTION Gets the header for a runbook file, containing the sane defaults Allows you to prepare the runbook file as much as possible, based on the parameters that you pass to it .PARAMETER SubscriptionId Id of the subscription that you want to work against At runtime / execution of Invoke-PsLaExtractor - your current powershell / az cli session either needs to "connected" to the subscription or at least have permissions to work against the subscription Useful when you know upfront what you want to work against, as you don't need to pass the parameter into the Invoke-PsLaExtractor .PARAMETER ResourceGroup Name of the resource group that you want to work against At runtime / execution of Invoke-PsLaExtractor - your current powershell / az cli session needs to have permissions to work against the resource group Useful when you know upfront what you want to work against, as you don't need to pass the parameter into the Invoke-PsLaExtractor .PARAMETER Name Name of the logic app, that you want to work against At runtime / execution of Invoke-PsLaExtractor - your current powershell / az cli session needs to have permissions to work against the logic app Useful when you know upfront what you want to work against, as you don't need to pass the parameter into the Invoke-PsLaExtractor .PARAMETER ApiVersion The ApiVersion that you want the LogicApp to be working against The default value is: "2019-05-01" .PARAMETER IncludePrefixSuffix Instruct the cmdlet to add the different prefix and suffix options, with the default values that comes with the module This make it easier to make the runbook file work across different environments, without having to worry about prepping different prefix and suffix value prior .EXAMPLE PS C:\> Get-BuildHeader Creates the bare minimum header for the runbook file Prepares the Properties object with sane defaults, allowing you to edit them after the file has been created .EXAMPLE PS C:\> Get-BuildHeader -SubscriptionId "f5608f3d-ab28-49d8-9b4e-1b1f812d12e0" -ResourceGroup "TestRg" Creates the bare minimum header for the runbook file Prepares the Properties object with SubscriptionId and ResourceGroup, and sane defaults for the remaining objects, allowing you to edit them after the file has been created .EXAMPLE PS C:\> Get-BuildHeader -Name "TestLogicApp" -ApiVersion "2019-05-01" Creates the bare minimum header for the runbook file Prepares the Properties object with Name and ApiVersion, and sane defaults for the remaining objects, allowing you to edit them after the file has been created .NOTES Author: Mötz Jensen (@Splaxi) #> function Get-BuildHeader { [CmdletBinding()] param ( [string] $SubscriptionId, [string] $ResourceGroup, [string] $Name, [string] $ApiVersion = "2019-05-01", [switch] $IncludePrefixSuffix ) $res = New-Object System.Collections.Generic.List[System.Object] $res.Add("# Object to store the needed parameters for when running the export") $res.Add("Properties {") if ($SubscriptionId) { $res.Add('$SubscriptionId = "{0}"' -f $SubscriptionId) } else { $res.Add('$SubscriptionId = $null') } if ($ResourceGroup) { $res.Add('$ResourceGroup = "{0}"' -f $ResourceGroup) } else { $res.Add('$ResourceGroup = $null') } if ($Name) { $res.Add('$Name = "{0}"' -f $Name) } else { $res.Add('$Name = ""') } if ($ApiVersion) { $res.Add('$ApiVersion = "{0}"' -f $ApiVersion) } else { $res.Add('$ApiVersion = ""') } if ($IncludePrefixSuffix) { $res.Add('$Tag_Prefix = "{0}"' -f $(Get-PSFConfigValue -FullName PsLogicAppExtractor.prefixsuffix.tag.prefix)) $res.Add('$Tag_Suffix = "{0}"' -f $(Get-PSFConfigValue -FullName PsLogicAppExtractor.prefixsuffix.tag.suffix)) $res.Add('$Parm_Prefix = "{0}"' -f $(Get-PSFConfigValue -FullName PsLogicAppExtractor.prefixsuffix.parm.prefix)) $res.Add('$Parm_Suffix = "{0}"' -f $(Get-PSFConfigValue -FullName PsLogicAppExtractor.prefixsuffix.parm.suffix)) $res.Add('$Connection_Prefix = "{0}"' -f $(Get-PSFConfigValue -FullName PsLogicAppExtractor.prefixsuffix.connection.prefix)) $res.Add('$Connection_Suffix = "{0}"' -f $(Get-PSFConfigValue -FullName PsLogicAppExtractor.prefixsuffix.connection.suffix)) $res.Add('$Trigger_Prefix = "{0}"' -f $(Get-PSFConfigValue -FullName PsLogicAppExtractor.prefixsuffix.trigger.prefix)) $res.Add('$Trigger_Suffix = "{0}"' -f $(Get-PSFConfigValue -FullName PsLogicAppExtractor.prefixsuffix.trigger.suffix)) } $res.Add('}') #Above line completes the Properties declaration $res.Add('') $res.Add('# Used to import the needed classes into the powershell session, to help with the export of the Logic App') $res.Add('."$(Get-PSFConfigValue -FullName PsLogicAppExtractor.ModulePath.Classes)\PsLogicAppExtractor.class.ps1"') $res.Add('') $res.Add("# Path variable for all the tasks that is available from the PsLogicAppExtractor module") $res.Add('$pathTasks = $(Get-PSFConfigValue -FullName PsLogicAppExtractor.ModulePath.Tasks)') $res.Add('') $res.Add("# Include all the tasks that is available from the PsLogicAppExtractor module") $res.Add("Include `"`$pathTasks\All\All.task.ps1`"") $res.Add('') $res } <# .SYNOPSIS Add new parameter to the ARM template .DESCRIPTION Adds or overwrites an ARM template parameter by the name provided, and allows you to specify the default value, type and the metadata decription .PARAMETER InputObject The ARM object that you want to work against It has to be a object of the type [ArmTemplate] for it to work properly .PARAMETER Name Name of the parameter that you want to work against If the parameter exists, the value gets overrided otherwise a new parameter is added to the list of parameters .PARAMETER Type The type of the ARM template parameter It supports all known types .PARAMETER Value The default value, that you want to assign to the ARM template parameter .PARAMETER Description The metadata description that you want to assign to the ARM template parameters .EXAMPLE PS C:\> Add-ArmParameter -InputObject $armObj -Name "logicAppName" -Type "string" -Value "TestLogicApp" Creates / updates the logicAppName ARM template parameter Sets the type of the parameter to: string Sets the default value to: TestLogicApp .EXAMPLE PS C:\> Add-ArmParameter -InputObject $armObj -Name "logicAppName" -Type "string" -Value "TestLogicApp" -Description "This is the name we extracted from the orignal LogicApp" Creates / updates the logicAppName ARM template parameter Sets the type of the parameter to: string Sets the default value to: TestLogicApp Sets the metadata description .NOTES Author: Mötz Jensen (@Splaxi) #> function Add-ArmParameter { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [object] $InputObject, [Alias('ParameterName')] [Parameter(Mandatory = $true)] [string] $Name, [Parameter(Mandatory = $true)] [string] $Type, [Parameter(Mandatory = $true)] [object] $Value, [string] $Description ) if ($Description) { $valueObj = $([ordered]@{ type = $Type; defaultValue = $Value; metadata = [ordered]@{ description = $Description } }) } else { $valueObj = $([ordered]@{ type = $Type; defaultValue = $Value; }) } if ($InputObject.parameters.$Name) { $InputObject.parameters.$Name = $($valueObj) } else { $InputObject.parameters | Add-Member -MemberType NoteProperty -Name $Name -Value $valueObj } $InputObject } <# .SYNOPSIS Add new variable to the ARM template .DESCRIPTION Adds or overwrites an ARM template variable by the name provided, and allows you to specify the value .PARAMETER InputObject The ARM object that you want to work against It has to be a object of the type [ArmTemplate] for it to work properly .PARAMETER Name Name of the variable that you want to work against If the variable exists, the value gets overrided otherwise a new variable is added to the list of variables .PARAMETER Value The value, that you want to assign to the ARM template variable .EXAMPLE PS C:\> Add-ArmVariable -InputObject $armObj -Name "logicAppName" -Value "TestLogicApp" Creates / updates the logicAppName ARM template variable Sets the value to: TestLogicApp .NOTES Author: Mötz Jensen (@Splaxi) #> function Add-ArmVariable { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [object] $InputObject, [Alias('VariableName')] [Parameter(Mandatory = $true)] [string] $Name, [Parameter(Mandatory = $true)] [object] $Value ) if ($InputObject.variables.$Name) { $InputObject.variables.$Name = $($Value) } else { $InputObject.variables | Add-Member -MemberType NoteProperty -Name $Name -Value $($Value) } $InputObject } <# .SYNOPSIS Add new parm (parameter) to the LogicApp object .DESCRIPTION Adds or overwrites a LogicApp parm (parameter) by the name provided, and allows you to specify the default value and the type .PARAMETER InputObject The ARM object that you want to work against It has to be a object of the type [LogicApp] for it to work properly .PARAMETER Name Name of the parm (parameter) that you want to work against If the parm (parameter) exists, the value gets overrided otherwise a new parm (parameter) is added to the list of parms (parameters) .PARAMETER Type The type of the LogicApp parm (parameter) It supports all known types .PARAMETER Value The default value, that you want to assign to the LogicApp parm (parameter) .EXAMPLE PS C:\> Add-LogicAppParm -InputObject $lgObj -Name "TriggerQueue" -Type "string" -Value "Inbound" Creates / updates the TriggerQueue LogicApp parm (parameter) Sets the type of the parameter to: string Sets the default value to: Inbound .NOTES Author: Mötz Jensen (@Splaxi) #> function Add-LogicAppParm { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [object] $InputObject, [Alias('ParmName')] [Parameter(Mandatory = $true)] [string] $Name, [Parameter(Mandatory = $true)] [string] $Type, [Parameter(Mandatory = $true)] [object] $Value ) $valueObj = $([ordered]@{ type = $Type; defaultValue = $Value; }) if ($InputObject.properties.definition.parameters.$Name) { $InputObject.properties.definition.parameters.$Name = $($valueObj) } else { $InputObject.properties.definition.parameters | Add-Member -MemberType NoteProperty -Name $Name -Value $valueObj } $InputObject } <# .SYNOPSIS Format the name with the prefix and suffix .DESCRIPTION Format the name with the prefix and suffix If the passed prefix and suffix is not $null, then they are used Otherwise the cmdlet will default back to the configuration for each type, that is persisted in the configuration store .PARAMETER Type The type of name that you want to work against Allowed values: Tag Connection Parameter Parm .PARAMETER Prefix The prefix that you want to append to the name If empty / $null - then the cmdlet will use the prefix that is stored for the specific type .PARAMETER Suffix The suffix that you want to append to the name If empty / $null - then the cmdlet will use the suffix that is stored for the specific type .PARAMETER Value The string value that you want to have the prefix and suffix concatenated with .EXAMPLE PS C:\> Format-Name -Type "Tag" -Value "CostCenter" Formats the value: CostCenter with the default prefix and suffix for the type: Tag The default prefix is: tag_ The default suffix is: $null The output will be: tag_CostCenter .NOTES Author: Mötz Jensen (@Splaxi) #> function Format-Name { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseOutputTypeCorrectly', '')] [CmdletBinding()] param ( [ValidateSet('Tag', 'Connection', 'Parameter', 'Parm', 'Trigger')] [Parameter(Mandatory = $true)] [string] $Type, [string] $Prefix, [string] $Suffix, [Alias('Name')] [Parameter(Mandatory = $true)] [string] $Value ) switch ($Type) { "Tag" { if ($Prefix -or $Suffix) { "$Prefix$Value$Suffix" } else { $Prefix = Get-PSFConfigValue -FullName PsLogicAppExtractor.prefixsuffix.tag.prefix $Suffix = Get-PSFConfigValue -FullName PsLogicAppExtractor.prefixsuffix.tag.suffix "$Prefix$Value$Suffix" } } "Connection" { if ($Prefix -or $Suffix) { "$Prefix$Value$Suffix" } else { $Prefix = Get-PSFConfigValue -FullName PsLogicAppExtractor.prefixsuffix.connection.prefix $Suffix = Get-PSFConfigValue -FullName PsLogicAppExtractor.prefixsuffix.connection.suffix "$Prefix$Value$Suffix" } } "Parameter" { } "Parm" { if ($Prefix -or $Suffix) { "$Prefix$Value$Suffix" } else { $Prefix = Get-PSFConfigValue -FullName PsLogicAppExtractor.prefixsuffix.parm.prefix $Suffix = Get-PSFConfigValue -FullName PsLogicAppExtractor.prefixsuffix.parm.suffix "$Prefix$Value$Suffix" } } "Trigger" { if ($Prefix -or $Suffix) { "$Prefix$Value$Suffix" } else { $Prefix = Get-PSFConfigValue -FullName PsLogicAppExtractor.prefixsuffix.trigger.prefix $Suffix = Get-PSFConfigValue -FullName PsLogicAppExtractor.prefixsuffix.trigger.suffix "$Prefix$Value$Suffix" } } Default { "$Prefix$Value$Suffix" } } } <# Set-PSFConfig -Module 'PsLogicAppExtractor' -Name 'prefixsuffix.tag.prefix' -Value "tag_" -Initialize -Description "The default prefix for Tag objects, used as a fallback value for the Format-Name cmdlet." Set-PSFConfig -Module 'PsLogicAppExtractor' -Name 'prefixsuffix.tag.suffix' -Value "" -Initialize -Description "The default suffix for Tag objects, used as a fallback value for the Format-Name cmdlet. Be default an empty string" Set-PSFConfig -Module 'PsLogicAppExtractor' -Name 'prefixsuffix.tag.prefix' -Value "tag_" -Initialize -Description "The default prefix for Tag objects, used as a fallback value for the Format-Name cmdlet." Set-PSFConfig -Module 'PsLogicAppExtractor' -Name 'prefixsuffix.tag.suffix' -Value "" -Initialize -Description "The default suffix for Tag objects, used as a fallback value for the Format-Name cmdlet. Be default an empty string." Set-PSFConfig -Module 'PsLogicAppExtractor' -Name 'prefixsuffix.parm.prefix' -Value "parm_" -Initialize -Description "The default prefix for parm (parameter) objects, used as a fallback value for the Format-Name cmdlet." Set-PSFConfig -Module 'PsLogicAppExtractor' -Name 'prefixsuffix.parm.suffix' -Value "" -Initialize -Description "The default suffix for parm (parameter) objects, used as a fallback value for the Format-Name cmdlet. Be default an empty string." Set-PSFConfig -Module 'PsLogicAppExtractor' -Name 'prefixsuffix.connection.prefix' -Value "connection_" -Initialize -Description "The default prefix for connection objects, used as a fallback value for the Format-Name cmdlet." Set-PSFConfig -Module 'PsLogicAppExtractor' -Name 'prefixsuffix.connection.suffix' -Value "_id" -Initialize -Description "The default suffix for connection objects, used as a fallback value for the Format-Name cmdlet." Set-PSFConfig -Module 'PsLogicAppExtractor' -Name 'prefixsuffix.trigger.prefix' -Value "trigger_" -Initialize -Description "The default prefix for trigger objects, used as a fallback value for the Format-Name cmdlet." Set-PSFConfig -Module 'PsLogicAppExtractor' -Name 'prefixsuffix.trigger.suffix' -Value "" -Initialize -Description "The default suffix for trigger objects, used as a fallback value for the Format-Name cmdlet. Be default an empty string." #> <# .SYNOPSIS Get action from the object, filtered by the type of the action .DESCRIPTION Get actions and all nested actions, filtered by type .PARAMETER InputObject The object that you want to work against Will by analyzed to see if it has nested actions, and will be recursively traversed to fetch all actions .PARAMETER Type The action type that will be outputted .EXAMPLE PS C:\> Get-ActionsByType -InputObject $obj -Type "Http" Will traverse the $obj and filter actions to only output the ones of the type Http .NOTES Author: Mötz Jensen (@Splaxi) #> function Get-ActionsByType { param ( [PsCustomObject] $InputObject, [string] $Type ) if ($InputObject.Type -eq $Type -or $InputObject.Value.Type -eq $Type) { $InputObject } if ($InputObject.Value.actions) { foreach ($item in $InputObject.Value.actions.PsObject.Properties) { Get-ActionsByType -InputObject $item -Type $Type } } elseif ($InputObject.actions) { foreach ($item in $InputObject.actions.PsObject.Properties) { Get-ActionsByType -InputObject $item -Type $Type } } } <# .SYNOPSIS Get the value from an ARM template parameter .DESCRIPTION Gets the current default value from the specified ARM template parameter .PARAMETER InputObject The ARM object that you want to work against It has to be a object of the type [ArmTemplate] for it to work properly .PARAMETER Name Name of the parameter that you want to work against .EXAMPLE PS C:\> Get-ArmParameterValue -InputObject $armObj -Name "logicAppName" Gets the default value from the ARM template parameter: logicAppName .NOTES Author: Mötz Jensen (@Splaxi) #> function Get-ArmParameterValue { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [object] $InputObject, [Alias('ParameterName')] [Parameter(Mandatory = $true)] [string] $Name ) if ($InputObject.parameters.$Name) { $InputObject.parameters.$Name.defaultValue } } <# .SYNOPSIS Get the output file .DESCRIPTION Get the full path of the "latest" file from the workpath of the runbook / extraction process .PARAMETER Path Path to the workpath where the runbook has been persisting files .EXAMPLE PS C:\> Get-ExtractOutput -Path "C:\temp\work_directory" Returns the full path of the latest written file from the "C:\temp\work_directory" path .NOTES Author: Mötz Jensen (@Splaxi) #> function Get-ExtractOutput { [CmdletBinding()] param ( [Alias('WorkPath')] [Parameter(Mandatory = $true)] [string] $Path ) $files = Get-ChildItem -Path $Path -Recurse -File $files | Sort-Object -Property LastWriteTime | Select-Object -Last 1 -ExpandProperty FullName } <# .SYNOPSIS Get parameters from ARM template .DESCRIPTION Get parameters from the ARM template You can include / exclude parameters, so your parameter file only contains the parameters you want to handle at deployment The default value is promoted as the initial value of the parameter .PARAMETER Path Path to the ARM template that you want to work against .PARAMETER Exclude Instruct the cmdlet to exclude the given set of parameter names Supports array / list .PARAMETER Include Instruct the cmdlet to include the given set of parameter names Supports array / list .PARAMETER AsFile Instruct the cmdlet to save a valid ARM template parameter file next to the ARM template file .PARAMETER BlankValues Instructs the cmdlet to blank the values in the parameter file .PARAMETER CopyMetadata Instructs the cmdlet to copy over the metadata property from the original parameter in the ARM template, if present .EXAMPLE PS C:\> Get-PsLaArmParameter -Path "C:\temp\work_directory\TestLogicApp.json" Gets all parameters from the "TestLogicApp.json" ARM template The output is written to the console .EXAMPLE PS C:\> Get-PsLaArmParameter -Path "C:\temp\work_directory\TestLogicApp.json" -Exclude "logicAppLocation","trigger_Frequency" Gets all parameters from the "TestLogicApp.json" ARM template Will exclude the parameters "logicAppLocation" & "trigger_Frequency" if present The output is written to the console .EXAMPLE PS C:\> Get-PsLaArmParameter -Path "C:\temp\work_directory\TestLogicApp.json" -Include "trigger_Interval","trigger_Frequency" Gets all parameters from the "TestLogicApp.json" ARM template Will only copy over the parameters "trigger_Interval" & "trigger_Frequency" if present The output is written to the console .EXAMPLE PS C:\> Get-PsLaArmParameter -Path "C:\temp\work_directory\TestLogicApp.json" -AsFile Gets all parameters from the "TestLogicApp.json" ARM template The output is written the "C:\temp\work_directory\TestLogicApp.parameters.json" file .EXAMPLE PS C:\> Get-PsLaArmParameter -Path "C:\temp\work_directory\TestLogicApp.json" -BlankValues Gets all parameters from the "TestLogicApp.json" ARM template Blank all values for each parameter The output is written to the console .EXAMPLE PS C:\> Get-PsLaArmParameter -Path "C:\temp\work_directory\TestLogicApp.json" -CopyMetadata Gets all parameters from the "TestLogicApp.json" ARM template Copies over the metadata property from the original parameter, if present The output is written to the console .NOTES Author: Mötz Jensen (@Splaxi) #> function Get-PsLaArmParameter { [CmdletBinding()] param ( [string] $Path, [string[]] $Exclude, [string[]] $Include, [switch] $AsFile, [switch] $BlankValues, [switch] $CopyMetadata ) $armObj = [ArmTemplate]$(Get-TaskWorkObject -Path $Path) $res = [ordered]@{} $res.'$schema' = "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#" $res.contentVersion = "1.0.0.0" $res.parameters = [ordered]@{} foreach ($item in $armObj.parameters.PsObject.Properties) { if ($item.Name -in $Exclude) { continue } if ($Include.Count -gt 0) { if (-not ($item.Name -in $Include)) { continue } } $valueObj = [ordered]@{} if ($BlankValues) { switch ($item.Value.Type) { "int" { $valueObj.value = 0 } "bool" { $valueObj.value = $false } "object" { $valueObj.value = $null } "array" { $valueObj.value = @() } Default { $valueObj.value = "" } } } else { $valueObj.value = $item.Value.DefaultValue } if ($CopyMetadata -and $item.Value.metadata) { $valueObj.metadata = $item.Value.metadata } $res.parameters."$($item.Name)" = $valueObj } if ($AsFile) { $pathLocal = $Path.Replace(".json", ".parameters.json") $encoding = New-Object System.Text.UTF8Encoding($true) [System.IO.File]::WriteAllLines($pathLocal, $($([PSCustomObject] $res) | ConvertTo-Json -Depth 10), $encoding) Get-Item -Path $pathLocal | Select-Object -ExpandProperty FullName } else { $([PSCustomObject] $res) | ConvertTo-Json -Depth 10 } } <# .SYNOPSIS Get tasks that are part of the module .DESCRIPTION List all avaiable tasks that are part of the module, to be used for exporting, sanitizing and converting a LogicApp into a deployable ARM template .PARAMETER Category Used to filter the number of tasks down to only being part of the category that you are looking for .PARAMETER Detailed Instruct the cmdlet to output the details about the tasks in a more detailed fashion, makes it easier to read the descriptions for each task .EXAMPLE PS C:\> Get-PsLaTask List all available tasks Output example: Category Name Description -------- ---- ----------- Arm Set-Arm.Connections.ManagedApis.AsParameter Loops all $connections childs… Arm Set-Arm.Connections.ManagedApis.AsVariable Loops all $connections childs… Arm Set-Arm.Connections.ManagedApis.IdFormatted Loops all $connections childs… Arm Set-Arm.IntegrationAccount.IdFormatted.Simple.AsParameter.AsVariable Creates an Arm variable: integrationAccount… .EXAMPLE PS C:\> Get-PsLaTask -Category Converter List all available tasks, which are in the Converter category Output example: Category Name Description -------- ---- ----------- Converter ConvertTo-Arm Converts the LogicApp json structure into a valid ARM template json Converter ConvertTo-Raw Converts the raw LogicApp json structure into the a valid LogicApp json,… .EXAMPLE PS C:\> Get-PsLaTask -Detailed List all available tasks, and outputs it in the detailed view Output example: Category : Arm Name : Set-Arm.Connections.ManagedApis.AsParameter Description : Loops all $connections childs -Creates an Arm parameter, with prefix & suffix --Sets the default value to the original name, extracted from connectionId property -Sets the connectionId to: [resourceId('Microsoft.Web/connections', parameters('XYZ'))] -Sets the connectionName to: [parameters('XYZ')] Category : Arm Name : Set-Arm.Connections.ManagedApis.AsVariable Description : Loops all $connections childs -Creates an Arm variable, with prefix & suffix --Sets the value to the original name, extracted from connectionId property -Sets the connectionId to: [resourceId('Microsoft.Web/connections', variables('XYZ'))] -Sets the connectionName to: [variables('XYZ')] .NOTES Author: Mötz Jensen (@Splaxi) #> function Get-PsLaTask { [CmdletBinding()] param ( [ValidateSet('Arm', 'Converter', 'Exporter', 'Raw')] [string] $Category, [switch] $Detailed ) $res = Get-PsLaTaskByPath -Path "$($MyInvocation.MyCommand.Module.ModuleBase)\internal\tasks" | Where-Object Category -like "*$Category*" | Select-Object -Property * -ExcludeProperty Path if ($Detailed) { $res | Format-List } else { $res } } <# .SYNOPSIS Get tasks that are references from a file .DESCRIPTION Get tasks that are references from a runbook file, to make it easier to understand what a given runbook file is doing .PARAMETER File Path to the runbook file, that you want to analyze .PARAMETER Category Used to filter the number of tasks down to only being part of the category that you are looking for .PARAMETER Detailed Instruct the cmdlet to output the details about the tasks in a more detailed fashion, makes it easier to read the descriptions for each task .EXAMPLE PS C:\> Get-PsLaTaskByFile -File "C:\temp\LogicApp.ExportOnly.psakefile.ps1" List all tasks that are referenced from the file The file needs to be a valid PSake runbook file Output example: Category Name Description -------- ---- ----------- Converter ConvertTo-Arm Converts the LogicApp json structure into a valid ARM template json Converter ConvertTo-Raw Converts the raw LogicApp json structure into the a valid LogicApp json,… Exporter Export-LogicApp.AzCli Exports the raw version of the Logic App from the Azure Portal .EXAMPLE PS C:\> Get-PsLaTaskByFile -File "C:\temp\LogicApp.ExportOnly.psakefile.ps1" -Category Converter List all tasks that are referenced from the file, which are in the Converter category The file needs to be a valid PSake runbook file Output example: Category Name Description -------- ---- ----------- Converter ConvertTo-Arm Converts the LogicApp json structure into a valid ARM template json Converter ConvertTo-Raw Converts the raw LogicApp json structure into the a valid LogicApp json,… .EXAMPLE PS C:\> Get-PsLaTaskByFile -File "C:\temp\LogicApp.ExportOnly.psakefile.ps1" -Detailed List all tasks that are referenced from the file, and outputs it in the detailed view The file needs to be a valid PSake runbook file Output example: Category : Converter Name : ConvertTo-Arm Description : Converts the LogicApp json structure into a valid ARM template json Category : Converter Name : ConvertTo-Raw Description : Converts the raw LogicApp json structure into the a valid LogicApp json, this will remove different properties that are not needed Category : Exporter Name : Export-LogicApp.AzCli Description : Exports the raw version of the Logic App from the Azure Portal .NOTES Author: Mötz Jensen (@Splaxi) #> function Get-PsLaTaskByFile { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseOutputTypeCorrectly', '')] [CmdletBinding()] param ( [Alias('Runbook')] [Parameter(Mandatory = $true)] [string] $File, [ValidateSet('Arm', 'Converter', 'Exporter', 'LogicApp')] [string] $Category, [switch] $Detailed ) # We are playing around with the internal / global psake object $psake.context = New-Object System.Collections.Stack $res = @(Get-PSakeScriptTasks -Runbook $File | Where-Object Name -ne "Default" | Select-Object -Property @{Label = "Category"; Expression = { $_.Alias.Split(".")[0] } }, Name, Description) $temp = $res | Where-Object Category -like "*$Category*" | Sort-Object Category, Name if ($Detailed) { $temp | Format-List } else { $temp } } <# .SYNOPSIS Get tasks based on files from a directory .DESCRIPTION Get tasks from individual files, that are located in a directory .PARAMETER Path Path to the directory where there are valid PSake tasks saved as ps1 files .EXAMPLE PS C:\> Get-PsLaTaskByPath -Path c:\temp\tasks List all available tasks, based on the files in the directory All files has to be valid PSake files saved as ps1 files Output example: Category : Arm Name : Set-Arm.Connections.ManagedApis.AsParameter Description : Loops all $connections childs -Creates an Arm parameter, with prefix & suffix --Sets the default value to the original name, extracted from connectionId property -Sets the connectionId to: [resourceId('Microsoft.Web/connections', parameters('XYZ'))] -Sets the connectionName to: [parameters('XYZ')] Path : c:\temp\tasks\Set-Arm.Connections.ManagedApis.AsParameter.task.ps1 File : Set-Arm.Connections.ManagedApis.AsParameter.task.ps1 Category : Arm Name : Set-Arm.Connections.ManagedApis.AsVariable Description : Loops all $connections childs -Creates an Arm variable, with prefix & suffix --Sets the value to the original name, extracted from connectionId property -Sets the connectionId to: [resourceId('Microsoft.Web/connections', variables('XYZ'))] -Sets the connectionName to: [variables('XYZ')] Path : c:\temp\tasks\Set-Arm.Connections.ManagedApis.AsVariable.task.ps1 File : Set-Arm.Connections.ManagedApis.AsVariable.task.ps1 .NOTES General notes #> function Get-PsLaTaskByPath { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string] $Path ) $files = Get-ChildItem -Path "$Path\*.ps1" $res = New-Object System.Collections.Generic.List[System.Object] # We are playing around with the internal / global psake object $psake.context = New-Object System.Collections.Stack $psake.context.push( @{ "tasks" = @{} "aliases" = @{} } ) foreach ($item in $files) { # We are playing around with the internal / global psake object $psake.context = New-Object System.Collections.Stack $psake.context.push( @{ "tasks" = @{} "aliases" = @{} } ) . $item.FullName foreach ($task in $psake.context.tasks) { foreach ($value in $task.Values) { $res.Add([PsCustomObject][ordered]@{ Category = $value.Alias.Split(".")[0] Name = $value.Name Description = $value.Description Path = $item.FullName File = $item.Name }) } } } # We are playing around with the internal / global psake object $psake.context = New-Object System.Collections.Stack $res.ToArray() | Where-Object Name -ne "Default" | Sort-Object Category, Name } <# .SYNOPSIS Get tasks that are references from a file, with the execution order .DESCRIPTION Get tasks that are references from a runbook file, to make it easier to understand what a given runbook file is doing Includes the execution order of the tasks, to visualize the tasks sequence .PARAMETER File Path to the runbook file, that you want to analyze .PARAMETER Detailed Instruct the cmdlet to output the details about the tasks in a more detailed fashion, makes it easier to read the descriptions for each task .EXAMPLE PS C:\> Get-PsLaTaskOrderByFile -File "C:\temp\LogicApp.ExportOnly.psakefile.ps1" List all tasks that are referenced from the file The file needs to be a valid PSake runbook file Output example: ExecutionOrder Category Name Description -------------- -------- ---- ----------- 1 Exporter Export-LogicApp.AzCli Exports the raw version of the Logic App from the Azure Portal 2 Converter ConvertTo-Raw Converts the raw LogicApp json structure into the a valid LogicApp j… 3 Converter ConvertTo-Arm Converts the LogicApp json structure into a valid ARM template json .EXAMPLE PS C:\> Get-PsLaTaskOrderByFile -File "C:\temp\LogicApp.ExportOnly.psakefile.ps1" -Detailed List all tasks that are referenced from the file, and outputs it in the detailed view The file needs to be a valid PSake runbook file Output example: ExecutionOrder : 1 Category : Exporter Name : Export-LogicApp.AzCli Description : Exports the raw version of the Logic App from the Azure Portal ExecutionOrder : 2 Category : Converter Name : ConvertTo-Raw Description : Converts the raw LogicApp json structure into the a valid LogicApp json, this will remove different properties that are not needed ExecutionOrder : 3 Category : Converter Name : ConvertTo-Arm Description : Converts the LogicApp json structure into a valid ARM template json .NOTES General notes #> function Get-PsLaTaskOrderByFile { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseOutputTypeCorrectly', '')] [CmdletBinding()] param ( [Alias('Runbook')] [Parameter(Mandatory = $true)] [string] $File, [switch] $Detailed ) # We are playing around with the internal / global psake object $psake.context = New-Object System.Collections.Stack $default = Get-PSakeScriptTasks -Runbook $File | Where-Object Name -eq "Default" | Select-Object -First 1 $tasks = @(Get-PSakeScriptTasks -Runbook $File | Where-Object Name -ne "Default" | Select-Object -Property @{Label = "Category"; Expression = { $_.Alias.Split(".")[0] } }, Name, Description) $res = @(for ($i = 0; $i -lt $default.DependsOn.Count; $i++) { $tasks | Where-Object Name -eq $($default.DependsOn[$i]) | Select-Object -Property @{Label = "ExecutionOrder"; Expression = { $i + 1 } }, * }) if ($Detailed) { $res | Format-List } else { $res } } <# .SYNOPSIS Short description .DESCRIPTION Long description .PARAMETER Category Instruct the cmdlet which template type you want to have outputted .PARAMETER OutputPath Path to were the Task template file will be persisted The path has to be a directory The file will be named: _set-XYA.Template.ps1 .EXAMPLE PS C:\> Get-PsLaTaskTemplate -Category "Arm" Outputs the task template of the type Arm to the console .EXAMPLE PS C:\> Get-PsLaTaskTemplate -Category "Arm" -OutputPath "C:\temp\work_directory" Outputs the task template of the type Arm Persists the file into the "C:\temp\work_directory" directory .NOTES Author: Mötz Jensen (@Splaxi) #> function Get-PsLaTaskTemplate { [CmdletBinding()] param ( [ValidateSet('Arm', 'Converter', 'Raw')] [string] $Category, [string] $OutputPath ) if ($OutputPath) { Copy-Item -Path "$($script:ModuleRoot)\internal\tasks\_Set-$Category.Template.tmp" -Destination "$OutputPath\_Set-$Category.Template.ps1" -PassThru -Force | Select-Object -ExpandProperty FullName } else { Get-Content -Path "$($script:ModuleRoot)\internal\tasks\_Set-$Category.Template.tmp" -Raw } } <# .SYNOPSIS Get the object that the task has to work against .DESCRIPTION Gets the object from the "previous" task, based on the persisted path and loads it into memory using ConvertFrom-Json .PARAMETER Path Path to the file that you want the task to work against .EXAMPLE PS C:\> Get-TaskWorkObject Returns the object that is stored at the location passed in the Path parameter .NOTES Author: Mötz Jensen (@Splaxi) #> function Get-TaskWorkObject { [CmdletBinding()] param ( [string] $Path = $(Get-PSFConfigValue -FullName PsLogicAppExtractor.Execution.TaskInputNext) ) Get-Content -Path $Path -Raw | ConvertFrom-Json } <# .SYNOPSIS Get the object that the task has to work against, as a raw string .DESCRIPTION Gets the object from the "previous" task, based on the persisted path and loads it into memory using Get-Content .PARAMETER Path Path to the file that you want the task to work against .EXAMPLE PS C:\> Get-TaskWorkObject Returns the object that is stored at the location passed in the Path parameter .NOTES Author: Mötz Jensen (@Splaxi) #> function Get-TaskWorkRaw { [CmdletBinding()] param ( [string] $Path = $(Get-PSFConfigValue -FullName PsLogicAppExtractor.Execution.TaskInputNext) ) Get-Content -Path $Path -Raw } <# .SYNOPSIS Execute the extractor process of the LogicApp .DESCRIPTION Execute all the tasks that have been defined in the runbook file, and get an ARM template as output Depending on the initial extractor task that you are using, your powershell / az cli session needs to be signed in Your runbook file can contain the tasks available from the module, but also your own custom tasks, that you want to have executed as part of the process .PARAMETER Runbook Path to the PSake valid runbook file that you want to have executed while exporting, sanitizing and converting a LogicApp into a deployable ARM template .PARAMETER SubscriptionId Id of the subscription that you want to work against, your current powershell / az cli session either needs to "connected" to the subscription or at least have permissions to work against the subscription .PARAMETER ResourceGroup Name of the resource group that you want to work against, your current powershell / az cli session needs to have permissions to work against the resource group .PARAMETER Name Name of the logic app, that you want to work against .PARAMETER Task List of task that you want to have executed, based on the runbook file that you pass This allows you to only run a small subset of all the tasks that you have defined inside your runbook Helpful when troubleshooting and trying to identify the best execution order of all the tasks .PARAMETER WorkPath Path to were the tasks will persist their outputs Each task will save a file into a unique folder, containing the formatted output from its operation You could risk that secrets or credentials are being stored on your disk, if they in some way are stored as clear text inside the logic app The default valus is the current users TempPath, where it creates a "\PsLogicAppExtractor\GUID\" directory for each invoke .PARAMETER OutputPath Path to were the ARM template file will be persisted The path has to be a directory The file will be named as the Logic App is named .PARAMETER KeepFiles Instruct the cmdlet to keep all the files, across all tasks This enables troubleshooting and comparison of input vs output, per task, as each task has an input file and the result of the work persisted in the same directory .EXAMPLE PS C:\> Invoke-PsLaExtractor -Runbook "C:\temp\LogicApp.ExportOnly.psakefile.ps1" -ResourceGroup "TestRg" -Name TestLogicApp Invokes the different tasks inside the runbook file, to export the TestLogicApp as an ARM template The file needs to be a valid PSake runbook file .EXAMPLE PS C:\> Invoke-PsLaExtractor -Runbook "C:\temp\LogicApp.ExportOnly.psakefile.ps1" -SubscriptionId "f5608f3d-ab28-49d8-9b4e-1b1f812d12e0" -ResourceGroup "TestRg" -Name "TestLogicApp" Invokes the different tasks inside the runbook file, to export the TestLogicApp as an ARM template The file needs to be a valid PSake runbook file .EXAMPLE PS C:\> Invoke-PsLaExtractor -Runbook "C:\temp\LogicApp.ExportOnly.psakefile.ps1" Invokes the different tasks inside the runbook file, to export the TestLogicApp as an ARM template The file needs to be a valid PSake runbook file The runbook file needs to have populated the Properties object, with the minimum: ResourceGroup and SubscriptionId .EXAMPLE PS C:\> Invoke-PsLaExtractor -Runbook "C:\temp\LogicApp.ExportOnly.psakefile.ps1" -ResourceGroup "TestRg" -Name TestLogicApp -WorkPath "C:\temp\work_directory" Invokes the different tasks inside the runbook file, to export the TestLogicApp as an ARM template The file needs to be a valid PSake runbook file Will output all tasks files into the "C:\temp\work_directory" location .EXAMPLE PS C:\> Invoke-PsLaExtractor -Runbook "C:\temp\LogicApp.ExportOnly.psakefile.ps1" -ResourceGroup "TestRg" -Name TestLogicApp -KeepFiles Invokes the different tasks inside the runbook file, to export the TestLogicApp as an ARM template The file needs to be a valid PSake runbook file All files that the different tasks has created, are keept, for the user to analyze them .NOTES Author: Mötz Jensen (@Splaxi) #> function Invoke-PsLaExtractor { [CmdletBinding(DefaultParameterSetName = "NameOnly")] param ( [Alias('File')] [Parameter(Mandatory = $true, ParameterSetName = "NameOnly")] [Parameter(Mandatory = $true, ParameterSetName = "PreppedFile")] [Parameter(Mandatory = $true, ParameterSetName = "ResourceGroup")] [Parameter(Mandatory = $true, ParameterSetName = "Subscription")] [string] $Runbook, [Parameter(Mandatory = $true, ParameterSetName = "Subscription")] [string] $SubscriptionId, [Parameter(Mandatory = $true, ParameterSetName = "ResourceGroup")] [Parameter(Mandatory = $true, ParameterSetName = "Subscription")] [string] $ResourceGroup, [Parameter(Mandatory = $true, ParameterSetName = "NameOnly")] [Parameter(Mandatory = $true, ParameterSetName = "ResourceGroup")] [Parameter(Mandatory = $true, ParameterSetName = "Subscription")] [string] $Name, [string[]] $Task, [string] $WorkPath = "$([System.IO.Path]::GetTempPath())PsLogicAppExtractor\$([System.Guid]::NewGuid().Guid)", [string] $OutputPath, [switch] $KeepFiles ) if (-not ($WorkPath -like "*$([System.IO.Path]::GetTempPath())*")) { if ($WorkPath -NotMatch '(?im)[{(]?[0-9A-F]{8}[-]?(?:[0-9A-F]{4}[-]?){3}[0-9A-F]{12}[)}]?') { $WorkPath = "$WorkPath\$([System.Guid]::NewGuid().Guid)" } } #The task counter needs to be reset prior running Set-PSFConfig -FullName PsLogicAppExtractor.Execution.TaskCounter -Value 0 Set-PSFConfig -FullName PsLogicAppExtractor.Execution.TaskInputNext -Value "" Set-PSFConfig -FullName PsLogicAppExtractor.Execution.TaskOutputFile -Value "" Set-PSFConfig -FullName PsLogicAppExtractor.Execution.TaskPath -Value "" #Make sure the work path is created and available New-Item -Path $WorkPath -ItemType Directory -Force -ErrorAction Ignore > $null $parms = @{} $parms.buildFile = $Runbook $parms.nologo = $true if ($Task) { $parms.taskList = $Task } $props = @{} if ($SubscriptionId) { $props.SubscriptionId = $SubscriptionId } if ($ResourceGroup) { $props.ResourceGroup = $ResourceGroup } if ($Name) { $props.Name = $Name } Set-PSFConfig -FullName PsLogicAppExtractor.Execution.WorkPath -Value $WorkPath $res = Invoke-psake @parms -properties $props -ErrorVariable errorsFound if ($errorsFound) { throw $res } $resPath = Get-ExtractOutput -Path $WorkPath if ($OutputPath) { $resPath = Copy-Item -Path $resPath -Destination "$OutputPath" -PassThru -Force | Select-Object -ExpandProperty FullName } $resPath if (-not $KeepFiles) { Get-ChildItem -Path $WorkPath -File -Recurse | Where-Object { $_.FullName -ne $resPath } | ForEach-Object { Remove-Item -Path $_.FullName -Force -ErrorAction SilentlyContinue -Confirm:$false -Recurse } Get-ChildItem -Path $WorkPath -Directory | Where-Object { $_.FullName -ne $(Split-Path -Path $resPath -Parent) } | ForEach-Object { Remove-Item -Path $_.FullName -Force -ErrorAction SilentlyContinue -Confirm:$false -Recurse } } } <# .SYNOPSIS Create valid runbook file based on the task files in the directory .DESCRIPTION Helps you build a valid runbook file, based on all the individual task files (ps1) that are located in the directory This makes it easy to get a starting point for a new runbook file, based on the tasks you have persisted as individual files Especially helpful for custom tasks that might be stored in a central repository The task files has to valid PSake tasks saved as ps1 files .PARAMETER Path Path to the directory where there are valid PSake tasks saved as ps1 files The default value is set for the internal directory where all the generic tasks that are part of the module is located .PARAMETER SubscriptionId Id of the subscription that you want to work against At runtime / execution of Invoke-PsLaExtractor - your current powershell / az cli session either needs to "connected" to the subscription or at least have permissions to work against the subscription Useful when you know upfront what you want to work against, as you don't need to pass the parameter into the Invoke-PsLaExtractor .PARAMETER ResourceGroup Name of the resource group that you want to work against At runtime / execution of Invoke-PsLaExtractor - your current powershell / az cli session needs to have permissions to work against the resource group Useful when you know upfront what you want to work against, as you don't need to pass the parameter into the Invoke-PsLaExtractor .PARAMETER Name Name of the logic app, that you want to work against At runtime / execution of Invoke-PsLaExtractor - your current powershell / az cli session needs to have permissions to work against the logic app Useful when you know upfront what you want to work against, as you don't need to pass the parameter into the Invoke-PsLaExtractor .PARAMETER OutputPath Path to were the runbook file will be persisted The path has to be a directory The runbook file will be named: PsLaExtractor.default.psakefile.ps1 .PARAMETER IncludePrefixSuffix Instruct the cmdlet to add the different prefix and suffix options, with the default values that comes with the module This make it easier to make the runbook file work across different environments, without having to worry about prepping different prefix and suffix value prior .EXAMPLE PS C:\> New-PsLaRunbookByPath Creates a valid runbook file, based on the bare minimum and with sane default values Reads all the internal / generic tasks that are part of the module and implements a valid default path for the includes .EXAMPLE PS C:\> New-PsLaRunbookByPath -Path c:\temp\tasks Creates a valid runbook file, based on the bare minimum and with sane default values Reads all ps1 files located in c:\temp\tasks Great to use when you have lots of custom tasks in a directory / repository, and want a good runbook file as starting point .EXAMPLE PS C:\> New-PsLaRunbookByPath -SubscriptionId "f5608f3d-ab28-49d8-9b4e-1b1f812d12e0" -ResourceGroup "TestRg" Creates a valid runbook file, based on the bare minimum and with sane default values Reads all the internal / generic tasks that are part of the module and implements a valid default path for the includes Prepares the Properties object with SubscriptionId and ResourceGroup Useful if you have multiple logic apps in the same resource group and you want them extracted using the same runbook file .EXAMPLE PS C:\> New-PsLaRunbookByPath -SubscriptionId "f5608f3d-ab28-49d8-9b4e-1b1f812d12e0" -ResourceGroup "TestRg" -Name "TestLogicApp" Creates a valid runbook file, based on the bare minimum and with sane default values Reads all the internal / generic tasks that are part of the module and implements a valid default path for the includes Prepares the Properties object with SubscriptionId and ResourceGroup and Name Useful if you want to have a ready to run runbook file, that makes it simple to run the command again and again Great for iterative work, where you make lots of small changes in the logic app and want to see how the changes affect your ARM template .EXAMPLE PS C:\> New-PsLaRunbookByPath -OutputPath c:\temp\PsLaRunbooks Creates a valid runbook file, based on the bare minimum and with sane default values Reads all the internal / generic tasks that are part of the module and implements a valid default path for the includes Outputs the build to c:\temp\PsLaRunbooks The runbook file is default named: PsLaExtractor.default.psakefile.ps1 .EXAMPLE PS C:\> New-PsLaRunbookByPath -IncludePrefixSuffix Creates a valid runbook file, based on the bare minimum and with sane default values Reads all the internal / generic tasks that are part of the module and implements a valid default path for the includes The Properties object inside the runbook file, will be pre-populated with the default prefix and suffix values from the module This make it easier to make the runbook file work across different environments, without having to worry about prepping different prefix and suffix value prior .NOTES Author: Mötz Jensen (@Splaxi) #> function New-PsLaRunbookByPath { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')] [CmdletBinding()] param ( [string] $Path = $(Get-PSFConfigValue -FullName PsLogicAppExtractor.ModulePath.Tasks), [string] $SubscriptionId, [string] $ResourceGroup, [string] $Name, [string] $OutputPath, [switch] $IncludePrefixSuffix ) $files = Get-ChildItem -Path "$Path\*.ps1" $res = New-Object System.Collections.Generic.List[System.Object] $res.AddRange($(Get-BuildHeader -SubscriptionId $SubscriptionId -ResourceGroup $ResourceGroup -Name $Name -IncludePrefixSuffix:$IncludePrefixSuffix)) if ($Path -ne $(Get-PSFConfigValue -FullName PsLogicAppExtractor.ModulePath.Tasks)) { $res.Add("# Path to where the custom task files are located") $res.Add("`$pathTasksCustom = `"$Path`"") } $res.Add("# Array to hold all tasks for the default task") $res.Add("`$listTasks = @()") $res.Add("") $includes = New-Object System.Collections.Generic.List[System.Object] $list = New-Object System.Collections.Generic.List[System.Object] $psake.context = New-Object System.Collections.Stack $psake.context.push( @{ "tasks" = @{} "aliases" = @{} } ) foreach ($item in $files) { $psake.context = New-Object System.Collections.Stack $psake.context.push( @{ "tasks" = @{} "aliases" = @{} } ) . $item.FullName foreach ($task in $psake.context.tasks) { foreach ($value in $task.Values) { $list.Add("`$listTasks += `"$($value.Name)`"") } } } $psake.context = New-Object System.Collections.Stack #TODO! Make sure that we actually need this - other places it was enough with the $psake.context = New-Object System.Collections.Stack # $psake.context.push( # @{ # "buildSetupScriptBlock" = {} # "buildTearDownScriptBlock" = {} # "taskSetupScriptBlock" = {} # "taskTearDownScriptBlock" = {} # "executedTasks" = new-object System.Collections.Stack # "callStack" = new-object System.Collections.Stack # "originalEnvPath" = $env:PATH # "originalDirectory" = get-location # "originalErrorActionPreference" = $global:ErrorActionPreference # "tasks" = @{} # "aliases" = @{} # "properties" = new-object System.Collections.Stack # "includes" = new-object System.Collections.Queue # } # ) $res.Add("# All tasks that needs to be include based on their path") $res.AddRange($includes) $res.Add("") $res.Add("# Building the list of tasks for the default task") $res.AddRange($list) $res.Add("") $res.Add("# Default tasks, the via the dependencies will run all tasks") $res.Add("Task -Name `"default`" -Depends `$listTasks") if ($OutputPath) { New-Item -Path $OutputPath -ItemType Directory -Force -ErrorAction Ignore > $null $path = Join-Path -Path $OutputPath -ChildPath "PsLaExtractor.default.psakefile.ps1" $encoding = New-Object System.Text.UTF8Encoding($true) [System.IO.File]::WriteAllLines($path, $($res.ToArray() -join "`r`n"), $encoding) Get-Item -Path $path | Select-Object -ExpandProperty FullName } else { $res.ToArray() -join "`r`n" } } <# .SYNOPSIS Create valid runbook file based on the task passed as inputs .DESCRIPTION Helps you build a valid runbook file, based on all the individual tasks that are passed as inputs This makes it easy to get a starting point for a new runbook file, based on the tasks you have build in your array and then passes into the cmdlet Tasks are expected to be the ones that are part of the module .PARAMETER Task Names of the tasks that you want to be part of your runbook file Supports array of task names Names of the different tasks are expected to be the ones that are part of the module .PARAMETER SubscriptionId Id of the subscription that you want to work against At runtime / execution of Invoke-PsLaExtractor - your current powershell / az cli session either needs to "connected" to the subscription or at least have permissions to work against the subscription Useful when you know upfront what you want to work against, as you don't need to pass the parameter into the Invoke-PsLaExtractor .PARAMETER ResourceGroup Name of the resource group that you want to work against At runtime / execution of Invoke-PsLaExtractor - your current powershell / az cli session needs to have permissions to work against the resource group Useful when you know upfront what you want to work against, as you don't need to pass the parameter into the Invoke-PsLaExtractor .PARAMETER Name Name of the logic app, that you want to work against At runtime / execution of Invoke-PsLaExtractor - your current powershell / az cli session needs to have permissions to work against the logic app Useful when you know upfront what you want to work against, as you don't need to pass the parameter into the Invoke-PsLaExtractor .PARAMETER OutputPath Path to were the runbook file will be persisted The path has to be a directory The runbook file will be named: PsLaExtractor.default.psakefile.ps1 .PARAMETER IncludePrefixSuffix Instruct the cmdlet to add the different prefix and suffix options, with the default values that comes with the module This make it easier to make the runbook file work across different environments, without having to worry about prepping different prefix and suffix value prior .EXAMPLE PS C:\> New-PsLaRunbookByTask -Task "Export-LogicApp.AzCli","ConvertTo-Raw","ConvertTo-Arm" Creates a valid runbook file, based on the bare minimum and with sane default values Will write the include and taskList in the mentioned order of the tasks .EXAMPLE PS C:\> New-PsLaRunbookByTask -Task "Export-LogicApp.AzCli","ConvertTo-Raw","ConvertTo-Arm" -SubscriptionId "f5608f3d-ab28-49d8-9b4e-1b1f812d12e0" -ResourceGroup "TestRg" Creates a valid runbook file, based on the bare minimum and with sane default values Will write the include and taskList in the mentioned order of the tasks Prepares the Properties object with SubscriptionId and ResourceGroup Useful if you have multiple logic apps in the same resource group and you want them extracted using the same runbook file .EXAMPLE PS C:\> New-PsLaRunbookByTask -Task "Export-LogicApp.AzCli","ConvertTo-Raw","ConvertTo-Arm" -SubscriptionId "f5608f3d-ab28-49d8-9b4e-1b1f812d12e0" -ResourceGroup "TestRg" -Name "TestLogicApp" Creates a valid runbook file, based on the bare minimum and with sane default values Will write the include and taskList in the mentioned order of the tasks Prepares the Properties object with SubscriptionId and ResourceGroup and Name Useful if you want to have a ready to run runbook file, that makes it simple to run the command again and again Great for iterative work, where you make lots of small changes in the logic app and want to see how the changes affect your ARM template .EXAMPLE PS C:\> New-PsLaRunbookByTask -Task "Export-LogicApp.AzCli","ConvertTo-Raw","ConvertTo-Arm" -OutputPath c:\temp\PsLaRunbooks Creates a valid runbook file, based on the bare minimum and with sane default values Will write the include and taskList in the mentioned order of the tasks Outputs the build to c:\temp\PsLaRunbooks The runbook file is default named: PsLaExtractor.default.psakefile.ps1 .EXAMPLE PS C:\> New-PsLaRunbookByTask -Task "Export-LogicApp.AzCli","ConvertTo-Raw","ConvertTo-Arm" -IncludePrefixSuffix Creates a valid runbook file, based on the bare minimum and with sane default values Will write the include and taskList in the mentioned order of the tasks The Properties object inside the runbook file, will be pre-populated with the default prefix and suffix values from the module This make it easier to make the runbook file work across different environments, without having to worry about prepping different prefix and suffix value prior .NOTES Author: Mötz Jensen (@Splaxi) #> function New-PsLaRunbookByTask { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')] [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string[]] $Task, [string] $SubscriptionId, [string] $ResourceGroup, [string] $Name, [string] $OutputPath, [switch] $IncludePrefixSuffix ) $res = New-Object System.Collections.Generic.List[System.Object] $res.AddRange($(Get-BuildHeader -SubscriptionId $SubscriptionId -ResourceGroup $ResourceGroup -Name $Name -IncludePrefixSuffix:$IncludePrefixSuffix)) $res.Add("# Array to hold all tasks for the default task") $res.Add('$listTasks = @()') $res.Add('') $list = New-Object System.Collections.Generic.List[System.Object] foreach ($item in $Task) { $list.Add("`$listTasks += `"$item`"") } $res.AddRange($list) $res.Add("") $res.Add("# Default tasks, the via the dependencies will run all tasks") $res.Add('Task -Name "default" -Depends $listTasks') if ($OutputPath) { New-Item -Path $OutputPath -ItemType Directory -Force -ErrorAction Ignore > $null $path = Join-Path -Path $OutputPath -ChildPath "PsLaExtractor.default.psakefile.ps1" $encoding = New-Object System.Text.UTF8Encoding($true) [System.IO.File]::WriteAllLines($path, $($res.ToArray() -join "`r`n"), $encoding) Get-Item -Path $path | Select-Object -ExpandProperty FullName } else { $res.ToArray() -join "`r`n" } } <# .SYNOPSIS Output the tasks result to a file .DESCRIPTION Persists the tasks output into a file, either the raw content or the object Sets the $Script:FilePath = $Path, to ensure the next tasks can pick up the file and continue its work .PARAMETER Path Path to where the tasks wants the ouput to be persisted .PARAMETER Content Raw string that should be written to the desired path .PARAMETER InputObject The object that should be written to the desired path Will be converted to a json string, usign the ConvertTo-Json Important note: If you need the InputObject to be written with a specific structure, the object has to be of the expected type before being passed into the cmdlet A simple cast can ensure this to work as intended .EXAMPLE PS C:\> Out-TaskFile -Path "C:\temp\work_directory\1_Export-LogicApp.AzCli" -InputObject $([ArmTemplate]$armObj) Outputs the armObj variable to the path: "C:\temp\work_directory\1_Export-LogicApp.AzCli" The armObj is casted to the [ArmTemplate] type, to ensure it is persisted as the expected json structure .EXAMPLE PS C:\> Out-TaskFile -Path "C:\temp\work_directory\1_Export-LogicApp.AzCli" -Content '{"Test":"Test"}' Outputs the content string: '{"Test":"Test"}' to the path: "C:\temp\work_directory\1_Export-LogicApp.AzCli" .NOTES Author: Mötz Jensen (@Splaxi) #> function Out-TaskFile { [CmdletBinding(DefaultParameterSetName = "InputObject")] param ( [string] $Path = $(Get-PSFConfigValue -FullName PsLogicAppExtractor.Execution.TaskOutputFile), [Parameter(Mandatory = $true, ParameterSetName = "Content")] [string] $Content, [Parameter(Mandatory = $true, ParameterSetName = "InputObject")] [object] $InputObject ) if ($InputObject) { $Content = $InputObject | ConvertTo-Json -Depth 20 } $encoding = New-Object System.Text.UTF8Encoding($true) [System.IO.File]::WriteAllLines($Path, $Content, $encoding) if ($(Get-PSFConfigValue -FullName PsLogicAppExtractor.Execution.TaskInputNext)) { $taskPath = Get-PSFConfigValue -FullName PsLogicAppExtractor.Execution.TaskPath Copy-Item -Path $(Get-PSFConfigValue -FullName PsLogicAppExtractor.Execution.TaskInputNext) -Destination "$taskPath\Input.json" } Set-PSFConfig -FullName PsLogicAppExtractor.Execution.TaskInputNext -Value $Path } <# .SYNOPSIS Output the tasks result to a file, as an ARM template .DESCRIPTION Persists the tasks output into a file, as an ARM template .PARAMETER InputObject The object that should be written to the desired path Will be converted to a json string, usign the ConvertTo-Json .EXAMPLE PS C:\> Out-TaskFileArm -InputObject $armObj Outputs the armObj variable The armObj is casted to the [ArmTemplate] type, to ensure it is persisted as the expected json structure .NOTES Author: Mötz Jensen (@Splaxi) #> function Out-TaskFileArm { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [object] $InputObject ) Out-TaskFile -InputObject $([ArmTemplate]$InputObject) } <# .SYNOPSIS Output the tasks result to a file, as a LogicApp json structure .DESCRIPTION Persists the tasks output into a file, as a LogicApp json structure .PARAMETER InputObject The object that should be written to the desired path Will be converted to a json string, usign the ConvertTo-Json .EXAMPLE PS C:\> Out-TaskFileLogicApp -InputObject $lgObj Outputs the armObj variable The armObj is casted to the [ArmTemplate] type, to ensure it is persisted as the expected json structure .NOTES Author: Mötz Jensen (@Splaxi) #> function Out-TaskFileLogicApp { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [object] $InputObject ) Out-TaskFile -InputObject $([LogicApp]$InputObject) } <# .SYNOPSIS Remove parameter from the ARM template .DESCRIPTION Removes an ARM template parameter by the name provided .PARAMETER InputObject The ARM object that you want to work against It has to be a object of the type [ArmTemplate] for it to work properly .PARAMETER Name Name of the parameter that you want to work against If the parameter exists, it will be removed from the InputObject .EXAMPLE PS C:\> Remove-ArmParameter -InputObject $armObj -Name "logicAppName" Removes the logicAppName ARM template parameter .NOTES Author: Mötz Jensen (@Splaxi) #> function Remove-ArmParameter { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')] [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [object] $InputObject, [Alias('ParameterName')] [Parameter(Mandatory = $true)] [string] $Name ) if ($InputObject.parameters.$Name) { $InputObject.parameters.PsObject.Properties.Remove($Name) } $InputObject } <# .SYNOPSIS Remove variable from the ARM template .DESCRIPTION Removes an ARM template variable by the name provided .PARAMETER InputObject The ARM object that you want to work against It has to be a object of the type [ArmTemplate] for it to work properly .PARAMETER Name Name of the variable that you want to work against If the variable exists, it will be removed from the InputObject .EXAMPLE PS C:\> Remove-ArmVariable -InputObject $armObj -Name "logicAppName" Removes the logicAppName ARM template variable .NOTES Author: Mötz Jensen (@Splaxi) #> function Remove-ArmVariable { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')] [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [object] $InputObject, [Alias('VariableName')] [Parameter(Mandatory = $true)] [string] $Name ) if ($InputObject.variables.$Name) { $InputObject.variables.PsObject.Properties.Remove($Name) } $InputObject } <# .SYNOPSIS Remove parm (parameter) from the LogicApp .DESCRIPTION Removes an LogicApp parm (parameter) by the name provided .PARAMETER InputObject The LogicApp object that you want to work against It has to be a object of the type [LogicApp] for it to work properly .PARAMETER Name Name of the parm (parameter) that you want to work against If the parm (parameter) exists, it will be removed from the InputObject .EXAMPLE PS C:\> Remove-LogicAppParm -InputObject $armObj -Name "TriggerQueue" Removes the TriggerQueue LogicApp parm (parameter) .NOTES Author: Mötz Jensen (@Splaxi) #> function Remove-LogicAppParm { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')] [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [object] $InputObject, [Alias('ParmName')] [Parameter(Mandatory = $true)] [string] $Name ) if ($InputObject.properties.definition.parameters.$Name) { $InputObject.properties.definition.parameters.PsObject.Properties.Remove($Name) } $InputObject } <# .SYNOPSIS Set the current working directory .DESCRIPTION Sets the current tasks working directory, based on the current PsLaWorkPath and the execution number that task is in the overall execution Outputs the path that has been constructed .PARAMETER Path Path to the current working directory The value passed in should always be the $PsLaWorkPath, to ensure that things are working .PARAMETER FileName The of the file that you want to be configured for the task Is normally equal to the name of the Logic App .EXAMPLE PS C:\> Set-TaskWorkDirectory Creates a new sub directory under the $PsLaWorkPath location The sub directory is named "$taskCounter`_$TaskName" The output will be: "$Path\$taskCounter`_$TaskName" .NOTES Author: Mötz Jensen (@Splaxi) #> function Set-TaskWorkDirectory { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')] [CmdletBinding()] param ( [string] $Path = $(Get-PSFConfigValue -FullName PsLogicAppExtractor.Execution.WorkPath), [string] $FileName = "$Name.json" ) Set-PSFConfig -FullName PsLogicAppExtractor.Execution.TaskCounter -Value $($(Get-PSFConfigValue -FullName PsLogicAppExtractor.Execution.TaskCounter) + 1) $taskCounter = $(Get-PSFConfigValue -FullName PsLogicAppExtractor.Execution.TaskCounter) $taskName = $($psake.context.Peek().CurrentTaskName) $newPath = "$Path\$taskCounter`_$TaskName" New-Item -Path $newPath -ItemType Directory -Force -ErrorAction Ignore > $null Set-PSFConfig -FullName PsLogicAppExtractor.Execution.TaskPath -Value $newPath Set-PSFConfig -FullName PsLogicAppExtractor.Execution.TaskOutputFile -Value "$newPath\$fileName" } <# This is an example configuration file By default, it is enough to have a single one of them, however if you have enough configuration settings to justify having multiple copies of it, feel totally free to split them into multiple files. #> <# # Example Configuration Set-PSFConfig -Module 'PsLogicAppExtractor' -Name 'Example.Setting' -Value 10 -Initialize -Validation 'integer' -Handler { } -Description "Example configuration setting. Your module can then use the setting using 'Get-PSFConfigValue'" #> Set-PSFConfig -Module 'PsLogicAppExtractor' -Name 'Import.DoDotSource' -Value $false -Initialize -Validation 'bool' -Description "Whether the module files should be dotsourced on import. By default, the files of this module are read as string value and invoked, which is faster but worse on debugging." Set-PSFConfig -Module 'PsLogicAppExtractor' -Name 'Import.IndividualFiles' -Value $false -Initialize -Validation 'bool' -Description "Whether the module files should be imported individually. During the module build, all module code is compiled into few files, which are imported instead by default. Loading the compiled versions is faster, using the individual files is easier for debugging and testing out adjustments." Set-PSFConfig -Module 'PsLogicAppExtractor' -Name 'ModulePath.Base' -Value $script:ModuleRoot -Initialize -Description "The base path for the module, used for the internal functions to be able to provide the full path for internal objects." Set-PSFConfig -Module 'PsLogicAppExtractor' -Name 'ModulePath.Tasks' -Value "$($script:ModuleRoot)\internal\tasks" -Initialize -Description "The full path for all generic tasks that are part of the module." Set-PSFConfig -Module 'PsLogicAppExtractor' -Name 'ModulePath.Classes' -Value "$($script:ModuleRoot)\internal\Classes" -Initialize -Description "The full path for all the generic classes that are part of the module." Set-PSFConfig -Module 'PsLogicAppExtractor' -Name 'prefixsuffix.tag.prefix' -Value "tag_" -Initialize -Description "The default prefix for Tag objects, used as a fallback value for the Format-Name cmdlet." Set-PSFConfig -Module 'PsLogicAppExtractor' -Name 'prefixsuffix.tag.suffix' -Value "" -Initialize -Description "The default suffix for Tag objects, used as a fallback value for the Format-Name cmdlet. Be default an empty string" Set-PSFConfig -Module 'PsLogicAppExtractor' -Name 'prefixsuffix.parm.prefix' -Value "parm_" -Initialize -Description "The default prefix for parm (parameter) objects, used as a fallback value for the Format-Name cmdlet." Set-PSFConfig -Module 'PsLogicAppExtractor' -Name 'prefixsuffix.parm.suffix' -Value "" -Initialize -Description "The default suffix for parm (parameter) objects, used as a fallback value for the Format-Name cmdlet. Be default an empty string." Set-PSFConfig -Module 'PsLogicAppExtractor' -Name 'prefixsuffix.connection.prefix' -Value "connection_" -Initialize -Description "The default prefix for connection objects, used as a fallback value for the Format-Name cmdlet." Set-PSFConfig -Module 'PsLogicAppExtractor' -Name 'prefixsuffix.connection.suffix' -Value "_id" -Initialize -Description "The default suffix for connection objects, used as a fallback value for the Format-Name cmdlet." Set-PSFConfig -Module 'PsLogicAppExtractor' -Name 'prefixsuffix.trigger.prefix' -Value "trigger_" -Initialize -Description "The default prefix for trigger objects, used as a fallback value for the Format-Name cmdlet." Set-PSFConfig -Module 'PsLogicAppExtractor' -Name 'prefixsuffix.trigger.suffix' -Value "" -Initialize -Description "The default suffix for trigger objects, used as a fallback value for the Format-Name cmdlet. Be default an empty string." <# Stored scriptblocks are available in [PsfValidateScript()] attributes. This makes it easier to centrally provide the same scriptblock multiple times, without having to maintain it in separate locations. It also prevents lengthy validation scriptblocks from making your parameter block hard to read. Set-PSFScriptblock -Name 'PsLogicAppExtractor.ScriptBlockName' -Scriptblock { } #> <# # Example: Register-PSFTeppScriptblock -Name "PsLogicAppExtractor.alcohol" -ScriptBlock { 'Beer','Mead','Whiskey','Wine','Vodka','Rum (3y)', 'Rum (5y)', 'Rum (7y)' } #> <# # Example: Register-PSFTeppArgumentCompleter -Command Get-Alcohol -Parameter Type -Name PsLogicAppExtractor.alcohol #> New-PSFLicense -Product 'PsLogicAppExtractor' -Manufacturer 'Motz' -ProductVersion $script:ModuleVersion -ProductType Module -Name MIT -Version "1.0.0.0" -Date (Get-Date "2022-04-04") -Text @" Copyright (c) 2022 Motz Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. "@ #endregion Load compiled code |